From 6f3862e10143f263a0fd3c2bf3857ac323d1262a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 28 Mar 2011 17:14:10 +0200 Subject: [PATCH] [ADD] non-literal contexts bzr revid: xmo@openerp.com-20110328151410-6ep57ay6sdc2k39r --- addons/base/controllers/main.py | 16 ++++-- addons/base/tests/test_view.py | 43 ++++++++++++++++ openerpweb/nonliterals.py | 55 +++++++++++++++++++- openerpweb/openerpweb.py | 47 +++++++++-------- openerpweb/tests/test_nonliterals.py | 76 +++++++++++++++++++++++++++- 5 files changed, 210 insertions(+), 27 deletions(-) diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 7981c717324..d38dce05fe1 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -176,8 +176,10 @@ class Menu(openerpweb.Controller): for _, _, action in actions: # values come from the server, we can just eval them - action['context'] = req.session.eval_context( - action['context']) or {} + if isinstance(action['context'], basestring): + action['context'] = eval( + action['context'], + req.session.evaluation_context()) or {} if isinstance(action['domain'], basestring): action['domain'] = eval( @@ -265,7 +267,6 @@ class DataRecord(openerpweb.Controller): value = r[0] return {'value': value} - class View(openerpweb.Controller): def fields_view_get(self, session, model, view_id, view_type, transform=True): Model = session.model(model) @@ -346,6 +347,15 @@ class View(openerpweb.Controller): # not a literal elem.set('domain', openerpweb.nonliterals.Domain(session, domain)) + context_string = elem.get('context') + if context_string: + try: + elem.set('context', + openerpweb.ast.literal_eval(context_string)) + except ValueError: + elem.set('context', + openerpweb.nonliterals.Context( + session, context_string)) class FormView(View): _cp_path = "/base/formview" diff --git a/addons/base/tests/test_view.py b/addons/base/tests/test_view.py index 2b668bb3ecd..98d074c99e0 100644 --- a/addons/base/tests/test_view.py +++ b/addons/base/tests/test_view.py @@ -72,3 +72,46 @@ class ViewTest(unittest2.TestCase): openerpweb.nonliterals.Domain( session, key=e.get('domain').key).get_domain_string(), domain_string) + + def test_convert_literal_context(self): + e = xml.etree.ElementTree.Element( + 'field', context="{'some_prop': 3}") + self.view.parse_domains_and_contexts(e, None) + + self.assertEqual( + e.get('context'), + {'some_prop': 3}) + + def test_convert_complex_domain(self): + e = xml.etree.ElementTree.Element( + 'field', + context="{'account_id.type': ['receivable','payable']," + "'reconcile_id': False," + "'reconcile_partial_id': False," + "'state': 'valid'}" + ) + self.view.parse_domains_and_contexts(e, None) + + self.assertEqual( + e.get('context'), + {'account_id.type': ['receivable', 'payable'], + 'reconcile_id': False, + 'reconcile_partial_id': False, + 'state': 'valid'} + ) + + def test_retrieve_nonliteral_domain(self): + session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession) + session.domains_store = {} + context_string = ("{'month': (datetime.date.today() - " + "datetime.timedelta(365/12)).strftime('%%m')}") + e = xml.etree.ElementTree.Element( + 'field', context=context_string) + + self.view.parse_domains_and_contexts(e, session) + + self.assertIsInstance(e.get('context'), openerpweb.nonliterals.Context) + self.assertEqual( + openerpweb.nonliterals.Context( + session, key=e.get('context').key).get_context_string(), + context_string) diff --git a/openerpweb/nonliterals.py b/openerpweb/nonliterals.py index f381536749d..acca17a2c26 100644 --- a/openerpweb/nonliterals.py +++ b/openerpweb/nonliterals.py @@ -9,7 +9,7 @@ import hashlib import simplejson.decoder import simplejson.encoder -__all__ = ['Domain'] +__all__ = ['Domain', 'Context', 'NonLiteralEncoder, non_literal_decoder'] #: 48 bits should be sufficient to have almost no chance of collision #: with a million hashes, according to hg@67081329d49a @@ -22,12 +22,19 @@ class NonLiteralEncoder(simplejson.encoder.JSONEncoder): '__ref': 'domain', '__id': object.key } + elif isinstance(object, Context): + return { + '__ref': 'context', + '__id': object.key + } return super(NonLiteralEncoder, self).default(object) def non_literal_decoder(dct): if '__ref' in dct: if dct['__ref'] == 'domain': return Domain(None, key=dct['__id']) + elif dct['__ref'] == 'context': + return Context(None, key=dct['__id']) return dct class Domain(object): @@ -74,3 +81,49 @@ class Domain(object): """ return eval(self.get_domain_string(), self.session.evaluation_context(context)) + +class Context(object): + def __init__(self, session, context_string=None, key=None): + """ Uses session information to store the context string and map it to + a key (stored in a secret location under a secret mountain), which can + be safely round-tripped to the client. + + If initialized with a context string, will generate a key for that + string and store the context string out of the way. When initialized + with a key, considers this key is a reference to an existing context + string. + + :param session: the OpenERP Session to use when evaluating the context + :type session: openerpweb.openerpweb.OpenERPSession + :param str context_string: a non-literal context in string form + :param str key: key used to retrieve the context string + """ + if context_string and key: + raise ValueError("A nonliteral domain can not take both a key " + "and a domain string") + + self.session = session + if context_string: + self.key = binascii.hexlify( + hashlib.sha256(context_string).digest()[:SHORT_HASH_BYTES_SIZE]) + self.session.domains_store[self.key] = context_string + elif key: + self.key = key + + def get_context_string(self): + """ Retrieves the context string linked to this non-literal context in + the provided session. + + :param session: the OpenERP Session used to store the context string in + the first place. + :type session: openerpweb.openerpweb.OpenERPSession + """ + return self.session.domains_store[self.key] + + def evaluate(self, context=None): + """ Forces the evaluation of the linked context, using the provided + context (as well as the session's base context), and returns the + evaluated result. + """ + return eval(self.get_context_string(), + self.session.evaluation_context(context)) diff --git a/openerpweb/openerpweb.py b/openerpweb/openerpweb.py index 0f8786b7b41..20d6fc60788 100644 --- a/openerpweb/openerpweb.py +++ b/openerpweb/openerpweb.py @@ -146,36 +146,41 @@ class OpenERPSession(object): d.update(context) return d - def eval_context(self, context_string, context=None, use_base=True): - """ Evaluates the provided context_string in the context (haha) of + def eval_context(self, context_to_eval, context=None): + """ Evaluates the provided context_to_eval in the context (haha) of the context. - :param str context_string: a context to evaluate, if not a string, - will be returned as-is - :param dict context: the context to use in the evaluation, if any. - :param bool use_base: whether the base eval context (combination - of the default context and the session - context) should be merged to the provided - context (or used alone) + :param context_to_eval: a context to evaluate. Must be a dict or a + non-literal context. If it's a dict, will be + returned as-is + :type context_to_eval: openerpweb.nonliterals.Context :returns: the evaluated context :rtype: dict + + :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor + a Context """ - if not isinstance(context_string, basestring): - return context_string + if not isinstance(context_to_eval, (dict, nonliterals.Domain)): + raise TypeError("Context %r is not a dict or a nonliteral Context", + context_to_eval) + + if isinstance(context_to_eval, dict): + return context_to_eval ctx = {} - if use_base: - ctx.update(self.base_eval_context) if context: ctx.update(context) ctx['context'] = ctx - return eval(context_string, ctx) + # if the domain was unpacked from JSON, it needs the current + # OpenERPSession for its data retrieval + context_to_eval.session = self + return context_to_eval.evaluate(ctx) def eval_contexts(self, contexts, context=None): """ Evaluates a sequence of contexts to build a single final result - :param list contexts: a list of string or dict contexts + :param list contexts: a list of Context or dict contexts :param dict context: a base context, if needed :returns: the final combination of all provided contexts :rtype: dict @@ -192,17 +197,17 @@ class OpenERPSession(object): # the result final_context.update( self.eval_context( - ctx, current_context, use_base=False)) + ctx, current_context)) # update the current evaluation context so that future # evaluations can use the results we just gathered current_context.update(final_context) return final_context def eval_domain(self, domain, context=None): - """ Evaluates the provided domain_string using the provided context + """ Evaluates the provided domain using the provided context (merged with the session's evaluation context) - :param domain: an OpenERP domain as a dict or as a + :param domain: an OpenERP domain as a list or as a :class:`openerpweb.nonliterals.Domain` instance In the second case, it will be evaluated and returned. @@ -211,10 +216,10 @@ class OpenERPSession(object): :returns: the evaluated domain :rtype: list - :raises: ``TypeError`` if ``domain`` is neither a dict nor a Domain + :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain """ if not isinstance(domain, (list, nonliterals.Domain)): - raise TypeError("Domain %r is not a dict or a nonliteral Domain", + raise TypeError("Domain %r is not a list or a nonliteral Domain", domain) if isinstance(domain, list): @@ -236,7 +241,7 @@ class OpenERPSession(object): Returns the final, concatenated result. - :param list domains: a list of string or list domains + :param list domains: a list of Domain or list domains :param dict context: the context in which the domains should be evaluated (if evaluations need to happen) diff --git a/openerpweb/tests/test_nonliterals.py b/openerpweb/tests/test_nonliterals.py index 4ebb50847d1..c522bfcb6be 100644 --- a/openerpweb/tests/test_nonliterals.py +++ b/openerpweb/tests/test_nonliterals.py @@ -3,7 +3,7 @@ import mock import simplejson import unittest2 -from openerpweb.nonliterals import Domain +from openerpweb.nonliterals import Domain, Context import openerpweb.nonliterals import openerpweb.openerpweb @@ -44,13 +44,85 @@ class NonLiteralDomainTest(unittest2.TestCase): self.assertEqual( result, [('a', '=', 3)]) +class NonLiteralContextTest(unittest2.TestCase): + def setUp(self): + self.session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession) + self.session.domains_store = {} + def test_store_domain(self): + c = Context(self.session, "some arbitrary string") + + self.assertEqual( + self.session.domains_store[c.key], + "some arbitrary string") + + def test_get_domain_back(self): + c = Context(self.session, "some arbitrary string") + + self.assertEqual( + c.get_context_string(), + "some arbitrary string") + def test_retrieve_second_domain(self): + """ A different domain should be able to retrieve the nonliteral set + previously + """ + key = Context(self.session, "some arbitrary string").key + + self.assertEqual( + Domain(self.session, key=key).get_domain_string(), + "some arbitrary string") + + def test_key_and_string(self): + self.assertRaises( + ValueError, Context, None, context_string="a", key="b") + + def test_eval(self): + self.session.evaluation_context.return_value = {'foo': 3} + result = Context(self.session, "[('a', '=', foo)]")\ + .evaluate({'foo': 3}) + self.assertEqual( + result, [('a', '=', 3)]) + class NonLiteralJSON(unittest2.TestCase): def setUp(self): self.session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession) self.session.domains_store = {} - def test_encode(self): + def test_encode_domain(self): d = Domain(self.session, "some arbitrary string") self.assertEqual( simplejson.dumps(d, cls=openerpweb.nonliterals.NonLiteralEncoder), simplejson.dumps({'__ref': 'domain', '__id': d.key})) + + def test_decode_domain(self): + encoded = simplejson.dumps( + Domain(self.session, "some arbitrary string"), + cls=openerpweb.nonliterals.NonLiteralEncoder) + + domain = simplejson.loads( + encoded, object_hook=openerpweb.nonliterals.non_literal_decoder) + domain.session = self.session + + self.assertEqual( + domain.get_domain_string(), + "some arbitrary string" + ) + + def test_encode_context(self): + c = Context(self.session, "some arbitrary string") + self.assertEqual( + simplejson.dumps(c, cls=openerpweb.nonliterals.NonLiteralEncoder), + simplejson.dumps({'__ref': 'context', '__id': c.key})) + + def test_decode_context(self): + encoded = simplejson.dumps( + Context(self.session, "some arbitrary string"), + cls=openerpweb.nonliterals.NonLiteralEncoder) + + context = simplejson.loads( + encoded, object_hook=openerpweb.nonliterals.non_literal_decoder) + context.session = self.session + + self.assertEqual( + context.get_context_string(), + "some arbitrary string" + )