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
13 __all__ = ['Domain', 'Context', 'NonLiteralEncoder, non_literal_decoder', 'CompoundDomain', 'CompoundContext']
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
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):
28 elif isinstance(object, Context):
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 def non_literal_decoder(dct):
48 """ Decodes JSON dicts into :class:`Domain` and :class:`Context` based on
51 Also handles private context section for the domain or section via the
52 ``own_values`` dict key.
55 if dct['__ref'] == 'domain':
56 domain = Domain(None, key=dct['__id'])
57 if 'own_values' in dct:
58 domain.own = dct['own_values']
60 elif dct['__ref'] == 'context':
61 context = Context(None, key=dct['__id'])
62 if 'own_values' in dct:
63 context.own = dct['own_values']
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"))
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"))
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()')
84 class BaseContext(object):
85 def evaluate(self, context=None):
86 raise NotImplementedError('Non literals must implement evaluate()')
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.
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
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
103 if domain_string and key:
104 raise ValueError("A nonliteral domain can not take both a key "
105 "and a domain string")
107 self.session = session
110 self.key = binascii.hexlify(
111 hashlib.sha256(domain_string).digest()[:SHORT_HASH_BYTES_SIZE])
112 self.session.domains_store[self.key] = domain_string
116 def get_domain_string(self):
117 """ Retrieves the domain string linked to this non-literal domain in
118 the provided session.
120 return self.session.domains_store[self.key]
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
127 ctx = self.session.evaluation_context(context)
130 return eval(self.get_domain_string(), SuperDict(ctx))
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.
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
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
148 if context_string and key:
149 raise ValueError("A nonliteral domain can not take both a key "
150 "and a domain string")
152 self.session = session
155 self.key = binascii.hexlify(
156 hashlib.sha256(context_string).digest()[:SHORT_HASH_BYTES_SIZE])
157 self.session.contexts_store[self.key] = context_string
161 def get_context_string(self):
162 """ Retrieves the context string linked to this non-literal context in
163 the provided session.
165 return self.session.contexts_store[self.key]
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
172 ctx = self.session.evaluation_context(context)
175 return eval(self.get_context_string(), SuperDict(ctx))
177 class SuperDict(dict):
178 def __getattr__(self, name):
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)
189 class CompoundDomain(BaseDomain):
190 def __init__(self, *domains):
193 self.eval_context = None
194 for domain in domains:
197 def evaluate(self, context=None):
199 for domain in self.domains:
200 if not isinstance(domain, (list, BaseDomain)):
202 "Domain %r is not a list or a nonliteral Domain" % domain)
204 if isinstance(domain, list):
205 final_domain.extend(domain)
208 ctx = dict(context or {})
209 ctx.update(self.get_eval_context() or {})
211 domain.session = self.session
212 final_domain.extend(domain.evaluate(ctx))
215 def add(self, domain):
216 self.domains.append(domain)
219 def set_eval_context(self, eval_context):
220 self.eval_context = eval_context
223 def get_eval_context(self):
224 return self.eval_context
226 class CompoundContext(BaseContext):
227 def __init__(self, *contexts):
229 self.eval_context = None
231 for context in contexts:
234 def evaluate(self, context=None):
235 ctx = dict(context or {})
236 ctx.update(self.get_eval_context() or {})
238 for context_to_eval in self.contexts:
239 if not isinstance(context_to_eval, (dict, BaseContext)):
241 "Context %r is not a dict or a nonliteral Context" % context_to_eval)
243 if isinstance(context_to_eval, dict):
244 final_context.update(context_to_eval)
247 ctx.update(final_context)
249 context_to_eval.session = self.session
250 final_context.update(context_to_eval.evaluate(ctx))
253 def add(self, context):
254 self.contexts.append(context)
257 def set_eval_context(self, eval_context):
258 self.eval_context = eval_context
261 def get_eval_context(self):
262 return self.eval_context