From 44d41334d731e761eca122cd88b669aa959949d7 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Jul 2011 10:27:52 +0200 Subject: [PATCH 01/29] [REF] osv: osv_memory is really just osv, the change is not enough as we still want to differentiate between regular osv and osv that should be wiped from the db from time to time. bzr revid: vmt@openerp.com-20110728082752-39jur9oo0m93ebzh --- openerp/addons/base/test/base_test.yml | 42 -------------------------- openerp/osv/orm.py | 2 +- openerp/osv/osv.py | 2 +- 3 files changed, 2 insertions(+), 44 deletions(-) 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/osv/orm.py b/openerp/osv/orm.py index ff0d17af47a..850d88d8460 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -3380,7 +3380,7 @@ class orm(orm_template): # return _proxy - def fields_get(self, cr, user, fields=None, context=None): + def fields_get(self, cr, user, fields=None, context=None, write_access=True): """ Get the description of list of fields diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 1327efe5731..c624bd7358f 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -199,7 +199,7 @@ class object_proxy(netsvc.Service): return res -class osv_memory(orm.orm_memory): +class osv_memory(orm.orm): """ Deprecated class. """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. From 2f5e8d48b3e98ddb6c7293c0ed06bf71044fdbe7 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 12 Aug 2011 13:33:55 +0200 Subject: [PATCH 02/29] [REF] osv: moved (and adapted) vacuum code to osv_memory. bzr revid: vmt@openerp.com-20110812113355-lylavdsc3mqjovrj --- openerp/__init__.py | 3 + .../addons/base/ir/osv_memory_autovacuum.py | 12 ++-- openerp/modules/module.py | 2 + openerp/osv/orm.py | 35 +---------- openerp/osv/osv.py | 60 +++++++++++++++++++ 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/openerp/__init__.py b/openerp/__init__.py index b7b5fc1b533..b74885674fa 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -42,5 +42,8 @@ import tools import wizard import workflow +# The hard-coded super-user id (a.k.a. administrator, or root user). +SUPERUSER = 1 + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/osv_memory_autovacuum.py b/openerp/addons/base/ir/osv_memory_autovacuum.py index b2ab938d740..f15adb77a02 100644 --- a/openerp/addons/base/ir/osv_memory_autovacuum.py +++ b/openerp/addons/base/ir/osv_memory_autovacuum.py @@ -19,18 +19,16 @@ # ############################################################################## -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) + if obj._transient: + obj.vacuum(cr, uid) return True -osv_memory_autovacuum() - diff --git a/openerp/modules/module.py b/openerp/modules/module.py index 1514e836dd8..6ce0374814f 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -284,6 +284,8 @@ def init_module_models(cr, module_name, obj_list): cr.commit() for obj in obj_list: obj._auto_end(cr, {'module': module_name}) + if obj._transient: + obj.vacuum(cr, openerp.SUPERUSER) cr.commit() todo.sort() for t in todo: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 850d88d8460..efd93f75e1b 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2250,8 +2250,6 @@ 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 @@ -2262,43 +2260,12 @@ class orm_memory(orm_template): 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 = {} @@ -2352,7 +2319,7 @@ class orm_memory(orm_template): return object_id def create(self, cr, user, vals, context=None): - self.vaccum(cr, user) + self.vacuum(cr, user) self.next_id += 1 id_new = self.next_id diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index c624bd7358f..b231abd54ab 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -22,11 +22,13 @@ #.apidoc title: Objects Services (OSV) 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.config import config from openerp.tools.func import wraps from openerp.tools.translate import translate from openerp.osv.orm import MetaModel @@ -203,12 +205,70 @@ class osv_memory(orm.orm): """ Deprecated class. """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. + _transient = True + _max_count = None + _max_hours = None + _check_time = 20 + + def __init__(self, pool, cr): + super(osv_memory, self).__init__(pool, cr) + self.check_id = 0 + self._max_count = config.get('osv_memory_count_limit') + self._max_hours = config.get('osv_memory_age_limit') + + def _clean_transient_rows_older_than(self, cr, seconds): + if not self._log_access: + self.logger = logging.getLogger('orm').warning( + "Transient model without write_date: %s" % (self._name,)) + return + + 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, openerp.SUPERUSER, ids) + + def _clean_old_transient_rows(self, cr, count): + if not self._log_access: + self.logger = logging.getLogger('orm').warning( + "Transient model without write_date: %s" % (self._name,)) + return + + 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, openerp.SUPERUSER, ids) + + def vacuum(self, cr, uid, force=False): + """ Run the vacuum cleaner, i.e. unlink 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): + self.check_id = 0 + return True + tounlink = [] + + # Age-based expiration + if self._max_hours: + self._clean_transient_rows_older_than(cr, self._max_hours * 60 * 60) + + # Count-based expiration + if self._max_count: + self._clean_old_transient_rows(cr, self._max_count) + + return True class osv(orm.orm): """ Deprecated class. """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. + _transient = False def start_object_proxy(): From ddd65ab5c143f23df75205f274ce70b46e858310 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 12 Aug 2011 14:41:28 +0200 Subject: [PATCH 03/29] [REF] osv: removed orm_memory, adapted the distinguished bits to osv_memory, so yes, there are still two user-visible classes instead of a boolean flag. bzr revid: vmt@openerp.com-20110812124128-43rr422swy6h2vs8 --- openerp/osv/expression.py | 7 ++ openerp/osv/orm.py | 200 +------------------------------------- openerp/osv/osv.py | 39 +++++++- 3 files changed, 44 insertions(+), 202 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 54726a289f2..4de6ae295c5 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -91,6 +91,13 @@ def OR(domains): """ OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """ return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains) +# Probably bad style: the domain should be already normalized (and non-empty). +def expression_and(term, domain): + if domain: + return ['&', term] + normalize(domain) + else: + return term + class expression(object): """ diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index efd93f75e1b..3dd0f38caec 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2246,205 +2246,6 @@ class orm_template(object): values = defaults return values -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 = {} - _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 - 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 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.vacuum(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(args) - e.parse(cr, user, 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 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, id, context=None): - return id in self.datas - class orm(orm_template): _sql_constraints = [] _table = None @@ -3541,6 +3342,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. diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index b231abd54ab..335be93464f 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -215,6 +215,7 @@ class osv_memory(orm.orm): 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 _clean_transient_rows_older_than(self, cr, seconds): if not self._log_access: @@ -222,9 +223,9 @@ class osv_memory(orm.orm): "Transient model without write_date: %s" % (self._name,)) return - cr.execute("SELECT id FROM " + self._table + " WHERE " - "COALESCE(write_date, create_date, now())::timestamp < " - "(now() - interval %s)", ("%s seconds" % seconds,)) + 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, openerp.SUPERUSER, ids) @@ -263,6 +264,38 @@ class osv_memory(orm.orm): return True + def check_access_rule(self, cr, uid, ids, operation, context=None): + # No access rules for osv_memory + if self._log_access and uid != openerp.SUPERUSER: + cr.execute("SELECT distinct create_uid FROM " + self._table + " WHERE" + " id in ", (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 osv_memory objects ' + '(except for the super-user).' % mode.capitalize()) + + def create(self, cr, uid, vals, context=None): + self.vacuum(cr, uid) + super(osv_memory, self).create(cr, uid, vals, context) + + def unlink(self, cr, uid, ids, context=None): + super(osv_memory, self).unlink(cr, uid, ids, context) + if isinstance(ids, (int, long)): + ids = [ids] + if ids: + cr.execute('delete from wkf_instance where res_type=%s and res_id IN %s', (self._name, tuple(ids))) + return True + + def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): + + # Restrict acces to the current user, except for the super-user. + if self._log_access and uid != openerp.SUPERUSER: + domain = expression.expression_and(('create_uid', '=', uid), domain) + + # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? + return self._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) + class osv(orm.orm): """ Deprecated class. """ From 5865633ffb9de4bfc47435826c8c6cc37a13cb82 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 12 Aug 2011 15:08:22 +0200 Subject: [PATCH 04/29] [FIX] osv: forgot the "return" statement. bzr revid: vmt@openerp.com-20110812130822-6q1qym3nww64zk4p --- openerp/osv/osv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 335be93464f..e848a2c1d52 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -277,7 +277,7 @@ class osv_memory(orm.orm): def create(self, cr, uid, vals, context=None): self.vacuum(cr, uid) - super(osv_memory, self).create(cr, uid, vals, context) + return super(osv_memory, self).create(cr, uid, vals, context) def unlink(self, cr, uid, ids, context=None): super(osv_memory, self).unlink(cr, uid, ids, context) From 80f65626183b89fa7fe45060a147a088d985547f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 12 Aug 2011 16:28:14 +0200 Subject: [PATCH 05/29] [REF] orm: merged orm_template inside orm. Things could be re-ordered but this way the diff remains small. bzr revid: vmt@openerp.com-20110812142814-6dbd0wodllw8xn37 --- openerp/osv/orm.py | 432 ++++++++++++++++++++------------------------- 1 file changed, 187 insertions(+), 245 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index e396fd8eb83..8c9b6ecc2b1 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -562,7 +562,7 @@ class MetaModel(type): self.module_to_models.setdefault(self._module, []).append(self) -class orm_template(object): +class orm(object): """ Base class for OpenERP models. OpenERP models are created by inheriting from this class (although @@ -604,8 +604,14 @@ 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' + def log(self, cr, uid, id, message, secondary=False, context=None): if context and context.get('disable_log'): return True @@ -623,9 +629,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. @@ -728,13 +731,6 @@ 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 @@ -829,7 +825,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 @@ -846,26 +850,89 @@ 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, fields2, 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() - """ - 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 __export_row(self, cr, uid, row, fields, context=None): if context is None: @@ -1219,30 +1286,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) @@ -1375,19 +1418,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 @@ -1396,56 +1426,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 # @@ -2089,17 +2069,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): """ @@ -2173,12 +2142,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, id, context=None): - 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 = {} @@ -2219,9 +2182,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 @@ -2251,12 +2211,6 @@ class orm_template(object): values = defaults return values -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 @@ -2972,109 +2926,11 @@ 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, fields2, 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 # @@ -3153,9 +3009,12 @@ class orm(orm_template): # return _proxy - def fields_get(self, cr, user, fields=None, context=None, write_access=True): - """ - 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 @@ -3165,12 +3024,74 @@ 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', raise_exception=False, context=context) or \ ira.check(cr, user, self._name, 'create', raise_exception=False, context=context) - 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', context=context) @@ -3959,6 +3880,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``, From 83f10149d048a1bf7e138036df34a201c40568f0 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 12 Aug 2011 16:31:06 +0200 Subject: [PATCH 06/29] [REF] fields: renamed orm_template in orm (in the docstrings). bzr revid: vmt@openerp.com-20110812143106-jk9ijbtc2yhal1sp --- openerp/osv/fields.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 056198a76d7..45851594be5 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -784,8 +784,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] @@ -848,8 +848,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 str field_name: name of the field to set :param fnct_inv_arg: arbitrary value passed when declaring the function field :return: True @@ -869,10 +869,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 @@ -917,7 +917,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 From 9964cd1d844ea4ce3cbf5cef081f65b254445ead Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 16 Aug 2011 09:44:31 +0200 Subject: [PATCH 07/29] [REF] osv: previous diff makes unneccessary one of makeInstance/createInstance. bzr revid: vmt@openerp.com-20110816074431-v16wfzcy55ojkhwi --- openerp/addons/base/ir/ir_model.py | 2 +- openerp/modules/registry.py | 4 ++-- openerp/osv/orm.py | 33 ++++++++++++++---------------- openerp/osv/osv.py | 7 +++---- 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index d3dfd987638..0f60ef44ab9 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -161,7 +161,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: diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 11f484a71a0..b2768925232 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -73,12 +73,12 @@ class Registry(object): # Instanciate classes registered through their constructor and # add them to the pool. for klass in openerp.osv.orm.module_class_list.get(module, []): - res.append(klass.createInstance(self, cr)) + res.append(klass.create_instance(self, cr)) # Instanciate classes automatically discovered. for cls in openerp.osv.orm.MetaModel.module_to_models.get(module, []): if cls not in openerp.osv.orm.module_class_list.get(module, []): - res.append(cls.createInstance(self, cr)) + res.append(cls.create_instance(self, cr)) return res diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 8c9b6ecc2b1..cdb8a96b1f9 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -562,22 +562,26 @@ class MetaModel(type): self.module_to_models.setdefault(self._module, []).append(self) -class orm(object): +class Model(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 (or from + TransientModel) - class user(osv): + class user(Model): ... - user() The system will later instanciate the class once per database (on which the class' module is installed). + To create a class that should not be instanciated (because it is not a + model per se, but a class to be inherited to really create a model later), + the _register class attribute should be set to False. + """ + __metaclass__ = MetaModel + _register = False # Set to false if the model shouldn't be automatically discovered. + _transient = False # True in a TransientModel _name = None _columns = {} _constraints = [] @@ -736,7 +740,7 @@ class orm(object): # 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 @@ -751,6 +755,9 @@ class orm(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)): @@ -807,9 +814,6 @@ class orm(object): This doesn't create an instance but simply register the model as being part of the module where it is defined. - TODO make it possible to not even have to call the constructor - to be registered. - """ # Set the module name (e.g. base, sale, accounting, ...) on the class. @@ -2926,11 +2930,6 @@ class orm(object): cr.execute(line2) cr.commit() - @classmethod - def createInstance(cls, pool, cr): - return cls.makeInstance(pool, cr, ['_columns', '_defaults', - '_inherits', '_constraints', '_sql_constraints']) - # # Update objects that uses this one to update their _inherits fields # @@ -3068,8 +3067,6 @@ class orm(object): return res - - def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'): """ Read records with given ids with the given fields diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index e848a2c1d52..616d9e310fd 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -31,7 +31,7 @@ from psycopg2 import IntegrityError, errorcodes from openerp.tools.config import config 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 class except_osv(Exception): @@ -201,7 +201,7 @@ class object_proxy(netsvc.Service): return res -class osv_memory(orm.orm): +class osv_memory(Model): """ Deprecated class. """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. @@ -297,11 +297,10 @@ class osv_memory(orm.orm): return self._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) -class osv(orm.orm): +class osv(Model): """ Deprecated class. """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. - _transient = False def start_object_proxy(): From 7e9c72d041b83bca0a7b9a9a99574b56e2633945 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 16 Aug 2011 10:09:00 +0200 Subject: [PATCH 08/29] [FIX] osv: super instead of self. bzr revid: vmt@openerp.com-20110816080900-fg6g5kz40ulbvxbr --- openerp/osv/osv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 616d9e310fd..cefccc12ef2 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -294,7 +294,7 @@ class osv_memory(Model): domain = expression.expression_and(('create_uid', '=', uid), domain) # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? - return self._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) + return super(osv_memory, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) class osv(Model): From 51117db34435806a4443aa05ff6d17a2e48fe1ba Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 16 Aug 2011 11:57:55 +0200 Subject: [PATCH 09/29] [REF] osv: replace isinstance(m,osv_memory) by m.is_transient(). bzr revid: vmt@openerp.com-20110816095755-2g5cblolcvchtewx --- openerp/addons/base/ir/ir_model.py | 4 ++-- openerp/addons/base/ir/ir_rule.py | 2 +- openerp/modules/loading.py | 8 ++++---- openerp/osv/orm.py | 8 ++++++++ openerp/osv/osv.py | 29 ++++++++++++++++++----------- openerp/tools/yaml_import.py | 2 +- 6 files changed, 34 insertions(+), 19 deletions(-) diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 0f60ef44ab9..9294bd4c0ff 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): @@ -483,7 +483,7 @@ class ir_model_access(osv.osv): # 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): + if model_obj.is_transient(): return True # We check if a specific rule exists diff --git a/openerp/addons/base/ir/ir_rule.py b/openerp/addons/base/ir/ir_rule.py index 2a36ee9ccc5..b18999da0a3 100644 --- a/openerp/addons/base/ir/ir_rule.py +++ b/openerp/addons/base/ir/ir_rule.py @@ -68,7 +68,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), diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 45a466e5462..85d86e8d7e1 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, 'The 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): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name)) + if 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(): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index cdb8a96b1f9..6a08b937676 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -4472,5 +4472,13 @@ class Model(object): results[k] = '' return results + def is_transient(self): + """ Return whether the model is transient. + + See TransientModel. + + """ + return self._transient + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index cefccc12ef2..71b1781d335 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -202,7 +202,12 @@ class object_proxy(netsvc.Service): class osv_memory(Model): - """ Deprecated class. """ + """ Model for transient records. + + A TransientModel works similarly to a regular Model but the assiociated + records will be cleaned automatically from the database after some time. + + """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. _transient = True @@ -212,7 +217,7 @@ class osv_memory(Model): def __init__(self, pool, cr): super(osv_memory, self).__init__(pool, cr) - self.check_id = 0 + self.check_count = 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,)) @@ -243,16 +248,18 @@ class osv_memory(Model): self.unlink(cr, openerp.SUPERUSER, ids) def vacuum(self, cr, uid, force=False): - """ Run the vacuum cleaner, i.e. unlink 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): - self.check_id = 0 + """ Clean the TransientModel records. + + This unlinks old records from the transient model tables whenever the + "_max_count" or "_max_age" conditions (if any) are reached. + Actual cleaning will happen only once every "_check_time" calls. + This means this method can be called frequently called (e.g. whenever + a new record is created). + """ + self.check_count += 1 + if (not force) and (self.check_count % self._check_time): + self.check_count = 0 return True - tounlink = [] # Age-based expiration if self._max_hours: diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index 00bb79f0499..b45a941bfc2 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -305,7 +305,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) From fa41b78f64bb09fa5b3b1e96110c076b159eb2f1 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 16 Aug 2011 12:35:31 +0200 Subject: [PATCH 10/29] [FIX] res_currency: changed super(osv,self) to super(res_currency,self). bzr revid: vmt@openerp.com-20110816103531-8f6cj11jsgacfsdk --- openerp/addons/base/res/res_currency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index d81248a735d..33c719ba8a3 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -68,7 +68,7 @@ class res_currency(osv.osv): _order = "name" 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) for r in res: if r.__contains__('rate_ids'): rates=r['rate_ids'] From ad716633bcbbeba84497562bd973c551b883a59f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 16 Aug 2011 12:46:50 +0200 Subject: [PATCH 11/29] [REF] osv: osv_memory is now TransientModel. bzr revid: vmt@openerp.com-20110816104650-euix946m490dliqp --- openerp/osv/osv.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 71b1781d335..0831e6f1944 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -201,12 +201,14 @@ class object_proxy(netsvc.Service): return res -class osv_memory(Model): +class TransientModel(Model): """ Model for transient records. A TransientModel works similarly to a regular Model but the assiociated records will be cleaned automatically from the database after some time. + A TransientModel has no access rules. + """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. @@ -216,7 +218,7 @@ class osv_memory(Model): _check_time = 20 def __init__(self, pool, cr): - super(osv_memory, self).__init__(pool, cr) + super(TransientModel, self).__init__(pool, cr) self.check_count = 0 self._max_count = config.get('osv_memory_count_limit') self._max_hours = config.get('osv_memory_age_limit') @@ -272,22 +274,22 @@ class osv_memory(Model): return True def check_access_rule(self, cr, uid, ids, operation, context=None): - # No access rules for osv_memory + # No access rules for transient models. if self._log_access and uid != openerp.SUPERUSER: cr.execute("SELECT distinct create_uid FROM " + self._table + " WHERE" " id in ", (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 osv_memory objects ' + 'restricted to your own records for transient models ' '(except for the super-user).' % mode.capitalize()) def create(self, cr, uid, vals, context=None): self.vacuum(cr, uid) - return super(osv_memory, self).create(cr, uid, vals, context) + return super(TransientModel, self).create(cr, uid, vals, context) def unlink(self, cr, uid, ids, context=None): - super(osv_memory, self).unlink(cr, uid, ids, context) + super(TransientModel, self).unlink(cr, uid, ids, context) if isinstance(ids, (int, long)): ids = [ids] if ids: @@ -301,13 +303,12 @@ class osv_memory(Model): domain = expression.expression_and(('create_uid', '=', uid), domain) # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? - return super(osv_memory, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) + return super(TransientModel, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) -class osv(Model): - """ Deprecated class. """ - __metaclass__ = MetaModel - _register = False # Set to false if the model shouldn't be automatically discovered. +# For backward compatibility. +osv = Model +osv_memory = TransientModel def start_object_proxy(): From c462ff9ad4a654237c6ac424076ceea2ac769477 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 16 Aug 2011 13:02:34 +0200 Subject: [PATCH 12/29] [REF] fields: removed osv_memory-related methods ({set,get,search}_memory()). bzr revid: vmt@openerp.com-20110816110234-8jw4pxjklip4kf43 --- openerp/osv/fields.py | 116 +----------------------------------------- 1 file changed, 1 insertion(+), 115 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 45851594be5..a7294f38884 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -110,12 +110,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 !')) @@ -124,9 +118,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 @@ -259,7 +250,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: @@ -283,9 +274,6 @@ class binary(_column): res[i] = val return res - get = get_memory - - class selection(_column): _type = 'selection' @@ -345,30 +333,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 = {} @@ -437,55 +401,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 = {} @@ -684,32 +599,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 @@ -1041,14 +930,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 From 4d2c765f75e343ff0572cb971fb18ee9c53170aa Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 23 Aug 2011 14:45:24 +0200 Subject: [PATCH 13/29] [FIX] osv: broken SQL query. bzr revid: vmt@openerp.com-20110823124524-p0e1jq49vgssgb1y --- openerp/osv/osv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 0831e6f1944..1c564cb83b5 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -277,7 +277,7 @@ class TransientModel(Model): # No access rules for transient models. if self._log_access and uid != openerp.SUPERUSER: cr.execute("SELECT distinct create_uid FROM " + self._table + " WHERE" - " id in ", (tuple(ids),)) + " id IN %s", (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 ' From e1b2dec2002168fcb44ee18216b015f9095cd678 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 29 Aug 2011 11:43:54 +0200 Subject: [PATCH 14/29] [IMP] orm: option to make many2many relation tables without foreign key constaint. The option is not completely flexible (no possibility to choose to have a foreign key on one of the fields. No similar option is provided for many2one either. It is seldom used (e.g. in account_followup addons). bzr revid: vmt@openerp.com-20110829094354-0yf6t6329j8e765q --- openerp/osv/fields.py | 9 ++++++++- openerp/osv/orm.py | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 1c8e2a11805..f37d40be41e 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -495,7 +495,13 @@ class many2many(_column): _classic_write = False _prefetch = False _type = 'many2many' - def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, **args): + def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, no_foreign_keys=False, **args): + """ + :param no_foreign_keys: specifies if the relation table should not have + foreign keys constaints on ``id1`` and ``id2``. This option is + useful for many2many relationships involving views instead of + regular tables. + """ _column.__init__(self, string=string, **args) self._obj = obj if '.' in rel: @@ -505,6 +511,7 @@ class many2many(_column): self._id1 = id1 self._id2 = id2 self._limit = limit + self._no_foreign_keys = no_foreign_keys def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): if not context: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 6a08b937676..b52359572a9 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2855,8 +2855,9 @@ class Model(object): 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')) + if not f._no_foreign_keys: + 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)) From abcef35c76c2f71f8ef4a927dc7ea9996b1dd550 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 29 Aug 2011 14:00:20 +0200 Subject: [PATCH 15/29] [FIX] osv: forgot to "import expression". bzr revid: vmt@openerp.com-20110829120020-b4tfvgizlednt452 --- openerp/osv/osv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 1c564cb83b5..f6d4223a0ad 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -300,6 +300,7 @@ class TransientModel(Model): # Restrict acces to the current user, except for the super-user. if self._log_access and uid != openerp.SUPERUSER: + import expression domain = expression.expression_and(('create_uid', '=', uid), domain) # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? From 12f977059cc410bb10827ca70c5bd2f142533a1f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 30 Aug 2011 16:14:21 +0200 Subject: [PATCH 16/29] [IMP] osv_memory_autovacuum: use the method instead of accessing the field directly. bzr revid: vmt@openerp.com-20110830141421-r6b48z00ae11kubr --- openerp/addons/base/ir/osv_memory_autovacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/osv_memory_autovacuum.py b/openerp/addons/base/ir/osv_memory_autovacuum.py index f15adb77a02..60d09d5e45d 100644 --- a/openerp/addons/base/ir/osv_memory_autovacuum.py +++ b/openerp/addons/base/ir/osv_memory_autovacuum.py @@ -28,7 +28,7 @@ class osv_memory_autovacuum(openerp.osv.osv.osv_memory): def power_on(self, cr, uid, context=None): for model in self.pool.obj_list(): obj = self.pool.get(model) - if obj._transient: + if obj.is_transient(): obj.vacuum(cr, uid) return True From aece39ac830d2c4f949dce923513cc1c7633197f Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 22 Sep 2011 18:50:03 +0200 Subject: [PATCH 17/29] [IMP] TransientModel: unnecessary unlink() override Workflow instance deletion is taken care of by the regular Model.unlink() method, by calling workflow.trg_delete(). bzr revid: odo@openerp.com-20110922165003-ub5761gjlnebz4yh --- openerp/osv/osv.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index f6d4223a0ad..7f2979be62e 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -288,14 +288,6 @@ class TransientModel(Model): self.vacuum(cr, uid) return super(TransientModel, self).create(cr, uid, vals, context) - def unlink(self, cr, uid, ids, context=None): - super(TransientModel, self).unlink(cr, uid, ids, context) - if isinstance(ids, (int, long)): - ids = [ids] - if ids: - cr.execute('delete from wkf_instance where res_type=%s and res_id IN %s', (self._name, tuple(ids))) - return True - def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): # Restrict acces to the current user, except for the super-user. From 7d4cdb69f256b84fe7a0e9bf78eff8b1f8e0b8ed Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 22 Sep 2011 19:20:27 +0200 Subject: [PATCH 18/29] [IMP] orm: minor cleanup: avoid duplicate functions to do the same thing bzr revid: odo@openerp.com-20110922172027-7kiae4yd2vuwwk6w --- openerp/osv/expression.py | 7 ------- openerp/osv/osv.py | 11 +++++------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index a0d10c7fe8e..bbf4569cba8 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -221,13 +221,6 @@ def OR(domains): """OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """ return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains) -# Probably bad style: the domain should be already normalized (and non-empty). -def expression_and(term, domain): - if domain: - return ['&', term] + normalize(domain) - else: - return term - def is_operator(element): """Test whether an object is a valid domain operator. """ return isinstance(element, basestring) and element in DOMAIN_OPERATORS diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 7f2979be62e..84954dcea14 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -21,19 +21,20 @@ #.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 +import expression # must not be first local import! from openerp.tools.config import config from openerp.tools.func import wraps from openerp.tools.translate import translate from openerp.osv.orm import MetaModel, Model - class except_osv(Exception): def __init__(self, name, value, exc_type='warning'): self.name = name @@ -289,11 +290,9 @@ class TransientModel(Model): return super(TransientModel, self).create(cr, uid, vals, context) def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): - # Restrict acces to the current user, except for the super-user. if self._log_access and uid != openerp.SUPERUSER: - import expression - domain = expression.expression_and(('create_uid', '=', uid), domain) + domain = expression.AND(([('create_uid', '=', uid)], domain)) # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? return super(TransientModel, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) From 2e7a1342283e2db729cae0556dc4763a53d6926d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 23 Sep 2011 14:45:25 +0200 Subject: [PATCH 19/29] [IMP] orm: introduce cleaner class hierarchy for models We'll now have a BaseModel with 3 subclasses, AbstractModel, TransientModel and Model. Model is for regular models, TransientModel for automatically-vacuumed models, and AbstractModel for common superclasses meant to be inherited by other models only, and not directly used. bzr revid: odo@openerp.com-20110923124525-jfzk55dk3ban2ps2 --- openerp/modules/loading.py | 8 +- openerp/osv/orm.py | 161 +++++++++++++++++++++++++++++++++---- openerp/osv/osv.py | 104 +----------------------- 3 files changed, 152 insertions(+), 121 deletions(-) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 24b23720119..1bdbbe3d0be 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -340,7 +340,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): for (model, name) in cr.fetchall(): model_obj = pool.get(model) if model_obj and not model_obj.is_transient(): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'The model %s (%s) has no access rules!' % (model, name)) + 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 @@ -348,7 +348,9 @@ def load_modules(db, force_demo=False, status=None, update_module=False): for (model, name) in cr.fetchall(): model_obj = pool.get(model) if model_obj.is_transient(): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'The transient model (formerly osv_memory) %s (%s) should not have explicit access rules!' % (model, name)) + import pdb + pdb.set_trace() + 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 +358,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/osv/orm.py b/openerp/osv/orm.py index 21163082ac6..b1d949765d8 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 expression 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 query import Query # List of etree._Element subclasses that we choose to ignore when parsing XML. from openerp.tools import SKIPPED_ELEMENT_TYPES @@ -585,22 +586,22 @@ class MetaModel(type): self.module_to_models.setdefault(self._module, []).append(self) -class Model(object): +class BaseModel(object): """ Base class for OpenERP models. - OpenERP models are created by inheriting from this class (or from - TransientModel) + OpenERP models are created by inheriting from this class' subclasses: - class user(Model): - ... + * 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 instanciated (because it is not a - model per se, but a class to be inherited to really create a model later), - the _register class attribute should be set to False. - + 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. @@ -847,6 +848,8 @@ class Model(object): 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'): @@ -4541,5 +4544,129 @@ class Model(object): """ return self._transient +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 + +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. + """ + __metaclass__ = MetaModel + _register = False # not visible in ORM registry, meant to be python-inherited only + _transient = True + _max_count = None + _max_hours = None + _check_time = 20 + + def __init__(self, pool, cr): + super(TransientModel, self).__init__(pool, cr) + self.check_count = 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,)) + assert self._log_access, "TransientModels must have log_access turned on, "\ + "in order to implement their access rights policy" + + def _clean_transient_rows_older_than(self, cr, seconds): + if not self._log_access: + self.logger = logging.getLogger('orm').warning( + "Transient model without write_date: %s" % (self._name,)) + return + + 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, openerp.SUPERUSER, ids) + + def _clean_old_transient_rows(self, cr, count): + if not self._log_access: + self.logger = logging.getLogger('orm').warning( + "Transient model without write_date: %s" % (self._name,)) + return + + 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, openerp.SUPERUSER, ids) + + def vacuum(self, cr, uid, force=False): + """ Clean the TransientModel records. + + This unlinks old records from the transient model tables whenever the + "_max_count" or "_max_age" conditions (if any) are reached. + Actual cleaning will happen only once every "_check_time" calls. + This means this method can be called frequently called (e.g. whenever + a new record is created). + """ + self.check_count += 1 + if (not force) and (self.check_count % self._check_time): + self.check_count = 0 + return True + + # Age-based expiration + if self._max_hours: + self._clean_transient_rows_older_than(cr, self._max_hours * 60 * 60) + + # Count-based expiration + if self._max_count: + self._clean_old_transient_rows(cr, self._max_count) + + return True + + def check_access_rule(self, cr, uid, ids, operation, context=None): + # No access rules for transient models. + if uid != openerp.SUPERUSER: + cr.execute("SELECT distinct create_uid FROM " + self._table + " WHERE" + " id IN %s", (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()) + + def create(self, cr, uid, vals, context=None): + self.vacuum(cr, uid) + return super(TransientModel, self).create(cr, uid, vals, context) + + def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): + # Restrict acces to the current user, except for the super-user. + if self._log_access and uid != openerp.SUPERUSER: + domain = expression.AND(([('create_uid', '=', uid)], domain)) + + # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? + return super(TransientModel, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) + +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: diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 84954dcea14..f827ff680fb 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -29,11 +29,9 @@ import openerp import openerp.netsvc as netsvc import openerp.pooler as pooler import openerp.sql_db as sql_db -import expression # must not be first local import! -from openerp.tools.config import config from openerp.tools.func import wraps from openerp.tools.translate import translate -from openerp.osv.orm import MetaModel, Model +from openerp.osv.orm import MetaModel, Model, TransientModel, AbstractModel class except_osv(Exception): def __init__(self, name, value, exc_type='warning'): @@ -201,106 +199,10 @@ class object_proxy(netsvc.Service): cr.close() return res - -class TransientModel(Model): - """ Model for transient records. - - A TransientModel works similarly to a regular Model but the assiociated - records will be cleaned automatically from the database after some time. - - A TransientModel has no access rules. - - """ - __metaclass__ = MetaModel - _register = False # Set to false if the model shouldn't be automatically discovered. - _transient = True - _max_count = None - _max_hours = None - _check_time = 20 - - def __init__(self, pool, cr): - super(TransientModel, self).__init__(pool, cr) - self.check_count = 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 _clean_transient_rows_older_than(self, cr, seconds): - if not self._log_access: - self.logger = logging.getLogger('orm').warning( - "Transient model without write_date: %s" % (self._name,)) - return - - 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, openerp.SUPERUSER, ids) - - def _clean_old_transient_rows(self, cr, count): - if not self._log_access: - self.logger = logging.getLogger('orm').warning( - "Transient model without write_date: %s" % (self._name,)) - return - - 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, openerp.SUPERUSER, ids) - - def vacuum(self, cr, uid, force=False): - """ Clean the TransientModel records. - - This unlinks old records from the transient model tables whenever the - "_max_count" or "_max_age" conditions (if any) are reached. - Actual cleaning will happen only once every "_check_time" calls. - This means this method can be called frequently called (e.g. whenever - a new record is created). - """ - self.check_count += 1 - if (not force) and (self.check_count % self._check_time): - self.check_count = 0 - return True - - # Age-based expiration - if self._max_hours: - self._clean_transient_rows_older_than(cr, self._max_hours * 60 * 60) - - # Count-based expiration - if self._max_count: - self._clean_old_transient_rows(cr, self._max_count) - - return True - - def check_access_rule(self, cr, uid, ids, operation, context=None): - # No access rules for transient models. - if self._log_access and uid != openerp.SUPERUSER: - cr.execute("SELECT distinct create_uid FROM " + self._table + " WHERE" - " id IN %s", (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).' % mode.capitalize()) - - def create(self, cr, uid, vals, context=None): - self.vacuum(cr, uid) - return super(TransientModel, self).create(cr, uid, vals, context) - - def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): - # Restrict acces to the current user, except for the super-user. - if self._log_access and uid != openerp.SUPERUSER: - domain = expression.AND(([('create_uid', '=', uid)], domain)) - - # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? - return super(TransientModel, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) - - -# For backward compatibility. +# deprecated - for backward compatibility. osv = Model osv_memory = TransientModel +osv_abstract = AbstractModel # ;-) def start_object_proxy(): From 2fbd4dfcdf98583611853346f03b9a6dc5febd52 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 23 Sep 2011 15:44:13 +0200 Subject: [PATCH 20/29] [FIX] loading: remove leftover debugging breakpoint bzr revid: odo@openerp.com-20110923134413-d71l4y5265z3nwcb --- openerp/modules/loading.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 1bdbbe3d0be..9f6c517dca7 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -348,8 +348,6 @@ def load_modules(db, force_demo=False, status=None, update_module=False): for (model, name) in cr.fetchall(): model_obj = pool.get(model) if model_obj.is_transient(): - import pdb - pdb.set_trace() 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") From 49be41ecebe002ecbc41d58212d2914bad0cd67f Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 23 Sep 2011 16:55:17 +0200 Subject: [PATCH 21/29] [FIX] orm.py: missing patch, lost during last merge See corresponding patch from trunk at revision 3612 revid:odo@openerp.com-20110919091952-05lfl2kncr3ep9nj bzr revid: odo@openerp.com-20110923145517-odyq9z1h5ibck6nu --- openerp/osv/orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index b1d949765d8..1f441640059 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -924,7 +924,7 @@ class BaseModel(object): if f == order: ok = False if ok: - self.pool._store_function[object].append( (self._name, store_field, fnct, fields2, order, length)) + 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: From 32b50ddca0fdbcc5838f855195e5743273066fcb Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 23 Sep 2011 17:45:14 +0200 Subject: [PATCH 22/29] [FIX] ir.model: typo - ir.model.model is the model `identifier` bzr revid: odo@openerp.com-20110923154514-8dzodtebfik892g3 --- openerp/addons/base/ir/ir_model.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 807ec56ec10..395bf3e8d60 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -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 model_obj.is_transient(): + # 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 = [] From f2f3c916c98721aafad9115d6ab4e17067b3edb7 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 23 Sep 2011 18:29:29 +0200 Subject: [PATCH 23/29] [REM] orm: m2m._no_foreign_keys attribute removed, replaced by SQL check bzr revid: odo@openerp.com-20110923162929-nhl32ie6zc6uuj8h --- openerp/osv/fields.py | 9 +-------- openerp/osv/orm.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 0b4e803a072..b29e3c11f4f 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -497,13 +497,7 @@ class many2many(_column): _classic_write = False _prefetch = False _type = 'many2many' - def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, no_foreign_keys=False, **args): - """ - :param no_foreign_keys: specifies if the relation table should not have - foreign keys constaints on ``id1`` and ``id2``. This option is - useful for many2many relationships involving views instead of - regular tables. - """ + def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, **args): _column.__init__(self, string=string, **args) self._obj = obj if '.' in rel: @@ -513,7 +507,6 @@ class many2many(_column): self._id1 = id1 self._id2 = id2 self._limit = limit - self._no_foreign_keys = no_foreign_keys def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): if not context: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 1f441640059..9d7a830686e 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2895,9 +2895,15 @@ class BaseModel(object): 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)) - if not f._no_foreign_keys: - self._foreign_keys.append((f._rel, f._id1, self._table, 'CASCADE')) + + # 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._foreign_keys.append((f._rel, f._id2, ref, 'CASCADE')) + cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (self._table,)) + if not cr.fetchall(): + self._foreign_keys.append((f._rel, f._id1, self._table, '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)) From 3b974972a3fa7c7ff55be9893e6c6ed24caa9059 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 23 Sep 2011 18:30:27 +0200 Subject: [PATCH 24/29] [IMP] review/cleanup + fix TransientModels inheritance bzr revid: odo@openerp.com-20110923163027-50ov6qj3daax8hpi --- openerp/osv/orm.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 9d7a830686e..4f7a0aeca45 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -4562,6 +4562,7 @@ class Model(BaseModel): 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 @@ -4572,7 +4573,6 @@ class TransientModel(BaseModel): records they created. The super-user has unrestricted access to all TransientModel records. """ - __metaclass__ = MetaModel _register = False # not visible in ORM registry, meant to be python-inherited only _transient = True _max_count = None @@ -4638,10 +4638,15 @@ class TransientModel(BaseModel): return True def check_access_rule(self, cr, uid, ids, operation, context=None): - # No access rules for transient models. + # 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. if uid != openerp.SUPERUSER: - cr.execute("SELECT distinct create_uid FROM " + self._table + " WHERE" - " id IN %s", (tuple(ids),)) + 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 ' @@ -4653,11 +4658,9 @@ class TransientModel(BaseModel): return super(TransientModel, self).create(cr, uid, vals, context) def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): - # Restrict acces to the current user, except for the super-user. + # Restrict acces to the current user, except for the super-user if self._log_access and uid != openerp.SUPERUSER: domain = expression.AND(([('create_uid', '=', uid)], domain)) - - # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid? return super(TransientModel, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) class AbstractModel(BaseModel): From 334dee04791f886b1503198fce52846715941db3 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 23 Sep 2011 19:40:18 +0200 Subject: [PATCH 25/29] [IMP] orm: fix Model hierarchy to avoid TransientModel leaking downstream bzr revid: odo@openerp.com-20110923174018-8l2b4qc1vecgs0sc --- .../addons/base/ir/osv_memory_autovacuum.py | 7 +- openerp/modules/module.py | 3 - openerp/osv/orm.py | 196 +++++++++--------- 3 files changed, 97 insertions(+), 109 deletions(-) diff --git a/openerp/addons/base/ir/osv_memory_autovacuum.py b/openerp/addons/base/ir/osv_memory_autovacuum.py index 60d09d5e45d..1231ef5f233 100644 --- a/openerp/addons/base/ir/osv_memory_autovacuum.py +++ b/openerp/addons/base/ir/osv_memory_autovacuum.py @@ -26,9 +26,8 @@ class osv_memory_autovacuum(openerp.osv.osv.osv_memory): _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 obj.is_transient(): - obj.vacuum(cr, uid) + for model in self.pool.models.values(): + if model.is_transient(): + obj._transient_vacuum(cr, uid) return True diff --git a/openerp/modules/module.py b/openerp/modules/module.py index 6ce0374814f..2bf86318fa4 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -271,7 +271,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 = [] @@ -284,8 +283,6 @@ def init_module_models(cr, module_name, obj_list): cr.commit() for obj in obj_list: obj._auto_end(cr, {'module': module_name}) - if obj._transient: - obj.vacuum(cr, openerp.SUPERUSER) cr.commit() todo.sort() for t in todo: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 4f7a0aeca45..d0f012bd112 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -605,7 +605,6 @@ class BaseModel(object): """ __metaclass__ = MetaModel _register = False # Set to false if the model shouldn't be automatically discovered. - _transient = False # True in a TransientModel _name = None _columns = {} _constraints = [] @@ -619,6 +618,12 @@ class BaseModel(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 = {} @@ -646,7 +651,6 @@ class BaseModel(object): __logger = logging.getLogger('orm') __schema = logging.getLogger('orm.schema') - CONCURRENCY_CHECK_FIELD = '__last_update' def log(self, cr, uid, id, message, secondary=False, context=None): @@ -974,6 +978,13 @@ class BaseModel(object): 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: @@ -3390,17 +3401,35 @@ class BaseModel(object): :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 == openerp.SUPERUSER: + 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): """ @@ -3771,6 +3800,10 @@ class BaseModel(object): """ 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) @@ -4257,6 +4290,10 @@ class BaseModel(object): 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 != openerp.SUPERUSER: + 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) @@ -4542,6 +4579,7 @@ class BaseModel(object): results[k] = '' return results + # Transience def is_transient(self): """ Return whether the model is transient. @@ -4550,6 +4588,48 @@ class BaseModel(object): """ 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, openerp.SUPERUSER, 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, openerp.SUPERUSER, 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 + class Model(BaseModel): """Main super-class for regular database-persisted OpenERP models. @@ -4575,93 +4655,6 @@ class TransientModel(BaseModel): """ _register = False # not visible in ORM registry, meant to be python-inherited only _transient = True - _max_count = None - _max_hours = None - _check_time = 20 - - def __init__(self, pool, cr): - super(TransientModel, self).__init__(pool, cr) - self.check_count = 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,)) - assert self._log_access, "TransientModels must have log_access turned on, "\ - "in order to implement their access rights policy" - - def _clean_transient_rows_older_than(self, cr, seconds): - if not self._log_access: - self.logger = logging.getLogger('orm').warning( - "Transient model without write_date: %s" % (self._name,)) - return - - 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, openerp.SUPERUSER, ids) - - def _clean_old_transient_rows(self, cr, count): - if not self._log_access: - self.logger = logging.getLogger('orm').warning( - "Transient model without write_date: %s" % (self._name,)) - return - - 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, openerp.SUPERUSER, ids) - - def vacuum(self, cr, uid, force=False): - """ Clean the TransientModel records. - - This unlinks old records from the transient model tables whenever the - "_max_count" or "_max_age" conditions (if any) are reached. - Actual cleaning will happen only once every "_check_time" calls. - This means this method can be called frequently called (e.g. whenever - a new record is created). - """ - self.check_count += 1 - if (not force) and (self.check_count % self._check_time): - self.check_count = 0 - return True - - # Age-based expiration - if self._max_hours: - self._clean_transient_rows_older_than(cr, self._max_hours * 60 * 60) - - # Count-based expiration - if self._max_count: - self._clean_old_transient_rows(cr, self._max_count) - - return True - - def check_access_rule(self, cr, uid, ids, operation, context=None): - # 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. - if uid != openerp.SUPERUSER: - 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()) - - def create(self, cr, uid, vals, context=None): - self.vacuum(cr, uid) - return super(TransientModel, self).create(cr, uid, vals, context) - - def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): - # Restrict acces to the current user, except for the super-user - if self._log_access and uid != openerp.SUPERUSER: - domain = expression.AND(([('create_uid', '=', uid)], domain)) - return super(TransientModel, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid) class AbstractModel(BaseModel): """Abstract Model super-class for creating an abstract class meant to be @@ -4677,5 +4670,4 @@ class AbstractModel(BaseModel): _register = False # not visible in ORM registry, meant to be python-inherited only -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file From 7fad8267613459df69efced6137a855975ea26b2 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Sat, 24 Sep 2011 04:59:51 +0200 Subject: [PATCH 26/29] [FIX] osv_memory_autovacuum: typo bzr revid: odo@openerp.com-20110924025951-zn3i5vgux35urbyp --- openerp/addons/base/ir/osv_memory_autovacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/osv_memory_autovacuum.py b/openerp/addons/base/ir/osv_memory_autovacuum.py index 1231ef5f233..975fcd0a403 100644 --- a/openerp/addons/base/ir/osv_memory_autovacuum.py +++ b/openerp/addons/base/ir/osv_memory_autovacuum.py @@ -28,6 +28,6 @@ class osv_memory_autovacuum(openerp.osv.osv.osv_memory): def power_on(self, cr, uid, context=None): for model in self.pool.models.values(): if model.is_transient(): - obj._transient_vacuum(cr, uid) + model._transient_vacuum(cr, uid) return True From 6462c85f6cc358c726b4677c6ca882a77d2cd37f Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 26 Sep 2011 11:01:56 +0200 Subject: [PATCH 27/29] [IMP] start unifying the SUPERUSER_ID constant bzr revid: odo@openerp.com-20110926090156-2smbh0ypzitauhmt --- openerp/__init__.py | 5 ++--- openerp/addons/base/ir/ir_rule.py | 9 ++++----- openerp/osv/orm.py | 28 +++++++++++++--------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/openerp/__init__.py b/openerp/__init__.py index b74885674fa..919dc4d9816 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 @@ -42,8 +44,5 @@ import tools import wizard import workflow -# The hard-coded super-user id (a.k.a. administrator, or root user). -SUPERUSER = 1 - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_rule.py b/openerp/addons/base/ir/ir_rule.py index 9c1e3f79c4f..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' @@ -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/osv/orm.py b/openerp/osv/orm.py index d0bbc630357..5ca0030ca8d 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -62,6 +62,7 @@ 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. @@ -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 = {} @@ -2465,7 +2463,7 @@ class BaseModel(object): 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] @@ -2671,7 +2669,7 @@ class BaseModel(object): # 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] @@ -2756,7 +2754,7 @@ class BaseModel(object): # 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] @@ -3405,7 +3403,7 @@ class BaseModel(object): :raise except_orm: * if current ir.rules do not permit this operation. :return: None if the operation is allowed """ - if uid == openerp.SUPERUSER: + if uid == SUPERUSER_ID: return if self.is_transient: @@ -3484,10 +3482,10 @@ class BaseModel(object): # 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, @@ -4004,7 +4002,7 @@ class BaseModel(object): 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_]) @@ -4071,7 +4069,7 @@ class BaseModel(object): 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(): @@ -4097,7 +4095,7 @@ class BaseModel(object): 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(): @@ -4294,7 +4292,7 @@ class BaseModel(object): 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 != openerp.SUPERUSER: + 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) @@ -4597,7 +4595,7 @@ class BaseModel(object): " COALESCE(write_date, create_date, now())::timestamp <" " (now() - interval %s)", ("%s seconds" % seconds,)) ids = [x[0] for x in cr.fetchall()] - self.unlink(cr, openerp.SUPERUSER, ids) + 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 @@ -4606,7 +4604,7 @@ class BaseModel(object): " AS t FROM " + self._table + " ORDER BY t LIMIT %s", (count,)) ids = [x[0] for x in cr.fetchall()] - self.unlink(cr, openerp.SUPERUSER, ids) + self.unlink(cr, SUPERUSER_ID, ids) def _transient_vacuum(self, cr, uid, force=False): """Clean the transient records. From f81fe2dc6e81d052add0d8a61c51a75060e6623b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 26 Sep 2011 13:12:26 +0200 Subject: [PATCH 28/29] [IMP] orm: enforce proper m2o ondelete rules between TransientModels and Models bzr revid: odo@openerp.com-20110926111226-fu834vqwm4gbxk8l --- openerp/osv/fields.py | 4 ++-- openerp/osv/orm.py | 40 ++++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index b29e3c11f4f..38d87b06a8b 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 diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 5ca0030ca8d..5104922802f 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2518,6 +2518,25 @@ class BaseModel(object): 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): """ @@ -2716,7 +2735,8 @@ class BaseModel(object): 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 ' @@ -2735,9 +2755,9 @@ class BaseModel(object): "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) @@ -2775,12 +2795,11 @@ class BaseModel(object): 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: @@ -2906,16 +2925,17 @@ class BaseModel(object): 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 + 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' % (f._rel, f._id1, f._id2, f._id1, f._id2)) # 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._foreign_keys.append((f._rel, f._id2, ref, 'CASCADE')) + self._m2o_add_foreign_key_unchecked(f._rel, f._id2, dest_model, 'cascade') cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (self._table,)) if not cr.fetchall(): - self._foreign_keys.append((f._rel, f._id1, self._table, 'CASCADE')) + self._m2o_add_foreign_key_unchecked(f._rel, f._id1, self, '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)) From ad4a216e7d301fe0737be43fab511375c5677114 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 26 Sep 2011 19:14:51 +0200 Subject: [PATCH 29/29] [IMP] fields.m2m: support implicit generation of m2m SQL names This allows for transparent inheritance of m2m columns via _inherit, as long as the m2m relationship table is not explicitly named in the declaration. bzr revid: odo@openerp.com-20110926171451-n2jg8pbl5mq715vk --- openerp/osv/fields.py | 77 ++++++++++++++++++++++++++++++++++--------- openerp/osv/orm.py | 20 +++++------ 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 38d87b06a8b..6619e66b075 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -493,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 @@ -508,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: @@ -521,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) @@ -551,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, @@ -566,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) @@ -598,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 diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 5104922802f..4a56c83b352 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2921,28 +2921,28 @@ class BaseModel(object): 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,)) + 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' % (f._rel, f._id1, f._id2, f._id1, f._id2)) + 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(f._rel, f._id2, dest_model, 'cascade') + 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(f._rel, f._id1, self, 'cascade') + self._m2o_add_foreign_key_unchecked(m2m_tbl, col1, self, '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)) + 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):