[REF] osv: moved (and adapted) vacuum code to osv_memory.
[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
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 osv_memory(orm.orm):
205     """ Deprecated class. """
206     __metaclass__ = MetaModel
207     _register = False # Set to false if the model shouldn't be automatically discovered.
208     _transient = True
209     _max_count = None
210     _max_hours = None
211     _check_time = 20
212
213     def __init__(self, pool, cr):
214         super(osv_memory, self).__init__(pool, cr)
215         self.check_id = 0
216         self._max_count = config.get('osv_memory_count_limit')
217         self._max_hours = config.get('osv_memory_age_limit')
218
219     def _clean_transient_rows_older_than(self, cr, seconds):
220         if not self._log_access:
221             self.logger = logging.getLogger('orm').warning(
222                 "Transient model without write_date: %s" % (self._name,))
223             return
224
225         cr.execute("SELECT id FROM " + self._table + " WHERE "
226             "COALESCE(write_date, create_date, now())::timestamp < "
227             "(now() - interval %s)", ("%s seconds" % seconds,))
228         ids = [x[0] for x in cr.fetchall()]
229         self.unlink(cr, openerp.SUPERUSER, ids)
230
231     def _clean_old_transient_rows(self, cr, count):
232         if not self._log_access:
233             self.logger = logging.getLogger('orm').warning(
234                 "Transient model without write_date: %s" % (self._name,))
235             return
236
237         cr.execute(
238             "SELECT id, COALESCE(write_date, create_date, now())::timestamp"
239             " AS t FROM " + self._table +
240             " ORDER BY t LIMIT %s", (count,))
241         ids = [x[0] for x in cr.fetchall()]
242         self.unlink(cr, openerp.SUPERUSER, ids)
243
244     def vacuum(self, cr, uid, force=False):
245         """ Run the vacuum cleaner, i.e. unlink old records from the
246         virtual osv_memory tables if the "max count" or "max age" conditions are enabled
247         and have been reached. This method can be called very often (e.g. everytime a record
248         is created), but will only actually trigger the cleanup process once out of
249         "_check_time" times (by default once out of 20 calls)."""
250         self.check_id += 1
251         if (not force) and (self.check_id % self._check_time):
252             self.check_id = 0
253             return True
254         tounlink = []
255
256         # Age-based expiration
257         if self._max_hours:
258             self._clean_transient_rows_older_than(cr, self._max_hours * 60 * 60)
259
260         # Count-based expiration
261         if self._max_count:
262             self._clean_old_transient_rows(cr, self._max_count)
263
264         return True
265
266
267 class osv(orm.orm):
268     """ Deprecated class. """
269     __metaclass__ = MetaModel
270     _register = False # Set to false if the model shouldn't be automatically discovered.
271     _transient = False
272
273
274 def start_object_proxy():
275     object_proxy()
276
277 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
278