[FIX] osv: forgot to "import expression".
[odoo/odoo.git] / openerp / osv / osv.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 #.apidoc title: Objects Services (OSV)
23
24 import orm
25 import openerp
26 import openerp.netsvc as netsvc
27 import openerp.pooler as pooler
28 import openerp.sql_db as sql_db
29 import logging
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
35
36
37 class except_osv(Exception):
38     def __init__(self, name, value, exc_type='warning'):
39         self.name = name
40         self.exc_type = exc_type
41         self.value = value
42         self.args = (exc_type, name)
43
44
45 class object_proxy(netsvc.Service):
46     def __init__(self):
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)
51
52     def check(f):
53         @wraps(f)
54         def wrapper(self, dbname, *args, **kwargs):
55             """ Wraps around OSV functions and normalises a few exceptions
56             """
57
58             def tr(src, ttype):
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 :(
62                 ctx = {}
63                 if not kwargs:
64                     if args and isinstance(args[-1], dict):
65                         ctx = args[-1]
66                 elif isinstance(kwargs, dict):
67                     ctx = kwargs.get('context', {})
68
69                 uid = 1
70                 if args and isinstance(args[0], (long, int)):
71                     uid = args[0]
72
73                 lang = ctx and ctx.get('lang')
74                 if not (lang or hasattr(src, '__call__')):
75                     return src
76
77                 # We open a *new* cursor here, one reason is that failed SQL
78                 # queries (as in IntegrityError) will invalidate the current one.
79                 cr = False
80
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,
84                     # or we skip..
85                     # our signature is f(osv_pool, dbname [,uid, obj, method, args])
86                     try:
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)):
90                                 ids = args[3]
91                             else:
92                                 ids = []
93                         cr = sql_db.db_connect(dbname).cursor()
94                         return src(obj, cr, uid, ids, context=(ctx or {}))
95                     except Exception:
96                         pass
97                     finally:
98                         if cr: cr.close()
99
100                     return False # so that the original SQL error will
101                                  # be returned, it is the best we have.
102
103                 try:
104                     cr = sql_db.db_connect(dbname).cursor()
105                     res = translate(cr, name=False, source_type=ttype,
106                                     lang=lang, source=src)
107                     if res:
108                         return res
109                     else:
110                         return src
111                 finally:
112                     if cr: cr.close()
113
114             def _(src):
115                 return tr(src, 'code')
116
117             try:
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():
130                     if key in inst[0]:
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)
136                     try:
137                         errortxt = inst.pgerror.replace('«','"').replace('»','"')
138                         if '"public".' in errortxt:
139                             context = errortxt.split('"public".')[1]
140                             model_name = table = context.split('"')[1]
141                         else:
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)
147                         if model_obj:
148                             model_name = model_obj._description or model_obj._name
149                         msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
150                     except Exception:
151                         pass
152                     self.abortResponse(1, _('Integrity Error'), 'warning', msg)
153                 else:
154                     self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
155             except Exception:
156                 self.logger.exception("Uncaught exception")
157                 raise
158
159         return wrapper
160
161     def execute_cr(self, cr, uid, obj, method, *args, **kw):
162         object = pooler.get_pool(cr.dbname).get(obj)
163         if not object:
164             raise except_osv('Object Error', 'Object %s doesn\'t exist' % str(obj))
165         return getattr(object, method)(cr, uid, *args, **kw)
166
167     @check
168     def execute(self, db, uid, obj, method, *args, **kw):
169         cr = pooler.get_db(db).cursor()
170         try:
171             try:
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)
175                 if res is None:
176                     self.logger.warning('The method %s of the object %s can not return `None` !', method, obj)
177                 cr.commit()
178             except Exception:
179                 cr.rollback()
180                 raise
181         finally:
182             cr.close()
183         return res
184
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)
188
189     @check
190     def exec_workflow(self, db, uid, obj, method, *args):
191         cr = pooler.get_db(db).cursor()
192         try:
193             try:
194                 res = self.exec_workflow_cr(cr, uid, obj, method, *args)
195                 cr.commit()
196             except Exception:
197                 cr.rollback()
198                 raise
199         finally:
200             cr.close()
201         return res
202
203
204 class TransientModel(Model):
205     """ Model for transient records.
206
207     A TransientModel works similarly to a regular Model but the assiociated
208     records will be cleaned automatically from the database after some time.
209
210     A TransientModel has no access rules.
211
212     """
213     __metaclass__ = MetaModel
214     _register = False # Set to false if the model shouldn't be automatically discovered.
215     _transient = True
216     _max_count = None
217     _max_hours = None
218     _check_time = 20
219
220     def __init__(self, pool, cr):
221         super(TransientModel, self).__init__(pool, cr)
222         self.check_count = 0
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,))
226
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,))
231             return
232
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)
238
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,))
243             return
244
245         cr.execute(
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)
251
252     def vacuum(self, cr, uid, force=False):
253         """ Clean the TransientModel records.
254
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).
260         """
261         self.check_count += 1
262         if (not force) and (self.check_count % self._check_time):
263             self.check_count = 0
264             return True
265
266         # Age-based expiration
267         if self._max_hours:
268             self._clean_transient_rows_older_than(cr, self._max_hours * 60 * 60)
269
270         # Count-based expiration
271         if self._max_count:
272             self._clean_old_transient_rows(cr, self._max_count)
273
274         return True
275
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())
286
287     def create(self, cr, uid, vals, context=None):
288         self.vacuum(cr, uid)
289         return super(TransientModel, self).create(cr, uid, vals, context)
290
291     def unlink(self, cr, uid, ids, context=None):
292         super(TransientModel, self).unlink(cr, uid, ids, context)
293         if isinstance(ids, (int, long)):
294             ids = [ids]
295         if ids:
296             cr.execute('delete from wkf_instance where res_type=%s and res_id IN %s', (self._name, tuple(ids)))
297         return True
298
299     def _search(self, cr, uid, domain, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
300
301         # Restrict acces to the current user, except for the super-user.
302         if self._log_access and uid != openerp.SUPERUSER:
303             import expression
304             domain = expression.expression_and(('create_uid', '=', uid), domain)
305
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)
308
309
310 # For backward compatibility.
311 osv = Model
312 osv_memory = TransientModel
313
314
315 def start_object_proxy():
316     object_proxy()
317
318 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
319