1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 #.apidoc title: Objects Services (OSV)
26 import openerp.netsvc as netsvc
27 import openerp.pooler as pooler
28 import openerp.sql_db as sql_db
30 from psycopg2 import IntegrityError, errorcodes
31 from openerp.tools.config import config
32 from openerp.tools.func import wraps
33 from openerp.tools.translate import translate
34 from openerp.osv.orm import MetaModel, Model
37 class except_osv(Exception):
38 def __init__(self, name, value, exc_type='warning'):
40 self.exc_type = exc_type
42 self.args = (exc_type, name)
45 class object_proxy(netsvc.Service):
47 self.logger = logging.getLogger('web-services')
48 netsvc.Service.__init__(self, 'object_proxy', audience='')
49 self.exportMethod(self.exec_workflow)
50 self.exportMethod(self.execute)
54 def wrapper(self, dbname, *args, **kwargs):
55 """ Wraps around OSV functions and normalises a few exceptions
59 # We try to do the same as the _(), but without the frame
60 # inspection, since we aready are wrapping an osv function
61 # trans_obj = self.get('ir.translation') cannot work yet :(
64 if args and isinstance(args[-1], dict):
66 elif isinstance(kwargs, dict):
67 ctx = kwargs.get('context', {})
70 if args and isinstance(args[0], (long, int)):
73 lang = ctx and ctx.get('lang')
74 if not (lang or hasattr(src, '__call__')):
77 # We open a *new* cursor here, one reason is that failed SQL
78 # queries (as in IntegrityError) will invalidate the current one.
81 if hasattr(src, '__call__'):
82 # callable. We need to find the right parameters to call
83 # the orm._sql_message(self, cr, uid, ids, context) function,
85 # our signature is f(osv_pool, dbname [,uid, obj, method, args])
87 if args and len(args) > 1:
88 obj = self.get(args[1])
89 if len(args) > 3 and isinstance(args[3], (long, int, list)):
93 cr = sql_db.db_connect(dbname).cursor()
94 return src(obj, cr, uid, ids, context=(ctx or {}))
100 return False # so that the original SQL error will
101 # be returned, it is the best we have.
104 cr = sql_db.db_connect(dbname).cursor()
105 res = translate(cr, name=False, source_type=ttype,
106 lang=lang, source=src)
115 return tr(src, 'code')
118 if pooler.get_pool(dbname)._init:
119 raise except_osv('Database not ready', 'Currently, this database is not fully loaded and can not be used.')
120 return f(self, dbname, *args, **kwargs)
121 except orm.except_orm, inst:
122 if inst.name == 'AccessError':
123 self.logger.debug("AccessError", exc_info=True)
124 self.abortResponse(1, inst.name, 'warning', inst.value)
125 except except_osv, inst:
126 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
127 except IntegrityError, inst:
128 osv_pool = pooler.get_pool(dbname)
129 for key in osv_pool._sql_error.keys():
131 self.abortResponse(1, _('Constraint Error'), 'warning',
132 tr(osv_pool._sql_error[key], 'sql_constraint') or inst[0])
133 if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
134 msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
135 self.logger.debug("IntegrityError", exc_info=True)
137 errortxt = inst.pgerror.replace('«','"').replace('»','"')
138 if '"public".' in errortxt:
139 context = errortxt.split('"public".')[1]
140 model_name = table = context.split('"')[1]
142 last_quote_end = errortxt.rfind('"')
143 last_quote_begin = errortxt.rfind('"', 0, last_quote_end)
144 model_name = table = errortxt[last_quote_begin+1:last_quote_end].strip()
145 model = table.replace("_",".")
146 model_obj = osv_pool.get(model)
148 model_name = model_obj._description or model_obj._name
149 msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
152 self.abortResponse(1, _('Integrity Error'), 'warning', msg)
154 self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
156 self.logger.exception("Uncaught exception")
161 def execute_cr(self, cr, uid, obj, method, *args, **kw):
162 object = pooler.get_pool(cr.dbname).get(obj)
164 raise except_osv('Object Error', 'Object %s doesn\'t exist' % str(obj))
165 return getattr(object, method)(cr, uid, *args, **kw)
168 def execute(self, db, uid, obj, method, *args, **kw):
169 cr = pooler.get_db(db).cursor()
172 if method.startswith('_'):
173 raise except_osv('Access Denied', 'Private methods (such as %s) cannot be called remotely.' % (method,))
174 res = self.execute_cr(cr, uid, obj, method, *args, **kw)
176 self.logger.warning('The method %s of the object %s can not return `None` !', method, obj)
185 def exec_workflow_cr(self, cr, uid, obj, method, *args):
186 wf_service = netsvc.LocalService("workflow")
187 return wf_service.trg_validate(uid, obj, args[0], method, cr)
190 def exec_workflow(self, db, uid, obj, method, *args):
191 cr = pooler.get_db(db).cursor()
194 res = self.exec_workflow_cr(cr, uid, obj, method, *args)
204 class TransientModel(Model):
205 """ Model for transient records.
207 A TransientModel works similarly to a regular Model but the assiociated
208 records will be cleaned automatically from the database after some time.
210 A TransientModel has no access rules.
213 __metaclass__ = MetaModel
214 _register = False # Set to false if the model shouldn't be automatically discovered.
220 def __init__(self, pool, cr):
221 super(TransientModel, self).__init__(pool, cr)
223 self._max_count = config.get('osv_memory_count_limit')
224 self._max_hours = config.get('osv_memory_age_limit')
225 cr.execute('delete from wkf_instance where res_type=%s', (self._name,))
227 def _clean_transient_rows_older_than(self, cr, seconds):
228 if not self._log_access:
229 self.logger = logging.getLogger('orm').warning(
230 "Transient model without write_date: %s" % (self._name,))
233 cr.execute("SELECT id FROM " + self._table + " WHERE"
234 " COALESCE(write_date, create_date, now())::timestamp <"
235 " (now() - interval %s)", ("%s seconds" % seconds,))
236 ids = [x[0] for x in cr.fetchall()]
237 self.unlink(cr, openerp.SUPERUSER, ids)
239 def _clean_old_transient_rows(self, cr, count):
240 if not self._log_access:
241 self.logger = logging.getLogger('orm').warning(
242 "Transient model without write_date: %s" % (self._name,))
246 "SELECT id, COALESCE(write_date, create_date, now())::timestamp"
247 " AS t FROM " + self._table +
248 " ORDER BY t LIMIT %s", (count,))
249 ids = [x[0] for x in cr.fetchall()]
250 self.unlink(cr, openerp.SUPERUSER, ids)
252 def vacuum(self, cr, uid, force=False):
253 """ Clean the TransientModel records.
255 This unlinks old records from the transient model tables whenever the
256 "_max_count" or "_max_age" conditions (if any) are reached.
257 Actual cleaning will happen only once every "_check_time" calls.
258 This means this method can be called frequently called (e.g. whenever
259 a new record is created).
261 self.check_count += 1
262 if (not force) and (self.check_count % self._check_time):
266 # Age-based expiration
268 self._clean_transient_rows_older_than(cr, self._max_hours * 60 * 60)
270 # Count-based expiration
272 self._clean_old_transient_rows(cr, self._max_count)
276 def check_access_rule(self, cr, uid, ids, operation, context=None):
277 # No access rules for transient models.
278 if self._log_access and uid != openerp.SUPERUSER:
279 cr.execute("SELECT distinct create_uid FROM " + self._table + " WHERE"
280 " id IN %s", (tuple(ids),))
281 uids = [x[0] for x in cr.fetchall()]
282 if len(uids) != 1 or uids[0] != uid:
283 raise orm.except_orm(_('AccessError'), '%s access is '
284 'restricted to your own records for transient models '
285 '(except for the super-user).' % mode.capitalize())
287 def create(self, cr, uid, vals, context=None):
289 return super(TransientModel, self).create(cr, uid, vals, context)
291 def unlink(self, cr, uid, ids, context=None):
292 super(TransientModel, self).unlink(cr, uid, ids, context)
293 if isinstance(ids, (int, long)):
296 cr.execute('delete from wkf_instance where res_type=%s and res_id IN %s', (self._name, tuple(ids)))
299 def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
301 # Restrict acces to the current user, except for the super-user.
302 if self._log_access and uid != openerp.SUPERUSER:
304 domain = expression.expression_and(('create_uid', '=', uid), domain)
306 # TODO unclear: shoudl access_rights_uid be set to None (effectively ignoring it) or used instead of uid?
307 return super(TransientModel, self)._search(cr, uid, domain, offset, limit, order, context, count, access_rights_uid)
310 # For backward compatibility.
312 osv_memory = TransientModel
315 def start_object_proxy():
318 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: