[IMP] access rights: improve error messages for ACLs and record rules
[odoo/odoo.git] / openerp / addons / base / ir / ir_model.py
1 # -*- coding: utf-8 -*-
2
3 ##############################################################################
4 #
5 #    OpenERP, Open Source Business Applications
6 #    Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22 import logging
23 import re
24 import time
25 import types
26
27 from openerp.osv import fields,osv
28 from openerp import netsvc, pooler, tools
29 from openerp.tools.safe_eval import safe_eval as eval
30 from openerp.tools import config
31 from openerp.tools.translate import _
32 from openerp.osv.orm import except_orm, browse_record, EXT_ID_PREFIX_FK, \
33                             EXT_ID_PREFIX_M2M_TABLE, EXT_ID_PREFIX_CONSTRAINT
34
35 _logger = logging.getLogger(__name__)
36
37 MODULE_UNINSTALL_FLAG = '_force_unlink'
38
39 def _get_fields_type(self, cr, uid, context=None):
40     # Avoid too many nested `if`s below, as RedHat's Python 2.6
41     # break on it. See bug 939653.
42     return sorted([(k,k) for k,v in fields.__dict__.iteritems()
43                       if type(v) == types.TypeType and \
44                          issubclass(v, fields._column) and \
45                          v != fields._column and \
46                          not v._deprecated and \
47                          not issubclass(v, fields.function)])
48
49 def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
50     #pseudo-method used by fields.function in ir.model/ir.model.fields
51     module_pool = self.pool.get("ir.module.module")
52     installed_module_ids = module_pool.search(cr, uid, [('state','=','installed')])
53     installed_module_names = module_pool.read(cr, uid, installed_module_ids, ['name'], context=context)
54     installed_modules = set(x['name'] for x in installed_module_names)
55
56     result = {}
57     xml_ids = osv.osv._get_xml_ids(self, cr, uid, ids)
58     for k,v in xml_ids.iteritems():
59         result[k] = ', '.join(sorted(installed_modules & set(xml_id.split('.')[0] for xml_id in v)))
60     return result
61
62
63 class ir_model(osv.osv):
64     _name = 'ir.model'
65     _description = "Models"
66     _order = 'model'
67
68     def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
69         models = self.browse(cr, uid, ids, context=context)
70         res = dict.fromkeys(ids)
71         for model in models:
72             res[model.id] = self.pool.get(model.model).is_transient()
73         return res
74
75     def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
76         if not domain:
77             return []
78         __, operator, value = domain[0]
79         if operator not in ['=', '!=']:
80             raise osv.except_osv(_('Invalid search criterions'), _('The osv_memory field can only be compared with = and != operator.'))
81         value = bool(value) if operator == '=' else not bool(value)
82         all_model_ids = self.search(cr, uid, [], context=context)
83         is_osv_mem = self._is_osv_memory(cr, uid, all_model_ids, 'osv_memory', arg=None, context=context)
84         return [('id', 'in', [id for id in is_osv_mem if bool(is_osv_mem[id]) == value])]
85
86     def _view_ids(self, cr, uid, ids, field_name, arg, context=None):
87         models = self.browse(cr, uid, ids)
88         res = {}
89         for model in models:
90             res[model.id] = self.pool.get("ir.ui.view").search(cr, uid, [('model', '=', model.model)])
91         return res
92
93     _columns = {
94         'name': fields.char('Model Description', size=64, translate=True, required=True),
95         'model': fields.char('Model', size=64, required=True, select=1),
96         'info': fields.text('Information'),
97         'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
98         'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
99         'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
100         'osv_memory': fields.function(_is_osv_memory, string='In-Memory Model', type='boolean',
101             fnct_search=_search_osv_memory,
102             help="Indicates whether this object model lives in memory only, i.e. is not persisted (osv.osv_memory)"),
103         'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the object is defined or inherited'),
104         'view_ids': fields.function(_view_ids, type='one2many', obj='ir.ui.view', string='Views'),
105     }
106
107     _defaults = {
108         'model': lambda *a: 'x_',
109         'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
110     }
111
112     def _check_model_name(self, cr, uid, ids, context=None):
113         for model in self.browse(cr, uid, ids, context=context):
114             if model.state=='manual':
115                 if not model.model.startswith('x_'):
116                     return False
117             if not re.match('^[a-z_A-Z0-9.]+$',model.model):
118                 return False
119         return True
120
121     def _model_name_msg(self, cr, uid, ids, context=None):
122         return _('The Object name must start with x_ and not contain any special character !')
123
124     _constraints = [
125         (_check_model_name, _model_name_msg, ['model']),
126     ]
127     _sql_constraints = [
128         ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
129     ]
130
131     # overridden to allow searching both on model name (model field)
132     # and model description (name field)
133     def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
134         if args is None:
135             args = []
136         domain = args + ['|', ('model', operator, name), ('name', operator, name)]
137         return self.name_get(cr, name_get_uid or uid,
138                              super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
139                              context=context)
140
141     def _drop_table(self, cr, uid, ids, context=None):
142         for model in self.browse(cr, uid, ids, context):
143             model_pool = self.pool.get(model.model)
144             cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
145             result = cr.fetchone()
146             if result and result[0] == 'v':
147                 cr.execute('DROP view %s' % (model_pool._table,))
148             elif result and result[0] == 'r':
149                 cr.execute('DROP TABLE %s' % (model_pool._table,))
150         return True
151
152     def unlink(self, cr, user, ids, context=None):
153         # Prevent manual deletion of module tables
154         if context is None: context = {}
155         if isinstance(ids, (int, long)):
156             ids = [ids]
157         if not context.get(MODULE_UNINSTALL_FLAG) and \
158                 any(model.state != 'manual' for model in self.browse(cr, user, ids, context)):
159             raise except_orm(_('Error'), _("Model '%s' contains module data and cannot be removed!") % (model.name,))
160
161         self._drop_table(cr, user, ids, context)
162         res = super(ir_model, self).unlink(cr, user, ids, context)
163         if not context.get(MODULE_UNINSTALL_FLAG):
164             # only reload pool for normal unlink. For module uninstall the
165             # reload is done independently in openerp.modules.loading
166             pooler.restart_pool(cr.dbname)
167
168         return res
169
170     def write(self, cr, user, ids, vals, context=None):
171         if context:
172             context.pop('__last_update', None)
173         # Filter out operations 4 link from field id, because openerp-web
174         # always write (4,id,False) even for non dirty items
175         if 'field_id' in vals:
176             vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
177         return super(ir_model,self).write(cr, user, ids, vals, context)
178
179     def create(self, cr, user, vals, context=None):
180         if  context is None:
181             context = {}
182         if context and context.get('manual',False):
183             vals['state']='manual'
184         res = super(ir_model,self).create(cr, user, vals, context)
185         if vals.get('state','base')=='manual':
186             self.instanciate(cr, user, vals['model'], context)
187             self.pool.get(vals['model']).__init__(self.pool, cr)
188             ctx = context.copy()
189             ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0')})
190             self.pool.get(vals['model'])._auto_init(cr, ctx)
191             #pooler.restart_pool(cr.dbname)
192         return res
193
194     def instanciate(self, cr, user, model, context=None):
195         class x_custom_model(osv.osv):
196             pass
197         x_custom_model._name = model
198         x_custom_model._module = False
199         a = x_custom_model.create_instance(self.pool, cr)
200         if (not a._columns) or ('x_name' in a._columns.keys()):
201             x_name = 'x_name'
202         else:
203             x_name = a._columns.keys()[0]
204         x_custom_model._rec_name = x_name
205 ir_model()
206
207 class ir_model_fields(osv.osv):
208     _name = 'ir.model.fields'
209     _description = "Fields"
210
211     _columns = {
212         'name': fields.char('Name', required=True, size=64, select=1),
213         'model': fields.char('Object Name', size=64, required=True, select=1,
214             help="The technical name of the model this field belongs to"),
215         'relation': fields.char('Object Relation', size=64,
216             help="For relationship fields, the technical name of the target model"),
217         'relation_field': fields.char('Relation Field', size=64,
218             help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
219         'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
220             help="The model this field belongs to"),
221         'field_description': fields.char('Field Label', required=True, size=256),
222         'ttype': fields.selection(_get_fields_type, 'Field Type',size=64, required=True),
223         'selection': fields.char('Selection Options',size=128, help="List of options for a selection field, "
224             "specified as a Python expression defining a list of (key, label) pairs. "
225             "For example: [('blue','Blue'),('yellow','Yellow')]"),
226         'required': fields.boolean('Required'),
227         'readonly': fields.boolean('Readonly'),
228         'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search (deprecated)')],'Searchable', required=True),
229         'translate': fields.boolean('Translate', help="Whether values for this field can be translated (enables the translation mechanism for that field)"),
230         'size': fields.integer('Size'),
231         'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
232         'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On Delete', help='On delete property for many2one fields'),
233         'domain': fields.char('Domain', size=256, help="The optional domain to restrict possible values for relationship fields, "
234             "specified as a Python expression defining a list of triplets. "
235             "For example: [('color','=','red')]"),
236         'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
237         'view_load': fields.boolean('View Auto-Load'),
238         'selectable': fields.boolean('Selectable'),
239         'modules': fields.function(_in_modules, type='char', size=128, string='In Modules', help='List of modules in which the field is defined'),
240         'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
241                                                   ondelete='cascade', help="If set, this field will be stored in the sparse "
242                                                                            "structure of the serialization field, instead "
243                                                                            "of having its own database column. This cannot be "
244                                                                            "changed after creation."),
245     }
246     _rec_name='field_description'
247     _defaults = {
248         'view_load': 0,
249         'selection': "",
250         'domain': "[]",
251         'name': 'x_',
252         'state': lambda self,cr,uid,ctx={}: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
253         'on_delete': 'set null',
254         'select_level': '0',
255         'size': 64,
256         'field_description': '',
257         'selectable': 1,
258     }
259     _order = "name"
260
261     def _check_selection(self, cr, uid, selection, context=None):
262         try:
263             selection_list = eval(selection)
264         except Exception:
265             _logger.warning('Invalid selection list definition for fields.selection', exc_info=True)
266             raise except_orm(_('Error'),
267                     _("The Selection Options expression is not a valid Pythonic expression." \
268                       "Please provide an expression in the [('key','Label'), ...] format."))
269
270         check = True
271         if not (isinstance(selection_list, list) and selection_list):
272             check = False
273         else:
274             for item in selection_list:
275                 if not (isinstance(item, (tuple,list)) and len(item) == 2):
276                     check = False
277                     break
278
279         if not check:
280                 raise except_orm(_('Error'),
281                     _("The Selection Options expression is must be in the [('key','Label'), ...] format!"))
282         return True
283
284     def _size_gt_zero_msg(self, cr, user, ids, context=None):
285         return _('Size of the field can never be less than 1 !')
286
287     _sql_constraints = [
288         ('size_gt_zero', 'CHECK (size>0)',_size_gt_zero_msg ),
289     ]
290
291     def _drop_column(self, cr, uid, ids, context=None):
292         for field in self.browse(cr, uid, ids, context):
293             model = self.pool.get(field.model)
294             cr.execute('select relkind from pg_class where relname=%s', (model._table,))
295             result = cr.fetchone()
296             cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
297             column_name = cr.fetchone()
298             if column_name and (result and result[0] == 'r'):
299                 cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
300             model._columns.pop(field.name, None)
301         return True
302
303     def unlink(self, cr, user, ids, context=None):
304         # Prevent manual deletion of module columns
305         if context is None: context = {}
306         if isinstance(ids, (int, long)):
307             ids = [ids]
308         if not context.get(MODULE_UNINSTALL_FLAG) and \
309                 any(field.state != 'manual' for field in self.browse(cr, user, ids, context)):
310             raise except_orm(_('Error'), _("This column contains module data and cannot be removed!"))
311
312         self._drop_column(cr, user, ids, context)
313         res = super(ir_model_fields, self).unlink(cr, user, ids, context)
314         return res
315
316     def create(self, cr, user, vals, context=None):
317         if 'model_id' in vals:
318             model_data = self.pool.get('ir.model').browse(cr, user, vals['model_id'])
319             vals['model'] = model_data.model
320         if context is None:
321             context = {}
322         if context and context.get('manual',False):
323             vals['state'] = 'manual'
324         if vals.get('ttype', False) == 'selection':
325             if not vals.get('selection',False):
326                 raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!'))
327             self._check_selection(cr, user, vals['selection'], context=context)
328         res = super(ir_model_fields,self).create(cr, user, vals, context)
329         if vals.get('state','base') == 'manual':
330             if not vals['name'].startswith('x_'):
331                 raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
332
333             if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]):
334                 raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
335
336             if self.pool.get(vals['model']):
337                 self.pool.get(vals['model']).__init__(self.pool, cr)
338                 #Added context to _auto_init for special treatment to custom field for select_level
339                 ctx = context.copy()
340                 ctx.update({'field_name':vals['name'],'field_state':'manual','select':vals.get('select_level','0'),'update_custom_fields':True})
341                 self.pool.get(vals['model'])._auto_init(cr, ctx)
342
343         return res
344
345     def write(self, cr, user, ids, vals, context=None):
346         if context is None:
347             context = {}
348         if context and context.get('manual',False):
349             vals['state'] = 'manual'
350
351         #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
352         if 'serialization_field_id' in vals or 'name' in vals:
353             for field in self.browse(cr, user, ids, context=context):
354                 if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
355                     raise except_orm(_('Error!'),  _('Changing the storing system for field "%s" is not allowed.')%field.name)
356                 if field.serialization_field_id and (field.name != vals['name']):
357                     raise except_orm(_('Error!'),  _('Renaming sparse field "%s" is not allowed')%field.name)
358
359         column_rename = None # if set, *one* column can be renamed here
360         obj = None
361         models_patch = {}    # structs of (obj, [(field, prop, change_to),..])
362                              # data to be updated on the orm model
363
364         # static table of properties
365         model_props = [ # (our-name, fields.prop, set_fn)
366             ('field_description', 'string', str),
367             ('required', 'required', bool),
368             ('readonly', 'readonly', bool),
369             ('domain', '_domain', eval),
370             ('size', 'size', int),
371             ('on_delete', 'ondelete', str),
372             ('translate', 'translate', bool),
373             ('view_load', 'view_load', bool),
374             ('selectable', 'selectable', bool),
375             ('select_level', 'select', int),
376             ('selection', 'selection', eval),
377             ]
378
379         if vals and ids:
380             checked_selection = False # need only check it once, so defer
381
382             for item in self.browse(cr, user, ids, context=context):
383                 if not (obj and obj._name == item.model):
384                     obj = self.pool.get(item.model)
385
386                 if item.state != 'manual':
387                     raise except_orm(_('Error!'),
388                         _('Properties of base fields cannot be altered in this manner! '
389                           'Please modify them through Python code, '
390                           'preferably through a custom addon!'))
391
392                 if item.ttype == 'selection' and 'selection' in vals \
393                         and not checked_selection:
394                     self._check_selection(cr, user, vals['selection'], context=context)
395                     checked_selection = True
396
397                 final_name = item.name
398                 if 'name' in vals and vals['name'] != item.name:
399                     # We need to rename the column
400                     if column_rename:
401                         raise except_orm(_('Error!'), _('Can only rename one column at a time!'))
402                     if vals['name'] in obj._columns:
403                         raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name'])
404                     if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
405                         raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!'))
406                     if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
407                         raise ValueError('Invalid character in column name')
408                     column_rename = (obj, (obj._table, item.name, vals['name']))
409                     final_name = vals['name']
410
411                 if 'model_id' in vals and vals['model_id'] != item.model_id:
412                     raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!"))
413
414                 if 'ttype' in vals and vals['ttype'] != item.ttype:
415                     raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. "
416                                 "Please drop it and create it again!"))
417
418                 # We don't check the 'state', because it might come from the context
419                 # (thus be set for multiple fields) and will be ignored anyway.
420                 if obj:
421                     models_patch.setdefault(obj._name, (obj,[]))
422                     # find out which properties (per model) we need to update
423                     for field_name, field_property, set_fn in model_props:
424                         if field_name in vals:
425                             property_value = set_fn(vals[field_name])
426                             if getattr(obj._columns[item.name], field_property) != property_value:
427                                 models_patch[obj._name][1].append((final_name, field_property, property_value))
428                         # our dict is ready here, but no properties are changed so far
429
430         # These shall never be written (modified)
431         for column_name in ('model_id', 'model', 'state'):
432             if column_name in vals:
433                 del vals[column_name]
434
435         res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context)
436
437         if column_rename:
438             cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1])
439             # This is VERY risky, but let us have this feature:
440             # we want to change the key of column in obj._columns dict
441             col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy
442             column_rename[0]._columns[column_rename[1][2]] = col
443
444         if models_patch:
445             # We have to update _columns of the model(s) and then call their
446             # _auto_init to sync the db with the model. Hopefully, since write()
447             # was called earlier, they will be in-sync before the _auto_init.
448             # Anything we don't update in _columns now will be reset from
449             # the model into ir.model.fields (db).
450             ctx = context.copy()
451             ctx.update({'select': vals.get('select_level','0'),'update_custom_fields':True})
452
453             for __, patch_struct in models_patch.items():
454                 obj = patch_struct[0]
455                 for col_name, col_prop, val in patch_struct[1]:
456                     setattr(obj._columns[col_name], col_prop, val)
457                 obj._auto_init(cr, ctx)
458         return res
459
460 ir_model_fields()
461
462 class ir_model_access(osv.osv):
463     _name = 'ir.model.access'
464     _columns = {
465         'name': fields.char('Name', size=64, required=True, select=True),
466         'model_id': fields.many2one('ir.model', 'Object', required=True, domain=[('osv_memory','=', False)], select=True, ondelete='cascade'),
467         'group_id': fields.many2one('res.groups', 'Group', ondelete='cascade', select=True),
468         'perm_read': fields.boolean('Read Access'),
469         'perm_write': fields.boolean('Write Access'),
470         'perm_create': fields.boolean('Create Access'),
471         'perm_unlink': fields.boolean('Delete Access'),
472     }
473
474     def check_groups(self, cr, uid, group):
475         grouparr  = group.split('.')
476         if not grouparr:
477             return False
478         cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN (select res_id from ir_model_data where module=%s and name=%s)", (uid, grouparr[0], grouparr[1],))
479         return bool(cr.fetchone())
480
481     def check_group(self, cr, uid, model, mode, group_ids):
482         """ Check if a specific group has the access mode to the specified model"""
483         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
484
485         if isinstance(model, browse_record):
486             assert model._table_name == 'ir.model', 'Invalid model object'
487             model_name = model.name
488         else:
489             model_name = model
490
491         if isinstance(group_ids, (int, long)):
492             group_ids = [group_ids]
493         for group_id in group_ids:
494             cr.execute("SELECT perm_" + mode + " "
495                    "  FROM ir_model_access a "
496                    "  JOIN ir_model m ON (m.id = a.model_id) "
497                    " WHERE m.model = %s AND a.group_id = %s", (model_name, group_id)
498                    )
499             r = cr.fetchone()
500             if r is None:
501                 cr.execute("SELECT perm_" + mode + " "
502                        "  FROM ir_model_access a "
503                        "  JOIN ir_model m ON (m.id = a.model_id) "
504                        " WHERE m.model = %s AND a.group_id IS NULL", (model_name, )
505                        )
506                 r = cr.fetchone()
507
508             access = bool(r and r[0])
509             if access:
510                 return True
511         # pass no groups -> no access
512         return False
513
514     def group_names_with_access(self, cr, model_name, access_mode):
515         """Returns the names of visible groups which have been granted ``access_mode`` on
516            the model ``model_name``.
517            :rtype: list
518         """
519         assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
520         cr.execute('''SELECT
521                         c.name, g.name
522                       FROM
523                         ir_model_access a
524                         JOIN ir_model m ON (a.model_id=m.id)
525                         JOIN res_groups g ON (a.group_id=g.id)
526                         LEFT JOIN ir_module_category c ON (c.id=g.category_id)
527                       WHERE
528                         m.model=%s AND
529                         a.perm_''' + access_mode, (model_name,))
530         return [('%s/%s' % x) if x[0] else x[1] for x in cr.fetchall()]
531
532     @tools.ormcache()
533     def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
534         if uid==1:
535             # User root have all accesses
536             # TODO: exclude xml-rpc requests
537             return True
538
539         assert mode in ['read','write','create','unlink'], 'Invalid access mode'
540
541         if isinstance(model, browse_record):
542             assert model._table_name == 'ir.model', 'Invalid model object'
543             model_name = model.model
544         else:
545             model_name = model
546
547         # TransientModel records have no access rights, only an implicit access rule
548         if self.pool.get(model_name).is_transient():
549             return True
550
551         # We check if a specific rule exists
552         cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
553                    '  FROM ir_model_access a '
554                    '  JOIN ir_model m ON (m.id = a.model_id) '
555                    '  JOIN res_groups_users_rel gu ON (gu.gid = a.group_id) '
556                    ' WHERE m.model = %s '
557                    '   AND gu.uid = %s '
558                    , (model_name, uid,)
559                    )
560         r = cr.fetchone()[0]
561
562         if r is None:
563             # there is no specific rule. We check the generic rule
564             cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
565                        '  FROM ir_model_access a '
566                        '  JOIN ir_model m ON (m.id = a.model_id) '
567                        ' WHERE a.group_id IS NULL '
568                        '   AND m.model = %s '
569                        , (model_name,)
570                        )
571             r = cr.fetchone()[0]
572
573         if not r and raise_exception:
574             groups = '\n\t'.join('- %s' % g for g in self.group_names_with_access(cr, model_name, mode))
575             msg_heads = {
576                 # Messages are declared in extenso so they are properly exported in translation terms
577                 'read': _("Sorry, you are not allowed to access this document."),
578                 'write':  _("Sorry, you are not allowed to modify this document."),
579                 'create': _("Sorry, you are not allowed to create this kind of document."),
580                 'unlink': _("Sorry, you are not allowed to delete this document."),
581             }
582             if groups:
583                 msg_tail = _("Only users with the following access level are currently allowed to do that") + ":\n%s\n\n(" + _("Document model") + ": %s)"
584                 msg_params = (groups, model_name)
585             else:
586                 msg_tail = _("Please contact your system administrator if you think this is an error.") + "\n\n(" + _("Document model") + ": %s)"
587                 msg_params = (model_name,)
588             _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
589             msg = '%s %s' % (msg_heads[mode], msg_tail)
590             raise except_orm(_('Access Denied'), msg % msg_params)
591         return r or False
592
593     __cache_clearing_methods = []
594
595     def register_cache_clearing_method(self, model, method):
596         self.__cache_clearing_methods.append((model, method))
597
598     def unregister_cache_clearing_method(self, model, method):
599         try:
600             i = self.__cache_clearing_methods.index((model, method))
601             del self.__cache_clearing_methods[i]
602         except ValueError:
603             pass
604
605     def call_cache_clearing_methods(self, cr):
606         self.check.clear_cache(self)    # clear the cache of check function
607         for model, method in self.__cache_clearing_methods:
608             object_ = self.pool.get(model)
609             if object_:
610                 getattr(object_, method)()
611
612     #
613     # Check rights on actions
614     #
615     def write(self, cr, uid, *args, **argv):
616         self.call_cache_clearing_methods(cr)
617         res = super(ir_model_access, self).write(cr, uid, *args, **argv)
618         return res
619
620     def create(self, cr, uid, *args, **argv):
621         self.call_cache_clearing_methods(cr)
622         res = super(ir_model_access, self).create(cr, uid, *args, **argv)
623         return res
624
625     def unlink(self, cr, uid, *args, **argv):
626         self.call_cache_clearing_methods(cr)
627         res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)
628         return res
629
630 ir_model_access()
631
632 class ir_model_data(osv.osv):
633     """Holds external identifier keys for records in the database.
634        This has two main uses:
635
636            * allows easy data integration with third-party systems,
637              making import/export/sync of data possible, as records
638              can be uniquely identified across multiple systems
639            * allows tracking the origin of data installed by OpenERP
640              modules themselves, thus making it possible to later
641              update them seamlessly.
642     """
643     _name = 'ir.model.data'
644     _order = 'module,model,name'
645     _columns = {
646         'name': fields.char('External Identifier', required=True, size=128, select=1,
647                             help="External Key/Identifier that can be used for "
648                                  "data integration with third-party systems"),
649         'model': fields.char('Model Name', required=True, size=64, select=1),
650         'module': fields.char('Module', required=True, size=64, select=1),
651         'res_id': fields.integer('Record ID', select=1,
652                                  help="ID of the target record in the database"),
653         'noupdate': fields.boolean('Non Updatable'),
654         'date_update': fields.datetime('Update Date'),
655         'date_init': fields.datetime('Init Date')
656     }
657     _defaults = {
658         'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
659         'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
660         'noupdate': False,
661         'module': ''
662     }
663     _sql_constraints = [
664         ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same external ID in the same module!'),
665     ]
666
667     def __init__(self, pool, cr):
668         osv.osv.__init__(self, pool, cr)
669         self.doinit = True
670         # also stored in pool to avoid being discarded along with this osv instance
671         if getattr(pool, 'model_data_reference_ids', None) is None:
672             self.pool.model_data_reference_ids = {}
673
674         self.loads = self.pool.model_data_reference_ids
675
676     def _auto_init(self, cr, context=None):
677         super(ir_model_data, self)._auto_init(cr, context)
678         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
679         if not cr.fetchone():
680             cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
681
682     @tools.ormcache()
683     def _get_id(self, cr, uid, module, xml_id):
684         """Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
685         ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
686         if not ids:
687             raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
688         # the sql constraints ensure us we have only one result
689         return ids[0]
690
691     @tools.ormcache()
692     def get_object_reference(self, cr, uid, module, xml_id):
693         """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
694         data_id = self._get_id(cr, uid, module, xml_id)
695         res = self.read(cr, uid, data_id, ['model', 'res_id'])
696         if not res['res_id']:
697             raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
698         return (res['model'], res['res_id'])
699
700     def get_object(self, cr, uid, module, xml_id, context=None):
701         """Returns a browsable record for the given module name and xml_id or raise ValueError if not found"""
702         res_model, res_id = self.get_object_reference(cr, uid, module, xml_id)
703         result = self.pool.get(res_model).browse(cr, uid, res_id, context=context)
704         if not result.exists():
705             raise ValueError('No record found for unique ID %s.%s. It may have been deleted.' % (module, xml_id))
706         return result
707
708     def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
709         if not xml_id:
710             return False
711         try:
712             id = self.read(cr, uid, [self._get_id(cr, uid, module, xml_id)], ['res_id'])[0]['res_id']
713             self.loads[(module,xml_id)] = (model,id)
714         except:
715             id = False
716         return id
717
718
719     def unlink(self, cr, uid, ids, context=None):
720         """ Regular unlink method, but make sure to clear the caches. """
721         self._get_id.clear_cache(self)
722         self.get_object_reference.clear_cache(self)
723         return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
724
725     def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
726         model_obj = self.pool.get(model)
727         if not context:
728             context = {}
729         # records created during module install should not display the messages of OpenChatter
730         context = dict(context, install_mode=True)
731         if xml_id and ('.' in xml_id):
732             assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id)
733             module, xml_id = xml_id.split('.')
734         if (not xml_id) and (not self.doinit):
735             return False
736         action_id = False
737         if xml_id:
738             cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
739                           FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
740                           WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
741                           (module, xml_id))
742             results = cr.fetchall()
743             for imd_id2,res_id2,real_id2,real_model in results:
744                 if not real_id2:
745                     self._get_id.clear_cache(self, uid, module, xml_id)
746                     self.get_object_reference.clear_cache(self, uid, module, xml_id)
747                     cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
748                     res_id = False
749                 else:
750                     assert model == real_model, "External ID conflict, %s already refers to a `%s` record,"\
751                         " you can't define a `%s` record with this ID." % (xml_id, real_model, model)
752                     res_id,action_id = res_id2,imd_id2
753
754         if action_id and res_id:
755             model_obj.write(cr, uid, [res_id], values, context=context)
756             self.write(cr, uid, [action_id], {
757                 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
758                 },context=context)
759         elif res_id:
760             model_obj.write(cr, uid, [res_id], values, context=context)
761             if xml_id:
762                 self.create(cr, uid, {
763                     'name': xml_id,
764                     'model': model,
765                     'module':module,
766                     'res_id':res_id,
767                     'noupdate': noupdate,
768                     },context=context)
769                 if model_obj._inherits:
770                     for table in model_obj._inherits:
771                         inherit_id = model_obj.browse(cr, uid,
772                                 res_id,context=context)[model_obj._inherits[table]]
773                         self.create(cr, uid, {
774                             'name': xml_id + '_' + table.replace('.', '_'),
775                             'model': table,
776                             'module': module,
777                             'res_id': inherit_id.id,
778                             'noupdate': noupdate,
779                             },context=context)
780         else:
781             if mode=='init' or (mode=='update' and xml_id):
782                 res_id = model_obj.create(cr, uid, values, context=context)
783                 if xml_id:
784                     self.create(cr, uid, {
785                         'name': xml_id,
786                         'model': model,
787                         'module': module,
788                         'res_id': res_id,
789                         'noupdate': noupdate
790                         },context=context)
791                     if model_obj._inherits:
792                         for table in model_obj._inherits:
793                             inherit_id = model_obj.browse(cr, uid,
794                                     res_id,context=context)[model_obj._inherits[table]]
795                             self.create(cr, uid, {
796                                 'name': xml_id + '_' + table.replace('.', '_'),
797                                 'model': table,
798                                 'module': module,
799                                 'res_id': inherit_id.id,
800                                 'noupdate': noupdate,
801                                 },context=context)
802         if xml_id:
803             if res_id:
804                 self.loads[(module, xml_id)] = (model, res_id)
805                 if model_obj._inherits:
806                     for table in model_obj._inherits:
807                         inherit_field = model_obj._inherits[table]
808                         inherit_id = model_obj.read(cr, uid, res_id,
809                                 [inherit_field])[inherit_field]
810                         self.loads[(module, xml_id + '_' + \
811                                 table.replace('.', '_'))] = (table, inherit_id)
812         return res_id
813
814     def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
815         if type(models[0])==type([]) or type(models[0])==type(()):
816             model,res_id = models[0]
817         else:
818             res_id=None
819             model = models[0]
820
821         if res_id:
822             where = ' and res_id=%s' % (res_id,)
823         else:
824             where = ' and (res_id is null)'
825
826         if key2:
827             where += ' and key2=\'%s\'' % (key2,)
828         else:
829             where += ' and (key2 is null)'
830
831         cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
832         res = cr.fetchone()
833         if not res:
834             ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
835             res = ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
836         elif xml_id:
837             cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
838         return True
839
840     def _module_data_uninstall(self, cr, uid, ids, context=None):
841         """Deletes all the records referenced by the ir.model.data entries
842         ``ids`` along with their corresponding database backed (including
843         dropping tables, columns, FKs, etc, as long as there is no other
844         ir.model.data entry holding a reference to them (which indicates that
845         they are still owned by another module). 
846         Attempts to perform the deletion in an appropriate order to maximize
847         the chance of gracefully deleting all records.
848         This step is performed as part of the full uninstallation of a module.
849         """ 
850
851         if uid != 1 and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"):
852             raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module')))
853
854         context = dict(context or {})
855         context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion
856
857         ids_set = set(ids)
858         wkf_todo = []
859         to_unlink = []
860         to_drop_table = []
861         ids.sort()
862         ids.reverse()
863         for data in self.browse(cr, uid, ids, context):
864             model = data.model
865             res_id = data.res_id
866             model_obj = self.pool.get(model)
867             name = tools.ustr(data.name)
868
869             if name.startswith(EXT_ID_PREFIX_FK) or name.startswith(EXT_ID_PREFIX_M2M_TABLE)\
870                  or name.startswith(EXT_ID_PREFIX_CONSTRAINT):
871                 # double-check we are really going to delete all the owners of this schema element
872                 cr.execute("""SELECT id from ir_model_data where name = %s and res_id IS NULL""", (data.name,))
873                 external_ids = [x[0] for x in cr.fetchall()]
874                 if (set(external_ids)-ids_set):
875                     # as installed modules have defined this element we must not delete it!
876                     continue
877
878             if name.startswith(EXT_ID_PREFIX_FK):
879                 name = name[len(EXT_ID_PREFIX_FK):]
880                 # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
881                 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
882                               WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table))
883                 if cr.fetchone():
884                     cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
885                     _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
886                 continue
887
888             if name.startswith(EXT_ID_PREFIX_M2M_TABLE):
889                 name = name[len(EXT_ID_PREFIX_M2M_TABLE):]
890                 cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,))
891                 if cr.fetchone() and not name in to_drop_table:
892                     to_drop_table.append(name)
893                 continue
894
895             if name.startswith(EXT_ID_PREFIX_CONSTRAINT):
896                 name = name[len(EXT_ID_PREFIX_CONSTRAINT):]
897                 # test if constraint exists
898                 cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
899                               WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table))
900                 if cr.fetchone():
901                     cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),)
902                     _logger.info('Dropped CONSTRAINT %s@%s', name, model)
903                 continue
904
905             pair_to_unlink = (model, res_id)
906             if pair_to_unlink not in to_unlink:
907                 to_unlink.append(pair_to_unlink)
908
909             if model == 'workflow.activity':
910                 # Special treatment for workflow activities: temporarily revert their
911                 # incoming transition and trigger an update to force all workflow items
912                 # to move out before deleting them
913                 cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,))
914                 wkf_todo.extend(cr.fetchall())
915                 cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
916
917         wf_service = netsvc.LocalService("workflow")
918         for model,res_id in wkf_todo:
919             try:
920                 wf_service.trg_write(uid, model, res_id, cr)
921             except:
922                 _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model)
923
924         # drop m2m relation tables
925         for table in to_drop_table:
926             cr.execute('DROP TABLE %s CASCADE'% (table),)
927             _logger.info('Dropped table %s', table)
928
929         def unlink_if_refcount(to_unlink):
930             for model, res_id in to_unlink:
931                 external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)])
932                 if (set(external_ids)-ids_set):
933                     # if other modules have defined this record, we must not delete it
934                     return
935                 _logger.info('Deleting %s@%s', res_id, model)
936                 try:
937                     self.pool.get(model).unlink(cr, uid, [res_id], context=context)
938                 except:
939                     _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
940
941         # Remove non-model records first, then model fields, and finish with models
942         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
943                                 if model not in ('ir.model','ir.model.fields'))
944         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
945                                 if model == 'ir.model.fields')
946         unlink_if_refcount((model, res_id) for model, res_id in to_unlink
947                                 if model == 'ir.model')
948
949         cr.commit()
950
951     def _process_end(self, cr, uid, modules):
952         """ Clear records removed from updated module data.
953         This method is called at the end of the module loading process.
954         It is meant to removed records that are no longer present in the
955         updated data. Such records are recognised as the one with an xml id
956         and a module in ir_model_data and noupdate set to false, but not
957         present in self.loads.
958         """
959         if not modules:
960             return True
961         to_unlink = []
962         cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data
963                       WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s""",
964                       (tuple(modules), False))
965         for (id, name, model, res_id, module) in cr.fetchall():
966             if (module,name) not in self.loads:
967                 to_unlink.append((model,res_id))
968         if not config.get('import_partial'):
969             for (model, res_id) in to_unlink:
970                 if self.pool.get(model):
971                     _logger.info('Deleting %s@%s', res_id, model)
972                     self.pool.get(model).unlink(cr, uid, [res_id])
973
974
975 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: