[ADD] doc: new documentation, with training tutorials, and new scaffolding
[odoo/odoo.git] / openerp / api.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
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 """ This module provides the elements for managing two different API styles,
23     namely the "traditional" and "record" styles.
24
25     In the "traditional" style, parameters like the database cursor, user id,
26     context dictionary and record ids (usually denoted as ``cr``, ``uid``,
27     ``context``, ``ids``) are passed explicitly to all methods. In the "record"
28     style, those parameters are hidden into model instances, which gives it a
29     more object-oriented feel.
30
31     For instance, the statements::
32
33         model = self.pool.get(MODEL)
34         ids = model.search(cr, uid, DOMAIN, context=context)
35         for rec in model.browse(cr, uid, ids, context=context):
36             print rec.name
37         model.write(cr, uid, ids, VALUES, context=context)
38
39     may also be written as::
40
41         env = Env(cr, uid, context)         # cr, uid, context wrapped in env
42         recs = env[MODEL]                   # retrieve an instance of MODEL
43         recs = recs.search(DOMAIN)          # search returns a recordset
44         for rec in recs:                    # iterate over the records
45             print rec.name
46         recs.write(VALUES)                  # update all records in recs
47
48     Methods written in the "traditional" style are automatically decorated,
49     following some heuristics based on parameter names.
50 """
51
52 __all__ = [
53     'Environment',
54     'Meta', 'guess', 'noguess',
55     'model', 'multi', 'one',
56     'cr', 'cr_context', 'cr_uid', 'cr_uid_context',
57     'cr_uid_id', 'cr_uid_id_context', 'cr_uid_ids', 'cr_uid_ids_context',
58     'constrains', 'depends', 'onchange', 'returns',
59 ]
60
61 import logging
62
63 from inspect import currentframe, getargspec
64 from collections import defaultdict, MutableMapping
65 from contextlib import contextmanager
66 from pprint import pformat
67 from weakref import WeakSet
68 from werkzeug.local import Local, release_local
69
70 from openerp.tools import frozendict
71
72 _logger = logging.getLogger(__name__)
73
74 # The following attributes are used, and reflected on wrapping methods:
75 #  - method._api: decorator function, used for re-applying decorator
76 #  - method._constrains: set by @constrains, specifies constraint dependencies
77 #  - method._depends: set by @depends, specifies compute dependencies
78 #  - method._returns: set by @returns, specifies return model
79 #  - method._onchange: set by @onchange, specifies onchange fields
80 #  - method.clear_cache: set by @ormcache, used to clear the cache
81 #
82 # On wrapping method only:
83 #  - method._orig: original method
84 #
85
86 WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', '_api', '_constrains',
87                  '_depends', '_onchange', '_returns', 'clear_cache')
88
89 INHERITED_ATTRS = ('_returns',)
90
91
92 class Meta(type):
93     """ Metaclass that automatically decorates traditional-style methods by
94         guessing their API. It also implements the inheritance of the
95         :func:`returns` decorators.
96     """
97
98     def __new__(meta, name, bases, attrs):
99         # dummy parent class to catch overridden methods decorated with 'returns'
100         parent = type.__new__(meta, name, bases, {})
101
102         for key, value in attrs.items():
103             if not key.startswith('__') and callable(value):
104                 # make the method inherit from decorators
105                 value = propagate(getattr(parent, key, None), value)
106
107                 # guess calling convention if none is given
108                 if not hasattr(value, '_api'):
109                     try:
110                         value = guess(value)
111                     except TypeError:
112                         pass
113
114                 attrs[key] = value
115
116         return type.__new__(meta, name, bases, attrs)
117
118
119 identity = lambda x: x
120
121 def decorate(method, attr, value):
122     """ Decorate `method` or its original method. """
123     # decorate the original method, and re-apply the api decorator, if any
124     orig = getattr(method, '_orig', method)
125     setattr(orig, attr, value)
126     return getattr(method, '_api', identity)(orig)
127
128 def propagate(from_method, to_method):
129     """ Propagate decorators from `from_method` to `to_method`, and return the
130         resulting method.
131     """
132     if from_method:
133         for attr in INHERITED_ATTRS:
134             if hasattr(from_method, attr) and not hasattr(to_method, attr):
135                 to_method = decorate(to_method, attr, getattr(from_method, attr))
136     return to_method
137
138
139 def constrains(*args):
140     """ Decorates a constraint checker. Each argument must be a field name
141     used in the check::
142
143         @api.one
144         @api.constrains('name', 'description')
145         def _check_description(self):
146             if self.name == self.description:
147                 raise ValidationError("Fields name and description must be different")
148
149     Invoked on the records on which one of the named fields has been modified.
150
151     Should raise :class:`~openerp.exceptions.ValidationError` if the
152     validation failed.
153     """
154     return lambda method: decorate(method, '_constrains', args)
155
156
157 def onchange(*args):
158     """ Return a decorator to decorate an onchange method for given fields.
159         Each argument must be a field name::
160
161             @api.onchange('partner_id')
162             def _onchange_partner(self):
163                 self.message = "Dear %s" % (self.partner_id.name or "")
164
165         In the form views where the field appears, the method will be called
166         when one of the given fields is modified. The method is invoked on a
167         pseudo-record that contains the values present in the form. Field
168         assignments on that record are automatically sent back to the client.
169     """
170     return lambda method: decorate(method, '_onchange', args)
171
172
173 def depends(*args):
174     """ Return a decorator that specifies the field dependencies of a "compute"
175         method (for new-style function fields). Each argument must be a string
176         that consists in a dot-separated sequence of field names::
177
178             pname = fields.Char(compute='_compute_pname')
179
180             @api.one
181             @api.depends('partner_id.name', 'partner_id.is_company')
182             def _compute_pname(self):
183                 if self.partner_id.is_company:
184                     self.pname = (self.partner_id.name or "").upper()
185                 else:
186                     self.pname = self.partner_id.name
187
188         One may also pass a single function as argument. In that case, the
189         dependencies are given by calling the function with the field's model.
190     """
191     if args and callable(args[0]):
192         args = args[0]
193     return lambda method: decorate(method, '_depends', args)
194
195
196 def returns(model, downgrade=None):
197     """ Return a decorator for methods that return instances of `model`.
198
199         :param model: a model name, or ``'self'`` for the current model
200
201         :param downgrade: a function `downgrade(value)` to convert the
202             record-style `value` to a traditional-style output
203
204         The decorator adapts the method output to the api style: `id`, `ids` or
205         ``False`` for the traditional style, and recordset for the record style::
206
207             @model
208             @returns('res.partner')
209             def find_partner(self, arg):
210                 ...     # return some record
211
212             # output depends on call style: traditional vs record style
213             partner_id = model.find_partner(cr, uid, arg, context=context)
214
215             # recs = model.browse(cr, uid, ids, context)
216             partner_record = recs.find_partner(arg)
217
218         Note that the decorated method must satisfy that convention.
219
220         Those decorators are automatically *inherited*: a method that overrides
221         a decorated existing method will be decorated with the same
222         ``@returns(model)``.
223     """
224     return lambda method: decorate(method, '_returns', (model, downgrade))
225
226
227 def make_wrapper(method, old_api, new_api):
228     """ Return a wrapper method for `method`. """
229     def wrapper(self, *args, **kwargs):
230         # avoid hasattr(self, '_ids') because __getattr__() is overridden
231         if '_ids' in self.__dict__:
232             return new_api(self, *args, **kwargs)
233         else:
234             return old_api(self, *args, **kwargs)
235
236     # propagate specific openerp attributes from method to wrapper
237     for attr in WRAPPED_ATTRS:
238         if hasattr(method, attr):
239             setattr(wrapper, attr, getattr(method, attr))
240     wrapper._orig = method
241
242     return wrapper
243
244
245 def get_downgrade(method):
246     """ Return a function `downgrade(value)` that adapts `value` from
247         record-style to traditional-style, following the convention of `method`.
248     """
249     spec = getattr(method, '_returns', None)
250     if spec:
251         model, downgrade = spec
252         return downgrade or (lambda value: value.ids)
253     else:
254         return lambda value: value
255
256
257 def get_upgrade(method):
258     """ Return a function `upgrade(self, value)` that adapts `value` from
259         traditional-style to record-style, following the convention of `method`.
260     """
261     spec = getattr(method, '_returns', None)
262     if spec:
263         model, downgrade = spec
264         if model == 'self':
265             return lambda self, value: self.browse(value)
266         else:
267             return lambda self, value: self.env[model].browse(value)
268     else:
269         return lambda self, value: value
270
271
272 def get_aggregate(method):
273     """ Return a function `aggregate(self, value)` that aggregates record-style
274         `value` for a method decorated with ``@one``.
275     """
276     spec = getattr(method, '_returns', None)
277     if spec:
278         # value is a list of instances, concatenate them
279         model, downgrade = spec
280         if model == 'self':
281             return lambda self, value: sum(value, self.browse())
282         else:
283             return lambda self, value: sum(value, self.env[model].browse())
284     else:
285         return lambda self, value: value
286
287
288 def get_context_split(method):
289     """ Return a function `split` that extracts the context from a pair of
290         positional and keyword arguments::
291
292             context, args, kwargs = split(args, kwargs)
293     """
294     pos = len(getargspec(method).args) - 1
295
296     def split(args, kwargs):
297         if pos < len(args):
298             return args[pos], args[:pos], kwargs
299         else:
300             return kwargs.pop('context', None), args, kwargs
301
302     return split
303
304
305 def model(method):
306     """ Decorate a record-style method where `self` is a recordset, but its
307         contents is not relevant, only the model is. Such a method::
308
309             @api.model
310             def method(self, args):
311                 ...
312
313         may be called in both record and traditional styles, like::
314
315             # recs = model.browse(cr, uid, ids, context)
316             recs.method(args)
317
318             model.method(cr, uid, args, context=context)
319
320         Notice that no `ids` are passed to the method in the traditional style.
321     """
322     method._api = model
323     split = get_context_split(method)
324     downgrade = get_downgrade(method)
325
326     def old_api(self, cr, uid, *args, **kwargs):
327         context, args, kwargs = split(args, kwargs)
328         recs = self.browse(cr, uid, [], context)
329         result = method(recs, *args, **kwargs)
330         return downgrade(result)
331
332     return make_wrapper(method, old_api, method)
333
334
335 def multi(method):
336     """ Decorate a record-style method where `self` is a recordset. The method
337         typically defines an operation on records. Such a method::
338
339             @api.multi
340             def method(self, args):
341                 ...
342
343         may be called in both record and traditional styles, like::
344
345             # recs = model.browse(cr, uid, ids, context)
346             recs.method(args)
347
348             model.method(cr, uid, ids, args, context=context)
349     """
350     method._api = multi
351     split = get_context_split(method)
352     downgrade = get_downgrade(method)
353
354     def old_api(self, cr, uid, ids, *args, **kwargs):
355         context, args, kwargs = split(args, kwargs)
356         recs = self.browse(cr, uid, ids, context)
357         result = method(recs, *args, **kwargs)
358         return downgrade(result)
359
360     return make_wrapper(method, old_api, method)
361
362
363 def one(method):
364     """ Decorate a record-style method where `self` is expected to be a
365         singleton instance. The decorated method automatically loops on records,
366         and makes a list with the results. In case the method is decorated with
367         @returns, it concatenates the resulting instances. Such a method::
368
369             @api.one
370             def method(self, args):
371                 return self.name
372
373         may be called in both record and traditional styles, like::
374
375             # recs = model.browse(cr, uid, ids, context)
376             names = recs.method(args)
377
378             names = model.method(cr, uid, ids, args, context=context)
379     """
380     method._api = one
381     split = get_context_split(method)
382     downgrade = get_downgrade(method)
383     aggregate = get_aggregate(method)
384
385     def old_api(self, cr, uid, ids, *args, **kwargs):
386         context, args, kwargs = split(args, kwargs)
387         recs = self.browse(cr, uid, ids, context)
388         result = new_api(recs, *args, **kwargs)
389         return downgrade(result)
390
391     def new_api(self, *args, **kwargs):
392         result = [method(rec, *args, **kwargs) for rec in self]
393         return aggregate(self, result)
394
395     return make_wrapper(method, old_api, new_api)
396
397
398 def cr(method):
399     """ Decorate a traditional-style method that takes `cr` as a parameter.
400         Such a method may be called in both record and traditional styles, like::
401
402             # recs = model.browse(cr, uid, ids, context)
403             recs.method(args)
404
405             model.method(cr, args)
406     """
407     method._api = cr
408     upgrade = get_upgrade(method)
409
410     def new_api(self, *args, **kwargs):
411         cr, uid, context = self.env.args
412         result = method(self._model, cr, *args, **kwargs)
413         return upgrade(self, result)
414
415     return make_wrapper(method, method, new_api)
416
417
418 def cr_context(method):
419     """ Decorate a traditional-style method that takes `cr`, `context` as parameters. """
420     method._api = cr_context
421     upgrade = get_upgrade(method)
422
423     def new_api(self, *args, **kwargs):
424         cr, uid, context = self.env.args
425         kwargs['context'] = context
426         result = method(self._model, cr, *args, **kwargs)
427         return upgrade(self, result)
428
429     return make_wrapper(method, method, new_api)
430
431
432 def cr_uid(method):
433     """ Decorate a traditional-style method that takes `cr`, `uid` as parameters. """
434     method._api = cr_uid
435     upgrade = get_upgrade(method)
436
437     def new_api(self, *args, **kwargs):
438         cr, uid, context = self.env.args
439         result = method(self._model, cr, uid, *args, **kwargs)
440         return upgrade(self, result)
441
442     return make_wrapper(method, method, new_api)
443
444
445 def cr_uid_context(method):
446     """ Decorate a traditional-style method that takes `cr`, `uid`, `context` as
447         parameters. Such a method may be called in both record and traditional
448         styles, like::
449
450             # recs = model.browse(cr, uid, ids, context)
451             recs.method(args)
452
453             model.method(cr, uid, args, context=context)
454     """
455     method._api = cr_uid_context
456     upgrade = get_upgrade(method)
457
458     def new_api(self, *args, **kwargs):
459         cr, uid, context = self.env.args
460         kwargs['context'] = context
461         result = method(self._model, cr, uid, *args, **kwargs)
462         return upgrade(self, result)
463
464     return make_wrapper(method, method, new_api)
465
466
467 def cr_uid_id(method):
468     """ Decorate a traditional-style method that takes `cr`, `uid`, `id` as
469         parameters. Such a method may be called in both record and traditional
470         styles. In the record style, the method automatically loops on records.
471     """
472     method._api = cr_uid_id
473     upgrade = get_upgrade(method)
474
475     def new_api(self, *args, **kwargs):
476         cr, uid, context = self.env.args
477         result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
478         return upgrade(self, result)
479
480     return make_wrapper(method, method, new_api)
481
482
483 def cr_uid_id_context(method):
484     """ Decorate a traditional-style method that takes `cr`, `uid`, `id`,
485         `context` as parameters. Such a method::
486
487             @api.cr_uid_id
488             def method(self, cr, uid, id, args, context=None):
489                 ...
490
491         may be called in both record and traditional styles, like::
492
493             # rec = model.browse(cr, uid, id, context)
494             rec.method(args)
495
496             model.method(cr, uid, id, args, context=context)
497     """
498     method._api = cr_uid_id_context
499     upgrade = get_upgrade(method)
500
501     def new_api(self, *args, **kwargs):
502         cr, uid, context = self.env.args
503         kwargs['context'] = context
504         result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
505         return upgrade(self, result)
506
507     return make_wrapper(method, method, new_api)
508
509
510 def cr_uid_ids(method):
511     """ Decorate a traditional-style method that takes `cr`, `uid`, `ids` as
512         parameters. Such a method may be called in both record and traditional
513         styles.
514     """
515     method._api = cr_uid_ids
516     upgrade = get_upgrade(method)
517
518     def new_api(self, *args, **kwargs):
519         cr, uid, context = self.env.args
520         result = method(self._model, cr, uid, self.ids, *args, **kwargs)
521         return upgrade(self, result)
522
523     return make_wrapper(method, method, new_api)
524
525
526 def cr_uid_ids_context(method):
527     """ Decorate a traditional-style method that takes `cr`, `uid`, `ids`,
528         `context` as parameters. Such a method::
529
530             @api.cr_uid_ids_context
531             def method(self, cr, uid, ids, args, context=None):
532                 ...
533
534         may be called in both record and traditional styles, like::
535
536             # recs = model.browse(cr, uid, ids, context)
537             recs.method(args)
538
539             model.method(cr, uid, ids, args, context=context)
540
541         It is generally not necessary, see :func:`guess`.
542     """
543     method._api = cr_uid_ids_context
544     upgrade = get_upgrade(method)
545
546     def new_api(self, *args, **kwargs):
547         cr, uid, context = self.env.args
548         kwargs['context'] = context
549         result = method(self._model, cr, uid, self.ids, *args, **kwargs)
550         return upgrade(self, result)
551
552     return make_wrapper(method, method, new_api)
553
554
555 def v7(method_v7):
556     """ Decorate a method that supports the old-style api only. A new-style api
557         may be provided by redefining a method with the same name and decorated
558         with :func:`~.v8`::
559
560             @api.v7
561             def foo(self, cr, uid, ids, context=None):
562                 ...
563
564             @api.v8
565             def foo(self):
566                 ...
567
568         Note that the wrapper method uses the docstring of the first method.
569     """
570     # retrieve method_v8 from the caller's frame
571     frame = currentframe().f_back
572     method = frame.f_locals.get(method_v7.__name__)
573     method_v8 = getattr(method, '_v8', method)
574
575     wrapper = make_wrapper(method_v7, method_v7, method_v8)
576     wrapper._v7 = method_v7
577     wrapper._v8 = method_v8
578     return wrapper
579
580
581 def v8(method_v8):
582     """ Decorate a method that supports the new-style api only. An old-style api
583         may be provided by redefining a method with the same name and decorated
584         with :func:`~.v7`::
585
586             @api.v8
587             def foo(self):
588                 ...
589
590             @api.v7
591             def foo(self, cr, uid, ids, context=None):
592                 ...
593
594         Note that the wrapper method uses the docstring of the first method.
595     """
596     # retrieve method_v7 from the caller's frame
597     frame = currentframe().f_back
598     method = frame.f_locals.get(method_v8.__name__)
599     method_v7 = getattr(method, '_v7', method)
600
601     wrapper = make_wrapper(method_v8, method_v7, method_v8)
602     wrapper._v7 = method_v7
603     wrapper._v8 = method_v8
604     return wrapper
605
606
607 def noguess(method):
608     """ Decorate a method to prevent any effect from :func:`guess`. """
609     method._api = False
610     return method
611
612
613 def guess(method):
614     """ Decorate `method` to make it callable in both traditional and record
615         styles. This decorator is applied automatically by the model's
616         metaclass, and has no effect on already-decorated methods.
617
618         The API style is determined by heuristics on the parameter names: ``cr``
619         or ``cursor`` for the cursor, ``uid`` or ``user`` for the user id,
620         ``id`` or ``ids`` for a list of record ids, and ``context`` for the
621         context dictionary. If a traditional API is recognized, one of the
622         decorators :func:`cr`, :func:`cr_context`, :func:`cr_uid`,
623         :func:`cr_uid_context`, :func:`cr_uid_id`, :func:`cr_uid_id_context`,
624         :func:`cr_uid_ids`, :func:`cr_uid_ids_context` is applied on the method.
625
626         Method calls are considered traditional style when their first parameter
627         is a database cursor.
628     """
629     if hasattr(method, '_api'):
630         return method
631
632     # introspection on argument names to determine api style
633     args, vname, kwname, defaults = getargspec(method)
634     names = tuple(args) + (None,) * 4
635
636     if names[0] == 'self':
637         if names[1] in ('cr', 'cursor'):
638             if names[2] in ('uid', 'user'):
639                 if names[3] == 'ids':
640                     if 'context' in names or kwname:
641                         return cr_uid_ids_context(method)
642                     else:
643                         return cr_uid_ids(method)
644                 elif names[3] == 'id':
645                     if 'context' in names or kwname:
646                         return cr_uid_id_context(method)
647                     else:
648                         return cr_uid_id(method)
649                 elif 'context' in names or kwname:
650                     return cr_uid_context(method)
651                 else:
652                     return cr_uid(method)
653             elif 'context' in names:
654                 return cr_context(method)
655             else:
656                 return cr(method)
657
658     # no wrapping by default
659     return noguess(method)
660
661
662 def expected(decorator, func):
663     """ Decorate `func` with `decorator` if `func` is not wrapped yet. """
664     return decorator(func) if not hasattr(func, '_orig') else func
665
666
667
668 class Environment(object):
669     """ An environment wraps data for ORM records:
670
671          - :attr:`cr`, the current database cursor;
672          - :attr:`uid`, the current user id;
673          - :attr:`context`, the current context dictionary.
674
675         It also provides access to the registry, a cache for records, and a data
676         structure to manage recomputations.
677     """
678     _local = Local()
679
680     @classmethod
681     @contextmanager
682     def manage(cls):
683         """ Context manager for a set of environments. """
684         if hasattr(cls._local, 'environments'):
685             yield
686         else:
687             try:
688                 cls._local.environments = WeakSet()
689                 yield
690             finally:
691                 release_local(cls._local)
692
693     def __new__(cls, cr, uid, context):
694         assert context is not None
695         args = (cr, uid, context)
696
697         # if env already exists, return it
698         env, envs = None, cls._local.environments
699         for env in envs:
700             if env.args == args:
701                 return env
702
703         # otherwise create environment, and add it in the set
704         self = object.__new__(cls)
705         self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
706         self.registry = RegistryManager.get(cr.dbname)
707         self.cache = defaultdict(dict)      # {field: {id: value, ...}, ...}
708         self.prefetch = defaultdict(set)    # {model_name: set(id), ...}
709         self.computed = defaultdict(set)    # {field: set(id), ...}
710         self.dirty = set()                  # set(record)
711         self.todo = {}                      # {field: records, ...}
712         self.mode = env.mode if env else Mode()
713         self.all = envs
714         envs.add(self)
715         return self
716
717     def __getitem__(self, model_name):
718         """ return a given model """
719         return self.registry[model_name]._browse(self, ())
720
721     def __call__(self, cr=None, user=None, context=None):
722         """ Return an environment based on `self` with modified parameters.
723
724             :param cr: optional database cursor to change the current cursor
725             :param user: optional user/user id to change the current user
726             :param context: optional context dictionary to change the current context
727         """
728         cr = self.cr if cr is None else cr
729         uid = self.uid if user is None else int(user)
730         context = self.context if context is None else context
731         return Environment(cr, uid, context)
732
733     def ref(self, xml_id, raise_if_not_found=True):
734         """ return the record corresponding to the given `xml_id` """
735         return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)
736
737     @property
738     def user(self):
739         """ return the current user (as an instance) """
740         return self(user=SUPERUSER_ID)['res.users'].browse(self.uid)
741
742     @property
743     def lang(self):
744         """ return the current language code """
745         return self.context.get('lang')
746
747     @contextmanager
748     def _do_in_mode(self, mode):
749         if self.mode.value:
750             yield
751         else:
752             try:
753                 self.mode.value = mode
754                 yield
755             finally:
756                 self.mode.value = False
757                 self.dirty.clear()
758
759     def do_in_draft(self):
760         """ Context-switch to draft mode, where all field updates are done in
761             cache only.
762         """
763         return self._do_in_mode(True)
764
765     @property
766     def in_draft(self):
767         """ Return whether we are in draft mode. """
768         return bool(self.mode.value)
769
770     def do_in_onchange(self):
771         """ Context-switch to 'onchange' draft mode, which is a specialized
772             draft mode used during execution of onchange methods.
773         """
774         return self._do_in_mode('onchange')
775
776     @property
777     def in_onchange(self):
778         """ Return whether we are in 'onchange' draft mode. """
779         return self.mode.value == 'onchange'
780
781     def invalidate(self, spec):
782         """ Invalidate some fields for some records in the cache of all
783             environments.
784
785             :param spec: what to invalidate, a list of `(field, ids)` pair,
786                 where `field` is a field object, and `ids` is a list of record
787                 ids or ``None`` (to invalidate all records).
788         """
789         if not spec:
790             return
791         for env in list(iter(self.all)):
792             c = env.cache
793             for field, ids in spec:
794                 if ids is None:
795                     if field in c:
796                         del c[field]
797                 else:
798                     field_cache = c[field]
799                     for id in ids:
800                         field_cache.pop(id, None)
801
802     def invalidate_all(self):
803         """ Clear the cache of all environments. """
804         for env in list(iter(self.all)):
805             env.cache.clear()
806             env.prefetch.clear()
807             env.computed.clear()
808             env.dirty.clear()
809
810     def check_cache(self):
811         """ Check the cache consistency. """
812         # make a full copy of the cache, and invalidate it
813         cache_dump = dict(
814             (field, dict(field_cache))
815             for field, field_cache in self.cache.iteritems()
816         )
817         self.invalidate_all()
818
819         # re-fetch the records, and compare with their former cache
820         invalids = []
821         for field, field_dump in cache_dump.iteritems():
822             ids = filter(None, field_dump)
823             records = self[field.model_name].browse(ids)
824             for record in records:
825                 try:
826                     cached = field_dump[record.id]
827                     fetched = record[field.name]
828                     if fetched != cached:
829                         info = {'cached': cached, 'fetched': fetched}
830                         invalids.append((field, record, info))
831                 except (AccessError, MissingError):
832                     pass
833
834         if invalids:
835             raise Warning('Invalid cache for fields\n' + pformat(invalids))
836
837
838 class Mode(object):
839     """ A mode flag shared among environments. """
840     value = False           # False, True (draft) or 'onchange' (onchange draft)
841
842
843 # keep those imports here in order to handle cyclic dependencies correctly
844 from openerp import SUPERUSER_ID
845 from openerp.exceptions import Warning, AccessError, MissingError
846 from openerp.modules.registry import RegistryManager