[FIX] sign handling when parsing float_time values
[odoo/odoo.git] / addons / web / common / nonliterals.py
1 # -*- coding: utf-8 -*-
2 """ Manages the storage and lifecycle of non-literal domains and contexts
3 (and potentially other structures) which have to be evaluated with client data,
4 but still need to be safely round-tripped to and from the browser (and thus
5 can't be sent there themselves).
6 """
7 import binascii
8 import hashlib
9 import simplejson.encoder
10
11 __all__ = ['Domain', 'Context', 'NonLiteralEncoder', 'non_literal_decoder', 'CompoundDomain', 'CompoundContext']
12
13 #: 48 bits should be sufficient to have almost no chance of collision
14 #: with a million hashes, according to hg@67081329d49a
15 SHORT_HASH_BYTES_SIZE = 6
16
17 class NonLiteralEncoder(simplejson.encoder.JSONEncoder):
18     def default(self, object):
19         if not isinstance(object, (BaseDomain, BaseContext)):
20             return super(NonLiteralEncoder, self).default(object)
21         if isinstance(object, Domain):
22             return {
23                 '__ref': 'domain',
24                 '__id': object.key,
25                 '__debug': object.get_domain_string()
26             }
27         elif isinstance(object, Context):
28             return {
29                 '__ref': 'context',
30                 '__id': object.key,
31                 '__debug': object.get_context_string()
32             }
33         elif isinstance(object, CompoundDomain):
34             return {
35                 '__ref': 'compound_domain',
36                 '__domains': object.domains,
37                 '__eval_context': object.get_eval_context()
38             }
39         elif isinstance(object, CompoundContext):
40             return {
41                 '__ref': 'compound_context',
42                 '__contexts': object.contexts,
43                 '__eval_context': object.get_eval_context()
44             }
45         raise TypeError('Could not encode unknown non-literal %s' % object)
46     
47 _ALLOWED_KEYS = frozenset(['__ref', "__id", '__domains', '__debug',
48                            '__contexts', '__eval_context', 'own_values'])
49
50 def non_literal_decoder(dct):
51     """ Decodes JSON dicts into :class:`Domain` and :class:`Context` based on
52     magic attribute tags.
53
54     Also handles private context section for the domain or section via the
55     ``own_values`` dict key.
56     """
57     if '__ref' in dct:
58         for x in dct:
59             if x not in _ALLOWED_KEYS:
60                 raise ValueError("'%s' key not allowed in non literal domain/context" % x)
61         if dct['__ref'] == 'domain':
62             domain = Domain(None, key=dct['__id'])
63             if 'own_values' in dct:
64                 domain.own = dct['own_values']
65             return domain
66         elif dct['__ref'] == 'context':
67             context = Context(None, key=dct['__id'])
68             if 'own_values' in dct:
69                 context.own = dct['own_values']
70             return context
71         elif dct["__ref"] == "compound_domain":
72             cdomain = CompoundDomain()
73             for el in dct["__domains"]:
74                 cdomain.domains.append(el)
75             cdomain.set_eval_context(dct.get("__eval_context"))
76             return cdomain
77         elif dct["__ref"] == "compound_context":
78             ccontext = CompoundContext()
79             for el in dct["__contexts"]:
80                 ccontext.contexts.append(el)
81             ccontext.set_eval_context(dct.get("__eval_context"))
82             return ccontext
83     return dct
84
85 # TODO: use abstract base classes if 2.6+?
86 class BaseDomain(object):
87     def evaluate(self, context=None):
88         raise NotImplementedError('Non literals must implement evaluate()')
89
90 class BaseContext(object):
91     def evaluate(self, context=None):
92         raise NotImplementedError('Non literals must implement evaluate()')
93
94 class Domain(BaseDomain):
95     def __init__(self, session, domain_string=None, key=None):
96         """ Uses session information to store the domain string and map it to a
97         domain key, which can be safely round-tripped to the client.
98
99         If initialized with a domain string, will generate a key for that
100         string and store the domain string out of the way. When initialized
101         with a key, considers this key is a reference to an existing domain
102         string.
103
104         :param session: the OpenERP Session to use when evaluating the domain
105         :type session: web.common.session.OpenERPSession
106         :param str domain_string: a non-literal domain in string form
107         :param str key: key used to retrieve the domain string
108         """
109         if domain_string and key:
110             raise ValueError("A nonliteral domain can not take both a key "
111                              "and a domain string")
112
113         self.session = session
114         self.own = {}
115         if domain_string:
116             self.key = binascii.hexlify(
117                 hashlib.sha256(domain_string).digest()[:SHORT_HASH_BYTES_SIZE])
118             self.session.domains_store[self.key] = domain_string
119         elif key:
120             self.key = key
121
122     def get_domain_string(self):
123         """ Retrieves the domain string linked to this non-literal domain in
124         the provided session.
125         """
126         return self.session.domains_store[self.key]
127
128     def evaluate(self, context=None):
129         """ Forces the evaluation of the linked domain, using the provided
130         context (as well as the session's base context), and returns the
131         evaluated result.
132         """
133         ctx = self.session.evaluation_context(context)
134         if self.own:
135             ctx.update(self.own)
136         try:
137             return eval(self.get_domain_string(), SuperDict(ctx))
138         except NameError as e:
139             raise ValueError('Error during evaluation of this domain: "%s", message: "%s"' % (self.get_domain_string(), e.message))
140
141 class Context(BaseContext):
142     def __init__(self, session, context_string=None, key=None):
143         """ Uses session information to store the context string and map it to
144         a key (stored in a secret location under a secret mountain), which can
145         be safely round-tripped to the client.
146
147         If initialized with a context string, will generate a key for that
148         string and store the context string out of the way. When initialized
149         with a key, considers this key is a reference to an existing context
150         string.
151
152         :param session: the OpenERP Session to use when evaluating the context
153         :type session: web.common.session.OpenERPSession
154         :param str context_string: a non-literal context in string form
155         :param str key: key used to retrieve the context string
156         """
157         if context_string and key:
158             raise ValueError("A nonliteral domain can not take both a key "
159                              "and a domain string")
160
161         self.session = session
162         self.own = {}
163         if context_string:
164             self.key = binascii.hexlify(
165                 hashlib.sha256(context_string).digest()[:SHORT_HASH_BYTES_SIZE])
166             self.session.contexts_store[self.key] = context_string
167         elif key:
168             self.key = key
169
170     def get_context_string(self):
171         """ Retrieves the context string linked to this non-literal context in
172         the provided session.
173         """
174         return self.session.contexts_store[self.key]
175
176     def evaluate(self, context=None):
177         """ Forces the evaluation of the linked context, using the provided
178         context (as well as the session's base context), and returns the
179         evaluated result.
180         """
181         ctx = self.session.evaluation_context(context)
182         if self.own:
183             ctx.update(self.own)
184         try:
185             return eval(self.get_context_string(), SuperDict(ctx))
186         except NameError as e:
187             raise ValueError('Error during evaluation of this context: "%s", message: "%s"' % (self.get_context_string(), e.message))
188
189 class SuperDict(dict):
190     def __getattr__(self, name):
191         try:
192             return self[name]
193         except KeyError:
194             raise AttributeError(name)
195     def __getitem__(self, key):
196         tmp = super(SuperDict, self).__getitem__(key)
197         if isinstance(tmp, dict):
198             return SuperDict(tmp)
199         return tmp
200
201 class CompoundDomain(BaseDomain):
202     def __init__(self, *domains):
203         self.domains = []
204         self.session = None
205         self.eval_context = None
206         for domain in domains:
207             self.add(domain)
208         
209     def evaluate(self, context=None):
210         ctx = dict(context or {})
211         eval_context = self.get_eval_context()
212         if eval_context:
213             eval_context = self.session.eval_context(eval_context)
214             ctx.update(eval_context)
215         final_domain = []
216         for domain in self.domains:
217             if not isinstance(domain, (list, BaseDomain)):
218                 raise TypeError(
219                     "Domain %r is not a list or a nonliteral Domain" % domain)
220
221             if isinstance(domain, list):
222                 final_domain.extend(domain)
223                 continue
224
225             domain.session = self.session
226             final_domain.extend(domain.evaluate(ctx))
227         return final_domain
228     
229     def add(self, domain):
230         self.domains.append(domain)
231         return self
232     
233     def set_eval_context(self, eval_context):
234         self.eval_context = eval_context
235         return self
236         
237     def get_eval_context(self):
238         return self.eval_context
239
240 class CompoundContext(BaseContext):
241     def __init__(self, *contexts):
242         self.contexts = []
243         self.eval_context = None
244         self.session = None
245         for context in contexts:
246             self.add(context)
247     
248     def evaluate(self, context=None):
249         ctx = dict(context or {})
250         eval_context = self.get_eval_context()
251         if eval_context:
252             eval_context = self.session.eval_context(eval_context)
253             ctx.update(eval_context)
254         final_context = {}
255         for context_to_eval in self.contexts:
256             if not isinstance(context_to_eval, (dict, BaseContext)):
257                 raise TypeError(
258                     "Context %r is not a dict or a nonliteral Context" % context_to_eval)
259
260             if isinstance(context_to_eval, dict):
261                 final_context.update(context_to_eval)
262                 continue
263             
264             ctx.update(final_context)
265
266             context_to_eval.session = self.session
267             final_context.update(context_to_eval.evaluate(ctx))
268         return final_context
269             
270     def add(self, context):
271         self.contexts.append(context)
272         return self
273     
274     def set_eval_context(self, eval_context):
275         self.eval_context = eval_context
276         return self
277         
278     def get_eval_context(self):
279         return self.eval_context