[MERGE] trunk

bzr revid: csaba.toth@i3rendszerhaz.hu-20121129114132-gfe3sn1p5t0mzofm
This commit is contained in:
tsabi 2012-11-29 12:41:32 +01:00
commit 76ca47d4fd
63 changed files with 6789 additions and 2130 deletions

View File

@ -42,6 +42,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/py.js/lib/py.js",
"static/src/js/boot.js",
"static/src/js/testing.js",
"static/src/js/pyeval.js",
"static/src/js/corelib.js",
"static/src/js/coresetup.js",
"static/src/js/dates.js",

View File

@ -31,7 +31,6 @@ import openerp
from openerp.tools.translate import _
from .. import http
from .. import nonliterals
openerpweb = http
#----------------------------------------------------------
@ -362,40 +361,15 @@ def set_cookie_and_redirect(req, redirect_url):
redirect.set_cookie('instance0|session_id', cookie_val)
return redirect
def eval_context_and_domain(session, context, domain=None):
e_context = session.eval_context(context)
# should we give the evaluated context as an evaluation context to the domain?
e_domain = session.eval_domain(domain or [])
return e_context, e_domain
def load_actions_from_ir_values(req, key, key2, models, meta):
context = req.session.eval_context(req.context)
Values = req.session.model('ir.values')
actions = Values.get(key, key2, models, meta, context)
actions = Values.get(key, key2, models, meta, req.context)
return [(id, name, clean_action(req, action, context))
return [(id, name, clean_action(req, action))
for id, name, action in actions]
def clean_action(req, action, context, do_not_eval=False):
def clean_action(req, action):
action.setdefault('flags', {})
context = context or {}
eval_ctx = req.session.evaluation_context(context)
if not do_not_eval:
# values come from the server, we can just eval them
if action.get('context') and isinstance(action.get('context'), basestring):
action['context'] = eval( action['context'], eval_ctx ) or {}
if action.get('domain') and isinstance(action.get('domain'), basestring):
action['domain'] = eval( action['domain'], eval_ctx ) or []
else:
if 'context' in action:
action['context'] = parse_context(action['context'], req.session)
if 'domain' in action:
action['domain'] = parse_domain(action['domain'], req.session)
action_type = action.setdefault('type', 'ir.actions.act_window_close')
if action_type == 'ir.actions.act_window':
return fix_view_modes(action)
@ -474,39 +448,6 @@ def fix_view_modes(action):
return action
def parse_domain(domain, session):
""" Parses an arbitrary string containing a domain, transforms it
to either a literal domain or a :class:`nonliterals.Domain`
:param domain: the domain to parse, if the domain is not a string it
is assumed to be a literal domain and is returned as-is
:param session: Current OpenERP session
:type session: openerpweb.OpenERPSession
"""
if not isinstance(domain, basestring):
return domain
try:
return ast.literal_eval(domain)
except ValueError:
# not a literal
return nonliterals.Domain(session, domain)
def parse_context(context, session):
""" Parses an arbitrary string containing a context, transforms it
to either a literal context or a :class:`nonliterals.Context`
:param context: the context to parse, if the context is not a string it
is assumed to be a literal domain and is returned as-is
:param session: Current OpenERP session
:type session: openerpweb.OpenERPSession
"""
if not isinstance(context, basestring):
return context
try:
return ast.literal_eval(context)
except ValueError:
return nonliterals.Context(session, context)
def _local_web_translations(trans_file):
messages = []
try:
@ -912,7 +853,7 @@ class Session(openerpweb.Controller):
@openerpweb.jsonrequest
def sc_list(self, req):
return req.session.model('ir.ui.view_sc').get_sc(
req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
req.session._uid, "ir.ui.menu", req.context)
@openerpweb.jsonrequest
def get_lang_list(self, req):
@ -926,59 +867,6 @@ class Session(openerpweb.Controller):
# return all installed modules. Web client is smart enough to not load a module twice
return module_installed(req)
@openerpweb.jsonrequest
def eval_domain_and_context(self, req, contexts, domains,
group_by_seq=None):
""" Evaluates sequences of domains and contexts, composing them into
a single context, domain or group_by sequence.
:param list contexts: list of contexts to merge together. Contexts are
evaluated in sequence, all previous contexts
are part of their own evaluation context
(starting at the session context).
:param list domains: list of domains to merge together. Domains are
evaluated in sequence and appended to one another
(implicit AND), their evaluation domain is the
result of merging all contexts.
:param list group_by_seq: list of domains (which may be in a different
order than the ``contexts`` parameter),
evaluated in sequence, their ``'group_by'``
key is extracted if they have one.
:returns:
a 3-dict of:
context (``dict``)
the global context created by merging all of
``contexts``
domain (``list``)
the concatenation of all domains
group_by (``list``)
a list of fields to group by, potentially empty (in which case
no group by should be performed)
"""
context, domain = eval_context_and_domain(req.session,
nonliterals.CompoundContext(*(contexts or [])),
nonliterals.CompoundDomain(*(domains or [])))
group_by_sequence = []
for candidate in (group_by_seq or []):
ctx = req.session.eval_context(candidate, context)
group_by = ctx.get('group_by')
if not group_by:
continue
elif isinstance(group_by, basestring):
group_by_sequence.append(group_by)
else:
group_by_sequence.extend(group_by)
return {
'context': context,
'domain': domain,
'group_by': group_by_sequence
}
@openerpweb.jsonrequest
def save_session_action(self, req, the_action):
"""
@ -1048,18 +936,19 @@ class Menu(openerpweb.Controller):
:rtype: list(int)
"""
s = req.session
context = s.eval_context(req.context)
Menus = s.model('ir.ui.menu')
# If a menu action is defined use its domain to get the root menu items
user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
req.context)[0]['menu_id']
menu_domain = [('parent_id', '=', False)]
if user_menu_id:
domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
domain_string = s.model('ir.actions.act_window').read(
[user_menu_id[0]], ['domain'],req.context)[0]['domain']
if domain_string:
menu_domain = ast.literal_eval(domain_string)
return Menus.search(menu_domain, 0, False, False, context)
return Menus.search(menu_domain, 0, False, False, req.context)
def do_load(self, req):
""" Loads all menu items (all applications and their sub-menus).
@ -1069,23 +958,30 @@ class Menu(openerpweb.Controller):
:return: the menu root
:rtype: dict('children': menu_nodes)
"""
context = req.session.eval_context(req.context)
Menus = req.session.model('ir.ui.menu')
menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
fields = ['name', 'sequence', 'parent_id', 'action',
'needaction_enabled', 'needaction_counter']
menu_roots = Menus.read(self.do_get_user_roots(req), fields, req.context)
menu_root = {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
'children': menu_roots
}
# menus are loaded fully unlike a regular tree view, cause there are a
# limited number of items (752 when all 6.1 addons are installed)
menu_ids = Menus.search([], 0, False, False, context)
menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
menu_ids = Menus.search([], 0, False, False, req.context)
menu_items = Menus.read(menu_ids, fields, req.context)
# adds roots at the end of the sequence, so that they will overwrite
# equivalent menu items from full menu read when put into id:item
# mapping, resulting in children being correctly set on the roots.
menu_items.extend(menu_roots)
# make a tree using parent_id
menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
menu_items_map = dict(
(menu_item["id"], menu_item) for menu_item in menu_items)
for menu_item in menu_items:
if menu_item['parent_id']:
parent = menu_item['parent_id'][0]
@ -1135,12 +1031,10 @@ class DataSet(openerpweb.Controller):
"""
Model = req.session.model(model)
context, domain = eval_context_and_domain(
req.session, req.context, domain)
ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
ids = Model.search(domain, offset or 0, limit or False, sort or False,
req.context)
if limit and len(ids) == limit:
length = Model.search_count(domain, context)
length = Model.search_count(domain, req.context)
else:
length = len(ids) + (offset or 0)
if fields and fields == ['id']:
@ -1150,7 +1044,7 @@ class DataSet(openerpweb.Controller):
'records': [{'id': id} for id in ids]
}
records = Model.read(ids, fields or False, context)
records = Model.read(ids, fields or False, req.context)
records.sort(key=lambda obj: ids.index(obj['id']))
return {
'length': length,
@ -1161,37 +1055,15 @@ class DataSet(openerpweb.Controller):
def load(self, req, model, id, fields):
m = req.session.model(model)
value = {}
r = m.read([id], False, req.session.eval_context(req.context))
r = m.read([id], False, req.context)
if r:
value = r[0]
return {'value': value}
def call_common(self, req, model, method, args, domain_id=None, context_id=None):
has_domain = domain_id is not None and domain_id < len(args)
has_context = context_id is not None and context_id < len(args)
domain = args[domain_id] if has_domain else []
context = args[context_id] if has_context else {}
c, d = eval_context_and_domain(req.session, context, domain)
if has_domain:
args[domain_id] = d
if has_context:
args[context_id] = c
return self._call_kw(req, model, method, args, {})
def _call_kw(self, req, model, method, args, kwargs):
for i in xrange(len(args)):
if isinstance(args[i], nonliterals.BaseContext):
args[i] = req.session.eval_context(args[i])
elif isinstance(args[i], nonliterals.BaseDomain):
args[i] = req.session.eval_domain(args[i])
for k in kwargs.keys():
if isinstance(kwargs[k], nonliterals.BaseContext):
kwargs[k] = req.session.eval_context(kwargs[k])
elif isinstance(kwargs[k], nonliterals.BaseDomain):
kwargs[k] = req.session.eval_domain(kwargs[k])
def _call_kw(self, req, model, method, args, kwargs):
# Temporary implements future display_name special field for model#read()
if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
if 'display_name' in args[1]:
@ -1204,39 +1076,9 @@ class DataSet(openerpweb.Controller):
return getattr(req.session.model(model), method)(*args, **kwargs)
@openerpweb.jsonrequest
def onchange(self, req, model, method, args, context_id=None):
""" Support method for handling onchange calls: behaves much like call
with the following differences:
* Does not take a domain_id
* Is aware of the return value's structure, and will parse the domains
if needed in order to return either parsed literal domains (in JSON)
or non-literal domain instances, allowing those domains to be used
from JS
:param req:
:type req: web.common.http.JsonRequest
:param str model: object type on which to call the method
:param str method: name of the onchange handler method
:param list args: arguments to call the onchange handler with
:param int context_id: index of the context object in the list of
arguments
:return: result of the onchange call with all domains parsed
"""
result = self.call_common(req, model, method, args, context_id=context_id)
if not result or 'domain' not in result:
return result
result['domain'] = dict(
(k, parse_domain(v, req.session))
for k, v in result['domain'].iteritems())
return result
@openerpweb.jsonrequest
def call(self, req, model, method, args, domain_id=None, context_id=None):
return self.call_common(req, model, method, args, domain_id, context_id)
return self._call_kw(req, model, method, args, {})
@openerpweb.jsonrequest
def call_kw(self, req, model, method, args, kwargs):
@ -1244,10 +1086,9 @@ class DataSet(openerpweb.Controller):
@openerpweb.jsonrequest
def call_button(self, req, model, method, args, domain_id=None, context_id=None):
context = req.session.eval_context(req.context)
action = self.call_common(req, model, method, args, domain_id, context_id)
action = self._call_kw(req, model, method, args, {})
if isinstance(action, dict) and action.get('type') != '':
return clean_action(req, action, context)
return clean_action(req, action)
return False
@openerpweb.jsonrequest
@ -1283,12 +1124,9 @@ class View(openerpweb.Controller):
def fields_view_get(self, req, model, view_id, view_type,
transform=True, toolbar=False, submenu=False):
Model = req.session.model(model)
context = req.session.eval_context(req.context)
fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
fvg = Model.fields_view_get(view_id, view_type, req.context, toolbar, submenu)
# todo fme?: check that we should pass the evaluated context here
self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
if toolbar and transform:
self.process_toolbar(req, fvg['toolbar'])
self.process_view(req.session, fvg, req.context, transform, (view_type == 'kanban'))
return fvg
def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
@ -1305,12 +1143,8 @@ class View(openerpweb.Controller):
arch = fvg['arch']
fvg['arch_string'] = arch
if transform:
evaluation_context = session.evaluation_context(context or {})
xml = self.transform_view(arch, session, evaluation_context)
else:
xml = ElementTree.fromstring(arch)
fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
fvg['arch'] = xml2json_from_elementtree(
ElementTree.fromstring(arch), preserve_whitespaces)
if 'id' in fvg['fields']:
# Special case for id's
@ -1319,29 +1153,8 @@ class View(openerpweb.Controller):
id_field['type'] = 'id'
for field in fvg['fields'].itervalues():
if field.get('views'):
for view in field["views"].itervalues():
self.process_view(session, view, None, transform)
if field.get('domain'):
field["domain"] = parse_domain(field["domain"], session)
if field.get('context'):
field["context"] = parse_context(field["context"], session)
def process_toolbar(self, req, toolbar):
"""
The toolbar is a mapping of section_key: [action_descriptor]
We need to clean all those actions in order to ensure correct
round-tripping
"""
for actions in toolbar.itervalues():
for action in actions:
if 'context' in action:
action['context'] = parse_context(
action['context'], req.session)
if 'domain' in action:
action['domain'] = parse_domain(
action['domain'], req.session)
for view in field.get("views", {}).itervalues():
self.process_view(session, view, None, transform)
@openerpweb.jsonrequest
def add_custom(self, req, view_id, arch):
@ -1350,57 +1163,22 @@ class View(openerpweb.Controller):
'user_id': req.session._uid,
'ref_id': view_id,
'arch': arch
}, req.session.eval_context(req.context))
}, req.context)
return {'result': True}
@openerpweb.jsonrequest
def undo_custom(self, req, view_id, reset=False):
CustomView = req.session.model('ir.ui.view.custom')
context = req.session.eval_context(req.context)
vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
0, False, False, context)
0, False, False, req.context)
if vcustom:
if reset:
CustomView.unlink(vcustom, context)
CustomView.unlink(vcustom, req.context)
else:
CustomView.unlink([vcustom[0]], context)
CustomView.unlink([vcustom[0]], req.context)
return {'result': True}
return {'result': False}
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",))
root = None
for event, elem in parser:
if event == "start":
if root is None:
root = elem
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
"""
for el in ['domain', 'filter_domain']:
domain = elem.get(el, '').strip()
if domain:
elem.set(el, parse_domain(domain, session))
elem.set(el + '_string', domain)
for el in ['context', 'default_get']:
context_string = elem.get(el, '').strip()
if context_string:
elem.set(el, parse_context(context_string, session))
elem.set(el + '_string', context_string)
@openerpweb.jsonrequest
def load(self, req, model, view_id, view_type, toolbar=False):
return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
@ -1414,50 +1192,6 @@ class TreeView(View):
req,'action', 'tree_but_open',[(model, id)],
False)
class SearchView(View):
_cp_path = "/web/searchview"
@openerpweb.jsonrequest
def load(self, req, model, view_id):
fields_view = self.fields_view_get(req, model, view_id, 'search')
return {'fields_view': fields_view}
@openerpweb.jsonrequest
def fields_get(self, req, model):
Model = req.session.model(model)
fields = Model.fields_get(False, req.session.eval_context(req.context))
for field in fields.values():
# shouldn't convert the views too?
if field.get('domain'):
field["domain"] = parse_domain(field["domain"], req.session)
if field.get('context'):
field["context"] = parse_context(field["context"], req.session)
return {'fields': fields}
@openerpweb.jsonrequest
def get_filters(self, req, model):
logger = logging.getLogger(__name__ + '.SearchView.get_filters')
Model = req.session.model("ir.filters")
filters = Model.get_filters(model)
for filter in filters:
try:
parsed_context = parse_context(filter["context"], req.session)
filter["context"] = (parsed_context
if not isinstance(parsed_context, nonliterals.BaseContext)
else req.session.eval_context(parsed_context))
parsed_domain = parse_domain(filter["domain"], req.session)
filter["domain"] = (parsed_domain
if not isinstance(parsed_domain, nonliterals.BaseDomain)
else req.session.eval_domain(parsed_domain))
except Exception:
logger.exception(_("Failed to parse custom filter %s in %s"),
filter['name'], model)
filter['disabled'] = True
del filter['context']
del filter['domain']
return filters
class Binary(openerpweb.Controller):
_cp_path = "/web/binary"
@ -1465,7 +1199,6 @@ class Binary(openerpweb.Controller):
def image(self, req, model, id, field, **kw):
last_update = '__last_update'
Model = req.session.model(model)
context = req.session.eval_context(req.context)
headers = [('Content-Type', 'image/png')]
etag = req.httprequest.headers.get('If-None-Match')
hashed_session = hashlib.md5(req.session_id).hexdigest()
@ -1476,22 +1209,22 @@ class Binary(openerpweb.Controller):
if not id and hashed_session == etag:
return werkzeug.wrappers.Response(status=304)
else:
date = Model.read([id], [last_update], context)[0].get(last_update)
date = Model.read([id], [last_update], req.context)[0].get(last_update)
if hashlib.md5(date).hexdigest() == etag:
return werkzeug.wrappers.Response(status=304)
retag = hashed_session
try:
if not id:
res = Model.default_get([field], context).get(field)
res = Model.default_get([field], req.context).get(field)
image_base64 = res
else:
res = Model.read([id], [last_update, field], context)[0]
res = Model.read([id], [last_update, field], req.context)[0]
retag = hashlib.md5(res.get(last_update)).hexdigest()
image_base64 = res.get(field)
if kw.get('resize'):
resize = kw.get('resize').split(',');
resize = kw.get('resize').split(',')
if len(resize) == 2 and int(resize[0]) and int(resize[1]):
width = int(resize[0])
height = int(resize[1])
@ -1533,14 +1266,13 @@ class Binary(openerpweb.Controller):
:returns: :class:`werkzeug.wrappers.Response`
"""
Model = req.session.model(model)
context = req.session.eval_context(req.context)
fields = [field]
if filename_field:
fields.append(filename_field)
if id:
res = Model.read([int(id)], fields, context)[0]
res = Model.read([int(id)], fields, req.context)[0]
else:
res = Model.default_get(fields, context)
res = Model.default_get(fields, req.context)
filecontent = base64.b64decode(res.get(field, ''))
if not filecontent:
return req.not_found()
@ -1559,9 +1291,8 @@ class Binary(openerpweb.Controller):
field = jdata['field']
id = jdata.get('id', None)
filename_field = jdata.get('filename_field', None)
context = jdata.get('context', dict())
context = jdata.get('context', {})
context = req.session.eval_context(context)
Model = req.session.model(model)
fields = [field]
if filename_field:
@ -1600,7 +1331,6 @@ class Binary(openerpweb.Controller):
@openerpweb.httprequest
def upload_attachment(self, req, callback, model, id, ufile):
context = req.session.eval_context(req.context)
Model = req.session.model('ir.attachment')
try:
out = """<script language="javascript" type="text/javascript">
@ -1613,7 +1343,7 @@ class Binary(openerpweb.Controller):
'datas_fname': ufile.filename,
'res_model': model,
'res_id': int(id)
}, context)
}, req.context)
args = {
'filename': ufile.filename,
'id': attachment_id
@ -1626,12 +1356,9 @@ class Action(openerpweb.Controller):
_cp_path = "/web/action"
@openerpweb.jsonrequest
def load(self, req, action_id, do_not_eval=False, eval_context=None):
def load(self, req, action_id, do_not_eval=False):
Actions = req.session.model('ir.actions.actions')
value = False
context = req.session.eval_context(req.context)
eval_context = req.session.eval_context(nonliterals.CompoundContext(context, eval_context or {}))
try:
action_id = int(action_id)
except ValueError:
@ -1642,25 +1369,24 @@ class Action(openerpweb.Controller):
except Exception:
action_id = 0 # force failed read
base_action = Actions.read([action_id], ['type'], context)
base_action = Actions.read([action_id], ['type'], req.context)
if base_action:
ctx = {}
action_type = base_action[0]['type']
if action_type == 'ir.actions.report.xml':
ctx.update({'bin_size': True})
ctx.update(context)
ctx.update(req.context)
action = req.session.model(action_type).read([action_id], False, ctx)
if action:
value = clean_action(req, action[0], eval_context, do_not_eval)
value = clean_action(req, action[0])
return value
@openerpweb.jsonrequest
def run(self, req, action_id):
context = req.session.eval_context(req.context)
return_action = req.session.model('ir.actions.server').run(
[action_id], req.session.eval_context(req.context))
[action_id], req.context)
if return_action:
return clean_action(req, return_action, context)
return clean_action(req, return_action)
else:
return False
@ -1683,7 +1409,7 @@ class Export(View):
def fields_get(self, req, model):
Model = req.session.model(model)
fields = Model.fields_get(False, req.session.eval_context(req.context))
fields = Model.fields_get(False, req.context)
return fields
@openerpweb.jsonrequest
@ -1835,12 +1561,11 @@ class Export(View):
'import_compat')(
simplejson.loads(data))
context = req.session.eval_context(req.context)
Model = req.session.model(model)
ids = ids or Model.search(domain, 0, False, False, context)
ids = ids or Model.search(domain, 0, False, False, req.context)
field_names = map(operator.itemgetter('name'), fields)
import_data = Model.export_data(ids, field_names, context).get('datas',[])
import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
if import_compat:
columns_headers = field_names
@ -1945,9 +1670,8 @@ class Reports(View):
action = simplejson.loads(action)
report_srv = req.session.proxy("report")
context = req.session.eval_context(
nonliterals.CompoundContext(
req.context or {}, action[ "context"]))
context = dict(req.context)
context.update(action["context"])
report_data = {}
report_ids = context["active_ids"]

View File

@ -11,7 +11,7 @@ from mako.template import Template
from openerp.modules import module
from .main import module_topological_sort
from ..http import Controller, httprequest
from .. import http
NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
<html>
@ -82,10 +82,10 @@ TESTING = Template(u"""<!DOCTYPE html>
</html>
""")
class TestRunnerController(Controller):
class TestRunnerController(http.Controller):
_cp_path = '/web/tests'
@httprequest
@http.httprequest
def index(self, req, mod=None, **kwargs):
ms = module.get_modules()
manifests = dict(

View File

@ -257,16 +257,17 @@ method you want to call) and a mapping of attributes to values (applied
as keyword arguments on the Python method [#]_). This function fetches
the return value of the Python methods, converted to JSON.
For instance, to call the ``eval_domain_and_context`` of the
:class:`~web.controllers.main.Session` controller:
For instance, to call the ``resequence`` of the
:class:`~web.controllers.main.DataSet` controller:
.. code-block:: javascript
openerp.connection.rpc('/web/session/eval_domain_and_context', {
domains: ds,
contexts: cs
openerp.connection.rpc('/web/dataset/resequence', {
model: some_model,
ids: array_of_ids,
offset: 42
}).then(function (result) {
// handle result
// resequenced on server
});
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``

View File

@ -17,7 +17,6 @@ import tempfile
import threading
import time
import traceback
import urllib
import urlparse
import uuid
import xmlrpclib
@ -33,7 +32,6 @@ import werkzeug.wsgi
import openerp
import nonliterals
import session
_logger = logging.getLogger(__name__)
@ -95,12 +93,12 @@ class WebRequest(object):
if not self.session:
self.session = session.OpenERPSession()
self.httpsession[self.session_id] = self.session
self.context = self.params.pop('context', None)
self.context = self.params.pop('context', {})
self.debug = self.params.pop('debug', False) is not False
# Determine self.lang
lang = self.params.get('lang', None)
if lang is None:
lang = self.session.eval_context(self.context).get('lang')
lang = self.context.get('lang')
if lang is None:
lang = self.httprequest.cookies.get('lang')
if lang is None:
@ -112,6 +110,11 @@ class WebRequest(object):
# we use _ as seprator where RFC2616 uses '-'
self.lang = lang.replace('-', '_')
def reject_nonliteral(dct):
if '__ref' in dct:
raise ValueError(
"Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
return dct
class JsonRequest(WebRequest):
""" JSON-RPC2 over HTTP.
@ -182,9 +185,9 @@ class JsonRequest(WebRequest):
try:
# Read POST content or POST Form Data named "request"
if requestf:
self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
self.jsonrequest = simplejson.load(requestf, object_hook=reject_nonliteral)
else:
self.jsonrequest = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
self.init(self.jsonrequest.get("params", {}))
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest))
@ -233,10 +236,10 @@ class JsonRequest(WebRequest):
# We need then to manage http sessions manually.
response['httpsessionid'] = self.httpsession.sid
mime = 'application/javascript'
body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),)
body = "%s(%s);" % (jsonp, simplejson.dumps(response),)
else:
mime = 'application/json'
body = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
body = simplejson.dumps(response)
r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
return r
@ -402,8 +405,10 @@ def session_context(request, session_store, session_lock, sid):
for k, v in request.session.iteritems():
stored = in_store.get(k)
if stored and isinstance(v, session.OpenERPSession):
v.contexts_store.update(stored.contexts_store)
v.domains_store.update(stored.domains_store)
if hasattr(v, 'contexts_store'):
del v.contexts_store
if hasattr(v, 'domains_store'):
del v.domains_store
if not hasattr(v, 'jsonp_requests'):
v.jsonp_requests = {}
v.jsonp_requests.update(getattr(

2067
addons/web/i18n/es_MX.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web
#. openerp-web

View File

@ -8,14 +8,15 @@ msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-21 16:26+0000\n"
"Last-Translator: Quentin THEURET <Unknown>\n"
"PO-Revision-Date: 2012-11-28 09:28+0000\n"
"Last-Translator: Frederic Clementi - Camptocamp.com "
"<frederic.clementi@camptocamp.com>\n"
"Language-Team: French <fr@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-25 06:40+0000\n"
"X-Generator: Launchpad (build 16293)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web
#. openerp-web
@ -36,7 +37,7 @@ msgstr "Langue par défaut :"
#: code:addons/web/static/src/js/chrome.js:326
#, python-format
msgid "Loading"
msgstr ""
msgstr "Chargement ..."
#. module: web
#. openerp-web
@ -50,21 +51,21 @@ msgstr "il y a %d minutes"
#: code:addons/web/static/src/js/views.js:1069
#, python-format
msgid "You must choose at least one record."
msgstr "Vous devez choisir au moins un enregistrement"
msgstr "Vous devez sélectionner au moins un enregistrement"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/coresetup.js:618
#, python-format
msgid "Still loading...<br />Please be patient."
msgstr ""
msgstr "Le chargement continue... <br />Soyez patient SVP."
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/coresetup.js:619
#, python-format
msgid "Don't leave yet,<br />it's still loading..."
msgstr ""
msgstr "Ne partez pas,<br />c'est presque terminé..."
#. module: web
#. openerp-web

View File

@ -8,21 +8,21 @@ msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-23 10:31+0000\n"
"PO-Revision-Date: 2012-11-28 10:21+0000\n"
"Last-Translator: Juhász Krisztián <Unknown>\n"
"Language-Team: Hungarian <hu@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-25 06:41+0000\n"
"X-Generator: Launchpad (build 16293)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:249
#, python-format
msgid "Restore Database"
msgstr ""
msgstr "Adatbázis Visszaállítása"
#. module: web
#. openerp-web
@ -36,7 +36,7 @@ msgstr "Alapértelmezett nyelv:"
#: code:addons/web/static/src/js/chrome.js:326
#, python-format
msgid "Loading"
msgstr ""
msgstr "Betöltés"
#. module: web
#. openerp-web
@ -57,21 +57,21 @@ msgstr "Választani kell legalább egy rekordot!"
#: code:addons/web/static/src/js/coresetup.js:618
#, python-format
msgid "Still loading...<br />Please be patient."
msgstr ""
msgstr "Még tölt...<br />Várjon türelemmel."
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/coresetup.js:619
#, python-format
msgid "Don't leave yet,<br />it's still loading..."
msgstr ""
msgstr "Még ne menjen el, <br />még tölt..."
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/search.js:1834
#, python-format
msgid "%(field)s %(operator)s \"%(value)s\""
msgstr ""
msgstr "%(field)s %(operator)s \"%(value)s\""
#. module: web
#. openerp-web
@ -87,7 +87,7 @@ msgstr "kisebb vagy egyenlő, mint"
#: code:addons/web/static/src/js/coresetup.js:596
#, python-format
msgid "%d months ago"
msgstr ""
msgstr "%d hónapja"
#. module: web
#. openerp-web
@ -101,7 +101,7 @@ msgstr "Helytelen keresés"
#: code:addons/web/static/src/js/coresetup.js:597
#, python-format
msgid "about a year ago"
msgstr ""
msgstr "kb. egy éve"
#. module: web
#. openerp-web
@ -110,28 +110,28 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:283
#, python-format
msgid "Master password:"
msgstr ""
msgstr "Mester jelszó"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:1270
#, python-format
msgid "Button"
msgstr ""
msgstr "Gomb"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:275
#, python-format
msgid "Change Master Password"
msgstr ""
msgstr "Mester jelszó változtatása"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/views.js:800
#, python-format
msgid "View Log (%s)"
msgstr ""
msgstr "Logok nézete (%s)"
#. module: web
#. openerp-web
@ -145,7 +145,7 @@ msgstr "Összes eltávolítása"
#: code:addons/web/static/src/js/coresetup.js:593
#, python-format
msgid "a day ago"
msgstr ""
msgstr "egy napja"
#. module: web
#. openerp-web
@ -166,21 +166,21 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:1738
#, python-format
msgid "Does your file have titles?"
msgstr ""
msgstr "A fájljának van címe?"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:1289
#, python-format
msgid "Method:"
msgstr ""
msgstr "Módzser:"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_list.js:1417
#, python-format
msgid "%(page)d/%(page_count)d"
msgstr ""
msgstr "%(page)d/%(page_count)d"
#. module: web
#. openerp-web
@ -250,14 +250,14 @@ msgstr "Feltöltés"
#: code:addons/web/static/src/xml/base.xml:126
#, python-format
msgid "Load Demonstration data:"
msgstr ""
msgstr "Bemutató adatok betöltése:"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:516
#, python-format
msgid "View"
msgstr ""
msgstr "Nézet"
#. module: web
#. openerp-web
@ -271,7 +271,7 @@ msgstr "Keresés: "
#: code:addons/web/static/src/js/view_form.js:4857
#, python-format
msgid "Save As..."
msgstr ""
msgstr "Mentés másként..."
#. module: web
#. openerp-web
@ -285,14 +285,14 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:217
#, python-format
msgid "Backup Database"
msgstr ""
msgstr "Adatbázis mentése"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:906
#, python-format
msgid "Size:"
msgstr ""
msgstr "Méret:"
#. module: web
#. openerp-web
@ -359,7 +359,7 @@ msgstr ""
#: code:addons/web/static/src/js/coresetup.js:589
#, python-format
msgid "about a minute ago"
msgstr ""
msgstr "kb. egy perce"
#. module: web
#. openerp-web
@ -373,7 +373,7 @@ msgstr ".CSV fájl importálása"
#: code:addons/web/static/src/xml/base.xml:257
#, python-format
msgid "File:"
msgstr ""
msgstr "Fájl:"
#. module: web
#. openerp-web
@ -388,7 +388,7 @@ msgstr "-- Műveletek --"
#: code:addons/web/static/src/xml/base.xml:311
#, python-format
msgid "Restore"
msgstr ""
msgstr "Visszaállít"
#. module: web
#. openerp-web
@ -404,14 +404,14 @@ msgstr "Duplikálás"
#: code:addons/web/static/src/js/formats.js:180
#, python-format
msgid "(%d records)"
msgstr ""
msgstr "(%d records)"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/search.js:1293
#, python-format
msgid "not a valid number"
msgstr ""
msgstr "nem érvényes szám"
#. module: web
#. openerp-web
@ -440,7 +440,7 @@ msgstr "Figyelmeztetés"
#: code:addons/web/static/src/xml/base.xml:517
#, python-format
msgid "Edit SearchView"
msgstr ""
msgstr "Keresési nézet szerkesztése"
#. module: web
#. openerp-web
@ -449,7 +449,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:261
#, python-format
msgid "Master Password:"
msgstr ""
msgstr "Mester Jelszó:"
#. module: web
#. openerp-web
@ -495,7 +495,7 @@ msgstr "Részletes Keresés"
#: code:addons/web/static/src/xml/base.xml:1323
#, python-format
msgid "Select"
msgstr ""
msgstr "Kiválaszt"
#. module: web
#. openerp-web
@ -558,7 +558,7 @@ msgstr "Létrehozás"
#: code:addons/web/static/src/xml/base.xml:186
#, python-format
msgid "Drop Database"
msgstr ""
msgstr "Adatbázis eldobása"
#. module: web
#. openerp-web
@ -692,7 +692,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:146
#, python-format
msgid "Admin password:"
msgstr ""
msgstr "Adminisztrátor jelszó:"
#. module: web
#. openerp-web
@ -713,14 +713,14 @@ msgstr "%d nappal ezelőtt"
#: code:addons/web/static/src/xml/base.xml:1802
#, python-format
msgid "Name:"
msgstr ""
msgstr "Név:"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:1728
#, python-format
msgid "CSV File:"
msgstr ""
msgstr "CSV Fájl:"
#. module: web
#. openerp-web
@ -734,7 +734,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:416
#, python-format
msgid "OpenERP.com"
msgstr ""
msgstr "OpenERP.com"
#. module: web
#. openerp-web
@ -748,7 +748,7 @@ msgstr "Új jelszó:"
#: code:addons/web/static/src/xml/base.xml:1628
#, python-format
msgid "Import-Compatible Export"
msgstr ""
msgstr "Import-kompatibilis Export"
#. module: web
#. openerp-web
@ -776,14 +776,14 @@ msgstr "Mindenkinek elérhető"
#: code:addons/web/static/src/js/coresetup.js:595
#, python-format
msgid "about a month ago"
msgstr ""
msgstr "kb. egy hónapja"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_tree.js:11
#, python-format
msgid "Tree"
msgstr ""
msgstr "Fa"
#. module: web
#. openerp-web
@ -797,7 +797,7 @@ msgstr "Egyedi szűrők"
#: code:addons/web/static/src/xml/base.xml:287
#, python-format
msgid "New master password:"
msgstr ""
msgstr "Új mester jelszó:"
#. module: web
#. openerp-web
@ -832,7 +832,7 @@ msgstr "Összes adat exportálása"
#: code:addons/web/static/src/xml/base.xml:414
#, python-format
msgid "OpenERP SA Company"
msgstr ""
msgstr "OpenERP SA Company"
#. module: web
#. openerp-web
@ -853,7 +853,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:1014
#, python-format
msgid "Select date"
msgstr ""
msgstr "Dátum választása"
#. module: web
#. openerp-web
@ -883,7 +883,7 @@ msgstr "Törlés"
#: code:addons/web/static/src/xml/base.xml:160
#, python-format
msgid "Duplicate Database"
msgstr ""
msgstr "Adatbázis másolása"
#. module: web
#. openerp-web
@ -912,7 +912,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:305
#, python-format
msgid "Database Management"
msgstr ""
msgstr "Adatbázis Kezelő"
#. module: web
#. openerp-web
@ -940,7 +940,7 @@ msgstr "Részletes keresés…"
#: code:addons/web/static/src/xml/base.xml:172
#, python-format
msgid "Original database name:"
msgstr ""
msgstr "Eredeti adatbázis neve:"
#. module: web
#. openerp-web
@ -1056,14 +1056,14 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:934
#, python-format
msgid "Selection:"
msgstr ""
msgstr "Kiválasztás:"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:1773
#, python-format
msgid "--- Don't Import ---"
msgstr ""
msgstr "--- Ne importáljon ---"
#. module: web
#. openerp-web
@ -1089,7 +1089,7 @@ msgstr "Új adatbázis neve:"
#: code:addons/web/static/src/xml/base.xml:1219
#, python-format
msgid "...Upload in progress..."
msgstr ""
msgstr "...Feltöltés folyamatban..."
#. module: web
#. openerp-web
@ -1117,7 +1117,7 @@ msgstr ""
#: code:addons/web/static/src/js/coresetup.js:617
#, python-format
msgid "Still loading..."
msgstr ""
msgstr "Még tölt..."
#. module: web
#. openerp-web
@ -1139,7 +1139,7 @@ msgstr ""
#: code:addons/web/static/src/js/view_form.js:4822
#, python-format
msgid "File upload"
msgstr ""
msgstr "Fájl feltöltés"
#. module: web
#. openerp-web
@ -1160,7 +1160,7 @@ msgstr "Minden felhasználó"
#: code:addons/web/static/src/js/coresetup.js:598
#, python-format
msgid "%d years ago"
msgstr ""
msgstr "%d éve"
#. module: web
#. openerp-web
@ -1188,7 +1188,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:408
#, python-format
msgid "Activate the developer mode"
msgstr ""
msgstr "Fejlesztői nézet aktiválása"
#. module: web
#. openerp-web
@ -1230,14 +1230,14 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:1554
#, python-format
msgid "Filter name"
msgstr ""
msgstr "Szűrő neve"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_form.js:4821
#, python-format
msgid "The selected file exceed the maximum file size of %s."
msgstr ""
msgstr "A kiválasztott fájl meghaladja a maximális %s fájlméretet."
#. module: web
#. openerp-web
@ -1258,7 +1258,7 @@ msgstr ""
#: code:addons/web/static/src/js/view_list.js:683
#, python-format
msgid "You must select at least one record."
msgstr ""
msgstr "Legalább egy a rekordot válasszon ki."
#. module: web
#. openerp-web
@ -1307,7 +1307,7 @@ msgstr ""
#: code:addons/web/static/src/js/coresetup.js:588
#, python-format
msgid "less than a minute ago"
msgstr ""
msgstr "kevesebb, mint egy perce"
#. module: web
#. openerp-web
@ -1328,7 +1328,7 @@ msgstr ""
#: code:addons/web/static/src/js/search.js:977
#, python-format
msgid "Filter"
msgstr ""
msgstr "Szűrő"
#. module: web
#. openerp-web
@ -1378,7 +1378,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:310
#, python-format
msgid "Backup"
msgstr ""
msgstr "Biztonsági mentés"
#. module: web
#. openerp-web
@ -1399,7 +1399,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:525
#, python-format
msgid "ID:"
msgstr ""
msgstr "ID:"
#. module: web
#. openerp-web
@ -1511,7 +1511,7 @@ msgstr "Biztos, hogy törölni akarja ezeket a rekordokat?"
#: code:addons/web/static/src/xml/base.xml:890
#, python-format
msgid "Field:"
msgstr ""
msgstr "Mező:"
#. module: web
#. openerp-web
@ -1579,7 +1579,7 @@ msgstr "Kliens hiba"
#: code:addons/web/static/src/xml/base.xml:914
#, python-format
msgid "Domain:"
msgstr ""
msgstr "Domain:"
#. module: web
#. openerp-web
@ -1593,7 +1593,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:1753
#, python-format
msgid "UTF-8"
msgstr ""
msgstr "UTF-8"
#. module: web
#. openerp-web
@ -1655,7 +1655,7 @@ msgstr "Importálási beállítások"
#: code:addons/web/static/src/xml/base.xml:437
#, python-format
msgid "OpenERP"
msgstr ""
msgstr "OpenERP"
#. module: web
#. openerp-web
@ -1698,7 +1698,7 @@ msgstr "Feltöltés..."
#: code:addons/web/static/src/js/view_form.js:426
#, python-format
msgid "%d / %d"
msgstr ""
msgstr "%d / %d"
#. module: web
#. openerp-web
@ -1862,7 +1862,7 @@ msgstr "Elvetés"
#: code:addons/web/static/src/xml/base.xml:1226
#, python-format
msgid "Delete this file"
msgstr ""
msgstr "Törölje ezt a fájlt"
#. module: web
#. openerp-web
@ -1901,7 +1901,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:110
#, python-format
msgid "Create Database"
msgstr ""
msgstr "Adatbázis létrehozása"
#. module: web
#. openerp-web
@ -1922,7 +1922,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:1744
#, python-format
msgid "Separator:"
msgstr ""
msgstr "Elválasztó:"
#. module: web
#. openerp-web
@ -1961,7 +1961,7 @@ msgstr "Jelszó megerősítése:"
#: code:addons/web/static/src/xml/base.xml:1579
#, python-format
msgid "or"
msgstr ""
msgstr "vagy"
#. module: web
#. openerp-web
@ -1999,7 +1999,7 @@ msgstr "Szűrő neve:"
#: code:addons/web/static/src/xml/base.xml:1614
#, python-format
msgid "Export"
msgstr ""
msgstr "Export"
#. module: web
#. openerp-web
@ -2013,7 +2013,7 @@ msgstr ""
#: code:addons/web/static/src/xml/base.xml:1246
#, python-format
msgid "File"
msgstr ""
msgstr "Fájl"
#. module: web
#. openerp-web

View File

@ -8,35 +8,35 @@ msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-10-22 14:36+0000\n"
"Last-Translator: Marius Marolla <mariusmarolla@areablu.net>\n"
"PO-Revision-Date: 2012-11-28 20:09+0000\n"
"Last-Translator: Davide Corio - agilebg.com <davide.corio@agilebg.com>\n"
"Language-Team: Italian <it@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-25 06:41+0000\n"
"X-Generator: Launchpad (build 16293)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:249
#, python-format
msgid "Restore Database"
msgstr ""
msgstr "Ripristina Database"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:134
#, python-format
msgid "Default language:"
msgstr "Lingua predefinita:"
msgstr "Lingua Predefinita:"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/chrome.js:326
#, python-format
msgid "Loading"
msgstr ""
msgstr "Caricamento"
#. module: web
#. openerp-web
@ -71,7 +71,7 @@ msgstr ""
#: code:addons/web/static/src/js/search.js:1834
#, python-format
msgid "%(field)s %(operator)s \"%(value)s\""
msgstr ""
msgstr "%(field)s %(operator)s \"%(value)s\""
#. module: web
#. openerp-web
@ -124,7 +124,7 @@ msgstr "Pulsante"
#: code:addons/web/static/src/xml/base.xml:275
#, python-format
msgid "Change Master Password"
msgstr ""
msgstr "Cambia Master Password"
#. module: web
#. openerp-web
@ -243,7 +243,7 @@ msgstr "Ok"
#: code:addons/web/static/src/js/views.js:1127
#, python-format
msgid "Uploading..."
msgstr ""
msgstr "Caricando..."
#. module: web
#. openerp-web
@ -271,21 +271,21 @@ msgstr "Cerca: "
#: code:addons/web/static/src/js/view_form.js:4857
#, python-format
msgid "Save As..."
msgstr ""
msgstr "Salva come..."
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_form.js:4982
#, python-format
msgid "Could not display the selected image."
msgstr ""
msgstr "Impossibile visualizzare l'immagine selezionata."
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:217
#, python-format
msgid "Backup Database"
msgstr ""
msgstr "Backup Database"
#. module: web
#. openerp-web
@ -340,7 +340,7 @@ msgstr "Traduzione tecnica"
#: code:addons/web/static/src/xml/base.xml:1552
#, python-format
msgid "Save current filter"
msgstr ""
msgstr "Salva il filtro corrente"
#. module: web
#. openerp-web
@ -406,7 +406,7 @@ msgstr "Duplica"
#: code:addons/web/static/src/js/formats.js:180
#, python-format
msgid "(%d records)"
msgstr ""
msgstr "(%d records)"
#. module: web
#. openerp-web

View File

@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-26 02:16+0000\n"
"Last-Translator: Wei \"oldrev\" Li <oldrev@gmail.com>\n"
"PO-Revision-Date: 2012-11-29 02:05+0000\n"
"Last-Translator: digitalsatori <digisatori@gmail.com>\n"
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-27 05:45+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web
#. openerp-web
@ -159,7 +159,7 @@ msgstr "删除"
#: code:addons/web/static/src/js/search.js:1394
#, python-format
msgid "Search %(field)s at: %(value)s"
msgstr ""
msgstr "搜索 %(field)s %(value)s"
#. module: web
#. openerp-web
@ -425,7 +425,7 @@ msgstr "修改默认值:"
#: code:addons/web/static/src/xml/base.xml:506
#, python-format
msgid "Toggle Form Layout Outline"
msgstr ""
msgstr "切换表单布局视图"
#. module: web
#. openerp-web
@ -604,14 +604,14 @@ msgstr "最后修改时间:"
#: code:addons/web/static/src/js/search.js:1455
#, python-format
msgid "M2O search fields do not currently handle multiple default values"
msgstr ""
msgstr "多对一搜索字段目前不支持多个默认值"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:581
#, python-format
msgid "Delete this attachment"
msgstr ""
msgstr "删除这个附件"
#. module: web
#. openerp-web
@ -640,7 +640,7 @@ msgstr "不限"
#: code:addons/web/static/src/xml/base.xml:350
#, python-format
msgid "More"
msgstr ""
msgstr "更多"
#. module: web
#. openerp-web
@ -654,7 +654,7 @@ msgstr "用户名"
#: code:addons/web/static/src/js/view_form.js:2912
#, python-format
msgid "Add %s"
msgstr ""
msgstr "增加 %s"
#. module: web
#. openerp-web
@ -715,7 +715,7 @@ msgstr "%d 天前"
#: code:addons/web/static/src/xml/base.xml:1802
#, python-format
msgid "Name:"
msgstr ""
msgstr "名称:"
#. module: web
#. openerp-web
@ -729,7 +729,7 @@ msgstr "CSV 文件:"
#: code:addons/web/static/src/js/search.js:1743
#, python-format
msgid "Advanced"
msgstr ""
msgstr "高级选项"
#. module: web
#. openerp-web
@ -750,7 +750,7 @@ msgstr "新密码:"
#: code:addons/web/static/src/xml/base.xml:1628
#, python-format
msgid "Import-Compatible Export"
msgstr ""
msgstr "兼容导入格式导出"
#. module: web
#. openerp-web
@ -771,7 +771,7 @@ msgstr "请选择要导出的字段..."
#: code:addons/web/static/src/xml/base.xml:1557
#, python-format
msgid "Share with all users"
msgstr ""
msgstr "与所有用户共享"
#. module: web
#. openerp-web
@ -792,7 +792,7 @@ msgstr "树形"
#: code:addons/web/static/src/xml/base.xml:1549
#, python-format
msgid "Custom Filters"
msgstr ""
msgstr "自定义过滤"
#. module: web
#. openerp-web
@ -841,14 +841,14 @@ msgstr "OpenERP SA 公司"
#: code:addons/web/static/src/js/views.js:1137
#, python-format
msgid "Do you really want to delete this attachment ?"
msgstr ""
msgstr "请确认是否要删除这个附件?"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_list.js:1018
#, python-format
msgid "Unknown m2m command "
msgstr ""
msgstr "未知的m2m命令 "
#. module: web
#. openerp-web
@ -862,14 +862,14 @@ msgstr "选择日期"
#: code:addons/web/static/src/js/search.js:1555
#, python-format
msgid "Custom Filter"
msgstr ""
msgstr "自定义筛选"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:399
#, python-format
msgid "About OpenERP"
msgstr ""
msgstr "关于 OpenERP"
#. module: web
#. openerp-web
@ -885,7 +885,7 @@ msgstr "删除"
#: code:addons/web/static/src/xml/base.xml:160
#, python-format
msgid "Duplicate Database"
msgstr ""
msgstr "复制数据库"
#. module: web
#. openerp-web
@ -914,14 +914,14 @@ msgstr "字段值有误。%(fieldname)s: [%(value)s] : %(message)s"
#: code:addons/web/static/src/xml/base.xml:305
#, python-format
msgid "Database Management"
msgstr ""
msgstr "数据库管理"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_form.js:4982
#, python-format
msgid "Image"
msgstr ""
msgstr "图像"
#. module: web
#. openerp-web
@ -935,14 +935,14 @@ msgstr "管理数据库"
#: code:addons/web/static/src/xml/base.xml:1377
#, python-format
msgid "Advanced Search..."
msgstr ""
msgstr "高级搜索..."
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:172
#, python-format
msgid "Original database name:"
msgstr ""
msgstr "原来的数据库名"
#. module: web
#. openerp-web
@ -1037,7 +1037,7 @@ msgstr "否"
#: code:addons/web/static/src/js/view_form.js:3779
#, python-format
msgid "The o2m record must be saved before an action can be used"
msgstr ""
msgstr "o2m记录必须先保存才能被一个动作使用"
#. module: web
#. openerp-web
@ -1065,7 +1065,7 @@ msgstr "下拉列表:"
#: code:addons/web/static/src/xml/base.xml:1773
#, python-format
msgid "--- Don't Import ---"
msgstr ""
msgstr "--- 无法导入 ---"
#. module: web
#. openerp-web
@ -1091,7 +1091,7 @@ msgstr "新数据库名称:"
#: code:addons/web/static/src/xml/base.xml:1219
#, python-format
msgid "...Upload in progress..."
msgstr ""
msgstr "... 上传中"
#. module: web
#. openerp-web
@ -1112,14 +1112,14 @@ msgstr "创建日期:"
#: code:addons/web/static/src/xml/base.xml:1573
#, python-format
msgid "Add a condition"
msgstr ""
msgstr "添加条件"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/coresetup.js:617
#, python-format
msgid "Still loading..."
msgstr ""
msgstr "正在加载..."
#. module: web
#. openerp-web
@ -1139,14 +1139,14 @@ msgstr ""
#: code:addons/web/static/src/js/view_form.js:4822
#, python-format
msgid "File upload"
msgstr ""
msgstr "文件上传"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_form.js:3778
#, python-format
msgid "Action Button"
msgstr ""
msgstr "动作按钮"
#. module: web
#. openerp-web
@ -1160,7 +1160,7 @@ msgstr "所有用户"
#: code:addons/web/static/src/js/coresetup.js:598
#, python-format
msgid "%d years ago"
msgstr ""
msgstr "%d 年前"
#. module: web
#. openerp-web
@ -1182,13 +1182,15 @@ msgstr "保存默认值"
#, python-format
msgid "Take a minute to get a coffee,<br />because it's loading..."
msgstr ""
"稍安勿躁, <br />\r\n"
"大菜烹饪中 : )"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:408
#, python-format
msgid "Activate the developer mode"
msgstr ""
msgstr "激活开发者模式"
#. module: web
#. openerp-web
@ -1202,7 +1204,7 @@ msgstr "正在加载(%d"
#: code:addons/web/static/src/js/view_form.js:4857
#, python-format
msgid "The field is empty, there's nothing to save !"
msgstr ""
msgstr "字段为空,没有需要保存的值!"
#. module: web
#. openerp-web
@ -1223,28 +1225,28 @@ msgstr "已备份"
#: code:addons/web/static/src/xml/base.xml:1559
#, python-format
msgid "Use by default"
msgstr ""
msgstr "默认使用"
#. module: web
#. openerp-web
#: code:addons/web/static/src/xml/base.xml:1554
#, python-format
msgid "Filter name"
msgstr ""
msgstr "过滤规则名称"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/view_form.js:4821
#, python-format
msgid "The selected file exceed the maximum file size of %s."
msgstr ""
msgstr "所选文件超出了文件的最大值设定:%s"
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/search.js:1110
#, python-format
msgid "GroupBy"
msgstr ""
msgstr "分组"
#. module: web
#. openerp-web
@ -1258,7 +1260,7 @@ msgstr "编码:"
#: code:addons/web/static/src/js/view_list.js:683
#, python-format
msgid "You must select at least one record."
msgstr ""
msgstr "请至少选择一条记录"
#. module: web
#. openerp-web
@ -1272,7 +1274,7 @@ msgstr "要跳过的行数"
#: code:addons/web/static/src/js/view_form.js:2834
#, python-format
msgid "Create \"<strong>%s</strong>\""
msgstr ""
msgstr "创建 \"<strong>%s</strong>\""
#. module: web
#. openerp-web
@ -1328,7 +1330,7 @@ msgstr "版权所有 © 2004-TODAY OpenERP SA。保留所有权利。"
#: code:addons/web/static/src/js/search.js:977
#, python-format
msgid "Filter"
msgstr ""
msgstr "过滤条件"
#. module: web
#. openerp-web
@ -1385,7 +1387,7 @@ msgstr "备份"
#: code:addons/web/static/src/xml/base.xml:509
#, python-format
msgid "JS Tests"
msgstr ""
msgstr "JS 测试"
#. module: web
#. openerp-web

View File

@ -1,267 +0,0 @@
# -*- 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
import simplejson.encoder
__all__ = ['Domain', 'Context', 'NonLiteralEncoder', 'non_literal_decoder', 'CompoundDomain', 'CompoundContext']
#: 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 NonLiteralEncoder(simplejson.encoder.JSONEncoder):
def default(self, object):
if not isinstance(object, (BaseDomain, BaseContext)):
return super(NonLiteralEncoder, self).default(object)
if isinstance(object, Domain):
return {
'__ref': 'domain',
'__id': object.key,
'__debug': object.get_domain_string()
}
elif isinstance(object, Context):
return {
'__ref': 'context',
'__id': object.key,
'__debug': object.get_context_string()
}
elif isinstance(object, CompoundDomain):
return {
'__ref': 'compound_domain',
'__domains': object.domains,
'__eval_context': object.get_eval_context()
}
elif isinstance(object, CompoundContext):
return {
'__ref': 'compound_context',
'__contexts': object.contexts,
'__eval_context': object.get_eval_context()
}
raise TypeError('Could not encode unknown non-literal %s' % object)
_ALLOWED_KEYS = frozenset(['__ref', "__id", '__domains', '__debug',
'__contexts', '__eval_context'])
def non_literal_decoder(dct):
""" Decodes JSON dicts into :class:`Domain` and :class:`Context` based on
magic attribute tags.
"""
if '__ref' in dct:
for x in dct:
if x not in _ALLOWED_KEYS:
raise ValueError("'%s' key not allowed in non literal domain/context" % x)
if dct['__ref'] == 'domain':
return Domain(None, key=dct['__id'])
elif dct['__ref'] == 'context':
return Context(None, key=dct['__id'])
elif dct["__ref"] == "compound_domain":
cdomain = CompoundDomain()
for el in dct["__domains"]:
cdomain.domains.append(el)
cdomain.set_eval_context(dct.get("__eval_context"))
return cdomain
elif dct["__ref"] == "compound_context":
ccontext = CompoundContext()
for el in dct["__contexts"]:
ccontext.contexts.append(el)
ccontext.set_eval_context(dct.get("__eval_context"))
return ccontext
return dct
# TODO: use abstract base classes if 2.6+?
class BaseDomain(object):
def evaluate(self, context=None):
raise NotImplementedError('Non literals must implement evaluate()')
class BaseContext(object):
def evaluate(self, context=None):
raise NotImplementedError('Non literals must implement evaluate()')
class Domain(BaseDomain):
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: web.common.session.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.
"""
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.
"""
ctx = self.session.evaluation_context(context)
try:
return eval(self.get_domain_string(), SuperDict(ctx))
except NameError as e:
raise ValueError('Error during evaluation of this domain: "%s", message: "%s"' % (self.get_domain_string(), e.message))
class Context(BaseContext):
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: web.common.session.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.contexts_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.
"""
return self.session.contexts_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.
"""
ctx = self.session.evaluation_context(context)
try:
return eval(self.get_context_string(), SuperDict(ctx))
except NameError as e:
raise ValueError('Error during evaluation of this context: "%s", message: "%s"' % (self.get_context_string(), e.message))
class SuperDict(dict):
def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(name)
def __getitem__(self, key):
tmp = super(SuperDict, self).__getitem__(key)
if isinstance(tmp, dict):
return SuperDict(tmp)
return tmp
class CompoundDomain(BaseDomain):
def __init__(self, *domains):
self.domains = []
self.session = None
self.eval_context = None
for domain in domains:
self.add(domain)
def evaluate(self, context=None):
ctx = dict(context or {})
eval_context = self.get_eval_context()
if eval_context:
eval_context = self.session.eval_context(eval_context)
ctx.update(eval_context)
final_domain = []
for domain in self.domains:
if not isinstance(domain, (list, BaseDomain)):
raise TypeError(
"Domain %r is not a list or a nonliteral Domain" % domain)
if isinstance(domain, list):
final_domain.extend(domain)
continue
domain.session = self.session
final_domain.extend(domain.evaluate(ctx))
return final_domain
def add(self, domain):
self.domains.append(domain)
return self
def set_eval_context(self, eval_context):
self.eval_context = eval_context
return self
def get_eval_context(self):
return self.eval_context
class CompoundContext(BaseContext):
def __init__(self, *contexts):
self.contexts = []
self.eval_context = None
self.session = None
for context in contexts:
self.add(context)
def evaluate(self, context=None):
ctx = dict(context or {})
eval_context = self.get_eval_context()
if eval_context:
eval_context = self.session.eval_context(eval_context)
ctx.update(eval_context)
final_context = {}
for context_to_eval in self.contexts:
if not isinstance(context_to_eval, (dict, BaseContext)):
raise TypeError(
"Context %r is not a dict or a nonliteral Context" % context_to_eval)
if isinstance(context_to_eval, dict):
final_context.update(context_to_eval)
continue
ctx.update(final_context)
context_to_eval.session = self.session
final_context.update(context_to_eval.evaluate(ctx))
return final_context
def add(self, context):
self.contexts.append(context)
return self
def set_eval_context(self, eval_context):
self.eval_context = eval_context
return self
def get_eval_context(self):
return self.eval_context

View File

@ -10,8 +10,6 @@ import xmlrpclib
import openerp
import nonliterals
_logger = logging.getLogger(__name__)
#----------------------------------------------------------
@ -81,8 +79,6 @@ class OpenERPSession(object):
self._password = False
self._suicide = False
self.context = {}
self.contexts_store = {}
self.domains_store = {}
self.jsonp_requests = {} # FIXME use a LRU
def send(self, service_name, method, *args):
@ -222,49 +218,4 @@ class OpenERPSession(object):
d['context'] = d
return d
def eval_context(self, context_to_eval, context=None):
""" Evaluates the provided context_to_eval in the context (haha) of
the context. Also merges the evaluated context with the session's context.
: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
"""
ctx = dict(
self.base_eval_context,
**(context or {}))
# adding the context of the session to send to the openerp server
ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {})
ccontext.session = self
return ccontext.evaluate(ctx)
def eval_domain(self, domain, context=None):
""" Evaluates the provided domain using the provided context
(merged with the session's evaluation context)
: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.
:type domain: openerpweb.nonliterals.Domain
:param dict context: the context to use in the evaluation, if any.
:returns: the evaluated domain
:rtype: list
:raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
"""
if isinstance(domain, list):
return domain
cdomain = nonliterals.CompoundDomain(domain)
cdomain.session = self
return cdomain.evaluate(context or {})
# vim:et:ts=4:sw=4:

View File

@ -1,5 +1,5 @@
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
node: 1758bfec1ec1dcff95dcc4c72269cc0e3d000afd
node: ccf62e5d0291f66a39f4049f3e880004e59c3654
branch: default
latesttag: 0.5
latesttagdistance: 11
latesttag: 0.7
latesttagdistance: 4

View File

@ -1,14 +1,7 @@
What
====
``py.js`` is a parser and evaluator of Python expressions, written in
pure javascript.
``py.js`` is not intended to implement a full Python interpreter
(although it could be used for such an effort later on), its
specification document is the `Python 2.7 Expressions spec
<http://docs.python.org/reference/expressions.html>`_ (along with the
lexical analysis part).
Syntax
------
@ -60,7 +53,7 @@ Builtins
Same as tuple (``list`` is currently an alias for ``tuple``)
``dict``
Implements just about nothing
Implements trivial getting and setting, nothing beyond that.
Note that most methods are probably missing from all of these.
@ -69,17 +62,17 @@ Data model protocols
``py.js`` currently implements the following protocols (or
sub-protocols) of the `Python 2.7 data model
<http://docs.python.org/reference/datamodel.html>`_:
<>`_:
Rich comparisons
Roughly complete implementation but for two limits: ``__eq__`` and
``__ne__`` can't return ``NotImplemented`` (well they can but it's
not going to work right), and the behavior is undefined if a
rich-comparison operation does not return a ``py.bool``.
Pretty much complete (including operator fallbacks), although the
behavior is currently undefined if an operation does not return
either a ``py.bool`` or ``NotImplemented``.
Also, a ``NotImplemented`` result does not try the reverse
operation, not sure if it's supposed to. It directly falls back to
comparing type names.
``__hash__`` is supported (and used), but it should return **a
javascript string**. ``py.js``'s dict build on javascript objects,
reimplementing numeral hashing is worthless complexity at this
point.
Boolean conversion
Implementing ``__nonzero__`` should work.
@ -93,6 +86,12 @@ Descriptor protocol
As with attributes, ``__delete__`` is not implemented.
Callable objects
Work, although the handling of arguments isn't exactly nailed
down. For now, callables get two (javascript) arguments ``args``
and ``kwargs``, holding (respectively) positional and keyword
arguments.
Conflicts are *not* handled at this point.
Collections Abstract Base Classes
Container is the only implemented ABC protocol (ABCs themselves
@ -119,8 +118,8 @@ implementation:
``py.js`` types.
When accessing instance methods, ``py.js`` automatically wraps
these in a variant of ``py.def`` automatically, to behave as
Python's (bound) methods.
these in a variant of ``py.def``, to behave as Python's (bound)
methods.
Why
===

View File

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyjs.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyjs.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/pyjs"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyjs"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -0,0 +1,55 @@
.. default-domain: python
.. _builtins:
Supported Python builtins
=========================
.. function:: py.type(object)
Gets the class of a provided object, if possible.
.. note:: currently doesn't work correctly when called on a class
object, will return the class itself (also, classes
don't currently have a type).
.. js:function:: py.type(name, bases, dict)
Not exactly a builtin as this form is solely javascript-level
(currently). Used to create new ``py.js`` types. See :doc:`types`
for its usage.
.. data:: py.None
.. data:: py.True
.. data:: py.False
.. data:: py.NotImplemented
.. class:: py.object
Base class for all types, even implicitly (if no bases are
provided to :js:func:`py.type`)
.. class:: py.bool([object])
.. class:: py.float([object])
.. class:: py.str([object])
.. class:: py.unicode([object])
.. class:: py.tuple()
.. class:: py.list()
.. class:: py.dict()
.. function:: py.len(object)
.. function:: py.isinstance(object, type)
.. function:: py.issubclass(type, other_type)
.. class:: py.classmethod

View File

@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
#
# py.js documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 9 19:36:23 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.todo']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'py.js'
copyright = u'2012, Xavier Morel'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.6'
# The full version, including alpha/beta/rc tags.
release = '0.6'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# Default sphinx domain
default_domain = 'js'
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# default code-block highlighting
highlight_language = 'javascript'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'pyjsdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'pyjs.tex', u'py.js Documentation',
u'Xavier Morel', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'pyjs', u'py.js Documentation',
[u'Xavier Morel'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'pyjs', u'py.js Documentation',
u'Xavier Morel', 'pyjs', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'

View File

@ -0,0 +1,64 @@
Differences with Python
=======================
* ``py.js`` completely ignores old-style classes as well as their
lookup details. All ``py.js`` types should be considered matching
the behavior of new-style classes
* New types can only have a single base. This is due to ``py.js``
implementing its types on top of Javascript's, and javascript being
a single-inheritance language.
This may change if ``py.js`` ever reimplements its object model from
scratch.
* Piggybacking on javascript's object model also means metaclasses are
not available (:js:func:`py.type` is a function)
* A python-level function (created through :js:class:`py.PY_def`) set
on a new type will not become a method, it'll remain a function.
* :js:func:`py.PY_parseArgs` supports keyword-only arguments (though
it's a Python 3 feature)
* Because the underlying type is a javascript ``String``, there
currently is no difference between :js:class:`py.str` and
:js:class:`py.unicode`. As a result, there also is no difference
between :js:func:`__str__` and :js:func:`__unicode__`.
Unsupported features
--------------------
These are Python features which are not supported at all in ``py.js``,
usually because they don't make sense or there is no way to support them
* The ``__delattr__``, ``__delete__`` and ``__delitem__``: as
``py.js`` only handles expressions and these are accessed via the
``del`` statement, there would be no way to call them.
* ``__del__`` the lack of cross-platform GC hook means there is no way
to know when an object is deallocated.
* ``__slots__`` are not handled
* Dedicated (and deprecated) slicing special methods are unsupported
Missing features
----------------
These are Python features which are missing because they haven't been
implemented yet:
* Class-binding of descriptors doesn't currently work.
* Instance and subclass checks can't be customized
* "poor" comparison methods (``__cmp__`` and ``__rcmp__``) are not
supported and won't be falled-back to.
* ``__coerce__`` is currently supported
* Context managers are not currently supported
* Unbound methods are not supported, instance methods can only be
accessed from instances.

View File

@ -0,0 +1,161 @@
.. py.js documentation master file, created by
sphinx-quickstart on Sun Sep 9 19:36:23 2012.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
py.js, a Python expressions parser and evaluator
================================================
``py.js`` is a parser and evaluator of Python expressions, written in
pure javascript.
``py.js`` is not intended to implement a full Python interpreter, its
specification document is the `Python 2.7 Expressions spec
<http://docs.python.org/reference/expressions.html>`_ (along with the
lexical analysis part) as well as the Python builtins.
.. toctree::
:maxdepth: 2
builtins
types
utility
differences
Usage
-----
To evaluate a Python expression, simply call
:func:`py.eval`. :func:`py.eval` takes a mandatory Python expression
parameter, as a string, and an optional evaluation context (namespace
for the expression's free variables), and returns a javascript value::
> py.eval("t in ('a', 'b', 'c') and foo", {t: 'c', foo: true});
true
If the expression needs to be repeatedly evaluated, or the result of
the expression is needed in its "python" form without being converted
back to javascript, you can use the underlying triplet of functions
:func:`py.tokenize`, :func:`py.parse` and :func:`py.evaluate`
directly.
API
---
Core functions
++++++++++++++
.. function:: py.eval(expr[, context])
"Do everything" function, to use for one-shot evaluation of Python
expressions. Chains tokenizing, parsing and evaluating the
expression then :ref:`converts the result back to javascript
<convert-js>`
:param expr: Python expression to evaluate
:type expr: String
:param context: evaluation context for the expression's free
variables
:type context: Object
:returns: the expression's result, converted back to javascript
.. function:: py.tokenize(expr)
Expression tokenizer
:param expr: Python expression to tokenize
:type expr: String
:returns: token stream
.. function:: py.parse(tokens)
Parses a token stream and returns the corresponding parse tree.
The parse tree is stateless and can be memoized and reused for
frequently evaluated expressions.
:param tokens: token stream from :func:`py.tokenize`
:returns: parse tree
.. function:: py.evaluate(tree[, context])
Evaluates the expression represented by the provided parse tree,
using the provided context for the exprssion's free variables.
:param tree: parse tree returned by :func:`py.parse`
:param context: evaluation context
:returns: the "python object" resulting from the expression's
evaluation
:rtype: :class:`py.object`
.. _convert-py:
Conversions from Javascript to Python
+++++++++++++++++++++++++++++++++++++
``py.js`` will automatically attempt to convert non-:class:`py.object`
values into their ``py.js`` equivalent in the following situations:
* Values passed through the context of :func:`py.eval` or
:func:`py.evaluate`
* Attributes accessed directly on objects
* Values of mappings passed to :class:`py.dict`
Notably, ``py.js`` will *not* attempt an automatic conversion of
values returned by functions or methods, these must be
:class:`py.object` instances.
The automatic conversions performed by ``py.js`` are the following:
* ``null`` is converted to :data:`py.None`
* ``true`` is converted to :data:`py.True`
* ``false`` is converted to :data:`py.False`
* numbers are converted to :class:`py.float`
* strings are converted to :class:`py.str`
* functions are wrapped into :class:`py.PY_dev`
* ``Array`` instances are converted to :class:`py.list`
The rest generates an error, except for ``undefined`` which
specifically generates a ``NameError``.
.. _convert-js:
Conversions from Python to Javascript
+++++++++++++++++++++++++++++++++++++
py.js types (extensions of :js:class:`py.object`) can be converted
back to javascript by calling their :js:func:`py.object.toJSON`
method.
The default implementation raises an error, as arbitrary objects can
not be converted back to javascript.
Most built-in objects provide a :js:func:`py.object.toJSON`
implementation out of the box.
Javascript-level exceptions
+++++++++++++++++++++++++++
Javascript allows throwing arbitrary things, but runtimes don't seem
to provide any useful information (when they ever do) if what is
thrown isn't a direct instance of ``Error``. As a result, while
``py.js`` tries to match the exception-throwing semantics of Python it
only ever throws bare ``Error`` at the javascript-level. Instead, it
prefixes the error message with the name of the Python expression, a
colon, a space, and the actual message.
For instance, where Python would throw ``KeyError("'foo'")`` when
accessing an invalid key on a ``dict``, ``py.js`` will throw
``Error("KeyError: 'foo'")``.
.. _Python Data Model: http://docs.python.org/reference/datamodel.html

View File

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyjs.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyjs.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View File

@ -0,0 +1,248 @@
Implementing a custom type
==========================
To implement a custom python-level type, one can use the
:func:`py.type` builtin. At the JS-level, it is a function with the
same signature as the :py:class:`type` builtin [#bases]_. It returns a
child type of its one base (or :py:class:`py.object` if no base is
provided).
The ``dict`` parameter to :func:`py.type` can contain any
attribute, javascript-level or python-level: the default
``__getattribute__`` implementation will ensure they are converted to
Python-level attributes if needed. Most methods are also wrapped and
converted to :ref:`types-methods-python`, although there are a number
of special cases:
* Most "magic methods" of the data model ("dunder" methods) remain
javascript-level. See :ref:`the listing of magic methods and their
signatures <types-methods-dunder>`. As a result, they do not respect
the :ref:`types-methods-python-call`
* The ``toJSON`` and ``fromJSON`` methods are special-cased to remain
javascript-level and don't follow the
:ref:`types-methods-python-call`
* Functions which have been wrapped explicitly (via
:class:`py.PY_def`, :py:class:`py.classmethod` or
:py:class:`py.staticmethod`) are associated to the class
untouched. But due to their wrapper, they will use the
:ref:`types-methods-python-call` anyway
.. _types-methods-python:
Python-level callable
---------------------
Wrapped javascript function *or* the :func:`__call__` method itself
follow the :ref:`types-methods-python-call`. As a result, they can't
(easily) be called directly from javascript code. Because
:func:`__new__` and :func:`__init__` follow from :func:`__call__`,
they also follow the :ref:`types-methods-python-call`.
:func:`py.PY_call` should be used when interacting with them from
javascript is necessary.
Because ``__call__`` follows the :ref:`types-methods-python-call`,
instantiating a ``py.js`` type from javascript requires using
:func:`py.PY_call`.
.. _types-methods-python-call:
Python calling conventions
++++++++++++++++++++++++++
The python-level arguments should be considered completely opaque,
they should be interacted with through :func:`py.PY_parseArgs` (to
extract python-level arguments to javascript implementation code) and
:func:`py.PY_call` (to call :ref:`types-methods-python` from
javascript code).
A callable following the :ref:`types-methods-python-call` *must*
return a ``py.js`` object, an error will be generated when failing to
do so.
.. todo:: arguments forwarding when e.g. overriding methods?
.. _types-methods-dunder:
Magic methods
-------------
``py.js`` doesn't support calling magic ("dunder") methods of the
datamodel from Python code, and these methods remain javascript-level
(they don't follow the :ref:`types-methods-python-call`).
Here is a list of the understood datamodel methods, refer to `the
relevant Python documentation
<http://docs.python.org/reference/datamodel.html?highlight=data%20model#special-method-names>`_
for their roles.
Basic customization
+++++++++++++++++++
.. function:: __hash__()
:returns: String
.. function:: __eq__(other)
The default implementation tests for identity
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __ne__(other)
The default implementation calls :func:`__eq__` and reverses
its result.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __lt__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __le__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __ge__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __gt__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __str__()
Simply calls :func:`__unicode__`. This method should not be
overridden, :func:`__unicode__` should be overridden instead.
:returns: :py:class:`py.str`
.. function:: __unicode__()
:returns: :py:class:`py.unicode`
.. function:: __nonzero__()
The default implementation always returns :data:`py.True`
:returns: :py:class:`py.bool`
Customizing attribute access
++++++++++++++++++++++++++++
.. function:: __getattribute__(name)
:param String name: name of the attribute, as a javascript string
:returns: :py:class:`py.object`
.. function:: __getattr__(name)
:param String name: name of the attribute, as a javascript string
:returns: :py:class:`py.object`
.. function:: __setattr__(name, value)
:param String name: name of the attribute, as a javascript string
:param value: :py:class:`py.object`
Implementing descriptors
++++++++++++++++++++++++
.. function:: __get__(instance)
.. note:: readable descriptors don't currently handle "owner
classes"
:param instance: :py:class:`py.object`
:returns: :py:class:`py.object`
.. function:: __set__(instance, value)
:param instance: :py:class:`py.object`
:param value: :py:class:`py.object`
Emulating Numeric Types
+++++++++++++++++++++++
* Non-in-place binary numeric methods (e.g. ``__add__``, ``__mul__``,
...) should all be supported including reversed calls (in case the
primary call is not available or returns
:py:data:`py.NotImplemented`). They take a single
:py:class:`py.object` parameter and return a single
:py:class:`py.object` parameter.
* Unary operator numeric methods are all supported:
.. function:: __pos__()
:returns: :py:class:`py.object`
.. function:: __neg__()
:returns: :py:class:`py.object`
.. function:: __invert__()
:returns: :py:class:`py.object`
* For non-operator numeric methods, support is contingent on the
corresponding :ref:`builtins <builtins>` being implemented
Emulating container types
+++++++++++++++++++++++++
.. function:: __len__()
:returns: :py:class:`py.int`
.. function:: __getitem__(name)
:param name: :py:class:`py.object`
:returns: :py:class:`py.object`
.. function:: __setitem__(name, value)
:param name: :py:class:`py.object`
:param value: :py:class:`py.object`
.. function:: __iter__()
:returns: :py:class:`py.object`
.. function:: __reversed__()
:returns: :py:class:`py.object`
.. function:: __contains__(other)
:param other: :py:class:`py.object`
:returns: :py:class:`py.bool`
.. [#bases] with the limitation that, because :ref:`py.js builds its
object model on top of javascript's
<details-object-model>`, only one base is allowed.

View File

@ -0,0 +1,226 @@
Utility functions for interacting with ``py.js`` objects
========================================================
Essentially the ``py.js`` version of the Python C API, these functions
are used to implement new ``py.js`` types or to interact with existing
ones.
They are prefixed with ``PY_``.
.. function:: py.PY_parseArgs(arguments, format)
Arguments parser converting from the :ref:`user-defined calling
conventions <types-methods-python-call>` to a JS object mapping
argument names to values. It serves the same role as
`PyArg_ParseTupleAndKeywords`_.
::
var args = py.PY_parseArgs(
arguments, ['foo', 'bar', ['baz', 3], ['qux', "foo"]]);
roughly corresponds to the argument spec:
.. code-block:: python
def func(foo, bar, baz=3, qux="foo"):
pass
.. note:: a significant difference is that "default values" will
be re-evaluated at each call, since they are within the
function.
:param arguments: array-like objects holding the args and kwargs
passed to the callable, generally the
``arguments`` of the caller.
:param format: mapping declaration to the actual arguments of the
function. A javascript array composed of five
possible types of elements:
* The literal string ``'*'`` marks all following
parameters as keyword-only, regardless of them
having a default value or not [#kwonly]_. Can
only be present once in the parameters list.
* A string prefixed by ``*``, marks the positional
variadic parameter for the function: gathers all
provided positional arguments left and makes all
following parameters keyword-only
[#star-args]_. ``*args`` is incompatible with
``*``.
* A string prefixed with ``**``, marks the
positional keyword variadic parameter for the
function: gathers all provided keyword arguments
left and closes the argslist. If present, this
must be the last parameter of the format list.
* A string defines a required parameter, accessible
positionally or through keyword
* A pair of ``[String, py.object]`` defines an
optional parameter and its default value.
For simplicity, when not using optional parameters
it is possible to use a simple string as the format
(using space-separated elements). The string will
be split on whitespace and processed as a normal
format array.
:returns: a javascript object mapping argument names to values
:raises: ``TypeError`` if the provided arguments don't match the
format
.. class:: py.PY_def(fn)
Type wrapping javascript functions into py.js callables. The
wrapped function follows :ref:`the py.js calling conventions
<types-methods-python-call>`
:param Function fn: the javascript function to wrap
:returns: a callable py.js object
Object Protocol
---------------
.. function:: py.PY_hasAttr(o, attr_name)
Returns ``true`` if ``o`` has the attribute ``attr_name``,
otherwise returns ``false``. Equivalent to Python's ``hasattr(o,
attr_name)``
:param o: A :class:`py.object`
:param attr_name: a javascript ``String``
:rtype: ``Boolean``
.. function:: py.PY_getAttr(o, attr_name)
Retrieve an attribute ``attr_name`` from the object ``o``. Returns
the attribute value on success, raises ``AttributeError`` on
failure. Equivalent to the python expression ``o.attr_name``.
:param o: A :class:`py.object`
:param attr_name: a javascript ``String``
:returns: A :class:`py.object`
:raises: ``AttributeError``
.. function:: py.PY_str(o)
Computes a string representation of ``o``, returns the string
representation. Equivalent to ``str(o)``
:param o: A :class:`py.object`
:returns: :class:`py.str`
.. function:: py.PY_isInstance(inst, cls)
Returns ``true`` if ``inst`` is an instance of ``cls``, ``false``
otherwise.
.. function:: py.PY_isSubclass(derived, cls)
Returns ``true`` if ``derived`` is ``cls`` or a subclass thereof.
.. function:: py.PY_call(callable[, args][, kwargs])
Call an arbitrary python-level callable from javascript.
:param callable: A ``py.js`` callable object (broadly speaking,
either a class or an object with a ``__call__``
method)
:param args: javascript Array of :class:`py.object`, used as
positional arguments to ``callable``
:param kwargs: javascript Object mapping names to
:class:`py.object`, used as named arguments to
``callable``
:returns: nothing or :class:`py.object`
.. function:: py.PY_isTrue(o)
Returns ``true`` if the object is considered truthy, ``false``
otherwise. Equivalent to ``bool(o)``.
:param o: A :class:`py.object`
:rtype: Boolean
.. function:: py.PY_not(o)
Inverse of :func:`py.PY_isTrue`.
.. function:: py.PY_size(o)
If ``o`` is a sequence or mapping, returns its length. Otherwise,
raises ``TypeError``.
:param o: A :class:`py.object`
:returns: ``Number``
:raises: ``TypeError`` if the object doesn't have a length
Number Protocol
---------------
.. function:: py.PY_add(o1, o2)
Returns the result of adding ``o1`` and ``o2``, equivalent to
``o1 + o2``.
:param o1: :class:`py.object`
:param o2: :class:`py.object`
:returns: :class:`py.object`
.. function:: py.PY_subtract(o1, o2)
Returns the result of subtracting ``o2`` from ``o1``, equivalent
to ``o1 - o2``.
:param o1: :class:`py.object`
:param o2: :class:`py.object`
:returns: :class:`py.object`
.. function:: py.PY_multiply(o1, o2)
Returns the result of multiplying ``o1`` by ``o2``, equivalent to
``o1 * o2``.
:param o1: :class:`py.object`
:param o2: :class:`py.object`
:returns: :class:`py.object`
.. function:: py.PY_divide(o1, o2)
Returns the result of dividing ``o1`` by ``o2``, equivalent to
``o1 / o2``.
:param o1: :class:`py.object`
:param o2: :class:`py.object`
:returns: :class:`py.object`
.. function:: py.PY_negative(o)
Returns the negation of ``o``, equivalent to ``-o``.
:param o: :class:`py.object`
:returns: :class:`py.object`
.. function:: py.PY_positive(o)
Returns the "positive" of ``o``, equivalent to ``+o``.
:param o: :class:`py.object`
:returns: :class:`py.object`
.. [#kwonly] Python 2, which py.js currently implements, does not
support Python-level keyword-only parameters (it can be
done through the C-API), but it seemed neat and easy
enough so there.
.. [#star-args] due to this and contrary to Python 2, py.js allows
arguments other than ``**kwargs`` to follow ``*args``.
.. _PyArg_ParseTupleAndKeywords:
http://docs.python.org/c-api/arg.html#PyArg_ParseTupleAndKeywords

View File

@ -148,14 +148,14 @@ var py = {};
comparator('>'); comparator('>=');
comparator('<>'); comparator('!='); comparator('==');
infix('|', 70); infix('^', 80), infix('&', 90);
infix('|', 70); infix('^', 80); infix('&', 90);
infix('<<', 100); infix('>>', 100);
infix('+', 110); infix('-', 110);
infix('*', 120); infix('/', 120);
infix('//', 120), infix('%', 120);
infix('//', 120); infix('%', 120);
prefix('-', 130); prefix('+', 130); prefix('~', 130);
@ -261,7 +261,7 @@ var py = {};
var Name = '[a-zA-Z_]\\w*';
var DecNumber = '\\d+';
var DecNumber = '\\d+(L|l)?';
var IntNumber = DecNumber;
var PointFloat = group('\\d+\\.\\d*', '\\.\\d+');
var FloatNumber = PointFloat;
@ -273,12 +273,16 @@ var py = {};
var Special = '[:;.,`@]';
var Funny = group(Operator, Bracket, Special);
var ContStr = group("'[^']*'", '"[^"]*"');
var ContStr = group("[uU]?'([^']*)'", '[uU]?"([^"]*)"');
var PseudoToken = Whitespace + group(Number, Funny, ContStr, Name);
var number_pattern = new RegExp('^' + Number + '$');
var string_pattern = new RegExp('^' + ContStr + '$');
var name_pattern = new RegExp('^' + Name + '$');
var strip = new RegExp('^' + Whitespace);
return function tokenize(s) {
var max=s.length, tokens = [];
var max=s.length, tokens = [], start, end = undefined;
// /g flag makes repeated exec() have memory
var pseudoprog = new RegExp(PseudoToken, 'g');
@ -294,18 +298,20 @@ var py = {};
+ '; parsed so far: ' + tokens);
}
var start = pseudomatch.index, end = pseudoprog.lastIndex;
start = pseudomatch.index;
end = pseudoprog.lastIndex;
// strip leading space caught by Whitespace
var token = s.slice(start, end).replace(new RegExp('^' + Whitespace), '');
var token = s.slice(start, end).replace(strip, '');
var initial = token[0];
if (/\d/.test(initial) || (initial === '.' && token !== '.')) {
if (number_pattern.test(token)) {
tokens.push(create(symbols['(number)'], {
value: parseFloat(token)
}));
} else if (/'|"/.test(initial)) {
} else if (string_pattern.test(token)) {
var m = string_pattern.exec(token);
tokens.push(create(symbols['(string)'], {
value: token.slice(1, -1)
value: m[2] || m[3]
}));
} else if (token in symbols) {
var symbol;
@ -320,7 +326,7 @@ var py = {};
symbol = symbols[token];
}
tokens.push(create(symbol));
} else if (/[_a-zA-Z]/.test(initial)) {
} else if (name_pattern.test(token)) {
tokens.push(create(symbols['(name)'], {
value: token
}));
@ -369,24 +375,26 @@ var py = {};
return py.False;
}
if (val instanceof py.object
|| val === py.object
|| py.issubclass.__call__([val, py.object]) === py.True) {
var fn = function () {};
fn.prototype = py.object;
if (py.PY_isInstance(val, py.object)
|| py.PY_isSubclass(val, py.object)) {
return val;
}
switch (typeof val) {
case 'number':
return new py.float(val);
return py.float.fromJSON(val);
case 'string':
return new py.str(val);
return py.str.fromJSON(val);
case 'function':
return new py.def(val);
return py.PY_def.fromJSON(val);
}
switch(val.constructor) {
case Object:
var o = new py.object();
// TODO: why py.object instead of py.dict?
var o = py.PY_call(py.object);
for (var prop in val) {
if (val.hasOwnProperty(prop)) {
o[prop] = val[prop];
@ -394,46 +402,275 @@ var py = {};
}
return o;
case Array:
var a = new py.list();
a.values = val;
var a = py.PY_call(py.list);
a._values = val;
return a;
}
throw new Error("Could not convert " + val + " to a pyval");
}
// Builtins
py.type = function type(constructor, base, dict) {
var proto;
if (!base) {
base = py.object;
// JSAPI, JS-level utility functions for implementing new py.js
// types
py.py = {};
py.PY_parseArgs = function PY_parseArgs(argument, format) {
var out = {};
var args = argument[0];
var kwargs = {};
for (var k in argument[1]) {
if (!argument[1].hasOwnProperty(k)) { continue; }
kwargs[k] = argument[1][k];
}
proto = constructor.prototype = create(base.prototype);
proto.constructor = constructor;
if (dict) {
for(var k in dict) {
if (!dict.hasOwnProperty(k)) { continue; }
proto[k] = dict[k];
if (typeof format === 'string') {
format = format.split(/\s+/);
}
var name = function (spec) {
if (typeof spec === 'string') {
return spec;
} else if (spec instanceof Array && spec.length === 2) {
return spec[0];
}
throw new Error(
"TypeError: unknown format specification " +
JSON.stringify(spec));
};
var spec;
// TODO: ensure all format arg names are actual names?
for(var i=0; i<args.length; ++i) {
spec = format[i];
// spec list ended, or specs switching to keyword-only
if (!spec || spec === '*') {
throw new Error(
"TypeError: function takes exactly " + (i-1) +
" positional arguments (" + args.length +
" given")
} else if(/^\*\w/.test(spec)) {
// *args, final
out[name(spec.slice(1))] = args.slice(i);
break;
}
out[name(spec)] = args[i];
}
for(var j=i; j<format.length; ++j) {
spec = format[j];
var n = name(spec);
if (n in out) {
throw new Error(
"TypeError: function got multiple values " +
"for keyword argument '" + kwarg + "'");
}
if (/^\*\*\w/.test(n)) {
// **kwarg
out[n.slice(2)] = kwargs;
kwargs = {};
break;
}
if (n in kwargs) {
out[n] = kwargs[n];
// Remove from args map
delete kwargs[n];
}
}
constructor.__call__ = function () {
// create equivalent type with same prototype
var instance = create(proto);
// call actual constructor
var res = constructor.apply(instance, arguments);
// return result of constructor if any, otherwise instance
return res || instance;
};
return constructor;
// Ensure all keyword arguments were consumed
for (var key in kwargs) {
throw new Error(
"TypeError: function got an unexpected keyword argument '"
+ key + "'");
}
// Fixup args count if there's a kwonly flag (or an *args)
var kwonly = 0;
for(var k = 0; k < format.length; ++k) {
if (/^\*/.test(format[k])) { kwonly = 1; break; }
}
// Check that all required arguments have been matched, add
// optional values
for(var k = 0; k < format.length; ++k) {
spec = format[k];
var n = name(spec);
// kwonly, va_arg or matched argument
if (/^\*/.test(n) || n in out) { continue; }
// Unmatched required argument
if (!(spec instanceof Array)) {
throw new Error(
"TypeError: function takes exactly " + (format.length - kwonly)
+ " arguments");
}
// Set default value
out[n] = spec[1];
}
return out;
};
py.PY_hasAttr = function (o, attr_name) {
try {
py.PY_getAttr(o, attr_name);
return true;
} catch (e) {
return false;
}
};
py.PY_getAttr = function (o, attr_name) {
return PY_ensurepy(o.__getattribute__(attr_name));
};
py.PY_str = function (o) {
var v = o.__str__();
if (py.PY_isInstance(v, py.str)) {
return v;
}
var typename;
if (v.__class__) { // py type
typename = v.__class__.__name__;
} else if(typeof v !== 'object') { // JS primitive
typename = typeof v;
} else { // JS object
typename = v.constructor.name;
}
throw new Error(
'TypeError: __str__ returned non-string (type '+typename+')');
};
py.PY_isInstance = function (inst, cls) {
var fn = function () {};
fn.prototype = cls;
return inst instanceof fn;
};
py.PY_isSubclass = function (derived, cls) {
var fn = function () {};
fn.prototype = cls;
return derived === cls || derived instanceof fn;
};
py.PY_call = function (callable, args, kwargs) {
if (!args) {
args = []; kwargs = {};
} else if (typeof args === 'object' && !(args instanceof Array)) {
kwargs = args;
args = [];
} else if (!kwargs) {
kwargs = {};
}
if (callable.__is_type) {
// class hack
var instance = callable.__new__.call(callable, args, kwargs);
var typ = function () {};
typ.prototype = callable;
if (instance instanceof typ) {
instance.__init__.call(instance, args, kwargs);
}
return instance
}
return callable.__call__(args, kwargs);
};
py.PY_isTrue = function (o) {
var res = o.__nonzero__();
if (res === py.True) {
return true;
}
if (res === py.False) {
return false;
}
throw new Error(
"TypeError: __nonzero__ should return bool, returned "
+ res.__class__.__name__);
};
py.PY_not = function (o) {
return !py.PY_isTrue(o);
};
py.PY_size = function (o) {
if (!o.__len__) {
throw new Error(
"TypeError: object of type '" +
o.__class__.__name__ +
"' has no len()");
}
var v = o.__len__();
if (typeof v !== 'number') {
throw new Error("TypeError: a number is required");
}
return v;
};
py.PY_add = function (o1, o2) {
return PY_op(o1, o2, '+');
};
py.PY_subtract = function (o1, o2) {
return PY_op(o1, o2, '-');
};
py.PY_multiply = function (o1, o2) {
return PY_op(o1, o2, '*');
};
py.PY_divide = function (o1, o2) {
return PY_op(o1, o2, '/');
};
py.PY_negative = function (o) {
if (!o.__neg__) {
throw new Error(
"TypeError: bad operand for unary -: '"
+ o.__class__.__name
+ "'");
}
return o.__neg__();
};
py.PY_positive = function (o) {
if (!o.__pos__) {
throw new Error(
"TypeError: bad operand for unary +: '"
+ o.__class__.__name
+ "'");
}
return o.__pos__();
};
// Builtins
py.type = function type(name, bases, dict) {
if (typeof name !== 'string') {
throw new Error("ValueError: a class name should be a string");
}
if (!bases || bases.length === 0) {
bases = [py.object];
} else if (bases.length > 1) {
throw new Error("ValueError: can't provide multiple bases for a "
+ "new type");
}
var base = bases[0];
var ClassObj = create(base);
if (dict) {
for (var k in dict) {
if (!dict.hasOwnProperty(k)) { continue; }
ClassObj[k] = dict[k];
}
}
ClassObj.__class__ = ClassObj;
ClassObj.__name__ = name;
ClassObj.__bases__ = bases;
ClassObj.__is_type = true;
return ClassObj;
};
py.type.__call__ = function () {
var args = py.PY_parseArgs(arguments, ['object']);
return args.object.__class__;
};
var hash_counter = 0;
py.object = py.type(function object() {}, {}, {
py.object = py.type('object', [{}], {
__new__: function () {
// If ``this`` isn't the class object, this is going to be
// beyond fucked up
var inst = create(this);
inst.__is_type = false;
return inst;
},
__init__: function () {},
// Basic customization
__hash__: function () {
if (this._hash) {
return this._hash;
}
return this._hash = hash_counter++;
// tagged counter, to avoid collisions with e.g. number hashes
return this._hash = '\0\0\0' + String(hash_counter++);
},
__eq__: function (other) {
return (this === other) ? py.True : py.False;
@ -453,8 +690,7 @@ var py = {};
return this.__unicode__();
},
__unicode__: function () {
// TODO: return python string
return '<object ' + this.constructor.name + '>';
return py.str.fromJSON('<' + this.__class__.__name__ + ' object>');
},
__nonzero__: function () {
return py.True;
@ -465,13 +701,13 @@ var py = {};
var val = this[name];
if (typeof val === 'object' && '__get__' in val) {
// TODO: second argument should be class
return val.__get__(this);
return val.__get__(this, py.PY_call(py.type, [this]));
}
if (typeof val === 'function' && !this.hasOwnProperty(name)) {
// val is a method from the class
return new PY_instancemethod(val, this);
return PY_instancemethod.fromJSON(val, this);
}
return PY_ensurepy(val);
return val;
}
if ('__getattr__' in this) {
return this.__getattr__(name);
@ -491,137 +727,181 @@ var py = {};
throw new Error(this.constructor.name + ' can not be converted to JSON');
}
});
var NoneType = py.type(function NoneType() {}, py.object, {
var NoneType = py.type('NoneType', null, {
__nonzero__: function () { return py.False; },
toJSON: function () { return null; }
});
py.None = new NoneType();
var NotImplementedType = py.type(function NotImplementedType(){});
py.NotImplemented = new NotImplementedType();
py.None = py.PY_call(NoneType);
var NotImplementedType = py.type('NotImplementedType', null, {});
py.NotImplemented = py.PY_call(NotImplementedType);
var booleans_initialized = false;
py.bool = py.type(function bool(value) {
value = (value instanceof Array) ? value[0] : value;
// The only actual instance of py.bool should be py.True
// and py.False. Return the new instance of py.bool if we
// are initializing py.True and py.False, otherwise always
// return either py.True or py.False.
if (!booleans_initialized) {
return;
}
if (value === undefined) { return py.False; }
return value.__nonzero__() === py.True ? py.True : py.False;
}, py.object, {
py.bool = py.type('bool', null, {
__new__: function () {
if (!booleans_initialized) {
return py.object.__new__.apply(this);
}
var ph = {};
var args = py.PY_parseArgs(arguments, [['value', ph]]);
if (args.value === ph) {
return py.False;
}
return py.PY_isTrue(args.value) ? py.True : py.False;
},
__str__: function () {
return py.str.fromJSON((this === py.True) ? "True" : "False");
},
__nonzero__: function () { return this; },
fromJSON: function (val) { return val ? py.True : py.False },
toJSON: function () { return this === py.True; }
});
py.True = new py.bool();
py.False = new py.bool();
py.True = py.PY_call(py.bool);
py.False = py.PY_call(py.bool);
booleans_initialized = true;
py.float = py.type(function float(value) {
value = (value instanceof Array) ? value[0] : value;
if (value === undefined) { this._value = 0; return; }
if (value instanceof py.float) { return value; }
if (typeof value === 'number' || value instanceof Number) {
this._value = value;
return;
}
if (typeof value === 'string' || value instanceof String) {
this._value = parseFloat(value);
return;
}
if (value instanceof py.object && '__float__' in value) {
var res = value.__float__();
if (res instanceof py.float) {
return res;
py.float = py.type('float', null, {
__init__: function () {
var placeholder = {};
var args = py.PY_parseArgs(arguments, [['value', placeholder]]);
var value = args.value;
if (value === placeholder) {
this._value = 0; return;
}
throw new Error('TypeError: __float__ returned non-float (type ' +
res.constructor.name + ')');
}
throw new Error('TypeError: float() argument must be a string or a number');
}, py.object, {
if (py.PY_isInstance(value, py.float)) {
this._value = value._value;
}
if (py.PY_isInstance(value, py.object) && '__float__' in value) {
var res = value.__float__();
if (py.PY_isInstance(res, py.float)) {
this._value = res._value;
return;
}
throw new Error('TypeError: __float__ returned non-float (type ' +
res.__class__.__name__ + ')');
}
throw new Error('TypeError: float() argument must be a string or a number');
},
__str__: function () {
return py.str.fromJSON(String(this._value));
},
__eq__: function (other) {
return this._value === other._value ? py.True : py.False;
},
__lt__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return this._value < other._value ? py.True : py.False;
},
__le__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return this._value <= other._value ? py.True : py.False;
},
__gt__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return this._value > other._value ? py.True : py.False;
},
__ge__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return this._value >= other._value ? py.True : py.False;
},
__add__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value + other._value);
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value + other._value);
},
__neg__: function () {
return new py.float(-this._value);
return py.float.fromJSON(-this._value);
},
__sub__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value - other._value);
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value - other._value);
},
__mul__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value * other._value);
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value * other._value);
},
__div__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value / other._value);
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value / other._value);
},
__nonzero__: function () {
return this._value ? py.True : py.False;
},
fromJSON: function (v) {
if (!(typeof v === 'number')) {
throw new Error('py.float.fromJSON can only take numbers');
}
var instance = py.PY_call(py.float);
instance._value = v;
return instance;
},
toJSON: function () {
return this._value;
}
});
py.str = py.type(function str(s) {
s = (s instanceof Array) ? s[0] : s;
if (s === undefined) { this._value = ''; return; }
if (s instanceof py.str) { return s; }
if (typeof s === 'string' || s instanceof String) {
this._value = s;
return;
}
var v = s.__str__();
if (v instanceof py.str) { return v; }
throw new Error('TypeError: __str__ returned non-string (type ' +
v.constructor.name + ')');
}, py.object, {
py.str = py.type('str', null, {
__init__: function () {
var placeholder = {};
var args = py.PY_parseArgs(arguments, [['value', placeholder]]);
var s = args.value;
if (s === placeholder) { this._value = ''; return; }
this._value = py.PY_str(s)._value;
},
__hash__: function () {
return '\1\0\1' + this._value;
},
__str__: function () {
return this;
},
__eq__: function (other) {
if (other instanceof py.str && this._value === other._value) {
if (py.PY_isInstance(other, py.str)
&& this._value === other._value) {
return py.True;
}
return py.False;
},
__lt__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (py.PY_not(py.PY_call(py.isinstance, [other, py.str]))) {
return py.NotImplemented;
}
return this._value < other._value ? py.True : py.False;
},
__le__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (!py.PY_isInstance(other, py.str)) {
return py.NotImplemented;
}
return this._value <= other._value ? py.True : py.False;
},
__gt__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (!py.PY_isInstance(other, py.str)) {
return py.NotImplemented;
}
return this._value > other._value ? py.True : py.False;
},
__ge__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (!py.PY_isInstance(other, py.str)) {
return py.NotImplemented;
}
return this._value >= other._value ? py.True : py.False;
},
__add__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
return new py.str(this._value + other._value);
if (!py.PY_isInstance(other, py.str)) {
return py.NotImplemented;
}
return py.str.fromJSON(this._value + other._value);
},
__nonzero__: function () {
return this._value.length ? py.True : py.False;
@ -629,37 +909,74 @@ var py = {};
__contains__: function (s) {
return (this._value.indexOf(s._value) !== -1) ? py.True : py.False;
},
fromJSON: function (s) {
if (typeof s === 'string') {
var instance = py.PY_call(py.str);
instance._value = s;
return instance;
}
throw new Error("str.fromJSON can only take strings");
},
toJSON: function () {
return this._value;
}
});
py.tuple = py.type(function tuple() {}, null, {
py.tuple = py.type('tuple', null, {
__init__: function () {
this._values = [];
},
__contains__: function (value) {
for(var i=0, len=this.values.length; i<len; ++i) {
if (this.values[i].__eq__(value) === py.True) {
for(var i=0, len=this._values.length; i<len; ++i) {
if (this._values[i].__eq__(value) === py.True) {
return py.True;
}
}
return py.False;
},
__getitem__: function (index) {
return PY_ensurepy(this.values[index.toJSON()]);
return PY_ensurepy(this._values[index.toJSON()]);
},
toJSON: function () {
var out = [];
for (var i=0; i<this.values.length; ++i) {
out.push(this.values[i].toJSON());
for (var i=0; i<this._values.length; ++i) {
out.push(this._values[i].toJSON());
}
return out;
}
});
py.list = py.tuple;
py.dict = py.type(function dict() {
this._store = {};
}, py.object, {
py.dict = py.type('dict', null, {
__init__: function () {
this._store = {};
},
__getitem__: function (key) {
var h = key.__hash__();
if (!(h in this._store)) {
throw new Error("KeyError: '" + key.toJSON() + "'");
}
return this._store[h][1];
},
__setitem__: function (key, value) {
this._store[key.__hash__()] = [key, value];
},
get: function () {
var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]);
var h = args.k.__hash__();
if (!(h in this._store)) {
return args.d;
}
return this._store[h][1];
},
fromJSON: function (d) {
var instance = py.PY_call(py.dict);
for (var k in (d || {})) {
if (!d.hasOwnProperty(k)) { continue; }
instance.__setitem__(
py.str.fromJSON(k),
PY_ensurepy(d[k]));
}
return instance;
},
toJSON: function () {
var out = {};
for(var k in this._store) {
@ -669,30 +986,56 @@ var py = {};
return out;
}
});
py.def = py.type(function def(nativefunc) {
this._inst = null;
this._func = nativefunc;
}, py.object, {
__call__: function (args, kwargs) {
py.PY_def = py.type('function', null, {
__call__: function () {
// don't want to rewrite __call__ for instancemethod
return this._func.call(this._inst, args, kwargs);
return this._func.apply(this._inst, arguments);
},
fromJSON: function (nativefunc) {
var instance = py.PY_call(py.PY_def);
instance._inst = null;
instance._func = nativefunc;
return instance;
},
toJSON: function () {
return this._func;
}
});
var PY_instancemethod = py.type(function instancemethod(nativefunc, instance, _cls) {
// could also use bind?
this._inst = instance;
this._func = nativefunc;
}, py.def, {});
py.classmethod = py.type('classmethod', null, {
__init__: function () {
var args = py.PY_parseArgs(arguments, 'function');
this._func = args['function'];
},
__get__: function (obj, type) {
return PY_instancemethod.fromJSON(this._func, type);
},
fromJSON: function (func) {
return py.PY_call(py.classmethod, [func]);
}
});
var PY_instancemethod = py.type('instancemethod', [py.PY_def], {
fromJSON: function (nativefunc, instance) {
var inst = py.PY_call(PY_instancemethod);
// could also use bind?
inst._inst = instance;
inst._func = nativefunc;
return inst;
}
});
py.issubclass = new py.def(function issubclass(args) {
var derived = args[0], parent = args[1];
// still hurts my brain that this can work
return derived.prototype instanceof py.object
? py.True
: py.False;
py.len = new py.PY_def.fromJSON(function len() {
var args = py.PY_parseArgs(arguments, ['object']);
return py.float.fromJSON(py.PY_size(args.object));
});
py.isinstance = new py.PY_def.fromJSON(function isinstance() {
var args = py.PY_parseArgs(arguments, ['object', 'class']);
return py.PY_isInstance(args.object, args['class'])
? py.True : py.False;
});
py.issubclass = new py.PY_def.fromJSON(function issubclass() {
var args = py.PY_parseArgs(arguments, ['C', 'B']);
return py.PY_isSubclass(args.C, args.B)
? py.True : py.False;
});
@ -700,10 +1043,11 @@ var py = {};
var PY_operators = {
'==': ['eq', 'eq', function (a, b) { return a === b; }],
'!=': ['ne', 'ne', function (a, b) { return a !== b; }],
'<': ['lt', 'gt', function (a, b) {return a.constructor.name < b.constructor.name;}],
'<=': ['le', 'ge', function (a, b) {return a.constructor.name <= b.constructor.name;}],
'>': ['gt', 'lt', function (a, b) {return a.constructor.name > b.constructor.name;}],
'>=': ['ge', 'le', function (a, b) {return a.constructor.name >= b.constructor.name;}],
'<>': ['ne', 'ne', function (a, b) { return a !== b; }],
'<': ['lt', 'gt', function (a, b) {return a.__class__.__name__ < b.__class__.__name__;}],
'<=': ['le', 'ge', function (a, b) {return a.__class__.__name__ <= b.__class__.__name__;}],
'>': ['gt', 'lt', function (a, b) {return a.__class__.__name__ > b.__class__.__name__;}],
'>=': ['ge', 'le', function (a, b) {return a.__class__.__name__ >= b.__class__.__name__;}],
'+': ['add', 'radd'],
'-': ['sub', 'rsub'],
@ -746,8 +1090,8 @@ var py = {};
}
throw new Error(
"TypeError: unsupported operand type(s) for " + op + ": '"
+ o1.constructor.name + "' and '"
+ o2.constructor.name + "'");
+ o1.__class__.__name__ + "' and '"
+ o2.__class__.__name__ + "'");
};
var PY_builtins = {
@ -761,10 +1105,16 @@ var py = {};
object: py.object,
bool: py.bool,
float: py.float,
str: py.str,
unicode: py.unicode,
tuple: py.tuple,
list: py.list,
dict: py.dict,
issubclass: py.issubclass
len: py.len,
isinstance: py.isinstance,
issubclass: py.issubclass,
classmethod: py.classmethod,
};
py.parse = function (toks) {
@ -774,7 +1124,6 @@ var py = {};
return expression();
};
var evaluate_operator = function (operator, a, b) {
var v;
switch (operator) {
case 'is': return a === b ? py.True : py.False;
case 'is not': return a !== b ? py.True : py.False;
@ -782,7 +1131,7 @@ var py = {};
return b.__contains__(a);
case 'not in':
return b.__contains__(a) === py.True ? py.False : py.True;
case '==': case '!=':
case '==': case '!=': case '<>':
case '<': case '<=':
case '>': case '>=':
return PY_op(a, b, operator);
@ -799,9 +1148,9 @@ var py = {};
}
return PY_ensurepy(val, expr.value);
case '(string)':
return new py.str(expr.value);
return py.str.fromJSON(expr.value);
case '(number)':
return new py.float(expr.value);
return py.float.fromJSON(expr.value);
case '(constant)':
switch (expr.value) {
case 'None': return py.None;
@ -820,9 +1169,7 @@ var py = {};
}
return py.True;
case 'not':
return py.evaluate(expr.first, context).__nonzero__() === py.True
? py.False
: py.True;
return py.PY_isTrue(py.evaluate(expr.first, context)) ? py.False : py.True;
case 'and':
var and_first = py.evaluate(expr.first, context);
if (and_first.__nonzero__() === py.True) {
@ -850,7 +1197,7 @@ var py = {};
py.evaluate(arg.second, context);
}
}
return callable.__call__(args, kwargs);
return py.PY_call(callable, args, kwargs);
}
var tuple_exprs = expr.first,
tuple_values = [];
@ -858,8 +1205,8 @@ var py = {};
tuple_values.push(py.evaluate(
tuple_exprs[j], context));
}
var t = new py.tuple();
t.values = tuple_values;
var t = py.PY_call(py.tuple);
t._values = tuple_values;
return t;
case '[':
if (expr.second) {
@ -871,11 +1218,11 @@ var py = {};
list_values.push(py.evaluate(
list_exprs[k], context));
}
var l = new py.list();
l.values = list_values;
var l = py.PY_call(py.list);
l._values = list_values;
return l;
case '{':
var dict_exprs = expr.first, dict = new py.dict;
var dict_exprs = expr.first, dict = py.PY_call(py.dict);
for(var l=0; l<dict_exprs.length; ++l) {
dict.__setitem__(
py.evaluate(dict_exprs[l][0], context),
@ -886,18 +1233,18 @@ var py = {};
if (expr.second.id !== '(name)') {
throw new Error('SyntaxError: ' + expr);
}
return py.evaluate(expr.first, context)
.__getattribute__(expr.second.value);
return py.PY_getAttr(py.evaluate(expr.first, context),
expr.second.value);
// numerical operators
case '~':
return (py.evaluate(expr.first, context)).__invert__();
case '+':
if (!expr.second) {
return (py.evaluate(expr.first, context)).__pos__();
return py.PY_positive(py.evaluate(expr.first, context));
}
case '-':
if (!expr.second) {
return (py.evaluate(expr.first, context)).__neg__();
return py.PY_negative(py.evaluate(expr.first, context));
}
case '*': case '/': case '//':
case '%':

View File

@ -1,132 +0,0 @@
var py = require('../lib/py.js'),
expect = require('expect.js');
expect.Assertion.prototype.tokens = function (n) {
var length = this.obj.length;
this.assert(length === n + 1,
'expected ' + this.obj + ' to have ' + n + ' tokens',
'expected ' + this.obj + ' to not have ' + n + ' tokens');
this.assert(this.obj[length-1].id === '(end)',
'expected ' + this.obj + ' to have and end token',
'expected ' + this.obj + ' to not have an end token');
};
expect.Assertion.prototype.named = function (value) {
this.assert(this.obj.id === '(name)',
'expected ' + this.obj + ' to be a name token',
'expected ' + this.obj + ' not to be a name token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
expect.Assertion.prototype.constant = function (value) {
this.assert(this.obj.id === '(constant)',
'expected ' + this.obj + ' to be a constant token',
'expected ' + this.obj + ' not to be a constant token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
expect.Assertion.prototype.number = function (value) {
this.assert(this.obj.id === '(number)',
'expected ' + this.obj + ' to be a number token',
'expected ' + this.obj + ' not to be a number token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
expect.Assertion.prototype.string = function (value) {
this.assert(this.obj.id === '(string)',
'expected ' + this.obj + ' to be a string token',
'expected ' + this.obj + ' not to be a string token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
describe('Tokenizer', function () {
describe('simple literals', function () {
it('tokenizes numbers', function () {
var toks = py.tokenize('1');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.number(1);
var toks = py.tokenize('-1');
expect(toks).to.have.tokens(2);
expect(toks[0].id).to.be('-');
expect(toks[1]).to.be.number(1);
var toks = py.tokenize('1.2');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.number(1.2);
var toks = py.tokenize('.42');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.number(0.42);
});
it('tokenizes strings', function () {
var toks = py.tokenize('"foo"');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.string('foo');
var toks = py.tokenize("'foo'");
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.string('foo');
});
it('tokenizes bare names', function () {
var toks = py.tokenize('foo');
expect(toks).to.have.tokens(1);
expect(toks[0].id).to.be('(name)');
expect(toks[0].value).to.be('foo');
});
it('tokenizes constants', function () {
var toks = py.tokenize('None');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('None');
var toks = py.tokenize('True');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('True');
var toks = py.tokenize('False');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('False');
});
it('does not fuck up on trailing spaces', function () {
var toks = py.tokenize('None ');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('None');
});
});
describe('collections', function () {
it('tokenizes opening and closing symbols', function () {
var toks = py.tokenize('()');
expect(toks).to.have.tokens(2);
expect(toks[0].id).to.be('(');
expect(toks[1].id).to.be(')');
});
});
describe('functions', function () {
it('tokenizes kwargs', function () {
var toks = py.tokenize('foo(bar=3, qux=4)');
expect(toks).to.have.tokens(10);
});
});
});
describe('Parser', function () {
describe('functions', function () {
var ast = py.parse(py.tokenize('foo(bar=3, qux=4)'));
expect(ast.id).to.be('(');
expect(ast.first).to.be.named('foo');
args = ast.second;
expect(args[0].id).to.be('=');
expect(args[0].first).to.be.named('bar');
expect(args[0].second).to.be.number(3);
expect(args[1].id).to.be('=');
expect(args[1].first).to.be.named('qux');
expect(args[1].second).to.be.number(4);
});
});

View File

@ -1,388 +0,0 @@
var py = require('../lib/py.js'),
expect = require('expect.js');
var ev = function (str, context) {
return py.evaluate(py.parse(py.tokenize(str)), context);
};
describe('Literals', function () {
describe('Number', function () {
it('should have the right type', function () {
expect(ev('1')).to.be.a(py.float);
});
it('should yield the corresponding JS value', function () {
expect(py.eval('1')).to.be(1);
expect(py.eval('42')).to.be(42);
expect(py.eval('9999')).to.be(9999);
});
it('should correctly handle negative literals', function () {
expect(py.eval('-1')).to.be(-1);
expect(py.eval('-42')).to.be(-42);
expect(py.eval('-9999')).to.be(-9999);
});
it('should correctly handle float literals', function () {
expect(py.eval('.42')).to.be(0.42);
expect(py.eval('1.2')).to.be(1.2);
});
});
describe('Booleans', function () {
it('should have the right type', function () {
expect(ev('False')).to.be.a(py.bool);
expect(ev('True')).to.be.a(py.bool);
});
it('should yield the corresponding JS value', function () {
expect(py.eval('False')).to.be(false);
expect(py.eval('True')).to.be(true);
});
});
describe('None', function () {
it('should have the right type', function () {
expect(ev('None')).to.be.a(py.object)
});
it('should yield a JS null', function () {
expect(py.eval('None')).to.be(null);
});
});
describe('String', function () {
it('should have the right type', function () {
expect(ev('"foo"')).to.be.a(py.str);
expect(ev("'foo'")).to.be.a(py.str);
});
it('should yield the corresponding JS string', function () {
expect(py.eval('"somestring"')).to.be('somestring');
expect(py.eval("'somestring'")).to.be('somestring');
});
});
describe('Tuple', function () {
it('shoud have the right type', function () {
expect(ev('()')).to.be.a(py.tuple);
});
it('should map to a JS array', function () {
expect(py.eval('()')).to.eql([]);
expect(py.eval('(1, 2, 3)')).to.eql([1, 2, 3]);
});
});
describe('List', function () {
it('shoud have the right type', function () {
expect(ev('[]')).to.be.a(py.list);
});
it('should map to a JS array', function () {
expect(py.eval('[]')).to.eql([]);
expect(py.eval('[1, 2, 3]')).to.eql([1, 2, 3]);
});
});
describe('Dict', function () {
it('shoud have the right type', function () {
expect(ev('{}')).to.be.a(py.dict);
});
it('should map to a JS object', function () {
expect(py.eval("{}")).to.eql({});
expect(py.eval("{'foo': 1, 'bar': 2}"))
.to.eql({foo: 1, bar: 2});
});
});
});
describe('Free variables', function () {
it('should return its identity', function () {
expect(py.eval('foo', {foo: 1})).to.be(1);
expect(py.eval('foo', {foo: true})).to.be(true);
expect(py.eval('foo', {foo: false})).to.be(false);
expect(py.eval('foo', {foo: null})).to.be(null);
expect(py.eval('foo', {foo: 'bar'})).to.be('bar');
});
});
describe('Comparisons', function () {
describe('equality', function () {
it('should work with literals', function () {
expect(py.eval('1 == 1')).to.be(true);
expect(py.eval('"foo" == "foo"')).to.be(true);
expect(py.eval('"foo" == "bar"')).to.be(false);
});
it('should work with free variables', function () {
expect(py.eval('1 == a', {a: 1})).to.be(true);
expect(py.eval('foo == "bar"', {foo: 'bar'})).to.be(true);
expect(py.eval('foo == "bar"', {foo: 'qux'})).to.be(false);
});
});
describe('inequality', function () {
it('should work with literals', function () {
expect(py.eval('1 != 2')).to.be(true);
expect(py.eval('"foo" != "foo"')).to.be(false);
expect(py.eval('"foo" != "bar"')).to.be(true);
});
it('should work with free variables', function () {
expect(py.eval('1 != a', {a: 42})).to.be(true);
expect(py.eval('foo != "bar"', {foo: 'bar'})).to.be(false);
expect(py.eval('foo != "bar"', {foo: 'qux'})).to.be(true);
expect(py.eval('foo != bar', {foo: 'qux', bar: 'quux'}))
.to.be(true);
});
});
describe('rich comparisons', function () {
it('should work with numbers', function () {
expect(py.eval('3 < 5')).to.be(true);
expect(py.eval('5 >= 3')).to.be(true);
expect(py.eval('3 >= 3')).to.be(true);
expect(py.eval('3 > 5')).to.be(false);
});
it('should support comparison chains', function () {
expect(py.eval('1 < 3 < 5')).to.be(true);
expect(py.eval('5 > 3 > 1')).to.be(true);
expect(py.eval('1 < 3 > 2 == 2 > -2')).to.be(true);
});
it('should compare strings', function () {
expect(py.eval('date >= current',
{date: '2010-06-08', current: '2010-06-05'}))
.to.be(true);
expect(py.eval('state == "cancel"', {state: 'cancel'}))
.to.be(true);
expect(py.eval('state == "cancel"', {state: 'open'}))
.to.be(false);
});
});
describe('missing eq/neq', function () {
it('should fall back on identity', function () {
var typ = new py.type(function MyType() {});
expect(py.eval('MyType() == MyType()', {MyType: typ})).to.be(false);
});
});
describe('un-comparable types', function () {
it('should default to type-name ordering', function () {
var t1 = new py.type(function Type1() {});
var t2 = new py.type(function Type2() {});
expect(py.eval('T1() < T2()', {T1: t1, T2: t2})).to.be(true);
expect(py.eval('T1() > T2()', {T1: t1, T2: t2})).to.be(false);
});
it('should handle native stuff', function () {
expect(py.eval('None < 42')).to.be(true);
expect(py.eval('42 > None')).to.be(true);
expect(py.eval('None > 42')).to.be(false);
expect(py.eval('None < False')).to.be(true);
expect(py.eval('None < True')).to.be(true);
expect(py.eval('False > None')).to.be(true);
expect(py.eval('True > None')).to.be(true);
expect(py.eval('None > False')).to.be(false);
expect(py.eval('None > True')).to.be(false);
expect(py.eval('False < ""')).to.be(true);
expect(py.eval('"" > False')).to.be(true);
expect(py.eval('False > ""')).to.be(false);
});
});
});
describe('Boolean operators', function () {
it('should work', function () {
expect(py.eval("foo == 'foo' or foo == 'bar'",
{foo: 'bar'}))
.to.be(true);;
expect(py.eval("foo == 'foo' and bar == 'bar'",
{foo: 'foo', bar: 'bar'}))
.to.be(true);;
});
it('should be lazy', function () {
// second clause should nameerror if evaluated
expect(py.eval("foo == 'foo' or bar == 'bar'",
{foo: 'foo'}))
.to.be(true);;
expect(py.eval("foo == 'foo' and bar == 'bar'",
{foo: 'bar'}))
.to.be(false);;
});
it('should return the actual object', function () {
expect(py.eval('"foo" or "bar"')).to.be('foo');
expect(py.eval('None or "bar"')).to.be('bar');
expect(py.eval('False or None')).to.be(null);
expect(py.eval('0 or 1')).to.be(1);
});
});
describe('Containment', function () {
describe('in sequences', function () {
it('should match collection items', function () {
expect(py.eval("'bar' in ('foo', 'bar')"))
.to.be(true);
expect(py.eval('1 in (1, 2, 3, 4)'))
.to.be(true);;
expect(py.eval('1 in (2, 3, 4)'))
.to.be(false);;
expect(py.eval('"url" in ("url",)'))
.to.be(true);
expect(py.eval('"foo" in ["foo", "bar"]'))
.to.be(true);
});
it('should not be recursive', function () {
expect(py.eval('"ur" in ("url",)'))
.to.be(false);;
});
it('should be negatable', function () {
expect(py.eval('1 not in (2, 3, 4)')).to.be(true);
expect(py.eval('"ur" not in ("url",)')).to.be(true);
expect(py.eval('-2 not in (1, 2, 3)')).to.be(true);
});
});
describe('in dict', function () {
// TODO
});
describe('in strings', function () {
it('should match the whole string', function () {
expect(py.eval('"view" in "view"')).to.be(true);
expect(py.eval('"bob" in "view"')).to.be(false);
});
it('should match substrings', function () {
expect(py.eval('"ur" in "url"')).to.be(true);
});
});
});
describe('Conversions', function () {
describe('to bool', function () {
describe('strings', function () {
it('should be true if non-empty', function () {
expect(py.eval('bool(date_deadline)',
{date_deadline: '2008'}))
.to.be(true);
});
it('should be false if empty', function () {
expect(py.eval('bool(s)', {s: ''})) .to.be(false);
});
});
});
});
describe('Attribute access', function () {
it("should return the attribute's value", function () {
var o = new py.object();
o.bar = py.True;
expect(py.eval('foo.bar', {foo: o})).to.be(true);
o.bar = py.False;
expect(py.eval('foo.bar', {foo: o})).to.be(false);
});
it("should work with functions", function () {
var o = new py.object();
o.bar = new py.def(function () {
return new py.str("ok");
});
expect(py.eval('foo.bar()', {foo: o})).to.be('ok');
});
it('should not convert function attributes into methods', function () {
var o = new py.object();
o.bar = new py.type(function bar() {});
o.bar.__getattribute__ = function () {
return o.bar.baz;
}
o.bar.baz = py.True;
expect(py.eval('foo.bar.baz', {foo: o})).to.be(true);
});
it('should work on instance attributes', function () {
var typ = py.type(function MyType() {
this.attr = new py.float(3);
}, py.object, {});
expect(py.eval('MyType().attr', {MyType: typ})).to.be(3);
});
it('should work on class attributes', function () {
var typ = py.type(function MyType() {}, py.object, {
attr: new py.float(3)
});
expect(py.eval('MyType().attr', {MyType: typ})).to.be(3);
});
it('should work with methods', function () {
var typ = py.type(function MyType() {
this.attr = new py.float(3);
}, py.object, {
some_method: function () { return new py.str('ok'); },
get_attr: function () { return this.attr; }
});
expect(py.eval('MyType().some_method()', {MyType: typ})).to.be('ok');
expect(py.eval('MyType().get_attr()', {MyType: typ})).to.be(3);
});
});
describe('Callables', function () {
it('should wrap JS functions', function () {
expect(py.eval('foo()', {foo: function foo() { return new py.float(3); }}))
.to.be(3);
});
it('should work on custom types', function () {
var typ = py.type(function MyType() {}, py.object, {
toJSON: function () { return true; }
});
expect(py.eval('MyType()', {MyType: typ})).to.be(true);
});
it('should accept kwargs', function () {
expect(py.eval('foo(ok=True)', {
foo: function foo() { return py.True; }
})).to.be(true);
});
it('should be able to get its kwargs', function () {
expect(py.eval('foo(ok=True)', {
foo: function foo(args, kwargs) { return kwargs.ok; }
})).to.be(true);
});
it('should be able to have both args and kwargs', function () {
expect(py.eval('foo(1, 2, 3, ok=True, nok=False)', {
foo: function (args, kwargs) {
expect(args).to.have.length(3);
expect(args[0].toJSON()).to.be(1);
expect(kwargs).to.only.have.keys('ok', 'nok')
expect(kwargs.nok.toJSON()).to.be(false);
return kwargs.ok;
}
})).to.be(true);
});
});
describe('issubclass', function () {
it('should say a type is its own subclass', function () {
expect(py.issubclass.__call__([py.dict, py.dict]).toJSON())
.to.be(true);
expect(py.eval('issubclass(dict, dict)'))
.to.be(true);
});
it('should work with subtypes', function () {
expect(py.issubclass.__call__([py.bool, py.object]).toJSON())
.to.be(true);
});
});
describe('builtins', function () {
it('should aways be available', function () {
expect(py.eval('bool("foo")')).to.be(true);
});
});
describe('numerical protocols', function () {
describe('True numbers (float)', function () {
describe('Basic arithmetic', function () {
it('can be added', function () {
expect(py.eval('1 + 1')).to.be(2);
expect(py.eval('1.5 + 2')).to.be(3.5);
expect(py.eval('1 + -1')).to.be(0);
});
it('can be subtracted', function () {
expect(py.eval('1 - 1')).to.be(0);
expect(py.eval('1.5 - 2')).to.be(-0.5);
expect(py.eval('2 - 1.5')).to.be(0.5);
});
it('can be multiplied', function () {
expect(py.eval('1 * 3')).to.be(3);
expect(py.eval('0 * 5')).to.be(0);
expect(py.eval('42 * -2')).to.be(-84);
});
it('can be divided', function () {
expect(py.eval('1 / 2')).to.be(0.5);
expect(py.eval('2 / 1')).to.be(2);
});
});
});
describe('Strings', function () {
describe('Basic arithmetics operators', function () {
it('can be added (concatenation)', function () {
expect(py.eval('"foo" + "bar"')).to.be('foobar');
});
});
});
});
describe('Type converter', function () {
it('should convert bare objects to objects', function () {
expect(py.eval('foo.bar', {foo: {bar: 3}})).to.be(3);
});
it('should convert arrays to lists', function () {
expect(py.eval('foo[3]', {foo: [9, 8, 7, 6, 5]}))
.to.be(6);
});
});

View File

@ -2348,7 +2348,7 @@
.openerp .oe_form .oe_form_field_image .oe_form_field_image_controls {
position: absolute;
top: 1px;
padding: 4px;
padding: 4px 0;
width: 100%;
display: none;
text-align: center;
@ -2376,6 +2376,7 @@
width: 100%;
left: 2px;
top: 7px;
overflow: hidden;
}
.openerp .oe_fileupload .oe_add button {
display: inline;
@ -2402,10 +2403,11 @@
}
.openerp .oe_fileupload .oe_add input.oe_form_binary_file {
display: inline-block;
margin-left: -5px;
height: 28px;
width: 52px;
margin-top: -26px;
margin-left: -85px;
height: 22px;
width: 152px;
margin-top: -24px;
cursor: pointer;
}
.openerp .oe_fileupload .oe_add .oe_attach_label {
color: #7c7bad;

View File

@ -1880,7 +1880,7 @@ $sheet-padding: 16px
.oe_form_field_image_controls
position: absolute
top: 1px
padding: 4px
padding: 4px 0
width: 100%
display: none
text-align: center
@ -1900,6 +1900,7 @@ $sheet-padding: 16px
width: 100%
left: +2px
top: +7px
overflow: hidden
button
display: inline
height: 24px
@ -1922,10 +1923,11 @@ $sheet-padding: 16px
left: -9px
input.oe_form_binary_file
display: inline-block
margin-left: -5px
height: 28px
width: 52px
margin-top: -26px
margin-left: -85px
height: 22px
width: 152px
margin-top: -24px
cursor: pointer
.oe_attach_label
color: #7C7BAD
margin-left: -3px

View File

@ -53,7 +53,7 @@
* OpenERP Web web module split
*---------------------------------------------------------*/
openerp.web = function(session) {
var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"];
var files = ["pyeval", "corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"];
for(var i=0; i<files.length; i++) {
if(openerp.web[files[i]]) {
openerp.web[files[i]](session);

View File

@ -1264,11 +1264,12 @@ instance.web.WebClient = instance.web.Client.extend({
var self = this;
return this.rpc("/web/action/load", { action_id: options.action_id })
.then(function (result) {
var action = result;
if (options.needaction) {
action.context.search_default_message_unread = true;
result.context = new instance.web.CompoundContext(
result.context,
{search_default_message_unread: true});
}
return $.when(self.action_manager.do_action(action, {
return $.when(self.action_manager.do_action(result, {
clear_breadcrumbs: true,
action_menu_id: self.menu.current_menu,
})).fail(function() {

View File

@ -948,296 +948,6 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
this.server = this.origin; // keep chs happy
this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
},
test_eval_get_context: function () {
var asJS = function (arg) {
if (arg instanceof py.object) {
return arg.toJSON();
}
return arg;
};
var datetime = new py.object();
datetime.datetime = new py.type(function datetime() {
throw new Error('datetime.datetime not implemented');
});
var date = datetime.date = new py.type(function date(y, m, d) {
if (y instanceof Array) {
d = y[2];
m = y[1];
y = y[0];
}
this.year = asJS(y);
this.month = asJS(m);
this.day = asJS(d);
}, py.object, {
strftime: function (args) {
var f = asJS(args[0]), self = this;
return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) {
switch (c) {
case 'Y': return self.year;
case 'm': return _.str.sprintf('%02d', self.month);
case 'd': return _.str.sprintf('%02d', self.day);
}
throw new Error('ValueError: No known conversion for ' + m);
}));
}
});
date.__getattribute__ = function (name) {
if (name === 'today') {
return date.today;
}
throw new Error("AttributeError: object 'date' has no attribute '" + name +"'");
};
date.today = new py.def(function () {
var d = new Date();
return new date(d.getFullYear(), d.getMonth() + 1, d.getDate());
});
datetime.time = new py.type(function time() {
throw new Error('datetime.time not implemented');
});
var time = new py.object();
time.strftime = new py.def(function (args) {
return date.today.__call__().strftime(args);
});
var relativedelta = new py.type(function relativedelta(args, kwargs) {
if (!_.isEmpty(args)) {
throw new Error('Extraction of relative deltas from existing datetimes not supported');
}
this.ops = kwargs;
}, py.object, {
__add__: function (other) {
if (!(other instanceof datetime.date)) {
return py.NotImplemented;
}
// TODO: test this whole mess
var year = asJS(this.ops.year) || asJS(other.year);
if (asJS(this.ops.years)) {
year += asJS(this.ops.years);
}
var month = asJS(this.ops.month) || asJS(other.month);
if (asJS(this.ops.months)) {
month += asJS(this.ops.months);
// FIXME: no divmod in JS?
while (month < 1) {
year -= 1;
month += 12;
}
while (month > 12) {
year += 1;
month -= 12;
}
}
var lastMonthDay = new Date(year, month, 0).getDate();
var day = asJS(this.ops.day) || asJS(other.day);
if (day > lastMonthDay) { day = lastMonthDay; }
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
if (days_offset) {
day = new Date(year, month-1, day + days_offset).getDate();
}
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
return new datetime.date(year, month, day);
},
__radd__: function (other) {
return this.__add__(other);
},
__sub__: function (other) {
if (!(other instanceof datetime.date)) {
return py.NotImplemented;
}
// TODO: test this whole mess
var year = asJS(this.ops.year) || asJS(other.year);
if (asJS(this.ops.years)) {
year -= asJS(this.ops.years);
}
var month = asJS(this.ops.month) || asJS(other.month);
if (asJS(this.ops.months)) {
month -= asJS(this.ops.months);
// FIXME: no divmod in JS?
while (month < 1) {
year -= 1;
month += 12;
}
while (month > 12) {
year += 1;
month -= 12;
}
}
var lastMonthDay = new Date(year, month, 0).getDate();
var day = asJS(this.ops.day) || asJS(other.day);
if (day > lastMonthDay) { day = lastMonthDay; }
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
if (days_offset) {
day = new Date(year, month-1, day - days_offset).getDate();
}
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
return new datetime.date(year, month, day);
},
__rsub__: function (other) {
return this.__sub__(other);
}
});
return {
uid: new py.float(this.uid),
datetime: datetime,
time: time,
relativedelta: relativedelta,
current_date: date.today.__call__().strftime(['%Y-%m-%d'])
};
},
/**
* FIXME: Huge testing hack, especially the evaluation context, rewrite + test for real before switching
*/
test_eval: function (source, expected) {
var match_template = '<ul>' +
'<li>Source: %(source)s</li>' +
'<li>Local: %(local)s</li>' +
'<li>Remote: %(remote)s</li>' +
'</ul>',
fail_template = '<ul>' +
'<li>Error: %(error)s</li>' +
'<li>Source: %(source)s</li>' +
'</ul>';
try {
// see Session.eval_context in Python
var ctx = this.test_eval_contexts(
([this.context] || []).concat(source.contexts));
if (!_.isEqual(ctx, expected.context)) {
instance.webclient.notification.warn('Context mismatch, report to xmo',
_.str.sprintf(match_template, {
source: JSON.stringify(source.contexts),
local: JSON.stringify(ctx),
remote: JSON.stringify(expected.context)
}), true);
}
} catch (e) {
instance.webclient.notification.warn('Context fail, report to xmo',
_.str.sprintf(fail_template, {
error: e.message,
source: JSON.stringify(source.contexts)
}), true);
}
try {
var dom = this.test_eval_domains(source.domains, this.test_eval_get_context());
if (!_.isEqual(dom, expected.domain)) {
instance.webclient.notification.warn('Domains mismatch, report to xmo',
_.str.sprintf(match_template, {
source: JSON.stringify(source.domains),
local: JSON.stringify(dom),
remote: JSON.stringify(expected.domain)
}), true);
}
} catch (e) {
instance.webclient.notification.warn('Domain fail, report to xmo',
_.str.sprintf(fail_template, {
error: e.message,
source: JSON.stringify(source.domains)
}), true);
}
try {
var groups = this.test_eval_groupby(source.group_by_seq);
if (!_.isEqual(groups, expected.group_by)) {
instance.webclient.notification.warn('GroupBy mismatch, report to xmo',
_.str.sprintf(match_template, {
source: JSON.stringify(source.group_by_seq),
local: JSON.stringify(groups),
remote: JSON.stringify(expected.group_by)
}), true);
}
} catch (e) {
instance.webclient.notification.warn('GroupBy fail, report to xmo',
_.str.sprintf(fail_template, {
error: e.message,
source: JSON.stringify(source.group_by_seq)
}), true);
}
},
test_eval_contexts: function (contexts, evaluation_context) {
evaluation_context = evaluation_context || {};
var self = this;
return _(contexts).reduce(function (result_context, ctx) {
// __eval_context evaluations can lead to some of `contexts`'s
// values being null, skip them as well as empty contexts
if (_.isEmpty(ctx)) { return result_context; }
var evaluated = ctx;
switch(ctx.__ref) {
case 'context':
evaluated = py.eval(ctx.__debug, evaluation_context);
break;
case 'compound_context':
var eval_context = self.test_eval_contexts([ctx.__eval_context]);
evaluated = self.test_eval_contexts(
ctx.__contexts, _.extend({}, evaluation_context, eval_context));
break;
}
// add newly evaluated context to evaluation context for following
// siblings
_.extend(evaluation_context, evaluated);
return _.extend(result_context, evaluated);
}, _.extend({}, this.user_context));
},
test_eval_domains: function (domains, evaluation_context) {
var result_domain = [], self = this;
_(domains).each(function (dom) {
switch(dom.__ref) {
case 'domain':
result_domain.push.apply(
result_domain, py.eval(dom.__debug, evaluation_context));
break;
case 'compound_domain':
var eval_context = self.test_eval_contexts([dom.__eval_context]);
result_domain.push.apply(
result_domain, self.test_eval_domains(
dom.__domains, _.extend(
{}, evaluation_context, eval_context)));
break;
default:
result_domain.push.apply(
result_domain, dom);
}
});
return result_domain;
},
test_eval_groupby: function (contexts) {
var result_group = [], self = this;
_(contexts).each(function (ctx) {
var group;
switch(ctx.__ref) {
case 'context':
group = py.eval(ctx.__debug).group_by;
break;
case 'compound_context':
group = self.test_eval_contexts(
ctx.__contexts, ctx.__eval_context).group_by;
break;
default:
group = ctx.group_by
}
if (!group) { return; }
if (typeof group === 'string') {
result_group.push(group);
} else if (group instanceof Array) {
result_group.push.apply(result_group, group);
} else {
throw new Error('Got invalid groupby {{'
+ JSON.stringify(group) + '}}');
}
});
return result_group;
},
/**
* Executes an RPC call, registering the provided callbacks.
*
@ -1271,22 +981,19 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
var deferred = $.Deferred();
if (! options.shadow)
this.trigger('request', url, payload);
var request = this.rpc_function(url, payload).done(
this.rpc_function(url, payload).then(
function (response, textStatus, jqXHR) {
if (! options.shadow)
self.trigger('response', response);
if (!response.error) {
if (url.url === '/web/session/eval_domain_and_context') {
self.test_eval(params, response.result);
}
deferred.resolve(response["result"], textStatus, jqXHR);
} else if (response.error.data.type === "session_invalid") {
self.uid = false;
} else {
deferred.reject(response.error, $.Event());
}
}
).fail(
},
function(jqXHR, textStatus, errorThrown) {
if (! options.shadow)
self.trigger('response_failed', jqXHR);

View File

@ -61,8 +61,10 @@ instance.web.Query = instance.web.Class.extend({
return instance.session.rpc('/web/dataset/search_read', {
model: this._model.name,
fields: this._fields || false,
domain: this._model.domain(this._filter),
context: this._model.context(this._context),
domain: instance.web.pyeval.eval('domains',
[this._model.domain(this._filter)]),
context: instance.web.pyeval.eval('contexts',
[this._model.context(this._context)]),
offset: this._offset,
limit: this._limit,
sort: instance.web.serialize_sort(this._order_by)
@ -121,9 +123,8 @@ instance.web.Query = instance.web.Class.extend({
var self = this;
// FIXME: when pyeval is merged
var ctx = instance.session.test_eval_contexts(
[this._model.context(this._context)]);
var ctx = instance.web.pyeval.eval(
'context', this._model.context(this._context));
return this._model.call('read_group', {
groupby: grouping,
fields: _.uniq(grouping.concat(this._fields || [])),
@ -289,6 +290,7 @@ instance.web.Model = instance.web.Class.extend({
kwargs = args;
args = [];
}
instance.web.pyeval.ensure_evaluated(args, kwargs);
var debug = instance.session.debug ? '/'+this.name+':'+method : '';
return instance.session.rpc('/web/dataset/call_kw' + debug, {
model: this.name,
@ -301,7 +303,7 @@ instance.web.Model = instance.web.Class.extend({
* Fetches a Query instance bound to this model, for searching
*
* @param {Array<String>} [fields] fields to ultimately fetch during the search
* @returns {openerp.web.Query}
* @returns {instance.web.Query}
*/
query: function (fields) {
return new instance.web.Query(this, fields);
@ -349,9 +351,11 @@ instance.web.Model = instance.web.Class.extend({
* FIXME: remove when evaluator integrated
*/
call_button: function (method, args) {
instance.web.pyeval.ensure_evaluated(args, {});
return instance.session.rpc('/web/dataset/call_button', {
model: this.name,
method: method,
// Should not be necessary anymore. Integrate remote in this?
domain_id: null,
context_id: args.length - 1,
args: args || []
@ -606,7 +610,8 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
return instance.session.rpc('/web/dataset/resequence', {
model: this.model,
ids: ids,
context: this.get_context(options.context),
context: instance.web.pyeval.eval(
'context', this.get_context(options.context)),
}).then(function (results) {
return results;
});

View File

@ -0,0 +1,774 @@
/*
* py.js helpers and setup
*/
openerp.web.pyeval = function (instance) {
instance.web.pyeval = {};
var obj = function () {};
obj.prototype = py.object;
var asJS = function (arg) {
if (arg instanceof obj) {
return arg.toJSON();
}
return arg;
};
var datetime = py.PY_call(py.object);
/**
* computes (Math.floor(a/b), a%b and passes that to the callback.
*
* returns the callback's result
*/
var divmod = function (a, b, fn) {
var mod = a%b;
// in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a
// round of b
if (mod > 0 && b < 0 || mod < 0 && b > 0) {
mod += b;
}
return fn(Math.floor(a/b), mod);
};
/**
* Passes the fractional and integer parts of x to the callback, returns
* the callback's result
*/
var modf = function (x, fn) {
var mod = x%1;
if (mod < 0) {
mod += 1;
}
return fn(mod, Math.floor(x));
};
var zero = py.float.fromJSON(0);
// Port from pypy/lib_pypy/datetime.py
var DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
var DAYS_BEFORE_MONTH = [null];
var dbm = 0;
for (var i=1; i<DAYS_IN_MONTH.length; ++i) {
DAYS_BEFORE_MONTH.push(dbm);
dbm += DAYS_IN_MONTH[i];
}
var is_leap = function (year) {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
};
var days_before_year = function (year) {
var y = year - 1;
return y*365 + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400);
};
var days_in_month = function (year, month) {
if (month === 2 && is_leap(year)) {
return 29;
}
return DAYS_IN_MONTH[month];
};
var days_before_month = function (year, month) {
var post_leap_feb = month > 2 && is_leap(year);
return DAYS_BEFORE_MONTH[month]
+ (post_leap_feb ? 1 : 0);
};
var ymd2ord = function (year, month, day) {
var dim = days_in_month(year, month);
if (!(1 <= day && day <= dim)) {
throw new Error("ValueError: day must be in 1.." + dim);
}
return days_before_year(year)
+ days_before_month(year, month)
+ day;
};
var DI400Y = days_before_year(401);
var DI100Y = days_before_year(101);
var DI4Y = days_before_year(5);
var assert = function (bool) {
if (!bool) {
throw new Error("AssertionError");
}
};
var ord2ymd = function (n) {
--n;
var n400, n100, n4, n1, n0;
divmod(n, DI400Y, function (_n400, n) {
n400 = _n400;
divmod(n, DI100Y, function (_n100, n) {
n100 = _n100;
divmod(n, DI4Y, function (_n4, n) {
n4 = _n4;
divmod(n, 365, function (_n1, n) {
n1 = _n1;
n0 = n;
})
});
});
});
n = n0;
var year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1;
if (n1 == 4 || n100 == 100) {
assert(n0 === 0);
return {
year: year - 1,
month: 12,
day: 31
};
}
var leapyear = n1 === 3 && (n4 !== 24 || n100 == 3);
assert(leapyear == is_leap(year));
var month = (n + 50) >> 5;
var preceding = DAYS_BEFORE_MONTH[month] + ((month > 2 && leapyear) ? 1 : 0);
if (preceding > n) {
--month;
preceding -= DAYS_IN_MONTH[month] + ((month === 2 && leapyear) ? 1 : 0);
}
n -= preceding;
return {
year: year,
month: month,
day: n+1
};
};
/**
* Converts the stuff passed in into a valid date, applying overflows as needed
*/
var tmxxx = function (year, month, day, hour, minute, second, microsecond) {
hour = hour || 0; minute = minute || 0; second = second || 0;
microsecond = microsecond || 0;
if (microsecond < 0 || microsecond > 999999) {
divmod(microsecond, 1000000, function (carry, ms) {
microsecond = ms;
second += carry
});
}
if (second < 0 || second > 59) {
divmod(second, 60, function (carry, s) {
second = s;
minute += carry;
});
}
if (minute < 0 || minute > 59) {
divmod(minute, 60, function (carry, m) {
minute = m;
hour += carry;
})
}
if (hour < 0 || hour > 23) {
divmod(hour, 24, function (carry, h) {
hour = h;
day += carry;
})
}
// That was easy. Now it gets muddy: the proper range for day
// can't be determined without knowing the correct month and year,
// but if day is, e.g., plus or minus a million, the current month
// and year values make no sense (and may also be out of bounds
// themselves).
// Saying 12 months == 1 year should be non-controversial.
if (month < 1 || month > 12) {
divmod(month-1, 12, function (carry, m) {
month = m + 1;
year += carry;
})
}
// Now only day can be out of bounds (year may also be out of bounds
// for a datetime object, but we don't care about that here).
// If day is out of bounds, what to do is arguable, but at least the
// method here is principled and explainable.
var dim = days_in_month(year, month);
if (day < 1 || day > dim) {
// Move day-1 days from the first of the month. First try to
// get off cheap if we're only one day out of range (adjustments
// for timezone alone can't be worse than that).
if (day === 0) {
--month;
if (month > 0) {
day = days_in_month(year, month);
} else {
--year; month=12; day=31;
}
} else if (day == dim + 1) {
++month;
day = 1;
if (month > 12) {
month = 1;
++year;
}
} else {
var r = ord2ymd(ymd2ord(year, month, 1) + (day - 1));
year = r.year;
month = r.month;
day = r.day;
}
}
return {
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
};
};
datetime.timedelta = py.type('timedelta', null, {
__init__: function () {
var args = py.PY_parseArgs(arguments, [
['days', zero], ['seconds', zero], ['microseconds', zero],
['milliseconds', zero], ['minutes', zero], ['hours', zero],
['weeks', zero]
]);
var d = 0, s = 0, m = 0;
var days = args.days.toJSON() + args.weeks.toJSON() * 7;
var seconds = args.seconds.toJSON()
+ args.minutes.toJSON() * 60
+ args.hours.toJSON() * 3600;
var microseconds = args.microseconds.toJSON()
+ args.milliseconds.toJSON() * 1000;
// Get rid of all fractions, and normalize s and us.
// Take a deep breath <wink>.
var daysecondsfrac = modf(days, function (dayfrac, days) {
d = days;
if (dayfrac) {
return modf(dayfrac * 24 * 3600, function (dsf, dsw) {
s = dsw;
return dsf;
});
}
return 0;
});
var secondsfrac = modf(seconds, function (sf, s) {
seconds = s;
return sf + daysecondsfrac;
});
divmod(seconds, 24*3600, function (days, seconds) {
d += days;
s += seconds
});
// seconds isn't referenced again before redefinition
microseconds += secondsfrac * 1e6;
divmod(microseconds, 1000000, function (seconds, microseconds) {
divmod(seconds, 24*3600, function (days, seconds) {
d += days;
s += seconds;
m += Math.round(microseconds);
});
});
// Carrying still possible here?
this.days = d;
this.seconds = s;
this.microseconds = m;
},
__str__: function () {
var hh, mm, ss;
divmod(this.seconds, 60, function (m, s) {
divmod(m, 60, function (h, m) {
hh = h;
mm = m;
ss = s;
});
});
var s = _.str.sprintf("%d:%02d:%02d", hh, mm, ss);
if (this.days) {
s = _.str.sprintf("%d day%s, %s",
this.days,
(this.days != 1 && this.days != -1) ? 's' : '',
s);
}
if (this.microseconds) {
s = _.str.sprintf("%s.%06d", s, this.microseconds);
}
return py.str.fromJSON(s);
},
__eq__: function (other) {
if (!py.PY_isInstance(other, datetime.timedelta)) {
return py.False;
}
return (this.days === other.days
&& this.seconds === other.seconds
&& this.microseconds === other.microseconds)
? py.True : py.False;
},
__add__: function (other) {
if (!py.PY_isInstance(other, datetime.timedelta)) {
return py.NotImplemented;
}
return py.PY_call(datetime.timedelta, [
py.float.fromJSON(this.days + other.days),
py.float.fromJSON(this.seconds + other.seconds),
py.float.fromJSON(this.microseconds + other.microseconds)
]);
},
__radd__: function (other) { return this.__add__(other); },
__sub__: function (other) {
if (!py.PY_isInstance(other, datetime.timedelta)) {
return py.NotImplemented;
}
return py.PY_call(datetime.timedelta, [
py.float.fromJSON(this.days - other.days),
py.float.fromJSON(this.seconds - other.seconds),
py.float.fromJSON(this.microseconds - other.microseconds)
]);
},
__rsub__: function (other) {
if (!py.PY_isInstance(other, datetime.timedelta)) {
return py.NotImplemented;
}
return this.__neg__().__add__(other);
},
__neg__: function () {
return py.PY_call(datetime.timedelta, [
py.float.fromJSON(-this.days),
py.float.fromJSON(-this.seconds),
py.float.fromJSON(-this.microseconds)
]);
},
__pos__: function () { return this; },
__mul__: function (other) {
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
var n = other.toJSON();
return py.PY_call(datetime.timedelta, [
py.float.fromJSON(this.days * n),
py.float.fromJSON(this.seconds * n),
py.float.fromJSON(this.microseconds * n)
]);
},
__rmul__: function (other) { return this.__mul__(other); },
__div__: function (other) {
if (!py.PY_isInstance(other, py.float)) {
return py.NotImplemented;
}
var usec = ((this.days * 24 * 3600) + this.seconds) * 1000000
+ this.microseconds;
return py.PY_call(
datetime.timedelta, [
zero, zero, py.float.fromJSON(usec / other.toJSON())]);
},
__floordiv__: function (other) { return this.__div__(other); },
total_seconds: function () {
return py.float.fromJSON(
this.days * 86400
+ this.seconds
+ this.microseconds / 1000000)
},
__nonzero__: function () {
return (!!this.days || !!this.seconds || !!this.microseconds)
? py.True
: py.False;
}
});
datetime.datetime = py.type('datetime', null, {
__init__: function () {
var zero = py.float.fromJSON(0);
var args = py.PY_parseArgs(arguments, [
'year', 'month', 'day',
['hour', zero], ['minute', zero], ['second', zero],
['microsecond', zero], ['tzinfo', py.None]
]);
for(var key in args) {
if (!args.hasOwnProperty(key)) { continue; }
this[key] = asJS(args[key]);
}
},
strftime: function () {
var self = this;
var args = py.PY_parseArgs(arguments, 'format');
return py.str.fromJSON(args.format.toJSON()
.replace(/%([A-Za-z])/g, function (m, c) {
switch (c) {
case 'Y': return self.year;
case 'm': return _.str.sprintf('%02d', self.month);
case 'd': return _.str.sprintf('%02d', self.day);
case 'H': return _.str.sprintf('%02d', self.hour);
case 'M': return _.str.sprintf('%02d', self.minute);
case 'S': return _.str.sprintf('%02d', self.second);
}
throw new Error('ValueError: No known conversion for ' + m);
}));
},
now: py.classmethod.fromJSON(function () {
var d = new Date();
return py.PY_call(datetime.datetime,
[d.getFullYear(), d.getMonth() + 1, d.getDate(),
d.getHours(), d.getMinutes(), d.getSeconds(),
d.getMilliseconds() * 1000]);
}),
today: py.classmethod.fromJSON(function () {
var d = new Date();
return py.PY_call(datetime.datetime,
[d.getFullYear(), d.getMonth() + 1, d.getDate()]);
}),
combine: py.classmethod.fromJSON(function () {
var args = py.PY_parseArgs(arguments, 'date time');
return py.PY_call(datetime.datetime, [
py.PY_getAttr(args.date, 'year'),
py.PY_getAttr(args.date, 'month'),
py.PY_getAttr(args.date, 'day'),
py.PY_getAttr(args.time, 'hour'),
py.PY_getAttr(args.time, 'minute'),
py.PY_getAttr(args.time, 'second')
]);
})
});
datetime.date = py.type('date', null, {
__init__: function () {
var args = py.PY_parseArgs(arguments, 'year month day');
this.year = asJS(args.year);
this.month = asJS(args.month);
this.day = asJS(args.day);
},
strftime: function () {
var self = this;
var args = py.PY_parseArgs(arguments, 'format');
return py.str.fromJSON(args.format.toJSON()
.replace(/%([A-Za-z])/g, function (m, c) {
switch (c) {
case 'Y': return self.year;
case 'm': return _.str.sprintf('%02d', self.month);
case 'd': return _.str.sprintf('%02d', self.day);
}
throw new Error('ValueError: No known conversion for ' + m);
}));
},
today: py.classmethod.fromJSON(function () {
var d = new Date();
return py.PY_call(
datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
}),
__eq__: function (other) {
return (this.year === other.year
&& this.month === other.month
&& this.day === other.day)
? py.True : py.False;
},
__add__: function (other) {
if (!py.PY_isInstance(other, datetime.timedelta)) {
return py.NotImplemented;
}
var s = tmxxx(this.year, this.month, this.day + other.days);
return datetime.date.fromJSON(s.year, s.month, s.day);
},
__radd__: function (other) { return this.__add__(other); },
__sub__: function (other) {
if (py.PY_isInstance(other, datetime.timedelta)) {
return this.__add__(other.__neg__());
}
if (py.PY_isInstance(other, datetime.date)) {
// FIXME: getattr and sub API methods
return py.PY_call(datetime.timedelta, [
py.PY_subtract(
py.PY_call(py.PY_getAttr(this, 'toordinal')),
py.PY_call(py.PY_getAttr(other, 'toordinal')))
]);
}
return py.NotImplemented;
},
toordinal: function () {
return py.float.fromJSON(ymd2ord(this.year, this.month, this.day));
},
fromJSON: function (year, month, day) {
return py.PY_call(datetime.date, [year, month, day])
}
});
datetime.time = py.type('time', null, {
__init__: function () {
var zero = py.float.fromJSON(0);
var args = py.PY_parseArgs(arguments, [
['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero],
['tzinfo', py.None]
]);
for(var k in args) {
if (!args.hasOwnProperty(k)) { continue; }
this[k] = asJS(args[k]);
}
}
});
var time = py.PY_call(py.object);
time.strftime = py.PY_def.fromJSON(function () {
var args = py.PY_parseArgs(arguments, 'format');
var dt_class = py.PY_getAttr(datetime, 'datetime');
var d = py.PY_call(py.PY_getAttr(dt_class, 'now'));
return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
});
var relativedelta = py.type('relativedelta', null, {
__init__: function () {
this.ops = py.PY_parseArgs(arguments,
'* year month day hour minute second microsecond '
+ 'years months weeks days hours minutes secondes microseconds '
+ 'weekday leakdays yearday nlyearday');
},
__add__: function (other) {
if (!py.PY_isInstance(other, datetime.date)) {
return py.NotImplemented;
}
// TODO: test this whole mess
var year = asJS(this.ops.year) || asJS(other.year);
if (asJS(this.ops.years)) {
year += asJS(this.ops.years);
}
var month = asJS(this.ops.month) || asJS(other.month);
if (asJS(this.ops.months)) {
month += asJS(this.ops.months);
// FIXME: no divmod in JS?
while (month < 1) {
year -= 1;
month += 12;
}
while (month > 12) {
year += 1;
month -= 12;
}
}
var lastMonthDay = new Date(year, month, 0).getDate();
var day = asJS(this.ops.day) || asJS(other.day);
if (day > lastMonthDay) { day = lastMonthDay; }
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
if (days_offset) {
day = new Date(year, month-1, day + days_offset).getDate();
}
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
// FIXME: use date.replace
return py.PY_call(datetime.date, [
py.float.fromJSON(year),
py.float.fromJSON(month),
py.float.fromJSON(day)
]);
},
__radd__: function (other) {
return this.__add__(other);
},
__sub__: function (other) {
if (!py.PY_isInstance(other, datetime.date)) {
return py.NotImplemented;
}
// TODO: test this whole mess
var year = asJS(this.ops.year) || asJS(other.year);
if (asJS(this.ops.years)) {
year -= asJS(this.ops.years);
}
var month = asJS(this.ops.month) || asJS(other.month);
if (asJS(this.ops.months)) {
month -= asJS(this.ops.months);
// FIXME: no divmod in JS?
while (month < 1) {
year -= 1;
month += 12;
}
while (month > 12) {
year += 1;
month -= 12;
}
}
var lastMonthDay = new Date(year, month, 0).getDate();
var day = asJS(this.ops.day) || asJS(other.day);
if (day > lastMonthDay) { day = lastMonthDay; }
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
if (days_offset) {
day = new Date(year, month-1, day - days_offset).getDate();
}
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
return py.PY_call(datetime.date, [
py.float.fromJSON(year),
py.float.fromJSON(month),
py.float.fromJSON(day)
]);
},
__rsub__: function (other) {
return this.__sub__(other);
}
});
var eval_contexts = function (contexts, evaluation_context) {
evaluation_context = evaluation_context || {};
return _(contexts).reduce(function (result_context, ctx) {
// __eval_context evaluations can lead to some of `contexts`'s
// values being null, skip them as well as empty contexts
if (_.isEmpty(ctx)) { return result_context; }
if (_.isString(ctx)) {
// wrap raw strings in context
ctx = { __ref: 'context', __debug: ctx };
}
var evaluated = ctx;
switch(ctx.__ref) {
case 'context':
evaluation_context.context = py.dict.fromJSON(evaluation_context);
evaluated = py.eval(ctx.__debug, evaluation_context);
break;
case 'compound_context':
var eval_context = eval_contexts([ctx.__eval_context]);
evaluated = eval_contexts(
ctx.__contexts, _.extend({}, evaluation_context, eval_context));
break;
}
// add newly evaluated context to evaluation context for following
// siblings
_.extend(evaluation_context, evaluated);
return _.extend(result_context, evaluated);
}, _.extend({}, instance.session.user_context));
};
var eval_domains = function (domains, evaluation_context) {
var result_domain = [];
_(domains).each(function (domain) {
if (_.isString(domain)) {
// wrap raw strings in domain
domain = { __ref: 'domain', __debug: domain };
}
switch(domain.__ref) {
case 'domain':
evaluation_context.context = py.dict.fromJSON(evaluation_context);
result_domain.push.apply(
result_domain, py.eval(domain.__debug, evaluation_context));
break;
case 'compound_domain':
var eval_context = eval_contexts([domain.__eval_context]);
result_domain.push.apply(
result_domain, eval_domains(
domain.__domains, _.extend(
{}, evaluation_context, eval_context)));
break;
default:
result_domain.push.apply(result_domain, domain);
}
});
return result_domain;
};
var eval_groupbys = function (contexts, evaluation_context) {
var result_group = [];
_(contexts).each(function (ctx) {
if (_.isString(ctx)) {
// wrap raw strings in context
ctx = { __ref: 'context', __debug: ctx };
}
var group;
var evaluated = ctx;
switch(ctx.__ref) {
case 'context':
evaluation_context.context = py.dict.fromJSON(evaluation_context);
evaluated = py.eval(ctx.__debug, evaluation_context);
break;
case 'compound_context':
var eval_context = eval_contexts([ctx.__eval_context]);
evaluated = eval_contexts(
ctx.__contexts, _.extend({}, evaluation_context, eval_context));
break;
}
group = evaluated.group_by;
if (!group) { return; }
if (typeof group === 'string') {
result_group.push(group);
} else if (group instanceof Array) {
result_group.push.apply(result_group, group);
} else {
throw new Error('Got invalid groupby {{'
+ JSON.stringify(group) + '}}');
}
_.extend(evaluation_context, evaluated);
});
return result_group;
};
instance.web.pyeval.context = function () {
return {
uid: py.float.fromJSON(instance.session.uid),
datetime: datetime,
time: time,
relativedelta: relativedelta,
current_date: py.PY_call(
time.strftime, [py.str.fromJSON('%Y-%m-%d')]),
};
};
/**
* @param {String} type "domains", "contexts" or "groupbys"
* @param {Array} object domains or contexts to evaluate
* @param {Object} [context] evaluation context
*/
instance.web.pyeval.eval = function (type, object, context) {
context = _.extend(instance.web.pyeval.context(), context || {});
context['context'] = py.dict.fromJSON(context);
//noinspection FallthroughInSwitchStatementJS
switch(type) {
case 'context': object = [object];
case 'contexts': return eval_contexts(object, context);
case 'domain': object = [object];
case 'domains': return eval_domains(object, context);
case 'groupbys': return eval_groupbys(object, context);
}
throw new Error("Unknow evaluation type " + type)
};
var eval_arg = function (arg) {
if (typeof arg !== 'object' || !arg.__ref) { return arg; }
switch(arg.__ref) {
case 'domain': case 'compound_domain':
return instance.web.pyeval.eval('domains', [arg]);
case 'context': case 'compound_context':
return instance.web.pyeval.eval('contexts', [arg]);
default:
throw new Error(instance.web._t("Unknown nonliteral type " + arg.__ref));
}
};
/**
* If args or kwargs are unevaluated contexts or domains (compound or not),
* evaluated them in-place.
*
* Potentially mutates both parameters.
*
* @param args
* @param kwargs
*/
instance.web.pyeval.ensure_evaluated = function (args, kwargs) {
for (var i=0; i<args.length; ++i) {
args[i] = eval_arg(args[i]);
}
for (var k in kwargs) {
if (!kwargs.hasOwnProperty(k)) { continue; }
kwargs[k] = eval_arg(kwargs[k]);
}
};
instance.web.pyeval.eval_domains_and_contexts = function (source) {
return new $.Deferred(function (d) {setTimeout(function () {
try {
var contexts = ([instance.session.user_context] || []).concat(source.contexts);
// see Session.eval_context in Python
d.resolve({
context: instance.web.pyeval.eval('contexts', contexts),
domain: instance.web.pyeval.eval('domains', source.domains),
group_by: instance.web.pyeval.eval('groupbys', source.group_by_seq || [])
});
} catch (e) {
d.resolve({ error: {
code: 400,
message: instance.web._t("Evaluation Error"),
data: {
type: 'local_exception',
debug: _.str.sprintf(
instance.web._t("Local evaluation failure\n%s\n\n%s"),
e.message, JSON.stringify(source))
}
}});
}
}, 0); });
}
};

View File

@ -341,17 +341,19 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
if (this.headless) {
this.ready.resolve();
} else {
var load_view = this.rpc("/web/searchview/load", {
var load_view = this.rpc("/web/view/load", {
model: this.model,
view_id: this.view_id,
context: this.dataset.get_context() });
view_type: 'search',
context: instance.web.pyeval.eval(
'context', this.dataset.get_context())
});
$.when(load_view)
.then(function(r) {
self.search_view_loaded(r)
}, function () {
self.ready.reject.apply(null, arguments);
});
$.when(load_view).then(function (r) {
return self.search_view_loaded(r)
}).fail(function () {
self.ready.reject.apply(null, arguments);
});
}
instance.web.bus.on('click', this, function(ev) {
@ -615,16 +617,16 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
search_view_loaded: function(data) {
var self = this;
this.fields_view = data.fields_view;
if (data.fields_view.type !== 'search' ||
data.fields_view.arch.tag !== 'search') {
this.fields_view = data;
if (data.type !== 'search' ||
data.arch.tag !== 'search') {
throw new Error(_.str.sprintf(
"Got non-search view after asking for a search view: type %s, arch root %s",
data.fields_view.type, data.fields_view.arch.tag));
data.type, data.arch.tag));
}
this.make_widgets(
data.fields_view['arch'].children,
data.fields_view.fields);
data['arch'].children,
data.fields);
this.add_common_inputs();
@ -632,6 +634,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
var drawer_started = $.when.apply(
null, _(this.select_for_drawer()).invoke(
'appendTo', this.$('.oe_searchview_drawer')));
// load defaults
var defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
@ -1506,16 +1509,10 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
this.$el.on('click', 'h4', function () {
self.$el.toggleClass('oe_opened');
});
// FIXME: local eval of domain and context to get rid of special endpoint
return this.rpc('/web/searchview/get_filters', {
model: this.view.model
})
return this.model.call('get_filters', [this.view.model])
.then(this.proxy('set_filters'))
.then(function () {
self.is_ready.resolve(null);
}, function () {
self.is_ready.reject();
});
.done(function () { self.is_ready.resolve(); })
.fail(function () { self.is_ready.reject.apply(self.is_ready, arguments); });
},
/**
* Special implementation delaying defaults until CustomFilters is loaded
@ -1614,7 +1611,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
var set_as_default = this.$('#oe_searchview_custom_default').prop('checked');
var search = this.view.build_search_data();
this.rpc('/web/session/eval_domain_and_context', {
instance.web.pyeval.eval_domains_and_contexts({
domains: search.domains,
contexts: search.contexts,
group_by_seq: search.groupbys || []
@ -1708,10 +1705,10 @@ instance.web.search.Advanced = instance.web.search.Input.extend({
});
return $.when(
this._super(),
this.rpc("/web/searchview/fields_get", {model: this.view.model}).done(function(data) {
new instance.web.Model(this.view.model).call('fields_get').done(function(data) {
self.fields = _.extend({
id: { string: 'ID', type: 'id' }
}, data.fields);
}, data);
})).done(function () {
self.append_proposition();
});

View File

@ -2,7 +2,8 @@
openerp.testing = {};
(function (testing) {
var dependencies = {
corelib: [],
pyeval: [],
corelib: ['pyeval'],
coresetup: ['corelib'],
data: ['corelib', 'coresetup'],
dates: [],
@ -270,6 +271,9 @@ openerp.testing = {};
openerp.web[module](instance);
});
}
if (instance.session) {
instance.session.uid = 42;
}
if (_.isNumber(opts.asserts)) {
expect(opts.asserts);
}

View File

@ -436,26 +436,26 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var method = call[1];
if (!_.str.trim(call[2])) {
return {method: method, args: [], context_index: null}
return {method: method, args: []}
}
var argument_replacement = {
'False': function () {return false;},
'True': function () {return true;},
'None': function () {return null;},
'context': function (i) {
context_index = i;
var ctx = new instance.web.CompoundContext(self.dataset.get_context(), widget.build_context() ? widget.build_context() : {});
return ctx;
'context': function () {
return new instance.web.CompoundContext(
self.dataset.get_context(),
widget.build_context() ? widget.build_context() : {});
}
};
var parent_fields = null, context_index = null;
var parent_fields = null;
var args = _.map(call[2].split(','), function (a, i) {
var field = _.str.trim(a);
// literal constant or context
if (field in argument_replacement) {
return argument_replacement[field](i);
return argument_replacement[field]();
}
// literal number
if (/^-?\d+(\.\d+)?$/.test(field)) {
@ -490,8 +490,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return {
method: method,
args: args,
context_index: context_index
args: args
};
},
do_onchange: function(widget, processed) {
@ -510,18 +509,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var on_change = widget.node.attrs.on_change;
if (on_change) {
var change_spec = self.parse_on_change(on_change, widget);
def = self.rpc('/web/dataset/onchange', {
model: self.dataset.model,
method: change_spec.method,
args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args),
context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1
});
var id = [self.datarecord.id == null ? [] : [self.datarecord.id]];
def = new instance.web.Model(self.dataset.model).call(
change_spec.method, id.concat(change_spec.args));
} else {
def = $.when({});
}
return def.then(function(response) {
if (widget.field['change_default']) {
var fieldname = widget.name
var fieldname = widget.name;
var value_;
if (response.value && (fieldname in response.value)) {
// Use value from onchange if onchange executed
@ -533,11 +529,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var condition = fieldname + '=' + value_;
if (value_) {
return self.rpc('/web/dataset/call', {
model: 'ir.values',
method: 'get_defaults',
args: [self.model, condition]
}).then(function (results) {
return new instance.web.Model('ir.values').call(
'get_defaults', [self.model, condition]
).then(function (results) {
if (!results.length) {
return response;
}
@ -4591,7 +4585,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend
var self = this;
this.init_dataset();
if (this.options.initial_view == "search") {
self.rpc('/web/session/eval_domain_and_context', {
instance.web.pyeval.eval_domains_and_contexts({
domains: [],
contexts: [this.context]
}).done(function (results) {
@ -4664,7 +4658,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend
},
do_search: function(domains, contexts, groupbys) {
var self = this;
this.rpc('/web/session/eval_domain_and_context', {
instance.web.pyeval.eval_domains_and_contexts({
domains: domains || [],
contexts: contexts || [],
group_by_seq: groupbys || []

View File

@ -44,7 +44,8 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
view_id: this.view_id,
view_type: "tree",
toolbar: this.view_manager ? !!this.view_manager.sidebar : false,
context: this.dataset.get_context()
context: instance.web.pyeval.eval(
'context', this.dataset.get_context())
}).done(this.on_loaded);
},
/**
@ -227,8 +228,9 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
return this.rpc('/web/treeview/action', {
id: id,
model: this.dataset.model,
context: new instance.web.CompoundContext(
this.dataset.get_context(), local_context)
context: instance.web.pyeval.eval(
'context', new instance.web.CompoundContext(
this.dataset.get_context(), local_context))
}).then(function (actions) {
if (!actions.length) { return; }
var action = actions[0][2];
@ -236,7 +238,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
if (action.context) {
c.add(action.context);
}
return self.rpc('/web/session/eval_domain_and_context', {
return instance.web.pyeval.eval_domains_and_contexts({
contexts: [c], domains: []
}).then(function (res) {
action.context = res.context;

View File

@ -256,7 +256,9 @@ instance.web.ActionManager = instance.web.Widget.extend({
on_close: function() {},
action_menu_id: null,
});
if (_.isString(action) && instance.web.client_actions.contains(action)) {
if (action === false) {
action = { type: 'ir.actions.act_window_close' };
} else if (_.isString(action) && instance.web.client_actions.contains(action)) {
var action_client = { type: "ir.actions.client", tag: action, params: {} };
return this.do_action(action_client, options);
} else if (_.isNumber(action) || _.isString(action)) {
@ -265,6 +267,17 @@ instance.web.ActionManager = instance.web.Widget.extend({
return self.do_action(result, options);
});
}
// Ensure context & domain are evaluated and can be manipulated/used
if (action.context) {
action.context = instance.web.pyeval.eval(
'context', action.context);
}
if (action.domain) {
action.domain = instance.web.pyeval.eval(
'domain', action.domain);
}
if (!action.type) {
console.error("No type for action", action);
return $.Deferred().reject();
@ -382,6 +395,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
options.on_close();
}
this.dialog_stop();
return $.when();
},
ir_actions_server: function (action, options) {
var self = this;
@ -395,29 +409,36 @@ instance.web.ActionManager = instance.web.Widget.extend({
ir_actions_report_xml: function(action, options) {
var self = this;
instance.web.blockUI();
self.rpc("/web/session/eval_domain_and_context", {
return instance.web.pyeval.eval_domains_and_contexts({
contexts: [action.context],
domains: []
}).done(function(res) {
}).then(function(res) {
action = _.clone(action);
action.context = res.context;
var c = instance.webclient.crashmanager;
self.session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
complete: instance.web.unblockUI,
success: function(){
if (!self.dialog) {
options.on_close();
return $.Deferred(function (d) {
self.session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
complete: instance.web.unblockUI,
success: function(){
if (!self.dialog) {
options.on_close();
}
self.dialog_stop();
d.resolve();
},
error: function () {
c.rpc_error.apply(c, arguments);
d.reject();
}
self.dialog_stop();
},
error: c.rpc_error.bind(c)
})
})
});
});
},
ir_actions_act_url: function (action) {
window.open(action.url, action.target === 'self' ? '_self' : '_blank');
return $.when();
},
});
@ -666,7 +687,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
var self = this,
controller = this.views[this.active_view].controller,
action_context = this.action.context || {};
this.rpc('/web/session/eval_domain_and_context', {
instance.web.pyeval.eval_domains_and_contexts({
domains: [this.action.domain || []].concat(domains || []),
contexts: [action_context].concat(contexts || []),
group_by_seq: groupbys || []
@ -880,7 +901,11 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
nested: true,
}
};
this.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, complete: instance.web.unblockUI });
this.session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
complete: instance.web.unblockUI
});
}
break;
default:
@ -1069,14 +1094,17 @@ instance.web.Sidebar = instance.web.Widget.extend({
active_id: ids[0],
active_ids: ids,
active_model: self.getParent().dataset.model
};
};
var c = instance.web.pyeval.eval('context',
new instance.web.CompoundContext(
sidebar_eval_context, active_ids_context));
self.rpc("/web/action/load", {
action_id: item.action.id,
context: active_ids_context,
eval_context: new instance.web.CompoundContext(sidebar_eval_context, active_ids_context),
context: c
}).done(function(result) {
console.log(result.context);
result.context = new instance.web.CompoundContext(result.context || {}, active_ids_context);
result.context = new instance.web.CompoundContext(
result.context || {}, active_ids_context)
.set_eval_context(c);
result.flags = result.flags || {};
result.flags.new_window = true;
self.do_action(result, {
@ -1137,7 +1165,6 @@ instance.web.Sidebar = instance.web.Widget.extend({
}
},
on_attachment_delete: function(e) {
var self = this;
e.preventDefault();
e.stopPropagation();
var self = this;
@ -1182,7 +1209,8 @@ instance.web.View = instance.web.Widget.extend({
"view_id": this.view_id,
"view_type": this.view_type,
toolbar: !!this.options.$sidebar,
context: this.dataset.get_context(context)
context: instance.web.pyeval.eval(
'context', this.dataset.get_context(context))
});
}
return view_loaded.then(function(r) {
@ -1226,8 +1254,7 @@ instance.web.View = instance.web.Widget.extend({
};
var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
var handler = function (r) {
var action = r;
var handler = function (action) {
if (action && action.constructor == Object) {
var ncontext = new instance.web.CompoundContext(context);
if (record_id) {
@ -1238,18 +1265,10 @@ instance.web.View = instance.web.Widget.extend({
});
}
ncontext.add(action.context || {});
return self.rpc('/web/session/eval_domain_and_context', {
contexts: [ncontext],
domains: []
}).then(function (results) {
action.context = results.context;
/* niv: previously we were overriding once more with action_data.context,
* I assumed this was not a correct behavior and removed it
*/
return self.do_action(action, {
on_close: result_handler,
});
}, null);
action.context = ncontext;
return self.do_action(action, {
on_close: result_handler,
});
} else {
self.do_action({"type":"ir.actions.act_window_close"});
return result_handler();
@ -1271,11 +1290,15 @@ instance.web.View = instance.web.Widget.extend({
}
}
args.push(context);
return dataset.call_button(action_data.name, args).done(handler);
return dataset.call_button(action_data.name, args).then(handler);
} else if (action_data.type=="action") {
return this.rpc('/web/action/load', { action_id: action_data.name, context: context, do_not_eval: true}).done(handler);
return this.rpc('/web/action/load', {
action_id: action_data.name,
context: instance.web.pyeval.eval('context', context),
do_not_eval: true
}).then(handler);
} else {
return dataset.exec_workflow(record_id, action_data.name).done(handler);
return dataset.exec_workflow(record_id, action_data.name).then(handler);
}
},
/**

View File

@ -929,11 +929,11 @@
</li>
<li t-if="widget.node.attrs.context" data-item="context">
<span class="oe_tooltip_technical_title">Context:</span>
<t t-esc="widget.node.attrs.context_string"/>
<t t-esc="widget.node.attrs.context"/>
</li>
<li t-if="widget.node.attrs.domain" data-item="domain">
<span class="oe_tooltip_technical_title">Domain:</span>
<t t-esc="widget.node.attrs.domain_string"/>
<t t-esc="widget.node.attrs.domain"/>
</li>
<li t-if="widget.node.attrs.modifiers and widget.node.attrs.modifiers != '{}'" data-item="modifiers">
<span class="oe_tooltip_technical_title">Modifiers:</span>

View File

@ -1,11 +1,566 @@
openerp.testing.section('eval.types', {
dependencies: ['web.coresetup'],
setup: function (instance) {
instance.session.uid = 42;
}
}, function (test) {
test('strftime', function (instance) {
var d = new Date();
var context = instance.web.pyeval.context();
strictEqual(
py.eval("time.strftime('%Y')", context),
String(d.getFullYear()));
strictEqual(
py.eval("time.strftime('%Y')+'-01-30'", context),
String(d.getFullYear()) + '-01-30');
strictEqual(
py.eval("time.strftime('%Y-%m-%d %H:%M:%S')", context),
_.str.sprintf('%04d-%02d-%02d %02d:%02d:%02d',
d.getFullYear(), d.getMonth() + 1, d.getDate(),
d.getHours(), d.getMinutes(), d.getSeconds()));
});
// Port from pypy/lib_pypy/test_datetime.py
var makeEq = function (instance, c2) {
var ctx = instance.web.pyeval.context();
var c = _.extend({ td: ctx.datetime.timedelta }, c2 || {});
return function (a, b, message) {
ok(py.eval(a + ' == ' + b, c), message);
};
};
test('timedelta.test_constructor', function (instance) {
var eq = makeEq(instance);
// keyword args to constructor
eq('td()', 'td(weeks=0, days=0, hours=0, minutes=0, seconds=0, ' +
'milliseconds=0, microseconds=0)');
eq('td(1)', 'td(days=1)');
eq('td(0, 1)', 'td(seconds=1)');
eq('td(0, 0, 1)', 'td(microseconds=1)');
eq('td(weeks=1)', 'td(days=7)');
eq('td(days=1)', 'td(hours=24)');
eq('td(hours=1)', 'td(minutes=60)');
eq('td(minutes=1)', 'td(seconds=60)');
eq('td(seconds=1)', 'td(milliseconds=1000)');
eq('td(milliseconds=1)', 'td(microseconds=1000)');
// Check float args to constructor
eq('td(weeks=1.0/7)', 'td(days=1)');
eq('td(days=1.0/24)', 'td(hours=1)');
eq('td(hours=1.0/60)', 'td(minutes=1)');
eq('td(minutes=1.0/60)', 'td(seconds=1)');
eq('td(seconds=0.001)', 'td(milliseconds=1)');
eq('td(milliseconds=0.001)', 'td(microseconds=1)');
});
test('timedelta.test_computations', function (instance) {
var c = instance.web.pyeval.context();
var zero = py.float.fromJSON(0);
var eq = makeEq(instance, {
// one week
a: py.PY_call(c.datetime.timedelta, [
py.float.fromJSON(7)]),
// one minute
b: py.PY_call(c.datetime.timedelta, [
zero, py.float.fromJSON(60)]),
// one millisecond
c: py.PY_call(c.datetime.timedelta, [
zero, zero, py.float.fromJSON(1000)]),
});
eq('a+b+c', 'td(7, 60, 1000)');
eq('a-b', 'td(6, 24*3600 - 60)');
eq('-a', 'td(-7)');
eq('+a', 'td(7)');
eq('-b', 'td(-1, 24*3600 - 60)');
eq('-c', 'td(-1, 24*3600 - 1, 999000)');
// eq('abs(a)', 'a');
// eq('abs(-a)', 'a');
eq('td(6, 24*3600)', 'a');
eq('td(0, 0, 60*1000000)', 'b');
eq('a*10', 'td(70)');
eq('a*10', '10*a');
// eq('a*10L', '10*a');
eq('b*10', 'td(0, 600)');
eq('10*b', 'td(0, 600)');
// eq('b*10L', 'td(0, 600)');
eq('c*10', 'td(0, 0, 10000)');
eq('10*c', 'td(0, 0, 10000)');
// eq('c*10L', 'td(0, 0, 10000)');
eq('a*-1', '-a');
eq('b*-2', '-b-b');
eq('c*-2', '-c+-c');
eq('b*(60*24)', '(b*60)*24');
eq('b*(60*24)', '(60*b)*24');
eq('c*1000', 'td(0, 1)');
eq('1000*c', 'td(0, 1)');
eq('a//7', 'td(1)');
eq('b//10', 'td(0, 6)');
eq('c//1000', 'td(0, 0, 1)');
eq('a//10', 'td(0, 7*24*360)');
eq('a//3600000', 'td(0, 0, 7*24*1000)');
// Issue #11576
eq('td(999999999, 86399, 999999) - td(999999999, 86399, 999998)', 'td(0, 0, 1)');
eq('td(999999999, 1, 1) - td(999999999, 1, 0)',
'td(0, 0, 1)')
});
test('timedelta.test_basic_attributes', function (instance) {
var ctx = instance.web.pyeval.context();
strictEqual(py.eval('datetime.timedelta(1, 7, 31).days', ctx), 1);
strictEqual(py.eval('datetime.timedelta(1, 7, 31).seconds', ctx), 7);
strictEqual(py.eval('datetime.timedelta(1, 7, 31).microseconds', ctx), 31);
});
test('timedelta.test_total_seconds', function (instance) {
var c = { timedelta: instance.web.pyeval.context().datetime.timedelta };
strictEqual(py.eval('timedelta(365).total_seconds()', c), 31536000);
strictEqual(
py.eval('timedelta(seconds=123456.789012).total_seconds()', c),
123456.789012);
strictEqual(
py.eval('timedelta(seconds=-123456.789012).total_seconds()', c),
-123456.789012);
strictEqual(
py.eval('timedelta(seconds=0.123456).total_seconds()', c), 0.123456);
strictEqual(py.eval('timedelta().total_seconds()', c), 0);
strictEqual(
py.eval('timedelta(seconds=1000000).total_seconds()', c), 1e6);
});
test('timedelta.test_str', function (instance) {
var c = { td: instance.web.pyeval.context().datetime.timedelta };
strictEqual(py.eval('str(td(1))', c), "1 day, 0:00:00");
strictEqual(py.eval('str(td(-1))', c), "-1 day, 0:00:00");
strictEqual(py.eval('str(td(2))', c), "2 days, 0:00:00");
strictEqual(py.eval('str(td(-2))', c), "-2 days, 0:00:00");
strictEqual(py.eval('str(td(hours=12, minutes=58, seconds=59))', c),
"12:58:59");
strictEqual(py.eval('str(td(hours=2, minutes=3, seconds=4))', c),
"2:03:04");
strictEqual(
py.eval('str(td(weeks=-30, hours=23, minutes=12, seconds=34))', c),
"-210 days, 23:12:34");
strictEqual(py.eval('str(td(milliseconds=1))', c), "0:00:00.001000");
strictEqual(py.eval('str(td(microseconds=3))', c), "0:00:00.000003");
strictEqual(
py.eval('str(td(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999))', c),
"999999999 days, 23:59:59.999999");
});
test('timedelta.test_massive_normalization', function (instance) {
var td = py.PY_call(
instance.web.pyeval.context().datetime.timedelta,
{microseconds: py.float.fromJSON(-1)});
strictEqual(td.days, -1);
strictEqual(td.seconds, 24 * 3600 - 1);
strictEqual(td.microseconds, 999999);
});
test('timedelta.test_bool', function (instance) {
var c = { td: instance.web.pyeval.context().datetime.timedelta };
ok(py.eval('bool(td(1))', c));
ok(py.eval('bool(td(0, 1))', c));
ok(py.eval('bool(td(0, 0, 1))', c));
ok(py.eval('bool(td(microseconds=1))', c));
ok(py.eval('bool(not td(0))', c));
});
test('date.test_computations', function (instance) {
var d = instance.web.pyeval.context().datetime;
var a = d.date.fromJSON(2002, 1, 31);
var b = d.date.fromJSON(1956, 1, 31);
strictEqual(
py.eval('(a - b).days', {a: a, b: b}),
46 * 365 + 12);
strictEqual(py.eval('(a - b).seconds', {a: a, b: b}), 0);
strictEqual(py.eval('(a - b).microseconds', {a: a, b: b}), 0);
var day = py.PY_call(d.timedelta, [py.float.fromJSON(1)]);
var week = py.PY_call(d.timedelta, [py.float.fromJSON(7)]);
a = d.date.fromJSON(2002, 3, 2);
var ctx = {
a: a,
day: day,
week: week,
date: d.date
};
ok(py.eval('a + day == date(2002, 3, 3)', ctx));
ok(py.eval('day + a == date(2002, 3, 3)', ctx)); // 5
ok(py.eval('a - day == date(2002, 3, 1)', ctx));
ok(py.eval('-day + a == date(2002, 3, 1)', ctx));
ok(py.eval('a + week == date(2002, 3, 9)', ctx));
ok(py.eval('a - week == date(2002, 2, 23)', ctx));
ok(py.eval('a + 52*week == date(2003, 3, 1)', ctx)); // 10
ok(py.eval('a - 52*week == date(2001, 3, 3)', ctx));
ok(py.eval('(a + week) - a == week', ctx));
ok(py.eval('(a + day) - a == day', ctx));
ok(py.eval('(a - week) - a == -week', ctx));
ok(py.eval('(a - day) - a == -day', ctx)); // 15
ok(py.eval('a - (a + week) == -week', ctx));
ok(py.eval('a - (a + day) == -day', ctx));
ok(py.eval('a - (a - week) == week', ctx));
ok(py.eval('a - (a - day) == day', ctx));
raises(function () {
py.eval('a + 1', ctx);
}, /^Error: TypeError:/); // 20
raises(function () {
py.eval('a - 1', ctx);
}, /^Error: TypeError:/);
raises(function () {
py.eval('1 + a', ctx);
}, /^Error: TypeError:/);
raises(function () {
py.eval('1 - a', ctx);
}, /^Error: TypeError:/);
// delta - date is senseless.
raises(function () {
py.eval('day - a', ctx);
}, /^Error: TypeError:/);
// mixing date and (delta or date) via * or // is senseless
raises(function () {
py.eval('day * a', ctx);
}, /^Error: TypeError:/); // 25
raises(function () {
py.eval('a * day', ctx);
}, /^Error: TypeError:/);
raises(function () {
py.eval('day // a', ctx);
}, /^Error: TypeError:/);
raises(function () {
py.eval('a // day', ctx);
}, /^Error: TypeError:/);
raises(function () {
py.eval('a * a', ctx);
}, /^Error: TypeError:/);
raises(function () {
py.eval('a // a', ctx);
}, /^Error: TypeError:/); // 30
// date + date is senseless
raises(function () {
py.eval('a + a', ctx);
}, /^Error: TypeError:/);
});
});
openerp.testing.section('eval.edc', {
dependencies: ['web.data'],
rpc: 'mock',
setup: function (instance, $fix, mock) {
var user = { login: 'admin', id: 1, lang: 'en_US', tz: false };
instance.edc = function (domains, contexts) {
return instance.web.pyeval.eval_domains_and_contexts({
contexts: contexts || [],
domains: domains || []
});
};
mock('res.lang:load_lang', function () { return true; });
mock('res.users:write', function (args) {
_.extend(user, args[1]);
return true;
});
mock('/web/session/get_session_info', function () {
return {
session_id: 'foobar',
db: '3',
login: user.login,
uid: user.id,
context: {
uid: user.id,
lang: user.lang,
tz: user.tz
}
};
});
return instance.session.session_reload();
}
}, function (test) {
test('empty, basic', {asserts: 3}, function (instance) {
return instance.edc().then(function (result) {
// default values for new db
deepEqual(result.context, {
lang: 'en_US',
tz: false,
uid: 1
});
deepEqual(result.domain, []);
deepEqual(result.group_by, []);
});
});
test('empty, context altered', {
asserts: 3,
setup: function (instance) {
var lang = new instance.web.Model('res.lang');
var users = new instance.web.Model('res.users');
return lang.call('load_lang', ['ru_RU']).then(function () {
return users.call('write', [instance.session.uid, {
lang: 'ru_RU',
tz: 'America/Santarem'
}]);
}).then(instance.session.session_reload.bind(instance.session));
}
}, function (instance) {
return instance.edc().then(function (result) {
// default values for new db
deepEqual(result.context, {
lang: 'ru_RU',
tz: 'America/Santarem',
uid: 1
});
deepEqual(result.domain, []);
deepEqual(result.group_by, []);
});
});
test('context_merge_00', {asserts: 1}, function (instance) {
var ctx = [
{
"__contexts": [
{ "lang": "en_US", "tz": false, "uid": 1 },
{
"active_id": 8,
"active_ids": [ 8 ],
"active_model": "sale.order",
"bin_raw": true,
"default_composition_mode": "comment",
"default_model": "sale.order",
"default_res_id": 8,
"default_template_id": 18,
"default_use_template": true,
"edi_web_url_view": "faaaake",
"lang": "en_US",
"mark_so_as_sent": null,
"show_address": null,
"tz": false,
"uid": null
},
{}
],
"__eval_context": null,
"__ref": "compound_context"
},
{ "active_id": 9, "active_ids": [ 9 ], "active_model": "mail.compose.message" }
];
return instance.edc([], ctx).then(function (result) {
deepEqual(result.context, {
active_id: 9,
active_ids: [9],
active_model: 'mail.compose.message',
bin_raw: true,
default_composition_mode: 'comment',
default_model: 'sale.order',
default_res_id: 8,
default_template_id: 18,
default_use_template: true,
edi_web_url_view: "faaaake",
lang: 'en_US',
mark_so_as_sent: null,
show_address: null,
tz: false,
uid: null
});
});
});
test('context_merge_01', {asserts: 1}, function (instance) {
var ctx = [{
"__contexts": [
{
"lang": "en_US",
"tz": false,
"uid": 1
},
{
"default_attachment_ids": [],
"default_body": "",
"default_content_subtype": "html",
"default_model": "res.users",
"default_parent_id": false,
"default_res_id": 1
},
{}
],
"__eval_context": null,
"__ref": "compound_context"
}];
return instance.edc([], ctx).then(function (result) {
deepEqual(result.context, {
"default_attachment_ids": [],
"default_body": "",
"default_content_subtype": "html",
"default_model": "res.users",
"default_parent_id": false,
"default_res_id": 1,
"lang": "en_US",
"tz": false,
"uid": 1
});
});
});
});
openerp.testing.section('eval.edc.nonliterals', {
dependencies: ['web.data'],
setup: function (instance) {
instance.session.user_context = {
lang: 'en_US',
tz: false,
uid: 1
};
_.extend(instance, {
edc: function (domains, contexts) {
return instance.web.pyeval.eval_domains_and_contexts({
contexts: contexts || [],
domains: domains || []
});
}
});
}
}, function (test) {
test('domain with time', {asserts: 1}, function (instance) {
return instance.edc([
[['type', '=', 'contract']],
{ "__domains": [["|"], [["state", "in", ["open", "draft"]]], [["state", "=", "pending"]]],
"__eval_context": null,
"__ref": "compound_domain"
},
"['|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)]",
[['user_id', '=', 1]]
]).then(function (result) {
var d = new Date();
var today = _.str.sprintf("%04d-%02d-%02d",
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
deepEqual(result.domain, [
["type", "=", "contract"],
"|", ["state", "in", ["open", "draft"]],
["state", "=", "pending"],
"|",
"&", ["date", "!=", false],
["date", "<=", today],
["is_overdue_quantity", "=", true],
["user_id", "=", 1]
]);
});
});
test('conditional context', {asserts: 2}, function (instance) {
var d = {
__ref: 'domain',
__debug: "[('company_id', '=', context.get('company_id',False))]"
};
var e1 = instance.edc([d]).then(function (result) {
deepEqual(result.domain, [
['company_id', '=', false]
]);
});
var cd = new instance.web.CompoundDomain(d);
cd.set_eval_context({company_id: 42});
var e2 = instance.edc([cd]).then(function (result) {
deepEqual(result.domain, [
['company_id', '=', 42]
]);
});
return $.when(e1, e2);
});
test('substitution in context', {asserts: 1}, function (instance) {
var c = "{'default_opportunity_id': active_id, 'default_duration': 1.0, 'lng': lang}";
var cc = new instance.web.CompoundContext(c);
cc.set_eval_context({active_id: 42});
return instance.edc([], [cc]).then(function (result) {
deepEqual(result.context, {
lang: "en_US",
tz: false,
uid: 1,
default_opportunity_id: 42,
default_duration: 1.0,
lng: "en_US"
});
});
});
test('date', {asserts: 1}, function (instance) {
var d = "[('state','!=','cancel'),('opening_date','>',datetime.date.today().strftime('%Y-%m-%d'))]";
return instance.edc([d]).then(function (result) {
var d = new Date();
var today = _.str.sprintf("%04d-%02d-%02d",
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
deepEqual(result.domain, [
['state', '!=', 'cancel'],
['opening_date', '>', today]
]);
});
});
test('delta', {asserts: 1}, function (instance) {
var d = "[('type','=','in'),('day','<=', time.strftime('%Y-%m-%d')),('day','>',(datetime.date.today()-datetime.timedelta(days=15)).strftime('%Y-%m-%d'))]";
return instance.edc([d]).then(function (result) {
var d = new Date();
var today = _.str.sprintf("%04d-%02d-%02d",
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
d.setDate(d.getDate() - 15);
var ago_15_d = _.str.sprintf("%04d-%02d-%02d",
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
deepEqual(result.domain, [
['type', '=', 'in'],
['day', '<=', today],
['day', '>', ago_15_d]
]);
});
});
test('horror from the deep', {asserts: 1}, function (instance) {
var cs = [
{"__ref": "compound_context",
"__contexts": [
{"__ref": "context", "__debug": "{'k': 'foo,' + str(context.get('test_key', False))}"},
{"__ref": "compound_context",
"__contexts": [
{"lang": "en_US", "tz": false, "uid": 1},
{"lang": "en_US", "tz": false, "uid": 1,
"active_model": "sale.order", "default_type": "out",
"show_address": 1, "contact_display": "partner_address",
"active_ids": [9], "active_id": 9},
{}
], "__eval_context": null },
{"active_id": 8, "active_ids": [8],
"active_model": "stock.picking.out"},
{"__ref": "context", "__debug": "{'default_ref': 'stock.picking.out,'+str(context.get('active_id', False))}", "__id": "54d6ad1d6c45"}
], "__eval_context": null}
];
return instance.edc([], cs).then(function (result) {
deepEqual(result.context, {
k: 'foo,False',
lang: 'en_US',
tz: false,
uid: 1,
active_model: 'stock.picking.out',
active_id: 8,
active_ids: [8],
default_type: 'out',
show_address: 1,
contact_display: 'partner_address',
default_ref: 'stock.picking.out,8'
});
});
});
});
openerp.testing.section('eval.contexts', {
dependencies: ['web.coresetup']
}, function (test) {
test('context_recursive', function (instance) {
var context_to_eval = [{
__ref: 'context',
__debug: '{"foo": context.get("bar", "qux")}'
}];
deepEqual(
instance.web.pyeval.eval('contexts', context_to_eval, {bar: "ok"}),
{foo: 'ok'});
deepEqual(
instance.web.pyeval.eval('contexts', context_to_eval, {bar: false}),
{foo: false});
deepEqual(
instance.web.pyeval.eval('contexts', context_to_eval),
{foo: 'qux'});
});
test('context_sequences', function (instance) {
// Context n should have base evaluation context + all of contexts
// 0..n-1 in its own evaluation context
var active_id = 4;
var result = instance.session.test_eval_contexts([
var result = instance.web.pyeval.eval('contexts', [
{
"__contexts": [
{
@ -49,7 +604,7 @@ openerp.testing.section('eval.contexts', {
});
});
test('non-literal_eval_contexts', function (instance) {
var result = instance.session.test_eval_contexts([{
var result = instance.web.pyeval.eval('contexts', [{
"__ref": "compound_context",
"__contexts": [
{"__ref": "context", "__debug": "{'type':parent.type}",
@ -127,17 +682,101 @@ openerp.testing.section('eval.contexts', {
deepEqual(result, {type: 'out_invoice'});
});
});
openerp.testing.section('eval.contexts', {
openerp.testing.section('eval.domains', {
dependencies: ['web.coresetup', 'web.dates']
}, function (test) {
test('current_date', function (instance) {
var current_date = instance.web.date_to_str(new Date());
var result = instance.session.test_eval_domains(
var result = instance.web.pyeval.eval('domains',
[[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}],
instance.session.test_eval_get_context());
instance.web.pyeval.context());
deepEqual(result, [
['name', '>=', current_date],
['name', '<=', current_date]
]);
})
});
test('context_freevar', function (instance) {
var domains_to_eval = [{
__ref: 'domain',
__debug: '[("foo", "=", context.get("bar", "qux"))]'
}, [['bar', '>=', 42]]];
deepEqual(
instance.web.pyeval.eval('domains', domains_to_eval, {bar: "ok"}),
[['foo', '=', 'ok'], ['bar', '>=', 42]]);
deepEqual(
instance.web.pyeval.eval('domains', domains_to_eval, {bar: false}),
[['foo', '=', false], ['bar', '>=', 42]]);
deepEqual(
instance.web.pyeval.eval('domains', domains_to_eval),
[['foo', '=', 'qux'], ['bar', '>=', 42]]);
});
});
openerp.testing.section('eval.groupbys', {
dependencies: ['web.coresetup']
}, function (test) {
test('groupbys_00', function (instance) {
var result = instance.web.pyeval.eval('groupbys', [
{group_by: 'foo'},
{group_by: ['bar', 'qux']},
{group_by: null},
{group_by: 'grault'}
]);
deepEqual(result, ['foo', 'bar', 'qux', 'grault']);
});
test('groupbys_01', function (instance) {
var result = instance.web.pyeval.eval('groupbys', [
{group_by: 'foo'},
{ __ref: 'context', __debug: '{"group_by": "bar"}' },
{group_by: 'grault'}
]);
deepEqual(result, ['foo', 'bar', 'grault']);
});
test('groupbys_02', function (instance) {
var result = instance.web.pyeval.eval('groupbys', [
{group_by: 'foo'},
{
__ref: 'compound_context',
__contexts: [ {group_by: 'bar'} ],
__eval_context: null
},
{group_by: 'grault'}
]);
deepEqual(result, ['foo', 'bar', 'grault']);
});
test('groupbys_03', function (instance) {
var result = instance.web.pyeval.eval('groupbys', [
{group_by: 'foo'},
{
__ref: 'compound_context',
__contexts: [
{ __ref: 'context', __debug: '{"group_by": value}' }
],
__eval_context: { value: 'bar' }
},
{group_by: 'grault'}
]);
deepEqual(result, ['foo', 'bar', 'grault']);
});
test('groupbys_04', function (instance) {
var result = instance.web.pyeval.eval('groupbys', [
{group_by: 'foo'},
{
__ref: 'compound_context',
__contexts: [
{ __ref: 'context', __debug: '{"group_by": value}' }
],
__eval_context: { value: 'bar' }
},
{group_by: 'grault'}
], { value: 'bar' });
deepEqual(result, ['foo', 'bar', 'grault']);
});
test('groupbys_05', function (instance) {
var result = instance.web.pyeval.eval('groupbys', [
{group_by: 'foo'},
{ __ref: 'context', __debug: '{"group_by": value}' },
{group_by: 'grault'}
], { value: 'bar' });
deepEqual(result, ['foo', 'bar', 'grault']);
});
});

View File

@ -152,9 +152,9 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
instance.dummy = {};
instance.dummy.DummyWidget = instance.web.search.Field.extend(
dummy_widget_attributes || {});
if (!('/web/searchview/load' in instance.session.responses)) {
instance.session.responses['/web/searchview/load'] = function () {
return {fields_view: {
if (!('/web/view/load' in instance.session.responses)) {
instance.session.responses['/web/view/load'] = function () {
return {
type: 'search',
fields: {
dummy: {type: 'char', string: "Dummy"}
@ -171,16 +171,16 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
children: []
}]
}
}};
};
};
}
instance.session.responses['/web/searchview/get_filters'] = function () {
instance.session.responses['ir.filters:get_filters'] = function () {
return [];
};
instance.session.responses['/web/searchview/fields_get'] = function () {
return {fields: {
instance.session.responses['dummy.model:fields_get'] = function () {
return {
dummy: {type: 'char', string: 'Dummy'}
}};
};
};
var dataset = {model: 'dummy.model', get_context: function () { return {}; }};
@ -890,9 +890,9 @@ openerp.testing.section('filters', {
rpc: 'mock',
templates: true,
setup: function (instance, $s, mock) {
mock('/web/searchview/load', function () {
mock('/web/view/load', function () {
// view with a single group of filters
return {fields_view: {
return {
type: 'search',
fields: {},
arch: {
@ -915,7 +915,7 @@ openerp.testing.section('filters', {
children: []
}]
}
}};
};
});
}
}, function (test) {
@ -992,7 +992,7 @@ openerp.testing.section('saved_filters', {
}, function (test) {
test('checkboxing', {asserts: 6}, function (instance, $fix, mock) {
var view = makeSearchView(instance);
mock('/web/searchview/get_filters', function () {
mock('ir.filters:get_filters', function () {
return [{ name: "filter name", user_id: 42 }];
});
@ -1015,7 +1015,7 @@ openerp.testing.section('saved_filters', {
});
test('removal', {asserts: 1}, function (instance, $fix, mock) {
var view = makeSearchView(instance);
mock('/web/searchview/get_filters', function () {
mock('ir.filters:get_filters', function () {
return [{ name: "filter name", user_id: 42 }];
});

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html style="height: 100%">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>OpenERP Web Test Suite</title>
<link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
<link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
<script src="/web/static/lib/qunit/qunit.js" type="text/javascript"></script>
<script src="/web/static/lib/underscore/underscore.js" type="text/javascript"></script>
<script src="/web/static/lib/underscore/underscore.string.js" type="text/javascript"></script>
<script src="/web/static/lib/backbone/backbone.js" type="text/javascript"></script>
<!-- jquery -->
<script src="/web/static/lib/jquery/jquery-1.7.2.js"></script>
<script src="/web/static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js"></script>
<script src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
<script src="/web/static/lib/datejs/globalization/en-US.js"></script>
<script src="/web/static/lib/datejs/core.js"></script>
<script src="/web/static/lib/datejs/parser.js"></script>
<script src="/web/static/lib/datejs/sugarpak.js"></script>
<script src="/web/static/lib/datejs/extras.js"></script>
<script src="/web/static/lib/qweb/qweb2.js"></script>
<script src="/web/static/lib/py.js/lib/py.js"></script>
<script src="/web/static/src/js/boot.js"></script>
<script src="/web/static/src/js/pyeval.js"></script>
<script src="/web/static/src/js/corelib.js"></script>
<script src="/web/static/src/js/coresetup.js"></script>
<script src="/web/static/src/js/dates.js"></script>
<script src="/web/static/src/js/formats.js"></script>
<script src="/web/static/src/js/chrome.js"></script>
<script src="/web/static/src/js/data.js"></script>
<script src="/web/static/src/js/views.js"></script>
<script src="/web/static/src/js/search.js"></script>
<script src="/web/static/src/js/view_form.js"></script>
<script src="/web/static/src/js/view_list.js"></script>
<script src="/web/static/src/js/view_list_editable.js"></script>
<script src="/web/static/test/testing.js"></script>
<script type="text/javascript">
QUnit.config.testTimeout = 2000;
</script>
</head>
<body id="oe" class="openerp">
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
<script type="text/javascript" src="/web/static/test/class.js"></script>
<script type="text/javascript" src="/web/static/test/registry.js"></script>
<script type="text/javascript" src="/web/static/test/form.js"></script>
<script type="text/javascript" src="/web/static/test/list-utils.js"></script>
<script type="text/javascript" src="/web/static/test/formats.js"></script>
<script type="text/javascript" src="/web/static/test/rpc.js"></script>
<script type="text/javascript" src="/web/static/test/evals.js"></script>
<script type="text/javascript" src="/web/static/test/search.js"></script>
<script type="text/javascript" src="/web/static/test/Widget.js"></script>
<script type="text/javascript" src="/web/static/test/list-editable.js"></script>
</html>

View File

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
from . import test_dataset, test_menu, test_serving_base, test_view, test_js
from . import test_dataset, test_menu, test_serving_base, test_js
fast_suite = []
checks = [
test_dataset,
test_menu,
test_serving_base,
test_view,
]

View File

@ -18,14 +18,14 @@ class TestDataSetController(unittest2.TestCase):
self.dataset.do_search_read(self.request, 'fake.model'),
{'records': [], 'length': 0})
self.read.assert_called_once_with(
[], False, self.request.session.eval_context())
[], False, self.request.context)
def test_regular_find(self):
self.search.return_value = [1, 2, 3]
self.dataset.do_search_read(self.request, 'fake.model')
self.read.assert_called_once_with(
[1, 2, 3], False,self.request.session.eval_context())
[1, 2, 3], False,self.request.context)
def test_ids_shortcut(self):
self.search.return_value = [1, 2, 3]

View File

@ -4,7 +4,6 @@ import mock
import unittest2
from ..controllers import main
from ..session import OpenERPSession
class Placeholder(object):
def __init__(self, **kwargs):
@ -40,11 +39,11 @@ class LoadTest(unittest2.TestCase):
root = self.menu.do_load(self.request)
self.MockMenus.search.assert_called_with(
[], 0, False, False, self.request.session.eval_context())
[], 0, False, False, self.request.context)
self.MockMenus.read.assert_called_with(
[], ['name', 'sequence', 'parent_id',
'action', 'needaction_enabled', 'needaction_counter'],
self.request.session.eval_context())
self.request.context)
self.assertListEqual(
root['children'],
@ -63,7 +62,7 @@ class LoadTest(unittest2.TestCase):
self.MockMenus.read.assert_called_with(
[1, 2, 3], ['name', 'sequence', 'parent_id',
'action', 'needaction_enabled', 'needaction_counter'],
self.request.session.eval_context())
self.request.context)
self.assertEqual(
root['children'],

View File

@ -1,128 +0,0 @@
import copy
import xml.etree.ElementTree
import mock
import unittest2
import simplejson
import openerp.addons.web.controllers.main
from .. import nonliterals, session as s
def field_attrs(fields_view_get, fieldname):
(field,) = filter(lambda f: f['attrs'].get('name') == fieldname,
fields_view_get['arch']['children'])
return field['attrs']
#noinspection PyCompatibility
class DomainsAndContextsTest(unittest2.TestCase):
def setUp(self):
self.view = openerp.addons.web.controllers.main.View()
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=s.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'), nonliterals.Domain)
self.assertEqual(
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_context(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_context(self):
session = mock.Mock(spec=s.OpenERPSession)
session.contexts_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'), nonliterals.Context)
self.assertEqual(
nonliterals.Context(
session, key=e.get('context').key).get_context_string(),
context_string)
class AttrsNormalizationTest(unittest2.TestCase):
def setUp(self):
self.view = openerp.addons.web.controllers.main.View()
def test_identity(self):
web_view = """
<form string="Title">
<group>
<field name="some_field"/>
<field name="some_other_field"/>
</group>
<field name="stuff"/>
</form>
"""
pristine = xml.etree.ElementTree.fromstring(web_view)
transformed = self.view.transform_view(web_view, None)
self.assertEqual(
xml.etree.ElementTree.tostring(transformed),
xml.etree.ElementTree.tostring(pristine)
)

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_calendar
#. openerp-web

View File

@ -0,0 +1,203 @@
# Spanish (Mexico) translation for openerp-web
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openerp-web package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-28 15:51+0000\n"
"Last-Translator: OscarAlca <oscarolar@hotmail.com>\n"
"Language-Team: Spanish (Mexico) <es_MX@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:151
#, python-format
msgid "New event"
msgstr "Nuevo evento"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:154
#, python-format
msgid "Details"
msgstr "Más"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:152
#, python-format
msgid "Save"
msgstr "Guardar"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:147
#, python-format
msgid "Today"
msgstr "Hoy"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:149
#, python-format
msgid "Week"
msgstr "Semana"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:161
#, python-format
msgid "Full day"
msgstr "Día completo"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:159
#: code:addons/web_calendar/static/src/js/calendar.js:172
#, python-format
msgid "Description"
msgstr "Descripción"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:158
#, python-format
msgid "Event will be deleted permanently, are you sure?"
msgstr "¿El evento sera eliminado permanentemente, esta seguro?"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/xml/web_calendar.xml:8
#: code:addons/web_calendar/static/src/xml/web_calendar.xml:9
#, python-format
msgid "&nbsp;"
msgstr "&nbsp;"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:171
#, python-format
msgid "Date"
msgstr "Fecha"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:468
#, python-format
msgid "Edit: "
msgstr "Editar: "
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:148
#, python-format
msgid "Day"
msgstr "Día"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:155
#, python-format
msgid "Edit"
msgstr "Editar"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:167
#, python-format
msgid "Enabled"
msgstr "Habilitado"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:164
#, python-format
msgid "Do you want to edit the whole set of repeated events?"
msgstr "¿Quiere editar todos los eventos repetidos?"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:80
#, python-format
msgid "Filter"
msgstr "Filtro"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:165
#, python-format
msgid "Repeat event"
msgstr "Repetir evento"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:170
#: code:addons/web_calendar/static/src/js/calendar.js:178
#, python-format
msgid "Agenda"
msgstr "Agenda"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:160
#, python-format
msgid "Time period"
msgstr "Periodo de tiempo"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:156
#, python-format
msgid "Delete"
msgstr "Borrar"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:150
#, python-format
msgid "Month"
msgstr "Mes"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:166
#, python-format
msgid "Disabled"
msgstr "Deshabilitado/a"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:433
#, python-format
msgid "Create: "
msgstr "Crear: "
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:175
#, python-format
msgid "Year"
msgstr "Año"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:153
#, python-format
msgid "Cancel"
msgstr "Cancelar"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:28
#, python-format
msgid "Calendar"
msgstr "Calendario"

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_calendar
#. openerp-web

View File

@ -0,0 +1,104 @@
# Spanish (Mexico) translation for openerp-web
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openerp-web package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-28 15:54+0000\n"
"Last-Translator: OscarAlca <oscarolar@hotmail.com>\n"
"Language-Team: Spanish (Mexico) <es_MX@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:254
#: code:addons/web_diagram/static/src/js/diagram.js:319
#, python-format
msgid "Open: "
msgstr "Abrir: "
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:217
#, python-format
msgid ""
"Deleting this node cannot be undone.\n"
"It will also delete all connected transitions.\n"
"\n"
"Are you sure ?"
msgstr ""
"Eliminar este nodo no puede ser revertido.\n"
"También eliminara todas las transisiones conectadas."
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/xml/base_diagram.xml:13
#, python-format
msgid "New Node"
msgstr "Nuevo Nodo"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:312
#: code:addons/web_diagram/static/src/js/diagram.js:331
#, python-format
msgid "Transition"
msgstr "Transición"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:11
#, python-format
msgid "Diagram"
msgstr "Diagrama"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:246
#: code:addons/web_diagram/static/src/js/diagram.js:280
#, python-format
msgid "Activity"
msgstr "Actividad"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:422
#, python-format
msgid "%d / %d"
msgstr "%d / %d"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:285
#: code:addons/web_diagram/static/src/js/diagram.js:337
#, python-format
msgid "Create:"
msgstr "Crear:"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:187
#, python-format
msgid "Are you sure?"
msgstr "¿Está Seguro?"
#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:235
#, python-format
msgid ""
"Deleting this transition cannot be undone.\n"
"\n"
"Are you sure ?"
msgstr ""
"Eliminar esta transision no puede ser revertido.\n"
"\n"
"¿Está seguro?"

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_diagram
#. openerp-web

View File

@ -0,0 +1,32 @@
# Spanish (Mexico) translation for openerp-web
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openerp-web package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-28 15:54+0000\n"
"Last-Translator: OscarAlca <oscarolar@hotmail.com>\n"
"Language-Team: Spanish (Mexico) <es_MX@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_gantt
#. openerp-web
#: code:addons/web_gantt/static/src/xml/web_gantt.xml:10
#, python-format
msgid "Create"
msgstr "Crear"
#. module: web_gantt
#. openerp-web
#: code:addons/web_gantt/static/src/js/gantt.js:11
#, python-format
msgid "Gantt"
msgstr "Gantt"

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_gantt
#. openerp-web

View File

@ -22,10 +22,11 @@ instance.web_gantt.GanttView = instance.web.View.extend({
var self = this;
this.fields_view = fields_view_get;
this.$el.addClass(this.fields_view.arch.attrs['class']);
return this.rpc("/web/searchview/fields_get", {"model": this.dataset.model}).then(function(fields_get) {
self.fields = fields_get.fields;
self.has_been_loaded.resolve();
});
return new instance.web.Model(this.dataset.model)
.call('fields_get').then(function (fields) {
self.fields = fields;
self.has_been_loaded.resolve();
});
},
do_search: function (domains, contexts, group_bys) {
var self = this;

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_graph
#. openerp-web

View File

@ -0,0 +1,137 @@
# Spanish (Mexico) translation for openerp-web
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openerp-web package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-28 15:57+0000\n"
"Last-Translator: OscarAlca <oscarolar@hotmail.com>\n"
"Language-Team: Spanish (Mexico) <es_MX@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:12
#, python-format
msgid "Bars"
msgstr "Barras"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:33
#, python-format
msgid "Show Data"
msgstr "Mostrar Datos"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/js/graph.js:22
#, python-format
msgid "Graph"
msgstr "Gráfico"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:25
#, python-format
msgid "Inside"
msgstr "Dentro de"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:3
#, python-format
msgid "&iacute;"
msgstr "&iacute;"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:11
#, python-format
msgid "Pie"
msgstr "Pastel"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:28
#, python-format
msgid "Actions"
msgstr "Acciones"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:7
#, python-format
msgid "Graph Mode"
msgstr "Modo de gráfico"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:18
#, python-format
msgid "Radar"
msgstr "Radar"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:34
#, python-format
msgid "Download as PNG"
msgstr "Descargando como PNG"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:26
#, python-format
msgid "Top"
msgstr "Arriba"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:24
#, python-format
msgid "Hidden"
msgstr "Oculto"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:3
#, python-format
msgid "Graph Options"
msgstr "Opciones del gráfico"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:14
#, python-format
msgid "Lines"
msgstr "Líneas"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:20
#, python-format
msgid "Legend"
msgstr "Leyenda:"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:32
#, python-format
msgid "Switch Axis"
msgstr "Cambiar Eje"
#. module: web_graph
#. openerp-web
#: code:addons/web_graph/static/src/xml/web_graph.xml:15
#, python-format
msgid "Areas"
msgstr "Areas"

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_graph
#. openerp-web

View File

@ -0,0 +1,146 @@
# Spanish (Mexico) translation for openerp-web
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openerp-web package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-28 15:59+0000\n"
"Last-Translator: OscarAlca <oscarolar@hotmail.com>\n"
"Language-Team: Spanish (Mexico) <es_MX@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:697
#, python-format
msgid "Edit column"
msgstr "Editar columna"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:70
#, python-format
msgid "Show more... ("
msgstr "Mostrar más... ("
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:10
#, python-format
msgid "Kanban"
msgstr "Kanban"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:551
#, python-format
msgid "Undefined"
msgstr "Sin definir"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:716
#, python-format
msgid "Are you sure to remove this column ?"
msgstr "¿Está seguro de querer eliminar esta columna?"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:42
#, python-format
msgid "Edit"
msgstr "Editar"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:188
#, python-format
msgid "Add column"
msgstr "Añadir columna"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:1090
#, python-format
msgid "Create: "
msgstr "Crear: "
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:24
#, python-format
msgid "Add a new column"
msgstr "Añadir una nueva columna"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:687
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:40
#, python-format
msgid "Fold"
msgstr "Doblar"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:88
#, python-format
msgid "Add"
msgstr "Agregar"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:35
#, python-format
msgid "Quick create"
msgstr "Creación rapida"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:687
#, python-format
msgid "Unfold"
msgstr "Desplegar"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:89
#, python-format
msgid "Cancel"
msgstr "Cancelar"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:70
#, python-format
msgid "remaining)"
msgstr "restante)"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:22
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:88
#, python-format
msgid "or"
msgstr "ó"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:928
#, python-format
msgid "Are you sure you want to delete this record ?"
msgstr "¿Está seguro que quiere eliminar este registro?"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:43
#, python-format
msgid "Delete"
msgstr "Borrar"

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_kanban
#. openerp-web

View File

@ -8,21 +8,29 @@ msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-02-10 08:25+0000\n"
"Last-Translator: 开阖软件 Jeff Wang <jeff@osbzr.com>\n"
"PO-Revision-Date: 2012-11-28 07:30+0000\n"
"Last-Translator: youring <youring@gmail.com>\n"
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-25 06:42+0000\n"
"X-Generator: Launchpad (build 16293)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:22
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:88
#, python-format
msgid "or"
msgstr "or"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:697
#, python-format
msgid "Edit column"
msgstr ""
msgstr "编辑列"
#. module: web_kanban
#. openerp-web
@ -50,35 +58,35 @@ msgstr "未定义"
#: code:addons/web_kanban/static/src/js/kanban.js:716
#, python-format
msgid "Are you sure to remove this column ?"
msgstr ""
msgstr "您确认要移除此列?"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:42
#, python-format
msgid "Edit"
msgstr ""
msgstr "编辑"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:188
#, python-format
msgid "Add column"
msgstr ""
msgstr "增加列"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:1090
#, python-format
msgid "Create: "
msgstr ""
msgstr "创建: "
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:24
#, python-format
msgid "Add a new column"
msgstr ""
msgstr "添加新列"
#. module: web_kanban
#. openerp-web
@ -86,35 +94,35 @@ msgstr ""
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:40
#, python-format
msgid "Fold"
msgstr ""
msgstr "收拢"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:88
#, python-format
msgid "Add"
msgstr ""
msgstr "添加"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:35
#, python-format
msgid "Quick create"
msgstr ""
msgstr "快速创建"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:687
#, python-format
msgid "Unfold"
msgstr ""
msgstr "展开"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:89
#, python-format
msgid "Cancel"
msgstr ""
msgstr "取消"
#. module: web_kanban
#. openerp-web
@ -123,14 +131,6 @@ msgstr ""
msgid "remaining)"
msgstr "剩下的)"
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:22
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:88
#, python-format
msgid "or"
msgstr ""
#. module: web_kanban
#. openerp-web
#: code:addons/web_kanban/static/src/js/kanban.js:928
@ -143,7 +143,7 @@ msgstr "您确定要删除此记录吗?"
#: code:addons/web_kanban/static/src/xml/web_kanban.xml:43
#, python-format
msgid "Delete"
msgstr ""
msgstr "删除"
#~ msgid "</tr><tr>"
#~ msgstr "</tr><tr>"

View File

@ -0,0 +1,149 @@
# Spanish (Mexico) translation for openerp-web
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openerp-web package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-28 16:01+0000\n"
"Last-Translator: OscarAlca <oscarolar@hotmail.com>\n"
"Language-Team: Spanish (Mexico) <es_MX@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:396
#, python-format
msgid "Preview"
msgstr "Vista Preliminar"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:90
#, python-format
msgid "Save"
msgstr "Guardar"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:64
#, python-format
msgid "Edit"
msgstr "Editar"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:37
#, python-format
msgid "Manage Views (%s)"
msgstr "Administrar vistas (%s)"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:14
#, python-format
msgid "Could not find current view declaration"
msgstr "No se ha encontrado la declaración de la vista actual"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:382
#, python-format
msgid "Inherited View"
msgstr "Vista Heredada"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:828
#: code:addons/web_view_editor/static/src/js/view_editor.js:954
#, python-format
msgid "Update"
msgstr "Actualizar"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:65
#, python-format
msgid "Remove"
msgstr "Eliminar"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:825
#: code:addons/web_view_editor/static/src/js/view_editor.js:951
#, python-format
msgid "Properties"
msgstr "Propiedades"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:379
#, python-format
msgid "View Editor %d - %s"
msgstr "Ver Editor %d - %s"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:386
#, python-format
msgid "Do you really wants to create an inherited view here?"
msgstr "¿De verdad desea crear una vista heredada aquí?"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:13
#, python-format
msgid "Manage Views"
msgstr "Administrar Vistas"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:112
#: code:addons/web_view_editor/static/src/js/view_editor.js:846
#: code:addons/web_view_editor/static/src/js/view_editor.js:974
#, python-format
msgid "Cancel"
msgstr "Cancelar"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:66
#: code:addons/web_view_editor/static/src/js/view_editor.js:413
#, python-format
msgid "Close"
msgstr "Cerrar"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:516
#, python-format
msgid "Do you really want to remove this node?"
msgstr "¿De verdad desea eliminar este nodo?"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:183
#, python-format
msgid "Do you really want to remove this view?"
msgstr "¿De verdad desea eliminar esta vista?"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:63
#, python-format
msgid "Create"
msgstr "Crear"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:88
#, python-format
msgid "Create a view (%s)"
msgstr "Crear vista (%s)"

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-28 04:57+0000\n"
"X-Generator: Launchpad (build 16309)\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_view_editor
#. openerp-web

View File

@ -0,0 +1,149 @@
# Chinese (Simplified) translation for openerp-web
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openerp-web package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
"PO-Revision-Date: 2012-11-28 06:44+0000\n"
"Last-Translator: youring <youring@gmail.com>\n"
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-29 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:396
#, python-format
msgid "Preview"
msgstr "预览"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:90
#, python-format
msgid "Save"
msgstr "保存"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:64
#, python-format
msgid "Edit"
msgstr "编辑"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:37
#, python-format
msgid "Manage Views (%s)"
msgstr "管理视图 (%s)"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:14
#, python-format
msgid "Could not find current view declaration"
msgstr ""
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:382
#, python-format
msgid "Inherited View"
msgstr "继承视图"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:828
#: code:addons/web_view_editor/static/src/js/view_editor.js:954
#, python-format
msgid "Update"
msgstr "更新"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:65
#, python-format
msgid "Remove"
msgstr "移除"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:825
#: code:addons/web_view_editor/static/src/js/view_editor.js:951
#, python-format
msgid "Properties"
msgstr "属性"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:379
#, python-format
msgid "View Editor %d - %s"
msgstr "视图编辑器 %d - %s"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:386
#, python-format
msgid "Do you really wants to create an inherited view here?"
msgstr "您确定要在此处创建继承视图吗?"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:13
#, python-format
msgid "Manage Views"
msgstr "管理视图"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:112
#: code:addons/web_view_editor/static/src/js/view_editor.js:846
#: code:addons/web_view_editor/static/src/js/view_editor.js:974
#, python-format
msgid "Cancel"
msgstr "取消"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:66
#: code:addons/web_view_editor/static/src/js/view_editor.js:413
#, python-format
msgid "Close"
msgstr "关闭"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:516
#, python-format
msgid "Do you really want to remove this node?"
msgstr "您确认要删除这个节点吗?"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:183
#, python-format
msgid "Do you really want to remove this view?"
msgstr "您确认要移除此视图吗?"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:63
#, python-format
msgid "Create"
msgstr "创建"
#. module: web_view_editor
#. openerp-web
#: code:addons/web_view_editor/static/src/js/view_editor.js:88
#, python-format
msgid "Create a view (%s)"
msgstr "创建视图 (%s)"