Merged Latest.

bzr revid: tta@openerp.com-20121206111229-q2fn8zfp71ntclxv
This commit is contained in:
Tejas Tank 2012-12-06 16:12:29 +05:00
commit 3464960518
263 changed files with 119190 additions and 82680 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

@ -28,9 +28,9 @@ except ImportError:
xlwt = None
import openerp
from openerp.tools.translate import _
from .. import http
from .. import nonliterals
openerpweb = http
#----------------------------------------------------------
@ -361,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)
@ -473,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:
@ -821,7 +763,7 @@ class Database(openerpweb.Controller):
except xmlrpclib.Fault, e:
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
return {'error': e.faultCode, 'title': 'Drop Database'}
return {'error': 'Could not drop database !', 'title': 'Drop Database'}
return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
@openerpweb.httprequest
def backup(self, req, backup_db, backup_pwd, token):
@ -839,7 +781,7 @@ class Database(openerpweb.Controller):
{'fileToken': int(token)}
)
except xmlrpclib.Fault, e:
return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
@openerpweb.httprequest
def restore(self, req, db_file, restore_pwd, new_db):
@ -860,8 +802,8 @@ class Database(openerpweb.Controller):
return req.session.proxy("db").change_admin_password(old_password, new_password)
except xmlrpclib.Fault, e:
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
return {'error': e.faultCode, 'title': 'Change Password'}
return {'error': 'Error, password not changed !', 'title': 'Change Password'}
return {'error': e.faultCode, 'title': _('Change Password')}
return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
class Session(openerpweb.Controller):
_cp_path = "/web/session"
@ -897,87 +839,34 @@ class Session(openerpweb.Controller):
old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
dict(map(operator.itemgetter('name', 'value'), fields)))
if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
return {'error':'You cannot leave any password empty.','title': 'Change Password'}
return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
if new_password != confirm_password:
return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
try:
if req.session.model('res.users').change_password(
old_password, new_password):
return {'new_password':new_password}
except Exception:
return {'error': 'The old password you provided is incorrect, your password was not changed.', 'title': 'Change Password'}
return {'error': 'Error, password not changed !', 'title': 'Change Password'}
return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
@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):
try:
return req.session.proxy("db").list_lang() or []
except Exception, e:
return {"error": e, "title": "Languages"}
return {"error": e, "title": _("Languages")}
@openerpweb.jsonrequest
def modules(self, req):
# 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):
"""
@ -1047,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).
@ -1068,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]
@ -1134,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']:
@ -1149,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,
@ -1160,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]:
@ -1203,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):
@ -1243,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
@ -1282,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):
@ -1304,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
@ -1318,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):
@ -1349,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)
@ -1413,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"
@ -1464,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()
@ -1475,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])
@ -1532,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()
@ -1558,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:
@ -1571,7 +1303,7 @@ class Binary(openerpweb.Controller):
res = Model.default_get(fields, context)
filecontent = base64.b64decode(res.get(field, ''))
if not filecontent:
raise ValueError("No content found for field '%s' on '%s:%s'" %
raise ValueError(_("No content found for field '%s' on '%s:%s'") %
(field, model, id))
else:
filename = '%s_%s' % (model.replace('.', '_'), id)
@ -1599,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">
@ -1612,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
@ -1625,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:
@ -1641,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
@ -1682,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
@ -1834,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
@ -1944,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

@ -32,11 +32,15 @@ information:
The major difference is in the lifecycle of these:
* if the client action maps to a function, the function will simply be
called when executing the action. The function can have no further
* if the client action maps to a function, the function will be called
when executing the action. The function can have no further
interaction with the Web Client itself, although it can return an
action which will be executed after it.
The function takes 2 parameters: the ActionManager calling it and
the descriptor for the current action (the ``ir.actions.client``
dictionary).
* if, on the other hand, the client action maps to a
:js:class:`~openerp.web.Widget`, that
:js:class:`~openerp.web.Widget` will be instantiated and added to
@ -51,7 +55,7 @@ object::
// Registers the object 'openerp.web_dashboard.Widget' to the client
// action tag 'board.home.widgets'
instance.web.client_actions.add(
'board.home.widgets', 'openerp.web_dashboard.Widget');
'board.home.widgets', 'instance.web_dashboard.Widget');
instance.web_dashboard.Widget = instance.web.Widget.extend({
template: 'HomeWidget'
});
@ -60,15 +64,15 @@ At this point, the generic :js:class:`~openerp.web.Widget` lifecycle
takes over, the template is rendered, inserted in the client DOM,
bound on the object's ``$el`` property and the object is started.
If the client action takes parameters, these parameters are passed in as a
second positional parameter to the constructor::
The second parameter to the constructor is the descriptor for the
action itself, which contains any parameter provided::
init: function (parent, params) {
init: function (parent, action) {
// execute the Widget's init
this._super(parent);
// board.home.widgets only takes a single param, the identifier of the
// res.widget object it should display. Store it for later
this.widget_id = params.widget_id;
this.widget_id = action.params.widget_id;
}
More complex initialization (DOM manipulations, RPC requests, ...)
@ -82,9 +86,6 @@ method.
code it should return a ``$.Deferred`` so callers know when it's
ready for interaction.
Although generally speaking client actions are not really
interacted with.
.. code-block:: javascript
start: function () {
@ -93,7 +94,7 @@ method.
// Simply read the res.widget object this action should display
new instance.web.Model('res.widget').call(
'read', [[this.widget_id], ['title']])
.then(this.proxy('on_widget_loaded'));
.then(this.proxy('on_widget_loaded'));
}
The client action can then behave exactly as it wishes to within its

View File

@ -11,14 +11,14 @@ Contents:
.. toctree::
:maxdepth: 1
presentation
async
module
testing
widget
qweb
async
rpc
qweb
client_action
form_view
search_view

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(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2572
addons/web/i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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):
@ -192,79 +188,4 @@ class OpenERPSession(object):
context['lang'] = lang
@property
def base_eval_context(self):
""" Default evaluation context for the session.
Used to evaluate contexts and domains.
"""
base = dict(
uid=self._uid,
current_date=datetime.date.today().strftime('%Y-%m-%d'),
time=time,
datetime=datetime,
relativedelta=dateutil.relativedelta.relativedelta
)
base.update(self.context)
return base
def evaluation_context(self, context=None):
""" Returns the session's evaluation context, augmented with the
provided context if any.
:param dict context: to add merge in the session's base eval context
:returns: the augmented context
:rtype: dict
"""
d = dict(self.base_eval_context)
if context:
d.update(context)
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: 142c22b230636674a0cee6bc29e6975f0f1600a5
branch: default
latesttag: 0.5
latesttagdistance: 11
latesttag: 0.7
latesttagdistance: 9

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,248 @@
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
.. function:: py.PY_getItem(o, key)
Returns the element of ``o`` corresponding to the object
``key``. This is equivalent to ``o[key]``.
:param o: :class:`py.object`
:param key: :class:`py.object`
:returns: :class:`py.object`
:raises: ``TypeError`` if ``o`` does not support the operation, if
``key`` or the return value is not a :class:`py.object`
.. function:: py.PY_setItem(o, key, v)
Maps the object ``key`` to the value ``v`` in ``o``. Equivalent to
``o[key] = v``.
:param o: :class:`py.object`
:param key: :class:`py.object`
:param v: :class:`py.object`
:raises: ``TypeError`` if ``o`` does not support the operation, or
if ``key`` or ``v`` are not :class:`py.object`
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

File diff suppressed because it is too large Load Diff

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

@ -652,7 +652,7 @@
cursor: pointer;
}
.openerp .oe_dropdown_toggle {
color: #4C4C4C;
color: #4c4c4c;
font-weight: normal;
}
.openerp .oe_dropdown_hover:hover .oe_dropdown_menu, .openerp .oe_dropdown_menu.oe_opened {
@ -795,6 +795,20 @@
.openerp .oe_notification {
z-index: 1050;
}
.openerp .oe_webclient_timezone_notification a {
color: white;
text-decoration: underline;
}
.openerp .oe_webclient_timezone_notification p {
margin-top: 1em;
}
.openerp .oe_webclient_timezone_notification dt {
font-weight: bold;
}
.openerp .oe_timezone_systray span {
margin-top: 1px;
background-color: #f6cf3b;
}
.openerp .oe_login {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=);
text-align: center;
@ -2249,6 +2263,7 @@
.openerp .oe_form .oe_form_field_url button img {
vertical-align: top;
}
.openerp .oe_form .oe_form_field_monetary,
.openerp .oe_form .oe_form_field_date,
.openerp .oe_form .oe_form_field_datetime {
white-space: nowrap;
@ -2348,7 +2363,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 +2391,7 @@
width: 100%;
left: 2px;
top: 7px;
overflow: hidden;
}
.openerp .oe_fileupload .oe_add button {
display: inline;
@ -2402,10 +2418,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

@ -671,9 +671,21 @@ $sheet-padding: 16px
border-bottom-right-radius: 8px
border-bottom-left-radius: 8px
// }}}
// Notification {{{
// Notifications {{{
.oe_notification
z-index: 1050
.oe_webclient_timezone_notification
a
color: white
text-decoration: underline
p
margin-top: 1em
dt
font-weight: bold
.oe_timezone_systray
span
margin-top: 1px
background-color: #f6cf3b
// }}}
// Login {{{
.oe_login
@ -1788,6 +1800,7 @@ $sheet-padding: 16px
border-left: 8px solid #eee
.oe_form_field_url button img
vertical-align: top
.oe_form_field_monetary,
.oe_form_field_date,
.oe_form_field_datetime
white-space: nowrap
@ -1880,7 +1893,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 +1913,7 @@ $sheet-padding: 16px
width: 100%
left: +2px
top: +7px
overflow: hidden
button
display: inline
height: 24px
@ -1922,10 +1936,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

@ -22,7 +22,7 @@
* @param {Array|String} modules list of modules to initialize
*/
init: function(modules) {
if (modules === "fuck your shit, don't load anything you cunt") {
if (modules === null) {
modules = [];
} else {
modules = _.union(['web'], modules || []);
@ -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

@ -24,7 +24,7 @@ instance.web.Notification = instance.web.Widget.extend({
if (sticky) {
opts.expires = false;
}
this.$el.notify('create', {
return this.$el.notify('create', {
title: title,
text: text
}, opts);
@ -35,7 +35,7 @@ instance.web.Notification = instance.web.Widget.extend({
if (sticky) {
opts.expires = false;
}
this.$el.notify('create', 'oe_notification_alert', {
return this.$el.notify('create', 'oe_notification_alert', {
title: title,
text: text
}, opts);
@ -289,7 +289,7 @@ instance.web.CrashManager = instance.web.Class.extend({
});
instance.web.Loading = instance.web.Widget.extend({
template: 'Loading',
template: _t("Loading"),
init: function(parent) {
this._super(parent);
this.count = 0;
@ -390,11 +390,11 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
self.$el.find("form[name=change_pwd_form]").validate({
messages: {
old_pwd: "Please enter your previous password",
new_pwd: "Please enter your new password",
old_pwd: _t("Please enter your previous password"),
new_pwd: _t("Please enter your new password"),
confirm_pwd: {
required: "Please confirm your new password",
equalTo: "The confirmation does not match the password"
required: _t("Please confirm your new password"),
equalTo: _t("The confirmation does not match the password")
}
},
submitHandler: self.do_change_password
@ -478,7 +478,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.display_error(result);
return;
}
self.do_notify("Duplicating database", "The database has been duplicated.");
self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
self.start();
});
},
@ -488,7 +488,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
fields = $form.serializeArray(),
$db_list = $form.find('[name=drop_db]'),
db = $db_list.val();
if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) {
if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
return;
}
self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
@ -496,7 +496,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.display_error(result);
return;
}
self.do_notify("Dropping database", "The database '" + db + "' has been dropped");
self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
self.start();
});
},
@ -511,7 +511,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
error: function(error){
if(error){
self.display_error({
title: 'Backup Database',
title: _t("Backup Database"),
error: 'AccessDenied'
});
}
@ -534,13 +534,13 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
if (body.indexOf('403 Forbidden') !== -1) {
self.display_error({
title: 'Access Denied',
error: 'Incorrect super-administrator password'
title: _t("Access Denied"),
error: _t("Incorrect super-administrator password")
});
} else {
self.display_error({
title: 'Restore Database',
error: 'Could not restore the database'
title: _t("Restore Database"),
error: _t("Could not restore the database")
});
}
},
@ -560,7 +560,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
return;
}
self.unblockUI();
self.do_notify("Changed Password", "Password has been changed successfully");
self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
});
},
do_exit: function () {
@ -642,7 +642,7 @@ instance.web.Login = instance.web.Widget.extend({
}
var db = this.$("form [name=db]").val();
if (!db) {
this.do_warn("Login", "No database selected !");
this.do_warn(_t("Login"), _t("No database selected !"));
return false;
}
var login = this.$("form input[name=login]").val();
@ -678,7 +678,7 @@ instance.web.Login = instance.web.Widget.extend({
self.trigger('login_successful');
}, function () {
self.$(".oe_login_pane").fadeIn("fast", function() {
self.show_error("Invalid username or password");
self.show_error(_t("Invalid username or password"));
});
});
},
@ -765,7 +765,7 @@ instance.web.ChangePassword = instance.web.Widget.extend({
template: "ChangePassword",
start: function() {
var self = this;
this.getParent().dialog_title = "Change Password";
this.getParent().dialog_title = _t("Change Password");
var $button = self.$el.find('.oe_form_button');
$button.appendTo(this.getParent().$buttons);
$button.eq(2).click(function(){
@ -822,7 +822,7 @@ instance.web.Menu = instance.web.Widget.extend({
this.renderElement();
this.limit_entries();
// Hide toplevel item if there is only one
var $toplevel = this.$("li")
var $toplevel = this.$("li");
if($toplevel.length == 1) {
$toplevel.hide();
}
@ -1106,7 +1106,6 @@ instance.web.WebClient = instance.web.Client.extend({
start: function() {
var self = this;
return $.when(this._super()).then(function() {
self.$(".oe_logo").attr("href", $.param.fragment("" + window.location, "", 2).slice(0, -1));
if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
$("body").addClass("kitten-mode-activated");
if ($.blockUI) {
@ -1163,6 +1162,36 @@ instance.web.WebClient = instance.web.Client.extend({
self.user_menu.do_update();
self.bind_hashchange();
self.set_title();
self.check_timezone();
},
check_timezone: function() {
var self = this;
return new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']]).then(function(result) {
var user_offset = result[0]['tz_offset'];
var offset = -(new Date().getTimezoneOffset());
// _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
var browser_offset = (offset < 0) ? "-" : "+";
browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
if (browser_offset !== user_offset) {
var $icon = $(QWeb.render('WebClient.timezone_systray'));
$icon.on('click', function() {
var notification = self.do_warn(_t("Timezone mismatch"), QWeb.render('WebClient.timezone_notification', {
user_timezone: instance.session.user_context.tz || 'UTC',
user_offset: user_offset,
browser_offset: browser_offset,
}), true);
notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
notification.close();
}).find('a').on('click', function() {
notification.close();
self.user_menu.on_menu_settings();
return false;
});
});
$icon.appendTo(self.$('.oe_systray'));
}
});
},
destroy_content: function() {
_.each(_.clone(this.getChildren()), function(el) {
@ -1179,11 +1208,11 @@ instance.web.WebClient = instance.web.Client.extend({
},
do_notify: function() {
var n = this.notification;
n.notify.apply(n, arguments);
return n.notify.apply(n, arguments);
},
do_warn: function() {
var n = this.notification;
n.warn.apply(n, arguments);
return n.warn.apply(n, arguments);
},
on_logout: function() {
var self = this;
@ -1240,11 +1269,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.
*
@ -1259,6 +969,9 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
if (_.isString(url)) {
url = { url: url };
}
_.defaults(params, {
context: this.user_context || {}
});
// Construct a JSON-RPC2 request, method is currently unused
if (this.debug)
params.debug = 1;
@ -1271,22 +984,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

@ -622,7 +622,7 @@ var messages_by_seconds = function() {
[120, _t("Don't leave yet,<br />it's still loading...")],
[300, _t("You may not believe it,<br />but the application is actually loading...")],
[420, _t("Take a minute to get a coffee,<br />because it's loading...")],
[3600, _t("Maybe you should consider reloading the application by pressing F5...")],
[3600, _t("Maybe you should consider reloading the application by pressing F5...")]
];
};

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

@ -1,5 +1,6 @@
openerp.web.dates = function(instance) {
var _t = instance.web._t;
/**
* Converts a string to a Date javascript object using OpenERP's
@ -18,11 +19,11 @@ instance.web.str_to_datetime = function(str) {
var regex = /^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)(?:\.\d+)?$/;
var res = regex.exec(str);
if ( !res ) {
throw new Error("'" + str + "' is not a valid datetime");
throw new Error(_.str.sprintf(_t("'%s' is not a valid datetime"), str));
}
var obj = Date.parseExact(res[1] + " UTC", 'yyyy-MM-dd HH:mm:ss zzz');
if (! obj) {
throw new Error("'" + str + "' is not a valid datetime");
throw new Error(_.str.sprintf(_t("'%s' is not a valid datetime"), str));
}
return obj;
};
@ -45,11 +46,11 @@ instance.web.str_to_date = function(str) {
var regex = /^\d\d\d\d-\d\d-\d\d$/;
var res = regex.exec(str);
if ( !res ) {
throw new Error("'" + str + "' is not a valid date");
throw new Error(_.str.sprintf(_t("'%s' is not a valid date"), str));
}
var obj = Date.parseExact(str, 'yyyy-MM-dd');
if (! obj) {
throw new Error("'" + str + "' is not a valid date");
throw new Error(_.str.sprintf(_t("'%s' is not a valid date"), str));
}
return obj;
};
@ -72,11 +73,11 @@ instance.web.str_to_time = function(str) {
var regex = /^(\d\d:\d\d:\d\d)(?:\.\d+)?$/;
var res = regex.exec(str);
if ( !res ) {
throw new Error("'" + str + "' is not a valid time");
throw new Error(_.str.sprintf(_t("'%s' is not a valid time"), str));
}
var obj = Date.parseExact("1970-01-01 " + res[1], 'yyyy-MM-dd HH:mm:ss');
if (! obj) {
throw new Error("'" + str + "' is not a valid time");
throw new Error(_.str.sprintf(_t("'%s' is not a valid time"), str));
}
return obj;
};

View File

@ -224,7 +224,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
} while(tmp !== value);
tmp = Number(value);
if (isNaN(tmp))
throw new Error(value + " is not a correct integer");
throw new Error(_.str.sprintf(_t("'%s' is not a correct integer"), value));
return tmp;
case 'float':
var tmp = Number(value);
@ -239,7 +239,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
var reformatted_value = tmp.replace(instance.web._t.database.parameters.decimal_point, ".");
var parsed = Number(reformatted_value);
if (isNaN(parsed))
throw new Error(value + " is not a correct float");
throw new Error(_.str.sprintf(_t("'%s' is not a correct float"), value));
return parsed;
case 'float_time':
var factor = 1;
@ -263,7 +263,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
datetime = Date.parse(value);
if (datetime !== null)
return instance.web.datetime_to_str(datetime);
throw new Error(value + " is not a valid datetime");
throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
case 'date':
var date = Date.parseExact(value, date_pattern);
if (date !== null)
@ -271,7 +271,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
date = Date.parse(value);
if (date !== null)
return instance.web.date_to_str(date);
throw new Error(value + " is not a valid date");
throw new Error(_.str.sprintf(_t("'%s' is not a correct date"), value));
case 'time':
var time = Date.parseExact(value, time_pattern);
if (time !== null)
@ -279,7 +279,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
time = Date.parse(value);
if (time !== null)
return instance.web.time_to_str(time);
throw new Error(value + " is not a valid time");
throw new Error(_.str.sprintf(_t("'%s' is not a correct time"), value));
}
return value;
};
@ -294,7 +294,7 @@ instance.web.auto_str_to_date = function(value, type) {
try {
return instance.web.str_to_time(value);
} catch(e) {}
throw new Error("'" + value + "' is not a valid date, datetime nor time");
throw new Error(_.str.sprintf(_t("'%s' is not a correct date, datetime nor time"), value));
};
instance.web.auto_date_to_str = function(value, type) {
@ -306,7 +306,7 @@ instance.web.auto_date_to_str = function(value, type) {
case 'time':
return instance.web.time_to_str(value);
default:
throw new Error(type + " is not convertible to date, datetime nor time");
throw new Error(_.str.sprintf(_t("'%s' is not convertible to date, datetime nor time"), type));
}
};

View File

@ -0,0 +1,776 @@
/*
* 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.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
d.getUTCMilliseconds() * 1000]);
}),
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);
}));
},
__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])
}
});
/**
Returns the current local date, which means the date on the client (which can be different
compared to the date of the server).
@return {datetime.date}
*/
var context_today = function() {
var d = new Date();
return py.PY_call(
datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
};
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,
context_today: context_today,
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(
@ -1006,6 +1009,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
get_context: function (facet) {
var contexts = facet.values.chain()
.map(function (f) { return f.get('value').attrs.context; })
.without('{}')
.reject(_.isEmpty)
.value();
@ -1024,6 +1028,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
get_groupby: function (facet) {
return facet.values.chain()
.map(function (f) { return f.get('value').attrs.context; })
.without('{}')
.reject(_.isEmpty)
.value();
},
@ -1036,6 +1041,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
get_domain: function (facet) {
var domains = facet.values.chain()
.map(function (f) { return f.get('value').attrs.domain; })
.without('[]')
.reject(_.isEmpty)
.value();
@ -1506,16 +1512,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 +1614,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 +1708,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: [],
@ -262,7 +263,7 @@ openerp.testing = {};
++di;
}
instance = openerp.init("fuck your shit, don't load anything you cunt");
instance = openerp.init(null);
_(d).chain()
.reverse()
.uniq()
@ -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

@ -148,7 +148,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
load_form: function(data) {
var self = this;
if (!data) {
throw new Error("No data provided.");
throw new Error(_t("No data provided."));
}
if (this.arch) {
throw "Form view does not support multiple calls to load_form";
@ -316,12 +316,12 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var self = this, set_values = [];
if (!record) {
this.set({ 'title' : undefined });
this.do_warn("Form", "The record could not be found in the database.", true);
this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
return $.Deferred().reject();
}
this.datarecord = record;
this._actualize_mode();
this.set({ 'title' : record.id ? record.display_name : "New" });
this.set({ 'title' : record.id ? record.display_name : _t("New") });
_(this.fields).each(function (field, f) {
field._dirty_flag = false;
@ -431,31 +431,31 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var onchange = _.str.trim(on_change);
var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
if (!call) {
throw new Error("Wrong on change format: " + onchange);
throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
}
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;
}
@ -872,7 +866,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}).value();
warnings.unshift('<ul>');
warnings.push('</ul>');
this.do_warn("The following fields are invalid :", warnings.join(''));
this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
},
/**
* Reload the form after saving
@ -1033,7 +1027,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|| field.get("readonly")
|| field.field.type === 'one2many'
|| field.field.type === 'many2many'
|| field.field.type === 'binary') {
|| field.field.type === 'binary'
|| field.password) {
return false;
}
@ -1240,11 +1235,11 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
_.each(this.fields_to_init, function($elem) {
var name = $elem.attr("name");
if (!self.fvg.fields[name]) {
throw new Error("Field '" + name + "' specified in view could not be found.");
throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
}
var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
if (!obj) {
throw new Error("Widget type '"+ $elem.attr('widget') + "' is not implemented");
throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
}
var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
var $label = self.labels[$elem.attr("name")];
@ -1681,11 +1676,11 @@ instance.web.form.compute_domain = function(expr, fields) {
switch (op.toLowerCase()) {
case '=':
case '==':
stack.push(field_value == val);
stack.push(_.isEqual(field_value, val));
break;
case '!=':
case '<>':
stack.push(field_value != val);
stack.push(!_.isEqual(field_value, val));
break;
case '<':
stack.push(field_value < val);
@ -2318,7 +2313,7 @@ instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
},
on_button_clicked: function() {
if (!this.get('value') || !this.is_syntax_valid()) {
this.do_warn("E-mail error", "Can't send email to invalid e-mail address");
this.do_warn(_t("E-mail error"), _t("Can't send email to invalid e-mail address"));
} else {
location.href = 'mailto:' + this.get('value');
}
@ -2347,7 +2342,7 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
},
on_button_clicked: function() {
if (!this.get('value')) {
this.do_warn("Resource error", "This resource is empty");
this.do_warn(_t("Resource error"), _t("This resource is empty"));
} else {
var url = $.trim(this.get('value'));
if(/^www\./i.test(url))
@ -3384,7 +3379,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
var views = [];
_.each(modes, function(mode) {
if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
throw new Error(_.str.sprintf("View type '%s' is not supported in One2Many.", mode));
throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
}
var view = {
view_id: false,
@ -4074,13 +4069,12 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
If you see this options, do not use it, it's basically a dirty hack to make one
precise o2m to behave the way we want.
*/
instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
multi_selection: false,
disable_utility_classes: true,
init: function(field_manager, node) {
this._super(field_manager, node);
this.is_loaded = $.Deferred();
this.initial_is_loaded = this.is_loaded;
this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
this.dataset.m2m = this;
var self = this;
@ -4088,24 +4082,44 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
self.dataset_changed();
});
this.set_value([]);
this.list_dm = new instance.web.DropMisordered();
this.render_value_dm = new instance.web.DropMisordered();
},
start: function() {
this.$el.addClass('oe_form_field oe_form_field_many2many');
initialize_content: function() {
var self = this;
self.load_view();
this.is_loaded.done(function() {
self.on("change:effective_readonly", self, function() {
self.is_loaded = self.is_loaded.then(function() {
self.list_view.destroy();
return $.when(self.load_view()).done(function() {
self.render_value();
});
});
this.$el.addClass('oe_form_field oe_form_field_many2many');
this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
'addable': this.get("effective_readonly") ? null : _t("Add"),
'deletable': this.get("effective_readonly") ? false : true,
'selectable': this.multi_selection,
'sortable': false,
'reorderable': false,
'import_enabled': false,
});
var embedded = (this.field.views || {}).tree;
if (embedded) {
this.list_view.set_embedded_view(embedded);
}
this.list_view.m2m_field = this;
var loaded = $.Deferred();
this.list_view.on("list_view_loaded", this, function() {
loaded.resolve();
});
this._super.apply(this, arguments);
this.list_view.appendTo(this.$el);
var old_def = self.is_loaded;
self.is_loaded = $.Deferred().done(function() {
old_def.resolve();
});
this.list_dm.add(loaded).then(function() {
self.is_loaded.resolve();
});
},
destroy_content: function() {
this.list_view.destroy();
this.list_view = undefined;
},
set_value: function(value_) {
value_ = value_ || [];
@ -4120,35 +4134,10 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
is_false: function () {
return _(this.get("value")).isEmpty();
},
load_view: function() {
var self = this;
this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
'addable': self.get("effective_readonly") ? null : _t("Add"),
'deletable': self.get("effective_readonly") ? false : true,
'selectable': self.multi_selection,
'sortable': false,
'reorderable': false,
'import_enabled': false,
});
var embedded = (this.field.views || {}).tree;
if (embedded) {
this.list_view.set_embedded_view(embedded);
}
this.list_view.m2m_field = this;
var loaded = $.Deferred();
this.list_view.on("list_view_loaded", self, function() {
self.initial_is_loaded.resolve();
loaded.resolve();
});
$.async_when().done(function () {
self.list_view.appendTo(self.$el);
});
return loaded;
},
render_value: function() {
var self = this;
this.dataset.set_ids(this.get("value"));
this.is_loaded = this.is_loaded.then(function() {
this.render_value_dm.add(this.is_loaded).then(function() {
return self.list_view.reload_content();
});
},
@ -4590,7 +4579,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) {
@ -4663,7 +4652,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 || []
@ -4839,7 +4828,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
},
on_file_uploaded: function(size, name, content_type, file_base64) {
if (size === false) {
this.do_warn("File Upload", "There was a problem while uploading your file");
this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
// TODO: use openerp web crashmanager
console.warn("Error while uploading file : ", name);
} else {
@ -5008,7 +4997,7 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
this.field_manager = field_manager;
this.node = node;
if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
throw "The type of the field '"+this.field.string+"' must be a many2many field with a relation to 'ir.attachment' model.";
throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
}
this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
@ -5271,6 +5260,7 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
template: "FieldMonetary",
widget_class: 'oe_form_field_float oe_form_field_monetary',
init: function() {
this._super.apply(this, arguments);
this.set({"currency": false});

View File

@ -389,7 +389,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
if (range_stop > total) {
range_stop = total;
}
spager = _.str.sprintf('%d-%d of %d', range_start, range_stop, total);
spager = _.str.sprintf(_t("%d-%d of %d"), range_start, range_stop, total);
}
this.$pager.find('.oe_list_pager_state').text(spager);
@ -600,6 +600,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
this.dataset.index = _(this.dataset.ids).indexOf(ids[0]);
if (this.sidebar) {
this.options.$sidebar.show();
this.sidebar.$el.show();
}
@ -887,8 +888,8 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
var $row;
if (attribute === 'id') {
if (old_value) {
throw new Error("Setting 'id' attribute on existing record "
+ JSON.stringify(record.attributes));
throw new Error(_.str.sprintf( _t("Setting 'id' attribute on existing record %s"),
JSON.stringify(record.attributes) ));
}
if (!_.contains(self.dataset.ids, value)) {
// add record to dataset if not already in (added by
@ -922,6 +923,18 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
}, this);
this.$current = $('<tbody>')
.delegate('input[readonly=readonly]', 'click', function (e) {
/*
Against all logic and sense, as of right now @readonly
apparently does nothing on checkbox and radio inputs, so
the trick of using @readonly to have, well, readonly
checkboxes (which still let clicks go through) does not
work out of the box. We *still* need to preventDefault()
on the event, otherwise the checkbox's state *will* toggle
on click
*/
e.preventDefault();
})
.delegate('th.oe_list_record_selector', 'click', function (e) {
e.stopPropagation();
var selection = self.get_selection();
@ -956,7 +969,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
if (row_id) {
e.stopPropagation();
if (!self.dataset.select_id(row_id)) {
throw new Error("Could not find id in dataset");
throw new Error(_t("Could not find id in dataset"));
}
self.row_clicked(e);
}
@ -1016,7 +1029,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
var command = value[0];
// 1. an array of m2m commands (usually (6, false, ids))
if (command[0] !== 6) {
throw new Error(_t("Unknown m2m command ") + command[0]);
throw new Error(_.str.sprintf( _t("Unknown m2m command %s"), command[0]));
}
ids = command[2];
} else {
@ -1324,7 +1337,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
}
// group_label is html-clean (through format or explicit
// escaping if format failed), can inject straight into HTML
$group_column.html(_.str.sprintf("%s (%d)",
$group_column.html(_.str.sprintf(_t("%s (%d)"),
group_label, group.length));
if (group.length && group.openable) {
@ -1743,7 +1756,7 @@ var Record = instance.web.Class.extend(/** @lends Record# */{
} else if (val instanceof Array) {
output[k] = val[0];
} else {
throw new Error("Can't convert value " + val + " to context");
throw new Error(_.str.sprintf(_t("Can't convert value %s to context"), val));
}
}
return output;

View File

@ -3,6 +3,8 @@
* @namespace
*/
openerp.web.list_editable = function (instance) {
var _t = instance.web._t;
// editability status of list rows
instance.web.ListView.prototype.defaults.editable = null;
@ -383,7 +385,8 @@ openerp.web.list_editable = function (instance) {
version: '7.0'
});
_(view.arch.children).chain()
.zip(this.columns)
.zip(_(this.columns).filter(function (c) {
return !(c instanceof instance.web.list.MetaColumn);}))
.each(function (ar) {
var widget = ar[0], column = ar[1];
var modifiers = _.extend({}, column.modifiers);
@ -775,7 +778,7 @@ openerp.web.list_editable = function (instance) {
cancel: function (force) {
if (!(force || this.form.can_be_discarded())) {
return $.Deferred().reject({
message: "The form's data can not be discarded"}).promise();
message: _t("The form's data can not be discarded")}).promise();
}
var record = this.record;
this.record = null;

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, action.context || {});
}
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 || []
@ -737,12 +758,6 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
dataset.index = 0;
}
this.dataset = dataset;
// setup storage for session-wise menu hiding
if (this.session.hidden_menutips) {
return;
}
this.session.hidden_menutips = {};
},
/**
* Initializes the ViewManagerAction: sets up the searchview (if the
@ -786,7 +801,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
break;
case 'tests':
this.do_action({
name: "JS Tests",
name: _t("JS Tests"),
target: 'new',
type : 'ir.actions.act_url',
url: '/web/tests?mod=*'
@ -814,7 +829,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
break;
case 'translate':
this.do_action({
name: "Technical Translation",
name: _t("Technical Translation"),
res_model : 'ir.translation',
domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
views: [[false, 'list'], [false, 'form']],
@ -880,7 +895,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 +1088,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 +1159,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 +1203,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 +1248,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 +1259,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 +1284,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);
}
},
/**
@ -1395,7 +1412,7 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
return sindent + node;
} else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
throw new Error(
_.str.sprintf("Node [%s] is not a JSONified XML node",
_.str.sprintf(_t("Node [%s] is not a JSONified XML node"),
JSON.stringify(node)));
}
for (var attr in node.attrs) {
@ -1429,7 +1446,7 @@ instance.web.xml_to_str = function(node) {
} else if (window.ActiveXObject) {
return node.xml;
} else {
throw new Error("Could not serialize XML");
throw new Error(_t("Could not serialize XML"));
}
};
instance.web.str_to_xml = function(s) {
@ -1437,7 +1454,7 @@ instance.web.str_to_xml = function(s) {
var dp = new DOMParser();
var r = dp.parseFromString(s, "text/xml");
if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
throw new Error("Could not parse string to xml");
throw new Error(_t("Could not parse string to xml"));
}
return r;
}
@ -1445,7 +1462,7 @@ instance.web.str_to_xml = function(s) {
try {
xDoc = new ActiveXObject("MSXML2.DOMDocument");
} catch (e) {
throw new Error("Could not find a DOM Parser: " + e.message);
throw new Error(_.str.sprintf( _t("Could not find a DOM Parser: %s"), e.message));
}
xDoc.async = false;
xDoc.preserveWhiteSpace = true;

View File

@ -436,7 +436,7 @@
</tr>
<tr>
<td class="oe_leftbar" valign="top">
<a class="oe_logo" href="#"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
<a class="oe_logo" t-attf-href="/?ts=#{Date.now()}"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
<div class="oe_secondary_menus_container"/>
<div class="oe_footer">
Powered by <a href="http://www.openerp.com" target="_blank"><span>OpenERP</span></a>
@ -448,6 +448,25 @@
</table>
</div>
</t>
<t t-name="WebClient.timezone_notification">
<div class="oe_webclient_timezone_notification">
<p>Your user's preference timezone does not match your browser timezone:</p>
<dl>
<dt>User's timezone</dt>
<dd><t t-esc="user_timezone"/> (<t t-esc="user_offset"/>)</dd>
<dt>Browser's timezone</dt>
<dd><t t-esc="browser_offset"/></dd>
</dl>
<p><a href="#">Click here to change your user's timezone.</a></p>
</div>
</t>
<t t-name="WebClient.timezone_systray">
<div class="oe_topbar_item oe_timezone_systray" title="Timezone mismatch">
<span class="ui-icon ui-state-error ui-icon-alert"/>
</div>
</t>
<t t-name="EmbedClient">
<div class="openerp">
@ -915,11 +934,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,573 @@
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.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()));
});
test('context_today', function (instance) {
var d = new Date();
var context = instance.web.pyeval.context();
strictEqual(
py.eval("context_today().strftime('%Y-%m-%d')", context),
String(_.str.sprintf('%04d-%02d-%02d', d.getFullYear(), d.getMonth() + 1, d.getDate())));
});
// 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','>',context_today().strftime('%Y-%m-%d'))]";
return instance.edc([d]).then(function (result) {
var d = new Date();
var today = _.str.sprintf("%04d-%02d-%02d",
d.getFullYear(), d.getMonth() + 1, d.getDate());
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','>',(context_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.getFullYear(), d.getMonth() + 1, d.getDate());
d.setDate(d.getDate() - 15);
var ago_15_d = _.str.sprintf("%04d-%02d-%02d",
d.getFullYear(), d.getMonth() + 1, d.getDate());
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 +611,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 +689,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 {}; }};
@ -845,6 +845,42 @@ openerp.testing.section('search-serialization', {
ok(!context.get_eval_context(), "context should have no evaluation context");
});
});
test('Empty filter domains', {asserts: 4}, function (instance) {
var view = {inputs: [], query: {on: function () {}}};
var filter_a = new instance.web.search.Filter(
{attrs: {name: 'a', context: '{}', domain: '[]'}}, view);
var filter_b = new instance.web.search.Filter(
{attrs: {name: 'b', context: '{}', domain: '[]'}}, view);
var filter_c = new instance.web.search.Filter(
{attrs: {name: 'c', context: '{b: 42}', domain: '[["a", "=", 3]]'}}, view);
var group = new instance.web.search.FilterGroup(
[filter_a, filter_b, filter_c], view);
var t1 = group.facet_for_defaults({a: true, c: true})
.done(function (facet) {
var model = facet;
if (!(model instanceof instance.web.search.Facet)) {
model = new instance.web.search.Facet(facet);
}
var domain = group.get_domain(model);
deepEqual(domain, '[["a", "=", 3]]', "domain should ignore empties");
var context = group.get_context(model);
deepEqual(context, '{b: 42}', "context should ignore empties");
});
var t2 = group.facet_for_defaults({a: true, b: true})
.done(function (facet) {
var model = facet;
if (!(model instanceof instance.web.search.Facet)) {
model = new instance.web.search.Facet(facet);
}
var domain = group.get_domain(model);
equal(domain, null, "domain should ignore empties");
var context = group.get_context(model);
equal(context, null, "context should ignore empties");
});
return $.when(t1, t2);
});
});
openerp.testing.section('removal', {
dependencies: ['web.search'],
@ -890,9 +926,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 +951,7 @@ openerp.testing.section('filters', {
children: []
}]
}
}};
};
});
}
}, function (test) {
@ -992,7 +1028,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 +1051,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

@ -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

@ -7,15 +7,15 @@ 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"
"POT-Creation-Date: 2012-11-30 18:13+0000\n"
"PO-Revision-Date: 2012-11-24 06:30+0000\n"
"Last-Translator: kifcaliph <Unknown>\n"
"Language-Team: Arabic <ar@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-12-01 05:21+0000\n"
"X-Generator: Launchpad (build 16319)\n"
#. module: web_calendar
#. openerp-web
@ -38,6 +38,13 @@ msgstr "التفاصيل"
msgid "Save"
msgstr "حفظ"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:99
#, python-format
msgid "Calendar view has a 'date_delay' type != float"
msgstr ""
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:147
@ -202,5 +209,12 @@ msgstr "إلغاء"
msgid "Calendar"
msgstr "التقويم"
#. module: web_calendar
#. openerp-web
#: code:addons/web_calendar/static/src/js/calendar.js:91
#, python-format
msgid "Calendar view has not defined 'date_start' attribute."
msgstr ""
#~ msgid "Navigator"
#~ msgstr "المتصفح"

Some files were not shown because too many files have changed in this diff Show More