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