[FIX] OPW 577963: ir_attachment: speed up ir.attachment search for large databases
[odoo/odoo.git] / bin / addons / base / ir / ir_attachment.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import itertools
23
24 from osv import fields,osv
25 from osv.orm import except_orm
26 import tools
27
28 class ir_attachment(osv.osv):
29     def check(self, cr, uid, ids, mode, context=None, values=None):
30         """Restricts the access to an ir.attachment, according to referred model
31         In the 'document' module, it is overriden to relax this hard rule, since
32         more complex ones apply there.
33         """
34         if not ids:
35             return
36         ima = self.pool.get('ir.model.access')
37         res_ids = {}
38         if ids:
39             if isinstance(ids, (int, long)):
40                 ids = [ids]
41             cr.execute('SELECT DISTINCT res_model, res_id FROM ir_attachment WHERE id = ANY (%s)', (ids,))
42             for rmod, rid in cr.fetchall():
43                 if not (rmod and rid):
44                     continue
45                 res_ids.setdefault(rmod,set()).add(rid)
46         if values:
47             if 'res_model' in values and 'res_id' in values:
48                 res_ids.setdefault(values['res_model'],set()).add(values['res_id'])
49
50         for model, mids in res_ids.items():
51             # ignore attachments that are not attached to a resource anymore when checking access rights
52             # (resource was deleted but attachment was not)
53             cr.execute('select id from '+self.pool.get(model)._table+' where id in %s', (tuple(mids),))
54             mids = [x[0] for x in cr.fetchall()]
55             ima.check(cr, uid, model, mode, context=context)
56             self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
57
58     def search(self, cr, uid, args, offset=0, limit=None, order=None,
59             context=None, count=False):
60         ids = super(ir_attachment, self).search(cr, uid, args, offset=offset,
61                                                 limit=limit, order=order,
62                                                 context=context, count=False)
63         if not ids:
64             if count:
65                 return 0
66             return []
67
68         # Work with a set, as list.remove() is prohibitive for large lists of documents
69         # (takes 20+ seconds on a db with 100k docs during search_count()!)
70         orig_ids = ids
71         ids = set(ids)
72
73         # For attachments, the permissions of the document they are attached to
74         # apply, so we must remove attachments for which the user cannot access
75         # the linked document.
76         # Use pure SQL rather than read() as it is about 50% faster for large dbs (100k+ docs),
77         # and the permissions are checked in super() and below anyway.
78         cr.execute("""SELECT id, res_model, res_id FROM ir_attachment WHERE id = ANY(%s)""", (list(ids),))
79         targets = cr.dictfetchall()
80         model_attachments = {}
81         for target_dict in targets:
82             if not (target_dict['res_id'] and target_dict['res_model']):
83                 continue
84             # model_attachments = { 'model': { 'res_id': [id1,id2] } }
85             model_attachments.setdefault(target_dict['res_model'],{}).setdefault(target_dict['res_id'],set()).add(target_dict['id'])
86
87         # To avoid multiple queries for each attachment found, checks are
88         # performed in batch as much as possible.
89         ima = self.pool.get('ir.model.access')
90         for model, targets in model_attachments.iteritems():
91             if not ima.check(cr, uid, model, 'read', raise_exception=False, context=context):
92                 # remove all corresponding attachment ids
93                 for attach_id in itertools.chain(*targets.values()):
94                     ids.remove(attach_id)
95                 continue # skip ir.rule processing, these ones are out already
96
97             # filter ids according to what access rules permit
98             target_ids = targets.keys()
99             allowed_ids = self.pool.get(model).search(cr, uid, [('id', 'in', target_ids)], context=context)
100             disallowed_ids = set(target_ids).difference(allowed_ids)
101             for res_id in disallowed_ids:
102                 for attach_id in targets[res_id]:
103                     ids.remove(attach_id)
104
105         # sort result according to the original sort ordering
106         result = [id for id in orig_ids if id in ids]
107         return len(result) if count else list(result)
108
109     def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
110         self.check(cr, uid, ids, 'read', context=context)
111         return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context, load)
112
113     def write(self, cr, uid, ids, vals, context=None):
114         self.check(cr, uid, ids, 'write', context=context, values=vals)
115         return super(ir_attachment, self).write(cr, uid, ids, vals, context)
116
117     def copy(self, cr, uid, id, default=None, context=None):
118         self.check(cr, uid, [id], 'write', context=context)
119         return super(ir_attachment, self).copy(cr, uid, id, default, context)
120
121     def unlink(self, cr, uid, ids, context=None):
122         self.check(cr, uid, ids, 'unlink', context=context)
123         return super(ir_attachment, self).unlink(cr, uid, ids, context)
124
125     def create(self, cr, uid, values, context=None):
126         self.check(cr, uid, [], mode='create', context=context, values=values)
127         return super(ir_attachment, self).create(cr, uid, values, context)
128
129     def action_get(self, cr, uid, context=None):
130         return self.pool.get('ir.actions.act_window').for_xml_id(
131             cr, uid, 'base', 'action_attachment', context=context)
132
133     def _name_get_resname(self, cr, uid, ids, object, method, context):
134         data = {}
135         for attachment in self.browse(cr, uid, ids, context=context):
136             model_object = attachment.res_model
137             res_id = attachment.res_id
138             if model_object and res_id:
139                 model_pool = self.pool.get(model_object)
140                 res = model_pool.name_get(cr,uid,[res_id],context)
141                 res_name = res and res[0][1] or False
142                 if res_name:
143                     field = self._columns.get('res_name',False)
144                     if field and len(res_name) > field.size:
145                         res_name = res_name[:field.size-3] + '...' 
146                 data[attachment.id] = res_name
147             else:
148                 data[attachment.id] = False
149         return data
150
151     _name = 'ir.attachment'
152     _columns = {
153         'name': fields.char('Attachment Name',size=256, required=True),
154         'datas': fields.binary('Data'),
155         'datas_fname': fields.char('Filename',size=256),
156         'description': fields.text('Description'),
157         'res_name': fields.function(_name_get_resname, type='char', size=128,
158                 string='Resource Name', method=True, store=True),
159         'res_model': fields.char('Resource Object',size=64, readonly=True,
160                 help="The database object this attachment will be attached to"),
161         'res_id': fields.integer('Resource ID', readonly=True,
162                 help="The record id this is attached to"),
163         'url': fields.char('Url', size=512, oldname="link"),
164         'type': fields.selection(
165                 [ ('url','URL'), ('binary','Binary'), ],
166                 'Type', help="Binary File or external URL", required=True, change_default=True),
167
168         'create_date': fields.datetime('Date Created', readonly=True),
169         'create_uid':  fields.many2one('res.users', 'Owner', readonly=True),
170         'company_id': fields.many2one('res.company', 'Company', change_default=True),
171     }
172
173     _defaults = {
174         'type': 'binary',
175         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c),
176     }
177
178     def _auto_init(self, cr, context=None):
179         super(ir_attachment, self)._auto_init(cr, context)
180         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_attachment_res_idx',))
181         if not cr.fetchone():
182             cr.execute('CREATE INDEX ir_attachment_res_idx ON ir_attachment (res_model, res_id)')
183             cr.commit()
184
185 ir_attachment()
186
187
188 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
189