[FIX] account : default date on statement lines should be the statement date
[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(), SuperDict(ctx))
176
177 class SuperDict(dict):
178     def __getattr__(self, name):
179         try:
180             return self[name]
181         except KeyError:
182             raise AttributeError(name)
183     def __getitem__(self, key):
184         tmp = super(SuperDict, self).__getitem__(key)
185         if isinstance(tmp, dict):
186             return SuperDict(tmp)
187         return tmp
188
189 class CompoundDomain(BaseDomain):
190     def __init__(self, *domains):
191         self.domains = []
192         self.session = None
193         self.eval_context = None
194         for domain in domains:
195             self.add(domain)
196         
197     def evaluate(self, context=None):
198         final_domain = []
199         for domain in self.domains:
200             if not isinstance(domain, (list, BaseDomain)):
201                 raise TypeError(
202                     "Domain %r is not a list or a nonliteral Domain" % domain)
203
204             if isinstance(domain, list):
205                 final_domain.extend(domain)
206                 continue
207             
208             ctx = dict(context or {})
209             ctx.update(self.get_eval_context() or {})
210
211             domain.session = self.session
212             final_domain.extend(domain.evaluate(ctx))
213         return final_domain
214     
215     def add(self, domain):
216         self.domains.append(domain)
217         return self
218     
219     def set_eval_context(self, eval_context):
220         self.eval_context = eval_context
221         return self
222         
223     def get_eval_context(self):
224         return self.eval_context
225
226 class CompoundContext(BaseContext):
227     def __init__(self, *contexts):
228         self.contexts = []
229         self.eval_context = None
230         self.session = None
231         for context in contexts:
232             self.add(context)
233     
234     def evaluate(self, context=None):
235         ctx = dict(context or {})
236         ctx.update(self.get_eval_context() or {})
237         final_context = {}
238         for context_to_eval in self.contexts:
239             if not isinstance(context_to_eval, (dict, BaseContext)):
240                 raise TypeError(
241                     "Context %r is not a dict or a nonliteral Context" % context_to_eval)
242
243             if isinstance(context_to_eval, dict):
244                 final_context.update(context_to_eval)
245                 continue
246             
247             ctx.update(final_context)
248
249             context_to_eval.session = self.session
250             final_context.update(context_to_eval.evaluate(ctx))
251         return final_context
252             
253     def add(self, context):
254         self.contexts.append(context)
255         return self
256     
257     def set_eval_context(self, eval_context):
258         self.eval_context = eval_context
259         return self
260         
261     def get_eval_context(self):
262         return self.eval_context