[ADD] start working on adding the concept of literal and non-lit domains, non-lit domains will *not* be pushed to the client but will be stored locally and only a ref will go through

bzr revid: xmo@openerp.com-20110328122724-gnxn8cnta4xyotab
This commit is contained in:
Xavier Morel 2011-03-28 14:27:24 +02:00
parent ec7510b839
commit fd9da7558a
7 changed files with 304 additions and 7 deletions

View File

@ -6,6 +6,8 @@ from cStringIO import StringIO
import simplejson
import openerpweb
import openerpweb.ast
import openerpweb.nonliterals
__all__ = ['Session', 'Menu', 'DataSet', 'DataRecord',
'View', 'FormView', 'ListView', 'SearchView',
@ -264,7 +266,7 @@ class View(openerpweb.Controller):
r = Model.fields_view_get(view_id, view_type)
if transform:
context = {} # TODO: dict(ctx_sesssion, **ctx_action)
xml = self.transform_view(r['arch'], context)
xml = self.transform_view(r['arch'], session, context)
else:
xml = ElementTree.fromstring(r['arch'])
r['arch'] = Xml2Json.convert_element(xml)
@ -303,7 +305,7 @@ class View(openerpweb.Controller):
if a == 'invisible' and 'attrs' in elem.attrib:
del elem.attrib['attrs']
def transform_view(self, view_string, context=None):
def transform_view(self, view_string, session, context=None):
# transform nodes on the fly via iterparse, instead of
# doing it statically on the parsing result
parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
@ -313,8 +315,32 @@ class View(openerpweb.Controller):
if root is None:
root = elem
self.normalize_attrs(elem, context)
self.parse_domains_and_contexts(elem, session)
return root
def parse_domains_and_contexts(self, elem, session):
""" Converts domains and contexts from the view into Python objects,
either literals if they can be parsed by literal_eval or a special
placeholder object if the domain or context refers to free variables.
:param elem: the current node being parsed
:type param: xml.etree.ElementTree.Element
:param session: OpenERP session object, used to store and retrieve
non-literal objects
:type session: openerpweb.openerpweb.OpenERPSession
"""
domain = elem.get('domain')
if domain:
try:
elem.set(
'domain',
openerpweb.ast.literal_eval(
domain))
except ValueError:
# not a literal
elem.set('domain',
openerpweb.nonliterals.Domain(session, domain))
class FormView(View):
_cp_path = "/base/formview"

View File

@ -1,12 +1,17 @@
import xml.etree.ElementTree
import mock
import unittest2
import base.controllers.main
import openerpweb.nonliterals
import openerpweb.openerpweb
#noinspection PyCompatibility
class ViewTest(unittest2.TestCase):
def setUp(self):
self.view = base.controllers.main.View()
def test_identity(self):
view = base.controllers.main.View()
base_view = """
<form string="Title">
<group>
@ -18,10 +23,52 @@ class ViewTest(unittest2.TestCase):
"""
pristine = xml.etree.ElementTree.fromstring(base_view)
transformed = view.transform_view(base_view)
transformed = self.view.transform_view(base_view, None)
self.assertEqual(
xml.etree.ElementTree.tostring(transformed),
xml.etree.ElementTree.tostring(pristine)
)
def test_convert_literal_domain(self):
e = xml.etree.ElementTree.Element(
'field', domain="[('somefield', '=', 3)]")
self.view.parse_domains_and_contexts(e, None)
self.assertEqual(
e.get('domain'),
[('somefield', '=', 3)])
def test_convert_complex_domain(self):
e = xml.etree.ElementTree.Element(
'field',
domain="[('account_id.type','in',['receivable','payable']),"
"('reconcile_id','=',False),"
"('reconcile_partial_id','=',False),"
"('state', '=', 'valid')]"
)
self.view.parse_domains_and_contexts(e, None)
self.assertEqual(
e.get('domain'),
[('account_id.type', 'in', ['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 = {}
domain_string = ("[('month','=',(datetime.date.today() - "
"datetime.timedelta(365/12)).strftime('%%m'))]")
e = xml.etree.ElementTree.Element(
'field', domain=domain_string)
self.view.parse_domains_and_contexts(e, session)
self.assertIsInstance(e.get('domain'), openerpweb.nonliterals.Domain)
self.assertEqual(
openerpweb.nonliterals.Domain(
session, key=e.get('domain').key).get_domain_string(),
domain_string)

48
openerpweb/ast.py Normal file
View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
""" Backport of Python 2.6's ast.py for Python 2.5
"""
__all__ = ['literal_eval']
try:
from ast import literal_eval
except ImportError:
from _ast import *
from _ast import __version__
def parse(expr, filename='<unknown>', mode='exec'):
"""
Parse an expression into an AST node.
Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
"""
return compile(expr, filename, mode, PyCF_ONLY_AST)
def literal_eval(node_or_string):
"""
Safely evaluate an expression node or a string containing a Python
expression. The string or node provided may only consist of the
following Python literal structures: strings, numbers, tuples, lists,
dicts, booleans, and None.
"""
_safe_names = {'None': None, 'True': True, 'False': False}
if isinstance(node_or_string, basestring):
node_or_string = parse(node_or_string, mode='eval')
if isinstance(node_or_string, Expression):
node_or_string = node_or_string.body
def _convert(node):
if isinstance(node, Str):
return node.s
elif isinstance(node, Num):
return node.n
elif isinstance(node, Tuple):
return tuple(map(_convert, node.elts))
elif isinstance(node, List):
return list(map(_convert, node.elts))
elif isinstance(node, Dict):
return dict((_convert(k), _convert(v)) for k, v
in zip(node.keys, node.values))
elif isinstance(node, Name):
if node.id in _safe_names:
return _safe_names[node.id]
raise ValueError('malformed string')
return _convert(node_or_string)

59
openerpweb/nonliterals.py Normal file
View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
""" Manages the storage and lifecycle of non-literal domains and contexts
(and potentially other structures) which have to be evaluated with client data,
but still need to be safely round-tripped to and from the browser (and thus
can't be sent there themselves).
"""
import binascii
import hashlib
__all__ = ['Domain']
#: 48 bits should be sufficient to have almost no chance of collision
#: with a million hashes, according to hg@67081329d49a
SHORT_HASH_BYTES_SIZE = 6
class Domain(object):
def __init__(self, session, domain_string=None, key=None):
""" Uses session information to store the domain string and map it to a
domain key, which can be safely round-tripped to the client.
If initialized with a domain string, will generate a key for that
string and store the domain string out of the way. When initialized
with a key, considers this key is a reference to an existing domain
string.
:param session: the OpenERP Session to use when evaluating the domain
:type session: openerpweb.openerpweb.OpenERPSession
:param str domain_string: a non-literal domain in string form
:param str key: key used to retrieve the domain string
"""
if domain_string and key:
raise ValueError("A nonliteral domain can not take both a key "
"and a domain string")
self.session = session
if domain_string:
self.key = binascii.hexlify(
hashlib.sha256(domain_string).digest()[:SHORT_HASH_BYTES_SIZE])
self.session.domains_store[self.key] = domain_string
elif key:
self.key = key
def get_domain_string(self):
""" Retrieves the domain string linked to this non-literal domain in
the provided session.
:param session: the OpenERP Session used to store the domain 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 domain, using the provided
context (as well as the session's base context), and returns the
evaluated result.
"""
return eval(self.get_domain_string(),
self.session.evaluation_context(context))

View File

@ -50,6 +50,13 @@ class OpenERPSession(object):
The session context, a ``dict``. Can be reloaded by calling
:meth:`openerpweb.openerpweb.OpenERPSession.get_context`
.. attribute:: domains_store
A ``dict`` matching domain keys to evaluable (but non-literal) domains.
Used to store references to non-literal domains which need to be
round-tripped to the client browser.
"""
def __init__(self, server='127.0.0.1', port=8069,
model_factory=OpenERPModel):
@ -62,6 +69,7 @@ class OpenERPSession(object):
self.model_factory = model_factory
self.context = {}
self.domains_store = {}
def proxy(self, service):
s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
@ -113,14 +121,29 @@ class OpenERPSession(object):
Used to evaluate contexts and domains.
"""
return dict(
base = dict(
uid=self._uid,
current_date=datetime.date.today().strftime('%Y-%m-%d'),
time=time,
datetime=datetime,
relativedelta=dateutil.relativedelta.relativedelta,
**self.context
relativedelta=dateutil.relativedelta.relativedelta
)
base.update(self.context)
return base
def evaluation_context(self, context=None):
""" Returns the session's evaluation context, augmented with the
provided context if any.
:param dict context: to add merge in the session's base eval context
:returns: the augmented context
:rtype: dict
"""
d = {}
d.update(self.base_eval_context)
if context:
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

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
import mock
import unittest2
from openerpweb.nonliterals import Domain
import openerpweb.openerpweb
class NonLiteralDomainTest(unittest2.TestCase):
def setUp(self):
self.session = mock.Mock(spec=openerpweb.openerpweb.OpenERPSession)
self.session.domains_store = {}
def test_store_domain(self):
d = Domain(self.session, "some arbitrary string")
self.assertEqual(
self.session.domains_store[d.key],
"some arbitrary string")
def test_get_domain_back(self):
d = Domain(self.session, "some arbitrary string")
self.assertEqual(
d.get_domain_string(),
"some arbitrary string")
def test_retrieve_second_domain(self):
""" A different domain should be able to retrieve the nonliteral set
previously
"""
key = Domain(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, Domain, None, domain_string="a", key="b")
def test_eval(self):
self.session.evaluation_context.return_value = {'foo': 3}
result = Domain(self.session, "[('a', '=', foo)]").evaluate({'foo': 3})
self.assertEqual(
result, [('a', '=', 3)])

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import unittest2
import openerpweb.openerpweb
class TestOpenERPSession(unittest2.TestCase):
def setUp(self):
self.module = object()
self.session = openerpweb.openerpweb.OpenERPSession()
self.session._uid = -1
self.session.context = {
'current_date': '1945-08-05',
'date': self.module,
'time': self.module,
'datetime': self.module,
'relativedelta': self.module
}
def test_base_eval_context(self):
self.assertEqual(type(self.session.base_eval_context), dict)
self.assertEqual(
self.session.base_eval_context,
{'uid': -1, 'current_date': '1945-08-05',
'date': self.module, 'datetime': self.module, 'time': self.module,
'relativedelta': self.module}
)
def test_evaluation_context_nocontext(self):
self.assertEqual(
type(self.session.evaluation_context()),
dict
)
self.assertEqual(
self.session.evaluation_context(),
self.session.base_eval_context
)
def test_evaluation_context(self):
ctx = self.session.evaluation_context({'foo': 3})
self.assertEqual(
type(ctx),
dict
)
self.assertIn('foo', ctx)
def test_eval_with_context(self):
self.assertEqual(
eval('current_date', self.session.evaluation_context()),
'1945-08-05')
self.assertEqual(
eval('foo + 3', self.session.evaluation_context({'foo': 4})),
7)