[ADD] fields: add option to store binary fields into attachments

Such a field is declared as:

    image = fields.Binary("Image", attachment=True)

With this option, the value of a binary field F is stored in an attachment with
`res_field=F`.  When attachments are stored into the filestore, binary fields
are consequently stored in the filestore as well.  Note that the option does
not work on old-API function fields.

By default searching on `ir.attachment` filters out attachments for binary
fields: the condition `res_field=False` is added to the domain unless a
condition on field `id` or `res_field` is already present.  The client uses
`search_read` with a domain like `[('id', 'in', ids)]` for reading the data on
a form view.  In that case, we should not filter out attachments that store
binary fields.

Also, do not filter search results for the superuser.
This commit is contained in:
Raphael Collet
2015-09-28 15:19:47 +02:00
parent 6327cda0a7
commit 1bb61c970e
4 changed files with 80 additions and 25 deletions
+12 -3
View File
@@ -274,6 +274,7 @@ class ir_attachment(osv.osv):
'description': fields.text('Description'),
'res_name': fields.function(_name_get_resname, type='char', string='Resource Name', store=True),
'res_model': fields.char('Resource Model', readonly=True, help="The database object this attachment will be attached to"),
'res_field': fields.char('Resource Field', readonly=True),
'res_id': fields.integer('Resource ID', readonly=True, help="The record id this is attached to"),
'create_date': fields.datetime('Date Created', readonly=True),
'create_uid': fields.many2one('res.users', 'Owner', readonly=True),
@@ -352,14 +353,22 @@ class ir_attachment(osv.osv):
raise AccessError(_("Sorry, you are not allowed to access this document."))
def _search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
# add res_field=False in domain if not present; the arg[0] trick below
# works for domain items and '&'/'|'/'!' operators too
if not any(arg[0] in ('id', 'res_field') for arg in args):
args.insert(0, ('res_field', '=', False))
ids = super(ir_attachment, self)._search(cr, uid, args, offset=offset,
limit=limit, order=order,
context=context, count=False,
access_rights_uid=access_rights_uid)
if uid == SUPERUSER_ID:
# rules do not apply for the superuser
return len(ids) if count else ids
if not ids:
if count:
return 0
return []
return 0 if count else []
# Work with a set, as list.remove() is prohibitive for large lists of documents
# (takes 20+ seconds on a db with 100k docs during search_count()!)
@@ -21,6 +21,7 @@
</group>
<group string="Attached To" groups="base.group_no_one">
<field name="res_model"/>
<field name="res_field"/>
<field name="res_id"/>
<field name="res_name"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
@@ -51,6 +52,7 @@
<field name="name"/>
<field name="datas_fname"/>
<field name="res_model"/>
<field name="res_field"/>
<field name="res_id"/>
<field name="type"/>
<field name="company_id" groups="base.group_multi_company"/>
+6
View File
@@ -1398,6 +1398,12 @@ class Datetime(Field):
class Binary(Field):
type = 'binary'
_slots = {
'attachment': False, # whether value is stored in attachment
}
_column_attachment = property(attrgetter('attachment'))
_description_attachment = property(attrgetter('attachment'))
class Selection(Field):
+60 -22
View File
@@ -572,6 +572,7 @@ class datetime(_column):
class binary(_column):
_type = 'binary'
_classic_read = False
_classic_write = property(lambda self: not self.attachment)
# Binary values may be byte strings (python 2.6 byte array), but
# the legacy OpenERP convention is to transfer and store binaries
@@ -584,35 +585,69 @@ class binary(_column):
_symbol_set = (_symbol_c, _symbol_f)
_symbol_get = lambda self, x: x and str(x)
__slots__ = ['filters']
__slots__ = ['attachment', 'filters']
def __init__(self, string='unknown', filters=None, **args):
args['_prefetch'] = args.get('_prefetch', False)
args['attachment'] = args.get('attachment', False)
_column.__init__(self, string=string, filters=filters, **args)
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
if not context:
context = {}
if not values:
values = []
res = {}
for i in ids:
val = None
for v in values:
if v['id'] == i:
val = v[name]
break
def to_field_args(self):
args = super(binary, self).to_field_args()
args['attachment'] = self.attachment
return args
# If client is requesting only the size of the field, we return it instead
# of the content. Presumably a separate request will be done to read the actual
# content if it's needed at some point.
# TODO: after 6.0 we should consider returning a dict with size and content instead of
# having an implicit convention for the value
if val and context.get('bin_size_%s' % name, context.get('bin_size')):
res[i] = tools.human_size(long(val))
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
result = dict.fromkeys(ids, False)
if self.attachment:
# values are stored in attachments, retrieve them
atts = obj.pool['ir.attachment'].browse(cr, SUPERUSER_ID, [], context)
domain = [
('res_model', '=', obj._name),
('res_field', '=', name),
('res_id', 'in', ids),
]
for att in atts.search(domain):
# the 'bin_size' flag is handled by the field 'datas' itself
result[att.res_id] = att.datas
else:
# If client is requesting only the size of the field, we return it
# instead of the content. Presumably a separate request will be done
# to read the actual content if it's needed at some point.
context = context or {}
if context.get('bin_size') or context.get('bin_size_%s' % name):
postprocess = lambda val: tools.human_size(long(val))
else:
res[i] = val
return res
postprocess = lambda val: val
for val in (values or []):
result[val['id']] = postprocess(val[name])
return result
def set(self, cr, obj, id, name, value, user=None, context=None):
assert self.attachment
# retrieve the attachment that stores the value, and adapt it
att = obj.pool['ir.attachment'].browse(cr, SUPERUSER_ID, [], context).search([
('res_model', '=', obj._name),
('res_field', '=', name),
('res_id', '=', id),
])
if value:
if att:
att.write({'datas': value})
else:
att.create({
'name': name,
'res_model': obj._name,
'res_field': name,
'res_id': id,
'type': 'binary',
'datas': value,
})
else:
att.unlink()
return []
class selection(_column):
_type = 'selection'
@@ -1397,6 +1432,9 @@ class function(_column):
args['store'] = bool(self.store)
if self._type in ('float',):
args['digits'] = self._digits_compute or self._digits
elif self._type in ('binary',):
# limitation: binary function fields cannot be stored in attachments
args['attachment'] = False
elif self._type in ('selection', 'reference'):
args['selection'] = self.selection
elif self._type in ('many2one', 'one2many', 'many2many'):