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