OSV: Fix callable _sql_constraint messages
[odoo/odoo.git] / bin / 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 #
23 # OSV: Objects Services
24 #
25
26 import orm
27 import netsvc
28 import pooler
29 import copy
30 import sys
31 import traceback
32 import logging
33 from psycopg2 import IntegrityError, errorcodes
34 from tools.func import wraps
35 from tools.translate import translate
36
37 module_list = []
38 module_class_list = {}
39 class_pool = {}
40
41 class except_osv(Exception):
42     def __init__(self, name, value, exc_type='warning'):
43         self.name = name
44         self.exc_type = exc_type
45         self.value = value
46         self.args = (exc_type, name)
47
48
49 class osv_pool(netsvc.Service):
50
51     def check(f):
52         @wraps(f)
53         def wrapper(self, dbname, *args, **kwargs):
54             """ Wraps around OSV functions and normalises a few exceptions
55             """
56
57             def tr(src, ttype):
58                 # We try to do the same as the _(), but without the frame
59                 # inspection, since we aready are wrapping an osv function
60                 # trans_obj = self.get('ir.translation') cannot work yet :(
61                 ctx = {}
62                 if not kwargs:
63                     if args and isinstance(args[-1], dict):
64                         ctx = args[-1]
65                     else:
66                         ctx = {}
67                 elif isinstance(kwargs, dict):
68                     ctx = kwargs.get('context', {})
69                 else:
70                     ctx = {}
71
72                 uid = 1
73                 if args and isinstance(args[0], (long, int)):
74                     uid = args[0]
75
76                 lang = ctx and ctx.get('lang', False)
77                 if not (lang or hasattr(src, '__call__')):
78                     return src
79
80                 # We open a *new* cursor here, one reason is that failed SQL
81                 # queries (as in IntegrityError) will invalidate the current one.
82                 cr = False
83                 
84                 if hasattr(src, '__call__'):
85                     # callable. We need to find the right parameters to call
86                     # the  orm._sql_message(self, cr, uid, ids, context) function,
87                     # or we skip..
88                     # our signature is f(osv_pool, dbname [,uid, obj, method, args])
89                     try:
90                         if args and len(args) > 1:
91                             obj = self.get(args[1])
92                             if len(args) > 3 and isinstance(args[3], (long, int, list)):
93                                 ids = args[3]
94                             else:
95                                 ids = []
96                         cr = pooler.get_db_only(dbname).cursor()
97                         return src(obj, cr, uid, ids, context=(ctx or {}))
98                     except Exception, e:
99                         pass
100                     finally:
101                         if cr: cr.close()
102                    
103                     return False # so that the original SQL error will
104                                  # be returned, it is the best we have.
105
106                 try:
107                     cr = pooler.get_db_only(dbname).cursor()
108                     #return trans_obj._get_source( name=?)
109                     res = translate(cr, name=False, source_type=ttype,
110                                     lang=lang, source=src)
111                     if res:
112                         return res
113                     else:
114                         return src
115                 finally:
116                     if cr: cr.close()
117
118             def _(src):
119                 return tr(src, 'code')
120
121             try:
122                 if not pooler.get_pool(dbname)._ready:
123                     raise except_osv('Database not ready', 'Currently, this database is not fully loaded and can not be used.')
124                 return f(self, dbname, *args, **kwargs)
125             except orm.except_orm, inst:
126                 if inst.name == 'AccessError':
127                     self.logger.debug("AccessError", exc_info=True)
128                 self.abortResponse(1, inst.name, 'warning', inst.value)
129             except except_osv, inst:
130                 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
131             except IntegrityError, inst:
132                 for key in self._sql_error.keys():
133                     if key in inst[0]:
134                         self.abortResponse(1, _('Constraint Error'), 'warning',
135                                         tr(self._sql_error[key], 'sql_constraint') or inst[0])
136                 if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
137                     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')
138                     self.logger.debug("IntegrityError", exc_info=True)
139                     try:
140                         errortxt = inst.pgerror.replace('«','"').replace('»','"')
141                         if '"public".' in errortxt:
142                             context = errortxt.split('"public".')[1]
143                             model_name = table = context.split('"')[1]
144                         else:
145                             last_quote_end = errortxt.rfind('"')
146                             last_quote_begin = errortxt.rfind('"', 0, last_quote_end)
147                             model_name = table = errortxt[last_quote_begin+1:last_quote_end].strip()
148                             print "MODEL", last_quote_begin, last_quote_end, model_name
149                         model = table.replace("_",".")
150                         model_obj = self.get(model)
151                         if model_obj:
152                             model_name = model_obj._description or model_obj._name
153                         msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
154                     except Exception:
155                         pass
156                     self.abortResponse(1, _('Integrity Error'), 'warning', msg)
157                 else:
158                     self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
159             except Exception, e:
160                 self.logger.exception("Uncaught exception")
161                 raise
162
163         return wrapper
164
165
166     def __init__(self):
167         self._ready = False
168         self.obj_pool = {}
169         self.module_object_list = {}
170         self.created = []
171         self._sql_error = {}
172         self._store_function = {}
173         self._init = True
174         self._init_parent = {}
175         self.logger = logging.getLogger("web-services")
176         netsvc.Service.__init__(self, 'object_proxy', audience='')
177         self.exportMethod(self.obj_list)
178         self.exportMethod(self.exec_workflow)
179         self.exportMethod(self.execute)
180
181     def init_set(self, cr, mode):
182         different = mode != self._init
183         if different:
184             if mode:
185                 self._init_parent = {}
186             if not mode:
187                 for o in self._init_parent:
188                     self.get(o)._parent_store_compute(cr)
189             self._init = mode
190
191         self._ready = True
192         return different
193
194     def execute_cr(self, cr, uid, obj, method, *args, **kw):
195         object = pooler.get_pool(cr.dbname).get(obj)
196         if not object:
197             raise except_osv('Object Error', 'Object %s doesn\'t exist' % str(obj))
198         return getattr(object, method)(cr, uid, *args, **kw)
199
200     @check
201     def execute(self, db, uid, obj, method, *args, **kw):
202         db, pool = pooler.get_db_and_pool(db)
203         cr = db.cursor()
204         try:
205             try:
206                 if method.startswith('_'):
207                     raise except_osv('Access Denied', 'Private methods (such as %s) cannot be called remotely.' % (method,))
208                 res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
209                 if res is None:
210                     self.logger.warning('The method %s of the object %s can not return `None` !', method, obj)
211                 cr.commit()
212             except Exception:
213                 cr.rollback()
214                 raise
215         finally:
216             cr.close()
217         return res
218
219     def exec_workflow_cr(self, cr, uid, obj, method, *args):
220         wf_service = netsvc.LocalService("workflow")
221         return wf_service.trg_validate(uid, obj, args[0], method, cr)
222
223     @check
224     def exec_workflow(self, db, uid, obj, method, *args):
225         cr = pooler.get_db(db).cursor()
226         try:
227             try:
228                 res = self.exec_workflow_cr(cr, uid, obj, method, *args)
229                 cr.commit()
230             except Exception:
231                 cr.rollback()
232                 raise
233         finally:
234             cr.close()
235         return res
236
237     def obj_list(self):
238         return self.obj_pool.keys()
239
240     # adds a new object instance to the object pool.
241     # if it already existed, the instance is replaced
242     def add(self, name, obj_inst):
243         if name in self.obj_pool:
244             del self.obj_pool[name]
245         self.obj_pool[name] = obj_inst
246
247         module = str(obj_inst.__class__)[6:]
248         module = module[:len(module)-1]
249         module = module.split('.')[0][2:]
250         self.module_object_list.setdefault(module, []).append(obj_inst)
251
252     # Return None if object does not exist
253     def get(self, name):
254         obj = self.obj_pool.get(name, None)
255         return obj
256
257     #TODO: pass a list of modules to load
258     def instanciate(self, module, cr):
259         res = []
260         class_list = module_class_list.get(module, [])
261         for klass in class_list:
262             res.append(klass.createInstance(self, module, cr))
263         return res
264
265 class osv_base(object):
266     def __init__(self, pool, cr):
267         pool.add(self._name, self)
268         self.pool = pool
269         super(osv_base, self).__init__(cr)
270
271     def __new__(cls):
272         module = str(cls)[6:]
273         module = module[:len(module)-1]
274         module = module.split('.')[0][2:]
275         if not hasattr(cls, '_module'):
276             cls._module = module
277         module_class_list.setdefault(cls._module, []).append(cls)
278         class_pool[cls._name] = cls
279         if module not in module_list:
280             module_list.append(cls._module)
281         return None
282
283 class osv_memory(osv_base, orm.orm_memory):
284     #
285     # Goal: try to apply inheritancy at the instanciation level and
286     #       put objects in the pool var
287     #
288     def createInstance(cls, pool, module, cr):
289         parent_names = getattr(cls, '_inherit', None)
290         if parent_names:
291             if isinstance(parent_names, (str, unicode)):
292                 name = cls._name or parent_names
293                 parent_names = [parent_names]
294             else:
295                 name = cls._name
296             if not name:
297                 raise TypeError('_name is mandatory in case of multiple inheritance')
298
299             for parent_name in ((type(parent_names)==list) and parent_names or [parent_names]):
300                 parent_class = pool.get(parent_name).__class__
301                 assert pool.get(parent_name), "parent class %s does not exist in module %s !" % (parent_name, module)
302                 nattr = {}
303                 for s in ('_columns', '_defaults'):
304                     new = copy.copy(getattr(pool.get(parent_name), s))
305                     if hasattr(new, 'update'):
306                         new.update(cls.__dict__.get(s, {}))
307                     else:
308                         new.extend(cls.__dict__.get(s, []))
309                     nattr[s] = new
310                 cls = type(name, (cls, parent_class), nattr)
311
312         obj = object.__new__(cls)
313         obj.__init__(pool, cr)
314         return obj
315     createInstance = classmethod(createInstance)
316
317 class osv(osv_base, orm.orm):
318     #
319     # Goal: try to apply inheritancy at the instanciation level and
320     #       put objects in the pool var
321     #
322     def createInstance(cls, pool, module, cr):
323         parent_names = getattr(cls, '_inherit', None)
324         if parent_names:
325             if isinstance(parent_names, (str, unicode)):
326                 name = cls._name or parent_names
327                 parent_names = [parent_names]
328             else:
329                 name = cls._name
330             if not name:
331                 raise TypeError('_name is mandatory in case of multiple inheritance')
332
333             for parent_name in ((type(parent_names)==list) and parent_names or [parent_names]):
334                 parent_class = pool.get(parent_name).__class__
335                 assert pool.get(parent_name), "parent class %s does not exist in module %s !" % (parent_name, module)
336                 nattr = {}
337                 for s in ('_columns', '_defaults', '_inherits', '_constraints', '_sql_constraints'):
338                     new = copy.copy(getattr(pool.get(parent_name), s))
339                     if hasattr(new, 'update'):
340                         new.update(cls.__dict__.get(s, {}))
341                     else:
342                         if s=='_constraints':
343                             for c in cls.__dict__.get(s, []):
344                                 exist = False
345                                 for c2 in range(len(new)):
346                                      #For _constraints, we should check field and methods as well
347                                      if new[c2][2]==c[2] and new[c2][0]==c[0]:
348                                         new[c2] = c
349                                         exist = True
350                                         break
351                                 if not exist:
352                                     new.append(c)
353                         else:
354                             new.extend(cls.__dict__.get(s, []))
355                     nattr[s] = new
356                 cls = type(name, (cls, parent_class), nattr)
357         obj = object.__new__(cls)
358         obj.__init__(pool, cr)
359         return obj
360     createInstance = classmethod(createInstance)
361
362 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
363