diff --git a/openerp/__init__.py b/openerp/__init__.py index 9026c7be90f..7e723c41f9d 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -22,6 +22,8 @@ """ OpenERP core library. """ +# The hard-coded super-user id (a.k.a. administrator, or root user). +SUPERUSER_ID = 1 import addons import conf diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 2e273f53b69..395bf3e8d60 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -63,7 +63,7 @@ class ir_model(osv.osv): models = self.browse(cr, uid, ids, context=context) res = dict.fromkeys(ids) for model in models: - res[model.id] = isinstance(self.pool.get(model.model), osv.osv_memory) + res[model.id] = self.pool.get(model.model).is_transient() return res def _search_osv_memory(self, cr, uid, model, name, domain, context=None): @@ -165,7 +165,7 @@ class ir_model(osv.osv): pass x_custom_model._name = model x_custom_model._module = False - a = x_custom_model.createInstance(self.pool, cr) + a = x_custom_model.create_instance(self.pool, cr) if (not a._columns) or ('x_name' in a._columns.keys()): x_name = 'x_name' else: @@ -481,14 +481,12 @@ class ir_model_access(osv.osv): if isinstance(model, browse_record): assert model._table_name == 'ir.model', 'Invalid model object' - model_name = model.name + model_name = model.model else: model_name = model - # osv_memory objects can be read by everyone, as they only return - # results that belong to the current user (except for superuser) - model_obj = self.pool.get(model_name) - if isinstance(model_obj, osv.osv_memory): + # TransientModel records have no access rights, only an implicit access rule + if self.pool.get(model_name).is_transient(): return True # We check if a specific rule exists @@ -523,7 +521,7 @@ class ir_model_access(osv.osv): } raise except_orm(_('AccessError'), msgs[mode] % (model_name, groups) ) - return r + return r or False __cache_clearing_methods = [] diff --git a/openerp/addons/base/ir/ir_rule.py b/openerp/addons/base/ir/ir_rule.py index 734d00eb9a2..33f926e55c2 100644 --- a/openerp/addons/base/ir/ir_rule.py +++ b/openerp/addons/base/ir/ir_rule.py @@ -26,8 +26,7 @@ from functools import partial import tools from tools.safe_eval import safe_eval as eval from tools.misc import unquote as unquote - -SUPERUSER_UID = 1 +from openerp import SUPERUSER_ID class ir_rule(osv.osv): _name = 'ir.rule' @@ -68,7 +67,7 @@ class ir_rule(osv.osv): return res def _check_model_obj(self, cr, uid, ids, context=None): - return not any(isinstance(self.pool.get(rule.model_id.model), osv.osv_memory) for rule in self.browse(cr, uid, ids, context)) + return not any(self.pool.get(rule.model_id.model).is_transient() for rule in self.browse(cr, uid, ids, context)) _columns = { 'name': fields.char('Name', size=128, select=1), @@ -104,7 +103,7 @@ class ir_rule(osv.osv): if mode not in self._MODES: raise ValueError('Invalid mode: %r' % (mode,)) - if uid == SUPERUSER_UID: + if uid == SUPERUSER_ID: return None cr.execute("""SELECT r.id FROM ir_rule r @@ -117,10 +116,10 @@ class ir_rule(osv.osv): rule_ids = [x[0] for x in cr.fetchall()] if rule_ids: # browse user as super-admin root to avoid access errors! - user = self.pool.get('res.users').browse(cr, SUPERUSER_UID, uid) + user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid) global_domains = [] # list of domains group_domains = {} # map: group -> list of domains - for rule in self.browse(cr, SUPERUSER_UID, rule_ids): + for rule in self.browse(cr, SUPERUSER_ID, rule_ids): # read 'domain' as UID to have the correct eval context for the rule. rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain'] dom = expression.normalize(rule_domain) diff --git a/openerp/addons/base/ir/osv_memory_autovacuum.py b/openerp/addons/base/ir/osv_memory_autovacuum.py index b2ab938d740..975fcd0a403 100644 --- a/openerp/addons/base/ir/osv_memory_autovacuum.py +++ b/openerp/addons/base/ir/osv_memory_autovacuum.py @@ -19,18 +19,15 @@ # ############################################################################## -from osv import osv -from osv.orm import orm_memory +import openerp -class osv_memory_autovacuum(osv.osv_memory): +class osv_memory_autovacuum(openerp.osv.osv.osv_memory): + """ Expose the osv_memory.vacuum() method to the cron jobs mechanism. """ _name = 'osv_memory.autovacuum' def power_on(self, cr, uid, context=None): - for model in self.pool.obj_list(): - obj = self.pool.get(model) - if isinstance(obj, orm_memory): - obj.vaccum(cr, uid) + for model in self.pool.models.values(): + if model.is_transient(): + model._transient_vacuum(cr, uid) return True -osv_memory_autovacuum() - diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index bb6a51a6bcb..01d8b745e6f 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -91,7 +91,7 @@ class res_currency(osv.osv): (name, (COALESCE(company_id,-1)))""") def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'): - res = super(osv.osv, self).read(cr, user, ids, fields, context, load) + res = super(res_currency, self).read(cr, user, ids, fields, context, load) currency_rate_obj = self.pool.get('res.currency.rate') for r in res: if r.__contains__('rate_ids'): diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 5a17ca108c3..6ea578a8659 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -144,45 +144,3 @@ !python {model: res.partner.category}: | self.pool._init = True -- - "OSV Memory: Verify that osv_memory properly handles large data allocation" -- - 1. No "count-based" auto-vaccuum when max_count is disabled -- - !python {model: base.language.export}: | - # setup special limits for the test, these will be reset at next pool reload anyway - self._max_count = None - num_recs = 250 - for i in xrange(num_recs): - self.create(cr, uid, {'format':'po'}) - assert (len(self.datas) >= num_recs), "OSV Memory must not auto-vaccum records from the current transaction if max_count is not set" -- - 2. Auto-vaccuum should be enabled when max_count is set -- - !python {model: base.language.export}: | - # setup special limits for the test, these will be reset at next pool reload anyway - self._max_count = 100 - num_recs = 219 - for i in xrange(num_recs): - self.create(cr, uid, {'name': i, 'format':'po'}) - assert (self._max_count <= len(self.datas) < self._max_count + self._check_time), "OSV Memory must auto-expire records when max_count is reached" - for k,v in self.datas.iteritems(): - assert (int(v['name']) >= (num_recs - (self._max_count + self._check_time))), "OSV Memory must auto-expire records based on age" -- - 3. Auto-vaccuum should be based on age only when max_count is not set -- - !python {model: base.language.export}: | - # setup special limits for the test, these will be reset at next pool reload anyway - self._max_count = None - self._max_hours = 0.01 #36 seconds - num_recs = 200 - for i in xrange(num_recs): - self.create(cr, uid, {'format':'po'}) - assert (len(self.datas) >= num_recs), "OSV Memory must not auto-expire records from the current transaction" - - # expire all records - for k,v in self.datas.iteritems(): - v['internal.date_access'] = 0 - self.vaccum(cr, 1, force=True) - - assert (len(self.datas) == 0), "OSV Memory must expire old records after vaccuum" diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 40986580d1b..b06368ba1b2 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -339,16 +339,16 @@ def load_modules(db, force_demo=False, status=None, update_module=False): cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") for (model, name) in cr.fetchall(): model_obj = pool.get(model) - if model_obj and not isinstance(model_obj, osv.osv.osv_memory): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name)) + if model_obj and not model_obj.is_transient(): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'Model %s (%s) has no access rules!' % (model, name)) # Temporary warning while we remove access rights on osv_memory objects, as they have # been replaced by owner-only access rights cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""") for (model, name) in cr.fetchall(): model_obj = pool.get(model) - if isinstance(model_obj, osv.osv.osv_memory) and not isinstance(model_obj, osv.osv.osv): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name)) + if model_obj and model_obj.is_transient(): + logger.notifyChannel('init', netsvc.LOG_WARNING, 'The transient model %s (%s) should not have explicit access rules!' % (model, name)) cr.execute("SELECT model from ir_model") for (model,) in cr.fetchall(): @@ -356,7 +356,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): if obj: obj._check_removed_columns(cr, log=True) else: - logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model) + logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)" % model) # Cleanup orphan records pool.get('ir.model.data')._process_end(cr, 1, processed_modules) diff --git a/openerp/modules/module.py b/openerp/modules/module.py index a33c2651a85..b40e186e9a1 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -277,7 +277,6 @@ def init_module_models(cr, module_name, obj_list): TODO better explanation of _auto_init and init. """ - logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name) todo = [] diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index f35adadbf2f..ea6d863e442 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -89,10 +89,10 @@ class Registry(object): res = [] - # Instantiate registered classes (via metamodel discovery or via explicit - # constructor call), and add them to the pool. + # Instantiate registered classes (via the MetaModel automatic discovery + # or via explicit constructor call), and add them to the pool. for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []): - res.append(cls.createInstance(self, cr)) + res.append(cls.create_instance(self, cr)) return res diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index ff2528db73d..cb8cfea583f 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -450,7 +450,7 @@ class expression(object): # field could not be found in model columns, it's probably invalid, unless # it's one of the _log_access special fields # TODO: make these fields explicitly available in self.columns instead! - if (field_path[0] not in MAGIC_COLUMNS) and (left not in MAGIC_COLUMNS): + if field_path[0] not in MAGIC_COLUMNS: raise ValueError("Invalid field %r in domain expression %r" % (left, exp)) continue diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index ccc1d740403..6619e66b075 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -72,7 +72,7 @@ class _column(object): _symbol_set = (_symbol_c, _symbol_f) _symbol_get = None - def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete="set null", translate=False, select=False, manual=False, **args): + def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args): """ The 'manual' keyword argument specifies if the field is a custom one. @@ -91,7 +91,7 @@ class _column(object): self.help = args.get('help', '') self.priority = priority self.change_default = change_default - self.ondelete = ondelete + self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM self.translate = translate self._domain = domain self._context = context @@ -112,12 +112,6 @@ class _column(object): def set(self, cr, obj, id, name, value, user=None, context=None): cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id)) - def set_memory(self, cr, obj, id, name, value, user=None, context=None): - raise Exception(_('Not implemented set_memory method !')) - - def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None): - raise Exception(_('Not implemented get_memory method !')) - def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): raise Exception(_('undefined get method !')) @@ -126,9 +120,6 @@ class _column(object): res = obj.read(cr, uid, ids, [name], context=context) return [x[name] for x in res] - def search_memory(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None): - raise Exception(_('Not implemented search_memory method !')) - # --------------------------------------------------------- # Simple fields @@ -269,7 +260,7 @@ class binary(_column): _column.__init__(self, string=string, **args) self.filters = filters - def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None): + def get(self, cr, obj, ids, name, user=None, context=None, values=None): if not context: context = {} if not values: @@ -293,9 +284,6 @@ class binary(_column): res[i] = val return res - get = get_memory - - class selection(_column): _type = 'selection' @@ -355,30 +343,6 @@ class many2one(_column): _column.__init__(self, string=string, **args) self._obj = obj - def set_memory(self, cr, obj, id, field, values, user=None, context=None): - obj.datas.setdefault(id, {}) - obj.datas[id][field] = values - - def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None): - result = {} - for id in ids: - result[id] = obj.datas[id].get(name, False) - - # build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource} - # we use uid=1 because the visibility of a many2one field value (just id and name) - # must be the access right of the parent form and not the linked object itself. - obj = obj.pool.get(self._obj) - records = dict(obj.name_get(cr, 1, - list(set([x for x in result.values() if x and isinstance(x, (int,long))])), - context=context)) - for id in ids: - if result[id] in records: - result[id] = (result[id], records[result[id]]) - else: - result[id] = False - - return result - def get(self, cr, obj, ids, name, user=None, context=None, values=None): if context is None: context = {} @@ -447,55 +411,6 @@ class one2many(_column): #one2many can't be used as condition for defaults assert(self.change_default != True) - def get_memory(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - if context is None: - context = {} - if self._context: - context = context.copy() - context.update(self._context) - if not values: - values = {} - res = {} - for id in ids: - res[id] = [] - ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id, 'in', ids)], limit=self._limit, context=context) - for r in obj.pool.get(self._obj).read(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'): - if r[self._fields_id] in res: - res[r[self._fields_id]].append(r['id']) - return res - - def set_memory(self, cr, obj, id, field, values, user=None, context=None): - if not context: - context = {} - if self._context: - context = context.copy() - context.update(self._context) - if not values: - return - obj = obj.pool.get(self._obj) - for act in values: - if act[0] == 0: - act[2][self._fields_id] = id - obj.create(cr, user, act[2], context=context) - elif act[0] == 1: - obj.write(cr, user, [act[1]], act[2], context=context) - elif act[0] == 2: - obj.unlink(cr, user, [act[1]], context=context) - elif act[0] == 3: - obj.datas[act[1]][self._fields_id] = False - elif act[0] == 4: - obj.datas[act[1]][self._fields_id] = id - elif act[0] == 5: - for o in obj.datas.values(): - if o[self._fields_id] == id: - o[self._fields_id] = False - elif act[0] == 6: - for id2 in (act[2] or []): - obj.datas[id2][self._fields_id] = id - - def search_memory(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None): - raise _('Not Implemented') - def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): if context is None: context = {} @@ -578,14 +493,34 @@ class one2many(_column): # (6, ?, ids) set a list of links # class many2many(_column): + """Encapsulates the logic of a many-to-many bidirectional relationship, handling the + low-level details of the intermediary relationship table transparently. + + :param str obj: destination model + :param str rel: optional name of the intermediary relationship table. If not specified, + a canonical name will be derived based on the alphabetically-ordered + model names of the source and destination (in the form: ``amodel_bmodel_rel``). + Automatic naming is not possible when the source and destination are + the same, for obvious ambiguity reasons. + :param str id1: optional name for the column holding the foreign key to the current + model in the relationship table. If not specified, a canonical name + will be derived based on the model name (in the form: `src_model_id`). + :param str id2: optional name for the column holding the foreign key to the destination + model in the relationship table. If not specified, a canonical name + will be derived based on the model name (in the form: `dest_model_id`) + :param str string: field label + """ _classic_read = False _classic_write = False _prefetch = False _type = 'many2many' - def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, **args): + + def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args): + """ + """ _column.__init__(self, string=string, **args) self._obj = obj - if '.' in rel: + if rel and '.' in rel: raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\ 'You used %s, which is not a valid SQL table name.')% (string,rel)) self._rel = rel @@ -593,7 +528,30 @@ class many2many(_column): self._id2 = id2 self._limit = limit - def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): + def _sql_names(self, source_model): + """Return the SQL names defining the structure of the m2m relationship table + + :return: (m2m_table, local_col, dest_col) where m2m_table is the table name, + local_col is the name of the column holding the current model's FK, and + dest_col is the name of the column holding the destination model's FK, and + """ + tbl, col1, col2 = self._rel, self._id1, self._id2 + if not all((tbl, col1, col2)): + # the default table name is based on the stable alphabetical order of tables + dest_model = source_model.pool.get(self._obj) + tables = tuple(sorted([source_model._table, dest_model._table])) + if not tbl: + assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\ + 'is not possible when source and destination models are '\ + 'the same' + tbl = '%s_%s_rel' % tables + if not col1: + col1 = '%s_id' % source_model._table + if not col2: + col2 = '%s_id' % dest_model._table + return (tbl, col1, col2) + + def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None): if not context: context = {} if not values: @@ -606,7 +564,8 @@ class many2many(_column): if offset: warnings.warn("Specifying offset at a many2many.get() may produce unpredictable results.", DeprecationWarning, stacklevel=2) - obj = obj.pool.get(self._obj) + obj = model.pool.get(self._obj) + rel, id1, id2 = self._sql_names(model) # static domains are lists, and are evaluated both here and on client-side, while string # domains supposed by dynamic and evaluated on client-side only (thus ignored here) @@ -636,11 +595,11 @@ class many2many(_column): %(order_by)s \ %(limit)s \ OFFSET %(offset)d' \ - % {'rel': self._rel, + % {'rel': rel, 'from_c': from_c, 'tbl': obj._table, - 'id1': self._id1, - 'id2': self._id2, + 'id1': id1, + 'id2': id2, 'where_c': where_c, 'limit': limit_str, 'order_by': order_by, @@ -651,31 +610,32 @@ class many2many(_column): res[r[1]].append(r[0]) return res - def set(self, cr, obj, id, name, values, user=None, context=None): + def set(self, cr, model, id, name, values, user=None, context=None): if not context: context = {} if not values: return - obj = obj.pool.get(self._obj) + rel, id1, id2 = self._sql_names(model) + obj = model.pool.get(self._obj) for act in values: if not (isinstance(act, list) or isinstance(act, tuple)) or not act: continue if act[0] == 0: idnew = obj.create(cr, user, act[2], context=context) - cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, idnew)) + cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew)) elif act[0] == 1: obj.write(cr, user, [act[1]], act[2], context=context) elif act[0] == 2: obj.unlink(cr, user, [act[1]], context=context) elif act[0] == 3: - cr.execute('delete from '+self._rel+' where ' + self._id1 + '=%s and '+ self._id2 + '=%s', (id, act[1])) + cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1])) elif act[0] == 4: # following queries are in the same transaction - so should be relatively safe - cr.execute('SELECT 1 FROM '+self._rel+' WHERE '+self._id1+' = %s and '+self._id2+' = %s', (id, act[1])) + cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1])) if not cr.fetchone(): - cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, act[1])) + cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1])) elif act[0] == 5: - cr.execute('delete from '+self._rel+' where ' + self._id1 + ' = %s', (id,)) + cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,)) elif act[0] == 6: d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context) @@ -683,10 +643,10 @@ class many2many(_column): d1 = ' and ' + ' and '.join(d1) else: d1 = '' - cr.execute('delete from '+self._rel+' where '+self._id1+'=%s AND '+self._id2+' IN (SELECT '+self._rel+'.'+self._id2+' FROM '+self._rel+', '+','.join(tables)+' WHERE '+self._rel+'.'+self._id1+'=%s AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2) + cr.execute('delete from '+rel+' where '+id1+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2) for act_nbr in act[2]: - cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s, %s)', (id, act_nbr)) + cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr)) # # TODO: use a name_search @@ -694,32 +654,6 @@ class many2many(_column): def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None): return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context) - def get_memory(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - result = {} - for id in ids: - result[id] = obj.datas[id].get(name, []) - return result - - def set_memory(self, cr, obj, id, name, values, user=None, context=None): - if not values: - return - for act in values: - # TODO: use constants instead of these magic numbers - if act[0] == 0: - raise _('Not Implemented') - elif act[0] == 1: - raise _('Not Implemented') - elif act[0] == 2: - raise _('Not Implemented') - elif act[0] == 3: - raise _('Not Implemented') - elif act[0] == 4: - raise _('Not Implemented') - elif act[0] == 5: - raise _('Not Implemented') - elif act[0] == 6: - obj.datas[id][name] = act[2] - def get_nice_size(value): size = 0 @@ -801,8 +735,8 @@ class function(_column): Implements the function field. - :param orm_template model: model to which the field belongs (should be ``self`` for - a model method) + :param orm model: model to which the field belongs (should be ``self`` for + a model method) :param field_name(s): name of the field to compute, or if ``multi`` is provided, list of field names to compute. :type field_name(s): str | [str] @@ -865,8 +799,8 @@ class function(_column): Callable that implements the ``write`` operation for the function field. - :param orm_template model: model to which the field belongs (should be ``self`` for - a model method) + :param orm model: model to which the field belongs (should be ``self`` for + a model method) :param int id: the identifier of the object to write on :param str field_name: name of the field to set :param fnct_inv_arg: arbitrary value passed when declaring the function field @@ -887,10 +821,10 @@ class function(_column): a search criterion based on the function field into a new domain based only on columns that are stored in the database. - :param orm_template model: model to which the field belongs (should be ``self`` for - a model method) - :param orm_template model_again: same value as ``model`` (seriously! this is for backwards - compatibility) + :param orm model: model to which the field belongs (should be ``self`` for + a model method) + :param orm model_again: same value as ``model`` (seriously! this is for backwards + compatibility) :param str field_name: name of the field to search on :param list criterion: domain component specifying the search criterion on the field. :rtype: list @@ -935,7 +869,7 @@ class function(_column): corresponding records in the source model (whose field values need to be recomputed). - :param orm_template model: trigger_model + :param orm model: trigger_model :param list trigger_ids: ids of the records of trigger_model that were modified :rtype: list @@ -1064,14 +998,11 @@ class function(_column): result[id] = self.postprocess(cr, uid, obj, name, result[id], context) return result - get_memory = get - def set(self, cr, obj, id, name, value, user=None, context=None): if not context: context = {} if self._fnct_inv: self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context) - set_memory = set # --------------------------------------------------------- # Related fields diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index ece5102e3f6..ecdb3a86186 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -45,24 +45,25 @@ import calendar import copy import datetime import logging -import warnings import operator import pickle import re +import simplejson import time import traceback import types -import simplejson - -import openerp.netsvc as netsvc +import warnings from lxml import etree -from openerp.tools.config import config -from openerp.tools.translate import _ import fields -from query import Query +import openerp +import openerp.netsvc as netsvc import openerp.tools as tools +from openerp.tools.config import config from openerp.tools.safe_eval import safe_eval as eval +from openerp.tools.translate import _ +from openerp import SUPERUSER_ID +from query import Query # List of etree._Element subclasses that we choose to ignore when parsing XML. from openerp.tools import SKIPPED_ELEMENT_TYPES @@ -70,9 +71,6 @@ from openerp.tools import SKIPPED_ELEMENT_TYPES regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I) regex_object_name = re.compile(r'^[a-z0-9_.]+$') -# Super-user identifier (aka Administrator aka root) -ROOT_USER_ID = 1 - def transfer_field_to_modifiers(field, modifiers): default_values = {} state_exceptions = {} @@ -585,22 +583,36 @@ class MetaModel(type): self.module_to_models.setdefault(self._module, []).append(self) -class orm_template(object): +# Definition of log access columns, automatically added to models if +# self._log_access is True +LOG_ACCESS_COLUMNS = { + 'create_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL', + 'create_date': 'TIMESTAMP', + 'write_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL', + 'write_date': 'TIMESTAMP' +} +# special columns automatically created by the ORM +MAGIC_COLUMNS = ['id'] + LOG_ACCESS_COLUMNS.keys() + +class BaseModel(object): """ Base class for OpenERP models. - OpenERP models are created by inheriting from this class (although - not directly; more specifically by inheriting from osv or - osv_memory). The constructor is called once, usually directly - after the class definition, e.g.: + OpenERP models are created by inheriting from this class' subclasses: - class user(osv): - ... - user() + * Model: for regular database-persisted models + * TransientModel: for temporary data, stored in the database but automatically + vaccuumed every so often + * AbstractModel: for abstract super classes meant to be shared by multiple + _inheriting classes (usually Models or TransientModels) - The system will later instanciate the class once per database (on + The system will later instantiate the class once per database (on which the class' module is installed). + To create a class that should not be instantiated, the _register class attribute + may be set to False. """ + __metaclass__ = MetaModel + _register = False # Set to false if the model shouldn't be automatically discovered. _name = None _columns = {} _constraints = [] @@ -614,6 +626,12 @@ class orm_template(object): _sequence = None _description = None + # Transience + _transient = False # True in a TransientModel + _transient_max_count = None + _transient_max_hours = None + _transient_check_time = 20 + # structure: # { 'parent_model': 'm2o_field', ... } _inherits = {} @@ -636,6 +654,10 @@ class orm_template(object): _table = None _invalids = set() _log_create = False + _sql_constraints = [] + _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists'] + __logger = logging.getLogger('orm') + __schema = logging.getLogger('orm.schema') CONCURRENCY_CHECK_FIELD = '__last_update' @@ -656,9 +678,6 @@ class orm_template(object): """Override this method to do specific things when a view on the object is opened.""" pass - def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): - raise NotImplementedError(_('The read_group method is not implemented on this object !')) - def _field_create(self, cr, context=None): """ Create entries in ir_model_fields for all the model's fields. @@ -761,19 +780,12 @@ class orm_template(object): break cr.commit() - def _auto_init(self, cr, context=None): - raise_on_invalid_object_name(self._name) - self._field_create(cr, context=context) - - def _auto_end(self, cr, context=None): - pass - # # Goal: try to apply inheritance at the instanciation level and # put objects in the pool var # @classmethod - def makeInstance(cls, pool, cr, attributes): + def create_instance(cls, pool, cr): """ Instanciate a given model. This class method instanciates the class of some model (i.e. a class @@ -788,6 +800,9 @@ class orm_template(object): this method. This is probably unnecessary. """ + attributes = ['_columns', '_defaults', '_inherits', '_constraints', + '_sql_constraints'] + parent_names = getattr(cls, '_inherit', None) if parent_names: if isinstance(parent_names, (str, unicode)): @@ -843,7 +858,10 @@ class orm_template(object): This doesn't create an instance but simply register the model as being part of the module where it is defined. + """ + + # Set the module name (e.g. base, sale, accounting, ...) on the class. module = cls.__module__.split('.')[0] if not hasattr(cls, '_module'): @@ -860,7 +878,15 @@ class orm_template(object): return None def __init__(self, pool, cr): - """ Initialize a model and make it part of the given registry.""" + """ Initialize a model and make it part of the given registry. + + - copy the stored fields' functions in the osv_pool, + - update the _columns with the fields found in ir_model_fields, + - ensure there is a many2one for each _inherits'd parent, + - update the children's _columns, + - give a chance to each field to initialize itself. + + """ pool.add(self._name, self) self.pool = pool @@ -877,26 +903,96 @@ class orm_template(object): if not self._table: self._table = self._name.replace('.', '_') - def browse(self, cr, uid, select, context=None, list_class=None, fields_process=None): - """Fetch records as objects allowing to use dot notation to browse fields and relations + if not hasattr(self, '_log_access'): + # If _log_access is not specified, it is the same value as _auto. + self._log_access = getattr(self, "_auto", True) - :param cr: database cursor - :param user: current user id - :param select: id or list of ids. - :param context: context arguments, like lang, time zone - :rtype: object or list of objects requested + self._columns = self._columns.copy() + for store_field in self._columns: + f = self._columns[store_field] + if hasattr(f, 'digits_change'): + f.digits_change(cr) + if not isinstance(f, fields.function): + continue + if not f.store: + continue + if self._columns[store_field].store is True: + sm = {self._name: (lambda self, cr, uid, ids, c={}: ids, None, 10, None)} + else: + sm = self._columns[store_field].store + for object, aa in sm.items(): + if len(aa) == 4: + (fnct, fields2, order, length) = aa + elif len(aa) == 3: + (fnct, fields2, order) = aa + length = None + else: + raise except_orm('Error', + ('Invalid function definition %s in object %s !\nYou must use the definition: store={object:(fnct, fields, priority, time length)}.' % (store_field, self._name))) + self.pool._store_function.setdefault(object, []) + ok = True + for x, y, z, e, f, l in self.pool._store_function[object]: + if (x==self._name) and (y==store_field) and (e==fields2): + if f == order: + ok = False + if ok: + self.pool._store_function[object].append( (self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length)) + self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4])) - """ - self._list_class = list_class or browse_record_list - cache = {} - # need to accepts ints and longs because ids coming from a method - # launched by button in the interface have a type long... - if isinstance(select, (int, long)): - return browse_record(cr, uid, select, self, cache, context=context, list_class=self._list_class, fields_process=fields_process) - elif isinstance(select, list): - return self._list_class([browse_record(cr, uid, id, self, cache, context=context, list_class=self._list_class, fields_process=fields_process) for id in select], context=context) - else: - return browse_null() + for (key, _, msg) in self._sql_constraints: + self.pool._sql_error[self._table+'_'+key] = msg + + # Load manual fields + + cr.execute("SELECT id FROM ir_model_fields WHERE name=%s AND model=%s", ('state', 'ir.model.fields')) + if cr.fetchone(): + cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (self._name, 'manual')) + for field in cr.dictfetchall(): + if field['name'] in self._columns: + continue + attrs = { + 'string': field['field_description'], + 'required': bool(field['required']), + 'readonly': bool(field['readonly']), + 'domain': eval(field['domain']) if field['domain'] else None, + 'size': field['size'], + 'ondelete': field['on_delete'], + 'translate': (field['translate']), + 'manual': True, + #'select': int(field['select_level']) + } + + if field['ttype'] == 'selection': + self._columns[field['name']] = fields.selection(eval(field['selection']), **attrs) + elif field['ttype'] == 'reference': + self._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs) + elif field['ttype'] == 'many2one': + self._columns[field['name']] = fields.many2one(field['relation'], **attrs) + elif field['ttype'] == 'one2many': + self._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs) + elif field['ttype'] == 'many2many': + _rel1 = field['relation'].replace('.', '_') + _rel2 = field['model'].replace('.', '_') + _rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name']) + self._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs) + else: + self._columns[field['name']] = getattr(fields, field['ttype'])(**attrs) + self._inherits_check() + self._inherits_reload() + if not self._sequence: + self._sequence = self._table + '_id_seq' + for k in self._defaults: + assert (k in self._columns) or (k in self._inherit_fields), 'Default function defined in %s but field %s does not exist !' % (self._name, k,) + for f in self._columns: + self._columns[f].restart() + + # Transience + if self.is_transient(): + self._transient_check_count = 0 + self._transient_max_count = config.get('osv_memory_count_limit') + self._transient_max_hours = config.get('osv_memory_age_limit') + assert self._log_access, "TransientModels must have log_access turned on, "\ + "in order to implement their access rights policy" def __export_row(self, cr, uid, row, fields, context=None): if context is None: @@ -1260,30 +1356,6 @@ class orm_template(object): self._parent_store_compute(cr) return (position, 0, 0, 0) - def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'): - """ - Read records with given ids with the given fields - - :param cr: database cursor - :param user: current user id - :param ids: id or list of the ids of the records to read - :param fields: optional list of field names to return (default: all fields would be returned) - :type fields: list (example ['field_name_1', ...]) - :param context: optional context dictionary - it may contains keys for specifying certain options - like ``context_lang``, ``context_tz`` to alter the results of the call. - A special ``bin_size`` boolean flag may also be passed in the context to request the - value of all fields.binary columns to be returned as the size of the binary instead of its - contents. This can also be selectively overriden by passing a field-specific flag - in the form ``bin_size_XXX: True/False`` where ``XXX`` is the name of the field. - Note: The ``bin_size_XXX`` form is new in OpenERP v6.0. - :return: list of dictionaries((dictionary per record asked)) with requested field values - :rtype: [{‘name_of_the_field’: value, ...}, ...] - :raise AccessError: * if user has no read rights on the requested object - * if user tries to bypass access rules for read on the requested object - - """ - raise NotImplementedError(_('The read method is not implemented on this object !')) - def get_invalid_fields(self, cr, uid): return list(self._invalids) @@ -1416,19 +1488,6 @@ class orm_template(object): defaults[key[8:]] = context[key] return defaults - - def perm_read(self, cr, user, ids, context=None, details=True): - raise NotImplementedError(_('The perm_read method is not implemented on this object !')) - - def unlink(self, cr, uid, ids, context=None): - raise NotImplementedError(_('The unlink method is not implemented on this object !')) - - def write(self, cr, user, ids, vals, context=None): - raise NotImplementedError(_('The write method is not implemented on this object !')) - - def create(self, cr, user, vals, context=None): - raise NotImplementedError(_('The create method is not implemented on this object !')) - def fields_get_keys(self, cr, user, context=None): res = self._columns.keys() # TODO I believe this loop can be replace by @@ -1437,56 +1496,6 @@ class orm_template(object): res.extend(self.pool.get(parent).fields_get_keys(cr, user, context)) return res - - def fields_get(self, cr, user, allfields=None, context=None, write_access=True): - """ Return the definition of each field. - - The returned value is a dictionary (indiced by field name) of - dictionaries. The _inherits'd fields are included. The string, - help, and selection (if present) attributes are translated. - - """ - if context is None: - context = {} - - res = {} - - translation_obj = self.pool.get('ir.translation') - for parent in self._inherits: - res.update(self.pool.get(parent).fields_get(cr, user, allfields, context)) - - for f, field in self._columns.iteritems(): - if allfields and f not in allfields: - continue - - res[f] = fields.field_to_dict(self, cr, user, context, field) - - if not write_access: - res[f]['readonly'] = True - res[f]['states'] = {} - - if 'string' in res[f]: - res_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'field', context.get('lang', False) or 'en_US') - if res_trans: - res[f]['string'] = res_trans - if 'help' in res[f]: - help_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'help', context.get('lang', False) or 'en_US') - if help_trans: - res[f]['help'] = help_trans - if 'selection' in res[f]: - if isinstance(field.selection, (tuple, list)): - sel = field.selection - sel2 = [] - for key, val in sel: - val2 = None - if val: - val2 = translation_obj._get_source(cr, user, self._name + ',' + f, 'selection', context.get('lang', False) or 'en_US', val) - sel2.append((key, val2 or val)) - res[f]['selection'] = sel2 - - return res - - # # Overload this method if you need a window title which depends on the context # @@ -2128,17 +2137,6 @@ class orm_template(object): """ return self._search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count) - def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): - """ - Private implementation of search() method, allowing specifying the uid to use for the access right check. - This is useful for example when filling in the selection list for a drop-down and avoiding access rights errors, - by specifying ``access_rights_uid=1`` to bypass access rights check, but not ir.rules! - - :param access_rights_uid: optional user ID to use when checking access rights - (not for ir.rules, this is only for ir.model.access) - """ - raise NotImplementedError(_('The search method is not implemented on this object !')) - def name_get(self, cr, user, ids, context=None): """Returns the preferred display value (text representation) for the records with the given ``ids``. By default this will be the value of the ``name`` column, unless @@ -2209,24 +2207,6 @@ class orm_template(object): res = self.name_get(cr, access_rights_uid, ids, context) return res - def copy(self, cr, uid, id, default=None, context=None): - raise NotImplementedError(_('The copy method is not implemented on this object !')) - - def exists(self, cr, uid, ids, context=None): - """Checks whether the given id or ids exist in this model, - and return the list of ids that do. This is simple to use for - a truth test on a browse_record:: - - if record.exists(): - pass - - :param ids: id or list of ids to check for existence - :type ids: int or [int] - :return: the list of ids that currently exist, out of - the given `ids` - """ - raise NotImplementedError(_('The exists method is not implemented on this object !')) - def read_string(self, cr, uid, id, langs, fields=None, context=None): res = {} res2 = {} @@ -2267,9 +2247,6 @@ class orm_template(object): self.pool.get(table).write_string(cr, uid, id, langs, vals, context) return True - def _check_removed_columns(self, cr, log=False): - raise NotImplementedError() - def _add_missing_default_values(self, cr, uid, values, context=None): missing_defaults = [] avoid_tables = [] # avoid overriding inherited values when parent is set @@ -2311,277 +2288,6 @@ class orm_template(object): except AttributeError: pass - def check_access_rule(self, cr, uid, ids, operation, context=None): - """Verifies that the operation given by ``operation`` is allowed for the user - according to ir.rules. - - :param operation: one of ``write``, ``unlink`` - :raise except_orm: * if current ir.rules do not permit this operation. - :return: None if the operation is allowed - """ - raise NotImplementedError(_('The check_access_rule method is not implemented on this object !')) - -class orm_memory(orm_template): - - _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists'] - _inherit_fields = {} - _max_count = None - _max_hours = None - _check_time = 20 - - @classmethod - def createInstance(cls, pool, cr): - return cls.makeInstance(pool, cr, ['_columns', '_defaults']) - - def __init__(self, pool, cr): - super(orm_memory, self).__init__(pool, cr) - self.datas = {} - self.next_id = 0 - self.check_id = 0 - self._max_count = config.get('osv_memory_count_limit') - self._max_hours = config.get('osv_memory_age_limit') - cr.execute('delete from wkf_instance where res_type=%s', (self._name,)) - - def _check_access(self, uid, object_id, mode): - if uid != 1 and self.datas[object_id]['internal.create_uid'] != uid: - raise except_orm(_('AccessError'), '%s access is only allowed on your own records for osv_memory objects except for the super-user' % mode.capitalize()) - - def vaccum(self, cr, uid, force=False): - """Run the vaccuum cleaning system, expiring and removing old records from the - virtual osv_memory tables if the "max count" or "max age" conditions are enabled - and have been reached. This method can be called very often (e.g. everytime a record - is created), but will only actually trigger the cleanup process once out of - "_check_time" times (by default once out of 20 calls).""" - self.check_id += 1 - if (not force) and (self.check_id % self._check_time): - return True - tounlink = [] - - # Age-based expiration - if self._max_hours: - max = time.time() - self._max_hours * 60 * 60 - for k,v in self.datas.iteritems(): - if v['internal.date_access'] < max: - tounlink.append(k) - self.unlink(cr, ROOT_USER_ID, tounlink) - - # Count-based expiration - if self._max_count and len(self.datas) > self._max_count: - # sort by access time to remove only the first/oldest ones in LRU fashion - records = self.datas.items() - records.sort(key=lambda x:x[1]['internal.date_access']) - self.unlink(cr, ROOT_USER_ID, [x[0] for x in records[:len(self.datas)-self._max_count]]) - - return True - - def read(self, cr, user, ids, fields_to_read=None, context=None, load='_classic_read'): - if not context: - context = {} - if not fields_to_read: - fields_to_read = self._columns.keys() - result = [] - if self.datas: - ids_orig = ids - if isinstance(ids, (int, long)): - ids = [ids] - for id in ids: - r = {'id': id} - for f in fields_to_read: - record = self.datas.get(id) - if record: - self._check_access(user, id, 'read') - r[f] = record.get(f, False) - if r[f] and isinstance(self._columns[f], fields.binary) and context.get('bin_size', False): - r[f] = len(r[f]) - result.append(r) - if id in self.datas: - self.datas[id]['internal.date_access'] = time.time() - fields_post = filter(lambda x: x in self._columns and not getattr(self._columns[x], load), fields_to_read) - for f in fields_post: - res2 = self._columns[f].get_memory(cr, self, ids, f, user, context=context, values=result) - for record in result: - record[f] = res2[record['id']] - if isinstance(ids_orig, (int, long)): - return result[0] - return result - - def write(self, cr, user, ids, vals, context=None): - if not ids: - return True - vals2 = {} - upd_todo = [] - for field in vals: - if self._columns[field]._classic_write: - vals2[field] = vals[field] - else: - upd_todo.append(field) - for object_id in ids: - self._check_access(user, object_id, mode='write') - self.datas[object_id].update(vals2) - self.datas[object_id]['internal.date_access'] = time.time() - for field in upd_todo: - self._columns[field].set_memory(cr, self, object_id, field, vals[field], user, context) - self._validate(cr, user, [object_id], context) - wf_service = netsvc.LocalService("workflow") - wf_service.trg_write(user, self._name, object_id, cr) - return object_id - - def create(self, cr, user, vals, context=None): - self.vaccum(cr, user) - self.next_id += 1 - id_new = self.next_id - - vals = self._add_missing_default_values(cr, user, vals, context) - - vals2 = {} - upd_todo = [] - for field in vals: - if self._columns[field]._classic_write: - vals2[field] = vals[field] - else: - upd_todo.append(field) - self.datas[id_new] = vals2 - self.datas[id_new]['internal.date_access'] = time.time() - self.datas[id_new]['internal.create_uid'] = user - - for field in upd_todo: - self._columns[field].set_memory(cr, self, id_new, field, vals[field], user, context) - self._validate(cr, user, [id_new], context) - if self._log_create and not (context and context.get('no_store_function', False)): - message = self._description + \ - " '" + \ - self.name_get(cr, user, [id_new], context=context)[0][1] + \ - "' "+ _("created.") - self.log(cr, user, id_new, message, True, context=context) - wf_service = netsvc.LocalService("workflow") - wf_service.trg_create(user, self._name, id_new, cr) - return id_new - - def _where_calc(self, cr, user, args, active_test=True, context=None): - if not context: - context = {} - args = args[:] - res = [] - # if the object has a field named 'active', filter out all inactive - # records unless they were explicitely asked for - if 'active' in self._columns and (active_test and context.get('active_test', True)): - if args: - active_in_args = False - for a in args: - if a[0] == 'active': - active_in_args = True - if not active_in_args: - args.insert(0, ('active', '=', 1)) - else: - args = [('active', '=', 1)] - if args: - import expression - e = expression.expression(cr, user, args, self, context) - res = e.exp - return res or [] - - def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): - if not context: - context = {} - - # implicit filter on current user except for superuser - if user != 1: - if not args: - args = [] - args.insert(0, ('internal.create_uid', '=', user)) - - result = self._where_calc(cr, user, args, context=context) - if result == []: - return self.datas.keys() - - res = [] - counter = 0 - #Find the value of dict - f = False - if result: - for id, data in self.datas.items(): - counter = counter + 1 - data['id'] = id - if limit and (counter > int(limit)): - break - f = True - for arg in result: - if len(arg) != 3: - # Amazing hack: orm_memory handles only simple domains. - continue - if arg[1] == '=': - val = eval('data[arg[0]]'+'==' +' arg[2]', locals()) - elif arg[1] in ['<', '>', 'in', 'not in', '<=', '>=', '<>']: - val = eval('data[arg[0]]'+arg[1] +' arg[2]', locals()) - elif arg[1] in ['ilike']: - val = (str(data[arg[0]]).find(str(arg[2]))!=-1) - - f = f and val - - if f: - res.append(id) - if count: - return len(res) - return res or [] - - def unlink(self, cr, uid, ids, context=None): - for id in ids: - self._check_access(uid, id, 'unlink') - self.datas.pop(id, None) - if len(ids): - cr.execute('delete from wkf_instance where res_type=%s and res_id IN %s', (self._name, tuple(ids))) - return True - - def perm_read(self, cr, user, ids, context=None, details=True): - result = [] - credentials = self.pool.get('res.users').name_get(cr, user, [user])[0] - create_date = time.strftime('%Y-%m-%d %H:%M:%S') - for id in ids: - self._check_access(user, id, 'read') - result.append({ - 'create_uid': credentials, - 'create_date': create_date, - 'write_uid': False, - 'write_date': False, - 'id': id, - 'xmlid' : False, - }) - return result - - def _check_removed_columns(self, cr, log=False): - # nothing to check in memory... - pass - - def exists(self, cr, uid, ids, context=None): - if isinstance(ids, (long,int)): - ids = [ids] - return [id for id in ids if id in self.datas] - - def check_access_rule(self, cr, uid, ids, operation, context=None): - # ir.rules do not currently apply for orm.memory instances, - # only the implicit visibility=owner one. - for id in ids: - self._check_access(uid, id, operation) - -# Definition of log access columns, automatically added to models if -# self._log_access is True -LOG_ACCESS_COLUMNS = { - 'create_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL', - 'create_date': 'TIMESTAMP', - 'write_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL', - 'write_date': 'TIMESTAMP' -} -# special columns automatically created by the ORM -MAGIC_COLUMNS = ['id'] + LOG_ACCESS_COLUMNS.keys() + \ - ['internal.create_uid', 'internal.date_access'] # for osv_memory only - -class orm(orm_template): - _sql_constraints = [] - _table = None - _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists'] - __logger = logging.getLogger('orm') - __schema = logging.getLogger('orm.schema') - def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): """ Get the list of records in list view grouped by the given ``groupby`` fields @@ -2773,7 +2479,7 @@ class orm(orm_template): while ids_lst: iids = ids_lst[:40] ids_lst = ids_lst[40:] - res = f.get(cr, self, iids, k, ROOT_USER_ID, {}) + res = f.get(cr, self, iids, k, SUPERUSER_ID, {}) for key, val in res.items(): if f._multi: val = val[k] @@ -2828,6 +2534,25 @@ class orm(orm_template): self.__schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", self._table, column['attname']) + # checked version: for direct m2o starting from `self` + def _m2o_add_foreign_key_checked(self, source_field, dest_model, ondelete): + assert self.is_transient() or not dest_model.is_transient(), \ + 'Many2One relationships from non-transient Model to TransientModel are forbidden' + if self.is_transient() and not dest_model.is_transient(): + # TransientModel relationships to regular Models are annoying + # usually because they could block deletion due to the FKs. + # So unless stated otherwise we default them to ondelete=cascade. + ondelete = ondelete or 'cascade' + self._foreign_keys.append((self._table, source_field, dest_model._table, ondelete or 'set null')) + self.__schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s", + self._table, source_field, dest_model._table, ondelete) + + # unchecked version: for custom cases, such as m2m relationships + def _m2o_add_foreign_key_unchecked(self, source_table, source_field, dest_model, ondelete): + self._foreign_keys.append((source_table, source_field, dest_model._table, ondelete or 'set null')) + self.__schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s", + source_table, source_field, dest_model._table, ondelete) + def _auto_init(self, cr, context=None): """ @@ -2979,7 +2704,7 @@ class orm(orm_template): # set the field to the default value if any if k in self._defaults: if callable(self._defaults[k]): - default = self._defaults[k](self, cr, ROOT_USER_ID, context) + default = self._defaults[k](self, cr, SUPERUSER_ID, context) else: default = self._defaults[k] @@ -3026,7 +2751,8 @@ class orm(orm_template): self.__schema.debug(msg, self._table, k, f._type) if isinstance(f, fields.many2one): - ref = self.pool.get(f._obj)._table + dest_model = self.pool.get(f._obj) + ref = dest_model._table if ref != 'ir_actions': cr.execute('SELECT confdeltype, conname FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, ' 'pg_attribute as att1, pg_attribute as att2 ' @@ -3045,9 +2771,9 @@ class orm(orm_template): "AND con.contype = 'f'", (self._table, ref, k, 'id')) res2 = cr.dictfetchall() if res2: - if res2[0]['confdeltype'] != POSTGRES_CONFDELTYPES.get(f.ondelete.upper(), 'a'): + if res2[0]['confdeltype'] != POSTGRES_CONFDELTYPES.get((f.ondelete or 'set null').upper(), 'a'): cr.execute('ALTER TABLE "' + self._table + '" DROP CONSTRAINT "' + res2[0]['conname'] + '"') - self._foreign_keys.append((self._table, k, ref, f.ondelete)) + self._m2o_add_foreign_key_checked(k, dest_model, f.ondelete) cr.commit() self.__schema.debug("Table '%s': column '%s': XXX", self._table, k) @@ -3064,7 +2790,7 @@ class orm(orm_template): # initialize it if not create and k in self._defaults: if callable(self._defaults[k]): - default = self._defaults[k](self, cr, ROOT_USER_ID, context) + default = self._defaults[k](self, cr, SUPERUSER_ID, context) else: default = self._defaults[k] @@ -3085,12 +2811,11 @@ class orm(orm_template): if isinstance(f, fields.many2one): if not self.pool.get(f._obj): raise except_orm('Programming Error', ('There is no reference available for %s') % (f._obj,)) - ref = self.pool.get(f._obj)._table + dest_model = self.pool.get(f._obj) + ref = dest_model._table # ir_actions is inherited so foreign key doesn't work on it if ref != 'ir_actions': - self._foreign_keys.append((self._table, k, ref, f.ondelete)) - self.__schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s", - self._table, k, ref, f.ondelete) + self._m2o_add_foreign_key_checked(k, dest_model, f.ondelete) if f.select: cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, k, self._table, k)) if f.required: @@ -3212,20 +2937,28 @@ class orm(orm_template): def _m2m_raise_or_create_relation(self, cr, f): - cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (f._rel,)) + m2m_tbl, col1, col2 = f._sql_names(self) + cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (m2m_tbl,)) if not cr.dictfetchall(): if not self.pool.get(f._obj): - raise except_orm('Programming Error', ('There is no reference available for %s') % (f._obj,)) - ref = self.pool.get(f._obj)._table - cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s")) WITH OIDS' % (f._rel, f._id1, f._id2, f._id1, f._id2)) - self._foreign_keys.append((f._rel, f._id1, self._table, 'CASCADE')) - self._foreign_keys.append((f._rel, f._id2, ref, 'CASCADE')) - cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (f._rel, f._id1, f._rel, f._id1)) - cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (f._rel, f._id2, f._rel, f._id2)) - cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (f._rel, self._table, ref)) + raise except_orm('Programming Error', ('Many2Many destination model does not exist: `%s`') % (f._obj,)) + dest_model = self.pool.get(f._obj) + ref = dest_model._table + cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s")) WITH OIDS' % (m2m_tbl, col1, col2, col1, col2)) + + # create foreign key references with ondelete=cascade, unless the targets are SQL views + cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (ref,)) + if not cr.fetchall(): + self._m2o_add_foreign_key_unchecked(m2m_tbl, col2, dest_model, 'cascade') + cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (self._table,)) + if not cr.fetchall(): + self._m2o_add_foreign_key_unchecked(m2m_tbl, col1, self, 'cascade') + + cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (m2m_tbl, col1, m2m_tbl, col1)) + cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (m2m_tbl, col2, m2m_tbl, col2)) + cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (m2m_tbl, self._table, ref)) cr.commit() - self.__schema.debug("Create table '%s': relation between '%s' and '%s'", - f._rel, self._table, ref) + self.__schema.debug("Create table '%s': m2m relation between '%s' and '%s'", m2m_tbl, self._table, ref) def _add_sql_constraints(self, cr): @@ -3293,109 +3026,6 @@ class orm(orm_template): cr.execute(line2) cr.commit() - - @classmethod - def createInstance(cls, pool, cr): - return cls.makeInstance(pool, cr, ['_columns', '_defaults', - '_inherits', '_constraints', '_sql_constraints']) - - def __init__(self, pool, cr): - """ - - - copy the stored fields' functions in the osv_pool, - - update the _columns with the fields found in ir_model_fields, - - ensure there is a many2one for each _inherits'd parent, - - update the children's _columns, - - give a chance to each field to initialize itself. - - """ - super(orm, self).__init__(pool, cr) - - if not hasattr(self, '_log_access'): - # if not access is not specify, it is the same value as _auto - self._log_access = getattr(self, "_auto", True) - - self._columns = self._columns.copy() - for store_field in self._columns: - f = self._columns[store_field] - if hasattr(f, 'digits_change'): - f.digits_change(cr) - if not isinstance(f, fields.function): - continue - if not f.store: - continue - if self._columns[store_field].store is True: - sm = {self._name: (lambda self, cr, uid, ids, c={}: ids, None, 10, None)} - else: - sm = self._columns[store_field].store - for object, aa in sm.items(): - if len(aa) == 4: - (fnct, fields2, order, length) = aa - elif len(aa) == 3: - (fnct, fields2, order) = aa - length = None - else: - raise except_orm('Error', - ('Invalid function definition %s in object %s !\nYou must use the definition: store={object:(fnct, fields, priority, time length)}.' % (store_field, self._name))) - self.pool._store_function.setdefault(object, []) - ok = True - for x, y, z, e, f, l in self.pool._store_function[object]: - if (x==self._name) and (y==store_field) and (e==fields2): - if f == order: - ok = False - if ok: - self.pool._store_function[object].append((self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length)) - self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4])) - - for (key, _, msg) in self._sql_constraints: - self.pool._sql_error[self._table+'_'+key] = msg - - # Load manual fields - - cr.execute("SELECT id FROM ir_model_fields WHERE name=%s AND model=%s", ('state', 'ir.model.fields')) - if cr.fetchone(): - cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (self._name, 'manual')) - for field in cr.dictfetchall(): - if field['name'] in self._columns: - continue - attrs = { - 'string': field['field_description'], - 'required': bool(field['required']), - 'readonly': bool(field['readonly']), - 'domain': eval(field['domain']) if field['domain'] else None, - 'size': field['size'], - 'ondelete': field['on_delete'], - 'translate': (field['translate']), - 'manual': True, - #'select': int(field['select_level']) - } - - if field['ttype'] == 'selection': - self._columns[field['name']] = fields.selection(eval(field['selection']), **attrs) - elif field['ttype'] == 'reference': - self._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs) - elif field['ttype'] == 'many2one': - self._columns[field['name']] = fields.many2one(field['relation'], **attrs) - elif field['ttype'] == 'one2many': - self._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs) - elif field['ttype'] == 'many2many': - _rel1 = field['relation'].replace('.', '_') - _rel2 = field['model'].replace('.', '_') - _rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name']) - self._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs) - else: - self._columns[field['name']] = getattr(fields, field['ttype'])(**attrs) - self._inherits_check() - self._inherits_reload() - if not self._sequence: - self._sequence = self._table + '_id_seq' - for k in self._defaults: - assert (k in self._columns) or (k in self._inherit_fields), 'Default function defined in %s but field %s does not exist !' % (self._name, k,) - for f in self._columns: - self._columns[f].restart() - - __init__.__doc__ = orm_template.__init__.__doc__ + __init__.__doc__ - # # Update objects that uses this one to update their _inherits fields # @@ -3474,9 +3104,12 @@ class orm(orm_template): # return _proxy - def fields_get(self, cr, user, fields=None, context=None): - """ - Get the description of list of fields + def fields_get(self, cr, user, allfields=None, context=None, write_access=True): + """ Return the definition of each field. + + The returned value is a dictionary (indiced by field name) of + dictionaries. The _inherits'd fields are included. The string, help, + and selection (if present) attributes are translated. :param cr: database cursor :param user: current user id @@ -3486,12 +3119,72 @@ class orm(orm_template): :raise AccessError: * if user has no create/write rights on the requested object """ + if context is None: + context = {} + ira = self.pool.get('ir.model.access') write_access = ira.check(cr, user, self._name, 'write', False) or \ ira.check(cr, user, self._name, 'create', False) - return super(orm, self).fields_get(cr, user, fields, context, write_access) + + res = {} + + translation_obj = self.pool.get('ir.translation') + for parent in self._inherits: + res.update(self.pool.get(parent).fields_get(cr, user, allfields, context)) + + for f, field in self._columns.iteritems(): + if allfields and f not in allfields: + continue + + res[f] = fields.field_to_dict(self, cr, user, context, field) + + if not write_access: + res[f]['readonly'] = True + res[f]['states'] = {} + + if 'string' in res[f]: + res_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'field', context.get('lang', False) or 'en_US') + if res_trans: + res[f]['string'] = res_trans + if 'help' in res[f]: + help_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'help', context.get('lang', False) or 'en_US') + if help_trans: + res[f]['help'] = help_trans + if 'selection' in res[f]: + if isinstance(field.selection, (tuple, list)): + sel = field.selection + sel2 = [] + for key, val in sel: + val2 = None + if val: + val2 = translation_obj._get_source(cr, user, self._name + ',' + f, 'selection', context.get('lang', False) or 'en_US', val) + sel2.append((key, val2 or val)) + res[f]['selection'] = sel2 + + return res def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'): + """ Read records with given ids with the given fields + + :param cr: database cursor + :param user: current user id + :param ids: id or list of the ids of the records to read + :param fields: optional list of field names to return (default: all fields would be returned) + :type fields: list (example ['field_name_1', ...]) + :param context: optional context dictionary - it may contains keys for specifying certain options + like ``context_lang``, ``context_tz`` to alter the results of the call. + A special ``bin_size`` boolean flag may also be passed in the context to request the + value of all fields.binary columns to be returned as the size of the binary instead of its + contents. This can also be selectively overriden by passing a field-specific flag + in the form ``bin_size_XXX: True/False`` where ``XXX`` is the name of the field. + Note: The ``bin_size_XXX`` form is new in OpenERP v6.0. + :return: list of dictionaries((dictionary per record asked)) with requested field values + :rtype: [{‘name_of_the_field’: value, ...}, ...] + :raise AccessError: * if user has no read rights on the requested object + * if user tries to bypass access rules for read on the requested object + + """ + if not context: context = {} self.pool.get('ir.model.access').check(cr, user, self._name, 'read') @@ -3668,6 +3361,7 @@ class orm(orm_template): vals[field] = False return res + # TODO check READ access def perm_read(self, cr, user, ids, context=None, details=True): """ Returns some metadata about the given records. @@ -3745,17 +3439,35 @@ class orm(orm_template): :raise except_orm: * if current ir.rules do not permit this operation. :return: None if the operation is allowed """ - where_clause, where_params, tables = self.pool.get('ir.rule').domain_get(cr, uid, self._name, operation, context=context) - if where_clause: - where_clause = ' and ' + ' and '.join(where_clause) - for sub_ids in cr.split_for_in_conditions(ids): - cr.execute('SELECT ' + self._table + '.id FROM ' + ','.join(tables) + - ' WHERE ' + self._table + '.id IN %s' + where_clause, - [sub_ids] + where_params) - if cr.rowcount != len(sub_ids): - raise except_orm(_('AccessError'), - _('Operation prohibited by access rules, or performed on an already deleted document (Operation: %s, Document type: %s).') - % (operation, self._description)) + if uid == SUPERUSER_ID: + return + + if self.is_transient: + # Only one single implicit access rule for transient models: owner only! + # This is ok to hardcode because we assert that TransientModels always + # have log_access enabled and this the create_uid column is always there. + # And even with _inherits, these fields are always present in the local + # table too, so no need for JOINs. + cr.execute("""SELECT distinct create_uid + FROM %s + WHERE id IN %%s""" % self._table, (tuple(ids),)) + uids = [x[0] for x in cr.fetchall()] + if len(uids) != 1 or uids[0] != uid: + raise orm.except_orm(_('AccessError'), '%s access is ' + 'restricted to your own records for transient models ' + '(except for the super-user).' % operation.capitalize()) + else: + where_clause, where_params, tables = self.pool.get('ir.rule').domain_get(cr, uid, self._name, operation, context=context) + if where_clause: + where_clause = ' and ' + ' and '.join(where_clause) + for sub_ids in cr.split_for_in_conditions(ids): + cr.execute('SELECT ' + self._table + '.id FROM ' + ','.join(tables) + + ' WHERE ' + self._table + '.id IN %s' + where_clause, + [sub_ids] + where_params) + if cr.rowcount != len(sub_ids): + raise except_orm(_('AccessError'), + _('Operation prohibited by access rules, or performed on an already deleted document (Operation: %s, Document type: %s).') + % (operation, self._description)) def unlink(self, cr, uid, ids, context=None): """ @@ -3806,10 +3518,10 @@ class orm(orm_template): # Note: following steps performed as admin to avoid access rights restrictions, and with no context # to avoid possible side-effects during admin calls. # Step 1. Calling unlink of ir_model_data only for the affected IDS - reference_ids = pool_model_data.search(cr, ROOT_USER_ID, [('res_id','in',list(sub_ids)),('model','=',self._name)]) + reference_ids = pool_model_data.search(cr, SUPERUSER_ID, [('res_id','in',list(sub_ids)),('model','=',self._name)]) # Step 2. Marching towards the real deletion of referenced records if reference_ids: - pool_model_data.unlink(cr, ROOT_USER_ID, reference_ids) + pool_model_data.unlink(cr, SUPERUSER_ID, reference_ids) # For the same reason, removing the record relevant to ir_values ir_value_ids = ir_values_obj.search(cr, uid, @@ -4126,6 +3838,10 @@ class orm(orm_template): """ if not context: context = {} + + if self.is_transient(): + self._transient_vacuum(cr, user) + self.pool.get('ir.model.access').check(cr, user, self._name, 'create') vals = self._add_missing_default_values(cr, user, vals, context) @@ -4279,6 +3995,27 @@ class orm(orm_template): wf_service.trg_create(user, self._name, id_new, cr) return id_new + def browse(self, cr, uid, select, context=None, list_class=None, fields_process=None): + """Fetch records as objects allowing to use dot notation to browse fields and relations + + :param cr: database cursor + :param user: current user id + :param select: id or list of ids. + :param context: context arguments, like lang, time zone + :rtype: object or list of objects requested + + """ + self._list_class = list_class or browse_record_list + cache = {} + # need to accepts ints and longs because ids coming from a method + # launched by button in the interface have a type long... + if isinstance(select, (int, long)): + return browse_record(cr, uid, select, self, cache, context=context, list_class=self._list_class, fields_process=fields_process) + elif isinstance(select, list): + return self._list_class([browse_record(cr, uid, id, self, cache, context=context, list_class=self._list_class, fields_process=fields_process) for id in select], context=context) + else: + return browse_null() + def _store_get_values(self, cr, uid, ids, fields, context): """Returns an ordered list of fields.functions to call due to an update operation on ``fields`` of records with ``ids``, @@ -4301,7 +4038,7 @@ class orm(orm_template): mapping = {} for function in to_compute: # use admin user for accessing objects having rules defined on store fields - target_ids = [id for id in function[id_mapping_fnct_](self, cr, ROOT_USER_ID, ids, context) if id] + target_ids = [id for id in function[id_mapping_fnct_](self, cr, SUPERUSER_ID, ids, context) if id] # the compound key must consider the priority and model name key = (function[priority_], function[model_name_]) @@ -4368,7 +4105,7 @@ class orm(orm_template): val = todo[key] if key: # use admin user for accessing objects having rules defined on store fields - result = self._columns[val[0]].get(cr, self, ids, val, ROOT_USER_ID, context=context) + result = self._columns[val[0]].get(cr, self, ids, val, SUPERUSER_ID, context=context) for id, value in result.items(): if field_flag: for f in value.keys(): @@ -4394,7 +4131,7 @@ class orm(orm_template): else: for f in val: # use admin user for accessing objects having rules defined on store fields - result = self._columns[f].get(cr, self, ids, f, ROOT_USER_ID, context=context) + result = self._columns[f].get(cr, self, ids, f, SUPERUSER_ID, context=context) for r in result.keys(): if field_flag: if r in field_dict.keys(): @@ -4443,7 +4180,6 @@ class orm(orm_template): domain = [('active', '=', 1)] if domain: - import expression e = expression.expression(cr, user, domain, self, context) tables = e.get_tables() where_clause, where_params = e.to_sql() @@ -4591,6 +4327,10 @@ class orm(orm_template): context = {} self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read') + # For transient models, restrict acces to the current user, except for the super-user + if self.is_transient() and self._log_access and user != SUPERUSER_ID: + args = expression.AND(([('create_uid', '=', user)], args or [])) + query = self._where_calc(cr, user, args, context=context) self._apply_ir_rules(cr, user, query, 'read', context=context) order_by = self._generate_order_by(order, query) @@ -4780,6 +4520,18 @@ class orm(orm_template): return new_id def exists(self, cr, uid, ids, context=None): + """Checks whether the given id or ids exist in this model, + and return the list of ids that do. This is simple to use for + a truth test on a browse_record:: + + if record.exists(): + pass + + :param ids: id or list of ids to check for existence + :type ids: int or [int] + :return: the list of ids that currently exist, out of + the given `ids` + """ if type(ids) in (int, long): ids = [ids] query = 'SELECT id FROM "%s"' % (self._table) @@ -4864,5 +4616,98 @@ class orm(orm_template): results[k] = '' return results -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + # Transience + def is_transient(self): + """ Return whether the model is transient. + See TransientModel. + + """ + return self._transient + + def _transient_clean_rows_older_than(self, cr, seconds): + assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name + cr.execute("SELECT id FROM " + self._table + " WHERE" + " COALESCE(write_date, create_date, now())::timestamp <" + " (now() - interval %s)", ("%s seconds" % seconds,)) + ids = [x[0] for x in cr.fetchall()] + self.unlink(cr, SUPERUSER_ID, ids) + + def _transient_clean_old_rows(self, cr, count): + assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name + cr.execute( + "SELECT id, COALESCE(write_date, create_date, now())::timestamp" + " AS t FROM " + self._table + + " ORDER BY t LIMIT %s", (count,)) + ids = [x[0] for x in cr.fetchall()] + self.unlink(cr, SUPERUSER_ID, ids) + + def _transient_vacuum(self, cr, uid, force=False): + """Clean the transient records. + + This unlinks old records from the transient model tables whenever the + "_transient_max_count" or "_max_age" conditions (if any) are reached. + Actual cleaning will happen only once every "_transient_check_time" calls. + This means this method can be called frequently called (e.g. whenever + a new record is created). + """ + assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name + self._transient_check_count += 1 + if (not force) and (self._transient_check_count % self._transient_check_time): + self._transient_check_count = 0 + return True + + # Age-based expiration + if self._transient_max_hours: + self._transient_clean_rows_older_than(cr, self._transient_max_hours * 60 * 60) + + # Count-based expiration + if self._transient_max_count: + self._transient_clean_old_rows(cr, self._transient_max_count) + + return True + +# keep this import here, at top it will cause dependency cycle errors +import expression + +class Model(BaseModel): + """Main super-class for regular database-persisted OpenERP models. + + OpenERP models are created by inheriting from this class:: + + class user(Model): + ... + + The system will later instantiate the class once per database (on + which the class' module is installed). + """ + _register = False # not visible in ORM registry, meant to be python-inherited only + _transient = False # True in a TransientModel + +class TransientModel(BaseModel): + """Model super-class for transient records, meant to be temporarily + persisted, and regularly vaccuum-cleaned. + + A TransientModel has a simplified access rights management, + all users can create new records, and may only access the + records they created. The super-user has unrestricted access + to all TransientModel records. + """ + _register = False # not visible in ORM registry, meant to be python-inherited only + _transient = True + +class AbstractModel(BaseModel): + """Abstract Model super-class for creating an abstract class meant to be + inherited by regular models (Models or TransientModels) but not meant to + be usable on its own, or persisted. + + Technical note: we don't want to make AbstractModel the super-class of + Model or BaseModel because it would not make sense to put the main + definition of persistence methods such as create() in it, and still we + should be able to override them within an AbstractModel. + """ + _auto = False # don't create any database backend for AbstractModels + _register = False # not visible in ORM registry, meant to be python-inherited only + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 02c6fbe3c5d..00672dd94bd 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -21,16 +21,17 @@ #.apidoc title: Objects Services (OSV) +import logging +from psycopg2 import IntegrityError, errorcodes + import orm +import openerp import openerp.netsvc as netsvc import openerp.pooler as pooler import openerp.sql_db as sql_db -import logging -from psycopg2 import IntegrityError, errorcodes from openerp.tools.func import wraps from openerp.tools.translate import translate -from openerp.osv.orm import MetaModel - +from openerp.osv.orm import MetaModel, Model, TransientModel, AbstractModel class except_osv(Exception): def __init__(self, name, value, exc_type='warning'): @@ -198,17 +199,10 @@ class object_proxy(): cr.close() return res - -class osv_memory(orm.orm_memory): - """ Deprecated class. """ - __metaclass__ = MetaModel - _register = False # Set to false if the model shouldn't be automatically discovered. - - -class osv(orm.orm): - """ Deprecated class. """ - __metaclass__ = MetaModel - _register = False # Set to false if the model shouldn't be automatically discovered. +# deprecated - for backward compatibility. +osv = Model +osv_memory = TransientModel +osv_abstract = AbstractModel # ;-) def start_object_proxy(): diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index ce4354d2b67..bb4ce9db87d 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -307,7 +307,7 @@ class YamlInterpreter(object): import openerp.osv as osv record, fields = node.items()[0] model = self.get_model(record.model) - if isinstance(model, osv.osv.osv_memory): + if model.is_transient(): record_dict=self.create_osv_memory_record(record, fields) else: self.validate_xml_id(record.id)