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).
9 import simplejson.encoder
11 __all__ = ['Domain', 'Context', 'NonLiteralEncoder', 'non_literal_decoder', 'CompoundDomain', 'CompoundContext']
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
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):
25 '__debug': object.get_domain_string()
27 elif isinstance(object, Context):
31 '__debug': object.get_context_string()
33 elif isinstance(object, CompoundDomain):
35 '__ref': 'compound_domain',
36 '__domains': object.domains,
37 '__eval_context': object.get_eval_context()
39 elif isinstance(object, CompoundContext):
41 '__ref': 'compound_context',
42 '__contexts': object.contexts,
43 '__eval_context': object.get_eval_context()
45 raise TypeError('Could not encode unknown non-literal %s' % object)
47 _ALLOWED_KEYS = frozenset(['__ref', "__id", '__domains', '__debug',
48 '__contexts', '__eval_context', 'own_values'])
50 def non_literal_decoder(dct):
51 """ Decodes JSON dicts into :class:`Domain` and :class:`Context` based on
54 Also handles private context section for the domain or section via the
55 ``own_values`` dict key.
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']
66 elif dct['__ref'] == 'context':
67 context = Context(None, key=dct['__id'])
68 if 'own_values' in dct:
69 context.own = dct['own_values']
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"))
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"))
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()')
90 class BaseContext(object):
91 def evaluate(self, context=None):
92 raise NotImplementedError('Non literals must implement evaluate()')
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.
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
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
109 if domain_string and key:
110 raise ValueError("A nonliteral domain can not take both a key "
111 "and a domain string")
113 self.session = session
116 self.key = binascii.hexlify(
117 hashlib.sha256(domain_string).digest()[:SHORT_HASH_BYTES_SIZE])
118 self.session.domains_store[self.key] = domain_string
122 def get_domain_string(self):
123 """ Retrieves the domain string linked to this non-literal domain in
124 the provided session.
126 return self.session.domains_store[self.key]
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
133 ctx = self.session.evaluation_context(context)
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))
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.
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
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
157 if context_string and key:
158 raise ValueError("A nonliteral domain can not take both a key "
159 "and a domain string")
161 self.session = session
164 self.key = binascii.hexlify(
165 hashlib.sha256(context_string).digest()[:SHORT_HASH_BYTES_SIZE])
166 self.session.contexts_store[self.key] = context_string
170 def get_context_string(self):
171 """ Retrieves the context string linked to this non-literal context in
172 the provided session.
174 return self.session.contexts_store[self.key]
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
181 ctx = self.session.evaluation_context(context)
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))
189 class SuperDict(dict):
190 def __getattr__(self, name):
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)
201 class CompoundDomain(BaseDomain):
202 def __init__(self, *domains):
205 self.eval_context = None
206 for domain in domains:
209 def evaluate(self, context=None):
210 ctx = dict(context or {})
211 eval_context = self.get_eval_context()
213 eval_context = self.session.eval_context(eval_context)
214 ctx.update(eval_context)
216 for domain in self.domains:
217 if not isinstance(domain, (list, BaseDomain)):
219 "Domain %r is not a list or a nonliteral Domain" % domain)
221 if isinstance(domain, list):
222 final_domain.extend(domain)
225 domain.session = self.session
226 final_domain.extend(domain.evaluate(ctx))
229 def add(self, domain):
230 self.domains.append(domain)
233 def set_eval_context(self, eval_context):
234 self.eval_context = eval_context
237 def get_eval_context(self):
238 return self.eval_context
240 class CompoundContext(BaseContext):
241 def __init__(self, *contexts):
243 self.eval_context = None
245 for context in contexts:
248 def evaluate(self, context=None):
249 ctx = dict(context or {})
250 eval_context = self.get_eval_context()
252 eval_context = self.session.eval_context(eval_context)
253 ctx.update(eval_context)
255 for context_to_eval in self.contexts:
256 if not isinstance(context_to_eval, (dict, BaseContext)):
258 "Context %r is not a dict or a nonliteral Context" % context_to_eval)
260 if isinstance(context_to_eval, dict):
261 final_context.update(context_to_eval)
264 ctx.update(final_context)
266 context_to_eval.session = self.session
267 final_context.update(context_to_eval.evaluate(ctx))
270 def add(self, context):
271 self.contexts.append(context)
274 def set_eval_context(self, eval_context):
275 self.eval_context = eval_context
278 def get_eval_context(self):
279 return self.eval_context