[MERGE] Merge with lp:openerp-web

bzr revid: hip@tinyerp.com-20121119054316-24eeo5y3011z3z53
This commit is contained in:
Hiral Patel (OpenERP) 2012-11-19 11:13:16 +05:30
commit 786b11f31d
327 changed files with 4781 additions and 2477 deletions

View File

@ -1,19 +1,14 @@
.*.swp .*
.bzrignore *.egg-info
.idea *.orig
.project *.vim
.pydevproject
.ropeproject
.settings
.DS_Store
openerp/addons/*
openerp/filestore*
.Python
*.pyc
*.pyo
bin/*
build/ build/
include/ RE:^bin/
lib/ RE:^dist/
share/ RE:^include/
doc/_build/*
RE:^share/
RE:^man/
RE:^lib/
RE:^addons/\w+/doc/_build/

View File

@ -1,6 +1,7 @@
{ {
'name': 'Web', 'name': 'Web',
'category': 'Hidden', 'category': 'Hidden',
'version': '7.0.1.0',
'description': 'description':
""" """
OpenERP Web core module. OpenERP Web core module.
@ -40,6 +41,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/cleditor/jquery.cleditor.js", "static/lib/cleditor/jquery.cleditor.js",
"static/lib/py.js/lib/py.js", "static/lib/py.js/lib/py.js",
"static/src/js/boot.js", "static/src/js/boot.js",
"static/src/js/testing.js",
"static/src/js/corelib.js", "static/src/js/corelib.js",
"static/src/js/coresetup.js", "static/src/js/coresetup.js",
"static/src/js/dates.js", "static/src/js/dates.js",
@ -67,5 +69,21 @@ This module provides the core of the OpenERP Web Client.
'qweb' : [ 'qweb' : [
"static/src/xml/*.xml", "static/src/xml/*.xml",
], ],
'test': [
"static/test/testing.js",
"static/test/class.js",
"static/test/registry.js",
"static/test/form.js",
"static/test/list-utils.js",
"static/test/formats.js",
"static/test/rpc.js",
"static/test/evals.js",
"static/test/search.js",
"static/test/Widget.js",
"static/test/list.js",
"static/test/list-editable.js",
"static/test/mutex.js"
],
'bootstrap': True, 'bootstrap': True,
'twitter': False,
} }

View File

@ -1 +1,2 @@
from . import main from . import main
from . import testing

View File

@ -258,10 +258,20 @@ def concat_files(file_list, reader=None, intersperse=""):
files_concat = intersperse.join(files_content) files_concat = intersperse.join(files_content)
return files_concat, checksum.hexdigest() return files_concat, checksum.hexdigest()
concat_js_cache = {}
def concat_js(file_list): def concat_js(file_list):
content, checksum = concat_files(file_list, intersperse=';') content, checksum = concat_files(file_list, intersperse=';')
content = rjsmin(content) if checksum in concat_js_cache:
return content, checksum content = concat_js_cache[checksum]
else:
content = rjsmin(content)
concat_js_cache[checksum] = content
return content, checksum
def fs2web(path):
"""convert FS path into web path"""
return '/'.join(path.split(os.path.sep))
def manifest_glob(req, addons, key): def manifest_glob(req, addons, key):
if addons is None: if addons is None:
@ -278,7 +288,7 @@ def manifest_glob(req, addons, key):
globlist = manifest.get(key, []) globlist = manifest.get(key, [])
for pattern in globlist: for pattern in globlist:
for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))): for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
r.append((path, path[len(addons_path):])) r.append((path, fs2web(path[len(addons_path):])))
return r return r
def manifest_list(req, mods, extension): def manifest_list(req, mods, extension):
@ -637,8 +647,7 @@ class WebClient(openerpweb.Controller):
data = fp.read().decode('utf-8') data = fp.read().decode('utf-8')
path = file_map[f] path = file_map[f]
# convert FS path into web path web_dir = os.path.dirname(path)
web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
data = re.sub( data = re.sub(
rx_import, rx_import,
@ -691,12 +700,10 @@ class WebClient(openerpweb.Controller):
until we have established a valid session. This is meant only until we have established a valid session. This is meant only
for translating the login page and db management chrome, using for translating the login page and db management chrome, using
the browser's language. """ the browser's language. """
lang = req.httprequest.accept_languages.best or 'en'
# For performance reasons we only load a single translation, so for # For performance reasons we only load a single translation, so for
# sub-languages (that should only be partially translated) we load the # sub-languages (that should only be partially translated) we load the
# main language PO instead - that should be enough for the login screen. # main language PO instead - that should be enough for the login screen.
if '-' in lang: # RFC2616 uses '-' separators for sublanguages lang = req.lang.split('_')[0]
lang = lang.split('-')[0]
translations_per_module = {} translations_per_module = {}
for addon_name in mods: for addon_name in mods:
@ -770,15 +777,31 @@ class Database(openerpweb.Controller):
@openerpweb.jsonrequest @openerpweb.jsonrequest
def create(self, req, fields): def create(self, req, fields):
params = dict(map(operator.itemgetter('name', 'value'), fields)) params = dict(map(operator.itemgetter('name', 'value'), fields))
create_attrs = ( return req.session.proxy("db").create_database(
params['super_admin_pwd'], params['super_admin_pwd'],
params['db_name'], params['db_name'],
bool(params.get('demo_data')), bool(params.get('demo_data')),
params['db_lang'], params['db_lang'],
params['create_admin_pwd'] params['create_admin_pwd'])
@openerpweb.jsonrequest
def duplicate(self, req, fields):
params = dict(map(operator.itemgetter('name', 'value'), fields))
return req.session.proxy("db").duplicate_database(
params['super_admin_pwd'],
params['db_original_name'],
params['db_name'])
@openerpweb.jsonrequest
def duplicate(self, req, fields):
params = dict(map(operator.itemgetter('name', 'value'), fields))
duplicate_attrs = (
params['super_admin_pwd'],
params['db_original_name'],
params['db_name'],
) )
return req.session.proxy("db").create_database(*create_attrs) return req.session.proxy("db").duplicate_database(*duplicate_attrs)
@openerpweb.jsonrequest @openerpweb.jsonrequest
def drop(self, req, fields): def drop(self, req, fields):
@ -886,10 +909,7 @@ class Session(openerpweb.Controller):
@openerpweb.jsonrequest @openerpweb.jsonrequest
def get_lang_list(self, req): def get_lang_list(self, req):
try: try:
return { return req.session.proxy("db").list_lang() or []
'lang_list': (req.session.proxy("db").list_lang() or []),
'error': ""
}
except Exception, e: except Exception, e:
return {"error": e, "title": "Languages"} return {"error": e, "title": "Languages"}
@ -1455,11 +1475,24 @@ class Binary(openerpweb.Controller):
try: try:
if not id: if not id:
res = Model.default_get([field], context).get(field) res = Model.default_get([field], context).get(field)
image_data = base64.b64decode(res) image_base64 = res
else: else:
res = Model.read([id], [last_update, field], context)[0] res = Model.read([id], [last_update, field], context)[0]
retag = hashlib.md5(res.get(last_update)).hexdigest() retag = hashlib.md5(res.get(last_update)).hexdigest()
image_data = base64.b64decode(res.get(field)) image_base64 = res.get(field)
if kw.get('resize'):
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])
# resize maximum 500*500
if width > 500: width = 500
if height > 500: height = 500
image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
image_data = base64.b64decode(image_base64)
except (TypeError, xmlrpclib.Fault): except (TypeError, xmlrpclib.Fault):
image_data = self.placeholder(req) image_data = self.placeholder(req)
headers.append(('ETag', retag)) headers.append(('ETag', retag))
@ -1949,112 +1982,4 @@ class Reports(View):
('Content-Length', len(report))], ('Content-Length', len(report))],
cookies={'fileToken': int(token)}) cookies={'fileToken': int(token)})
class Import(View):
_cp_path = "/web/import"
def fields_get(self, req, model):
Model = req.session.model(model)
fields = Model.fields_get(False, req.session.eval_context(req.context))
return fields
@openerpweb.httprequest
def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
try:
data = list(csv.reader(
csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
except csv.Error, e:
csvfile.seek(0)
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {
'message': 'Error parsing CSV file: %s' % e,
# decodes each byte to a unicode character, which may or
# may not be printable, but decoding will succeed.
# Otherwise simplejson will try to decode the `str` using
# utf-8, which is very likely to blow up on characters out
# of the ascii range (in range [128, 256))
'preview': csvfile.read(200).decode('iso-8859-1')}}))
try:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps(
{'records': data[:10]}, encoding=csvcode))
except UnicodeDecodeError:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({
'message': u"Failed to decode CSV file using encoding %s, "
u"try switching to a different encoding" % csvcode
}))
@openerpweb.httprequest
def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
meta):
modle_obj = req.session.model(model)
skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
simplejson.loads(meta))
error = None
if not (csvdel and len(csvdel) == 1):
error = u"The CSV delimiter must be a single character"
if not indices and fields:
error = u"You must select at least one field to import"
if error:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message': error}}))
# skip ignored records (@skip parameter)
# then skip empty lines (not valid csv)
# nb: should these operations be reverted?
rows_to_import = itertools.ifilter(
None,
itertools.islice(
csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
skip, None))
# if only one index, itemgetter will return an atom rather than a tuple
if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
else: mapper = operator.itemgetter(*indices)
data = None
error = None
try:
# decode each data row
data = [
[record.decode(csvcode) for record in row]
for row in itertools.imap(mapper, rows_to_import)
# don't insert completely empty rows (can happen due to fields
# filtering in case of e.g. o2m content rows)
if any(row)
]
except UnicodeDecodeError:
error = u"Failed to decode CSV file using encoding %s" % csvcode
except csv.Error, e:
error = u"Could not process CSV file: %s" % e
# If the file contains nothing,
if not data:
error = u"File to import is empty"
if error:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message': error}}))
try:
(code, record, message, _nope) = modle_obj.import_data(
fields, data, 'init', '', False,
req.session.eval_context(req.context))
except xmlrpclib.Fault, e:
error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error':error}))
if code != -1:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'success':True}))
msg = u"Error during import: %s\n\nTrying to import record %r" % (
message, record)
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message':msg}}))
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,162 @@
# coding=utf-8
# -*- encoding: utf-8 -*-
import glob
import itertools
import json
import operator
import os
from mako.template import Template
from openerp.modules import module
from .main import module_topological_sort
from ..http import Controller, httprequest
NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>OpenERP Testing</title>
</head>
<body>
<form action="/web/tests" method="GET">
<button name="mod" value="*">Run all tests</button>
<ul>
% for name, module in modules:
<li>${name} <button name="mod" value="${module}">
Run Tests</button></li>
% endfor
</ul>
</form>
</body>
</html>
""")
NOTFOUND = Template(u"""
<p>Unable to find the module [${module}], please check that the module
name is correct and the module is on OpenERP's path.</p>
<a href="/web/tests">&lt;&lt; Back to tests</a>
""")
TESTING = Template(u"""<!DOCTYPE html>
<html style="height: 100%">
<%def name="to_path(module, p)">/${module}/${p}</%def>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>OpenERP Web Tests</title>
<link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
<link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
<script src="/web/static/lib/qunit/qunit.js"></script>
<script type="text/javascript">
var oe_db_info = ${db_info};
// List of modules, each module is preceded by its dependencies
var oe_all_dependencies = ${dependencies};
QUnit.config.testTimeout = 5 * 60 * 1000;
</script>
</head>
<body id="oe" class="openerp">
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
% for module, jss, tests, templates in files:
% for js in jss:
<script src="${to_path(module, js)}"></script>
% endfor
% if tests or templates:
<script>
openerp.testing.current_module = "${module}";
% for template in templates:
openerp.testing.add_template("${to_path(module, template)}");
% endfor
</script>
% endif
% if tests:
% for test in tests:
<script type="text/javascript" src="${to_path(module, test)}"></script>
% endfor
% endif
% endfor
</html>
""")
class TestRunnerController(Controller):
_cp_path = '/web/tests'
@httprequest
def index(self, req, mod=None, **kwargs):
ms = module.get_modules()
manifests = dict(
(name, desc)
for name, desc in zip(ms, map(self.load_manifest, ms))
if desc # remove not-actually-openerp-modules
)
if not mod:
return NOMODULE_TEMPLATE.render(modules=(
(manifest['name'], name)
for name, manifest in manifests.iteritems()
if any(testfile.endswith('.js')
for testfile in manifest['test'])
))
sorted_mods = module_topological_sort(dict(
(name, manifest.get('depends', []))
for name, manifest in manifests.iteritems()
))
# to_load and to_test should be zippable lists of the same length.
# A falsy value in to_test indicate nothing to test at that index (just
# load the corresponding part of to_load)
to_test = sorted_mods
if mod != '*':
if mod not in manifests:
return req.not_found(NOTFOUND.render(module=mod))
idx = sorted_mods.index(mod)
to_test = [None] * len(sorted_mods)
to_test[idx] = mod
tests_candicates = [
filter(lambda path: path.endswith('.js'),
manifests[mod]['test'] if mod else [])
for mod in to_test]
# remove trailing test-less modules
tests = reversed(list(
itertools.dropwhile(
operator.not_,
reversed(tests_candicates))))
files = [
(mod, manifests[mod]['js'], tests, manifests[mod]['qweb'])
for mod, tests in itertools.izip(sorted_mods, tests)
]
# if all three db_info parameters are present, send them to the page
db_info = dict((k, v) for k, v in kwargs.iteritems()
if k in ['source', 'supadmin', 'password'])
if len(db_info) != 3:
db_info = None
return TESTING.render(files=files, dependencies=json.dumps(
[name for name in sorted_mods
if module.get_module_resource(name, 'static')
if manifests[name]['js']]), db_info=json.dumps(db_info))
def load_manifest(self, name):
manifest = module.load_information_from_description_file(name)
if manifest:
path = module.get_module_path(name)
manifest['js'] = list(
self.expand_patterns(path, manifest.get('js', [])))
manifest['test'] = list(
self.expand_patterns(path, manifest.get('test', [])))
manifest['qweb'] = list(
self.expand_patterns(path, manifest.get('qweb', [])))
return manifest
def expand_patterns(self, root, patterns):
for pattern in patterns:
normalized_pattern = os.path.normpath(os.path.join(root, pattern))
for path in glob.glob(normalized_pattern):
# replace OS path separators (from join & normpath) by URI ones
yield path[len(root):].replace(os.path.sep, '/')

View File

@ -1,9 +1,6 @@
Asynchronous Operations Asynchronous Operations
======================= =======================
This documentation is outdated as then() is now pipe. Please never copy api
from other libraries just link to it.
As a language (and runtime), javascript is fundamentally As a language (and runtime), javascript is fundamentally
single-threaded. This means any blocking request or computation will single-threaded. This means any blocking request or computation will
blocks the whole page (and, in older browsers, the software itself blocks the whole page (and, in older browsers, the software itself
@ -46,8 +43,8 @@ directly to asynchronous methods is the ability to :ref:`compose them
Using deferreds Using deferreds
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
deferreds have only one method of importance: :js:func:`Deferred.then`. This Deferreds's most important method is :js:func:`Deferred.then`. It is
method is used to attach new callbacks to the deferred object. used to attach new callbacks to the deferred object.
* the first parameter attaches a success callback, called when the * the first parameter attaches a success callback, called when the
deferred object is successfully resolved and provided with the deferred object is successfully resolved and provided with the
@ -179,9 +176,9 @@ Deferred chaining
A second useful composition is starting an asynchronous operation as A second useful composition is starting an asynchronous operation as
the result of an other asynchronous operation, and wanting the result the result of an other asynchronous operation, and wanting the result
of both: :js:func:`Deferred.then` returns the deferred on which it was of both: with the tools described so far, handling e.g. OpenERP's
called, so handle e.g. OpenERP's search/read sequence with this would search/read sequence with this would require something along the lines
require something along the lines of: of:
.. code-block:: javascript .. code-block:: javascript
@ -196,21 +193,14 @@ require something along the lines of:
While it doesn't look too bad for trivial code, this quickly gets While it doesn't look too bad for trivial code, this quickly gets
unwieldy. unwieldy.
Instead, jQuery provides a tool to handle this kind of chains: But :js:func:`~Deferred.then` also allows handling this kind of
:js:func:`Deferred.pipe`. chains: it returns a new promise object, not the one it was called
with, and the return values of the callbacks is actually important to
:js:func:`~Deferred.pipe` has the same signature as it: whichever callback is called,
:js:func:`~Deferred.then` and could be used in the same manner
provided its return value was not used.
It differs from :js:func:`~Deferred.then` in two ways: it returns a
new promise object, not the one it was called with, and the return
values of the callbacks is actually important to it: whichever
callback is called,
* If the callback is not set (not provided or left to null), the * If the callback is not set (not provided or left to null), the
resolution or rejection value(s) is simply forwarded to resolution or rejection value(s) is simply forwarded to
:js:func:`~Deferred.pipe`'s promise (it's essentially a noop) :js:func:`~Deferred.then`'s promise (it's essentially a noop)
* If the callback is set and does not return an observable object (a * If the callback is set and does not return an observable object (a
deferred or a promise), the value it returns (``undefined`` if it deferred or a promise), the value it returns (``undefined`` if it
@ -218,7 +208,7 @@ callback is called,
.. code-block:: javascript .. code-block:: javascript
promise.pipe(function () { promise.then(function () {
console.log('called'); console.log('called');
}); });
@ -235,7 +225,7 @@ callback is called,
.. code-block:: javascript .. code-block:: javascript
return Model.search(condition).pipe(function (ids) { return Model.search(condition).then(function (ids) {
return Model.read(ids, fields); return Model.read(ids, fields);
}); });
@ -244,12 +234,109 @@ callback is called,
will be resolved with ``read``'s resolution values if the chain will be resolved with ``read``'s resolution values if the chain
executes correctly. executes correctly.
:js:func:`~Deferred.pipe` is also useful to adapt third-party :js:func:`~Deferred.then` is also useful to adapt third-party
promise-based APIs, in order to filter their resolution value counts promise-based APIs, in order to filter their resolution value counts
for instance (to take advantage of :js:func:`when` 's special treatment for instance (to take advantage of :js:func:`when` 's special
of single-value promises). treatment of single-value promises).
jQuery.Deferred API
~~~~~~~~~~~~~~~~~~~
.. js:function:: when(deferreds…)
:param deferreds: deferred objects to multiplex
:returns: a multiplexed deferred
:rtype: :js:class:`Deferred`
.. js:class:: Deferred
.. js:function:: Deferred.then(doneCallback[, failCallback])
Attaches new callbacks to the resolution or rejection of the
deferred object. Callbacks are executed in the order they are
attached to the deferred.
To provide only a failure callback, pass ``null`` as the
``doneCallback``, to provide only a success callback the
second argument can just be ignored (and not passed at all).
Returns a new deferred which resolves to the result of the
corresponding callback, if a callback returns a deferred
itself that new deferred will be used as the resolution of the
chain.
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.done(doneCallback)
Attaches a new success callback to the deferred, shortcut for
``deferred.then(doneCallback)``.
This is a jQuery extension to `CommonJS Promises/A`_ providing
little value over calling :js:func:`~Deferred.then` directly,
it should be avoided.
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.fail(failCallback)
Attaches a new failure callback to the deferred, shortcut for
``deferred.then(null, failCallback)``.
A second jQuery extension to `Promises/A <CommonJS
Promises/A>`_. Although it provides more value than
:js:func:`~Deferred.done`, it still is not much and should be
avoided as well.
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.promise()
Returns a read-only view of the deferred object, with all
mutators (resolve and reject) methods removed.
.. js:function:: Deferred.resolve(value…)
Called to resolve a deferred, any value provided will be
passed onto the success handlers of the deferred object.
Resolving a deferred which has already been resolved or
rejected has no effect.
.. js:function:: Deferred.reject(value…)
Called to reject (fail) a deferred, any value provided will be
passed onto the failure handler of the deferred object.
Rejecting a deferred which has already been resolved or
rejected has no effect.
.. [#] or simply calling :js:class:`Deferred` as a function, the
result is the same
.. [#] or not-promises, the `CommonJS Promises/B`_ role of
:js:func:`when` is to be able to treat values and promises
uniformly: :js:func:`when` will pass promises through directly,
but non-promise values and objects will be transformed into a
resolved promise (resolving themselves with the value itself).
jQuery's :js:func:`when` keeps this behavior making deferreds
easy to build from "static" values, or allowing defensive code
where expected promises are wrapped in :js:func:`when` just in
case.
.. _promises: http://en.wikipedia.org/wiki/Promise_(programming) .. _promises: http://en.wikipedia.org/wiki/Promise_(programming)
.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/ .. _jQuery's deferred: http://api.jquery.com/category/deferred-object/
.. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A .. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A

View File

@ -15,7 +15,7 @@ DataSet -> Model
---------------- ----------------
The 6.1 ``DataSet`` API has been deprecated in favor of the smaller The 6.1 ``DataSet`` API has been deprecated in favor of the smaller
and more orthogonal :doc:`Model </dev_rpc>` API, which more closely and more orthogonal :doc:`Model </rpc>` API, which more closely
matches the API in OpenERP Web's Python side and in OpenObject addons matches the API in OpenERP Web's Python side and in OpenObject addons
and removes most stateful behavior of DataSet. and removes most stateful behavior of DataSet.

View File

@ -16,10 +16,9 @@ import sys, os
# If extensions (or modules to document with autodoc) are in another directory, # 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 # 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. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.append(os.path.abspath('_themes')) sys.path.append(os.path.abspath('_themes'))
sys.path.append(os.path.abspath('..')) sys.path.insert(0, os.path.abspath('../addons'))
sys.path.append(os.path.abspath('../openerp')) sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
@ -43,7 +42,7 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'OpenERP Server Developers Documentation' project = u'OpenERP Web Developers Documentation'
copyright = u'2012, OpenERP s.a.' copyright = u'2012, OpenERP s.a.'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@ -53,7 +52,7 @@ copyright = u'2012, OpenERP s.a.'
# The short X.Y version. # The short X.Y version.
version = '7.0' version = '7.0'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '7.0b' release = '7.0'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -171,7 +170,7 @@ html_sidebars = {
#html_file_suffix = None #html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'openerp-server-doc' htmlhelp_basename = 'openerp-web-doc'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
@ -190,7 +189,7 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'openerp-server-doc.tex', u'OpenERP Server Developers Documentation', ('index', 'openerp-web-doc.tex', u'OpenERP Web Developers Documentation',
u'OpenERP s.a.', 'manual'), u'OpenERP s.a.', 'manual'),
] ]
@ -220,7 +219,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'openerp-server-doc', u'OpenERP Server Developers Documentation', ('index', 'openerp-web-doc', u'OpenERP Web Developers Documentation',
[u'OpenERP s.a.'], 1) [u'OpenERP s.a.'], 1)
] ]
@ -234,8 +233,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'OpenERPServerDocumentation', u'OpenERP Server Developers Documentation', ('index', 'OpenERPWebDocumentation', u'OpenERP Web Developers Documentation',
u'OpenERP s.a.', 'OpenERPServerDocumentation', 'Developers documentation for the openobject-server project.', u'OpenERP s.a.', 'OpenERPWebDocumentation', 'Developers documentation for the openerp-web project.',
'Miscellaneous'), 'Miscellaneous'),
] ]
@ -248,10 +247,12 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'. # How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote' #texinfo_show_urls = 'footnote'
todo_include_todos = True
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = { intersphinx_mapping = {
'python': ('http://docs.python.org/', None), 'python': ('http://docs.python.org/', None),
'openerpweb': ('http://doc.openerp.com/trunk/developers/web', None), 'openerpserver': ('http://doc.openerp.com/trunk/developers/server', None),
'openerpdev': ('http://doc.openerp.com/trunk/developers', None), 'openerpdev': ('http://doc.openerp.com/trunk/developers', None),
'openerpcommand': ('http://doc.openerp.com/trunk/developers/command', None),
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -14,18 +14,18 @@ Contents:
presentation presentation
async async
dev_rpc testing
dev_widget
dev_qweb
dev_client_action
internal_form widget
internal_list qweb
internal_search rpc
client_action
form_view
search_view
list_view
changelog-7.0 changelog-7.0
Indices and tables Indices and tables
================== ==================

View File

@ -451,7 +451,7 @@ formview, delegating instead to its
created. created.
The result should be a valid form view, see :doc:`Form Notes The result should be a valid form view, see :doc:`Form Notes
<internal_form>` for various peculiarities of the form view <form_view>` for various peculiarities of the form view
format. format.
:param editor: editor object asking for the view :param editor: editor object asking for the view

View File

@ -0,0 +1,25 @@
test_empty_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
test_ids_shortcut (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
test_regular_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
web.testing.stack: direct, value, success ... ok
web.testing.stack: direct, deferred, success ... ok
web.testing.stack: direct, value, error ... ok
web.testing.stack: direct, deferred, failure ... ok
web.testing.stack: successful setup ... ok
web.testing.stack: successful teardown ... ok
web.testing.stack: successful setup and teardown ... ok
[snip ~150 lines]
test_convert_complex_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_convert_complex_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_convert_literal_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_convert_literal_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_retrieve_nonliteral_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_retrieve_nonliteral_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
----------------------------------------------------------------------
Ran 181 tests in 15.706s
OK

693
addons/web/doc/testing.rst Normal file
View File

@ -0,0 +1,693 @@
.. highlight:: javascript
Testing in OpenERP Web
======================
Javascript Unit Testing
-----------------------
OpenERP Web 7.0 includes means to unit-test both the core code of
OpenERP Web and your own javascript modules. On the javascript side,
unit-testing is based on QUnit_ with a number of helpers and
extensions for better integration with OpenERP.
To see what the runner looks like, find (or start) an OpenERP server
with the web client enabled, and navigate to ``/web/tests`` e.g. `on
OpenERP's CI <http://trunk.runbot.openerp.com/web/tests>`_. This will
show the runner selector, which lists all modules with javascript unit
tests, and allows starting any of them (or all javascript tests in all
modules at once).
.. image:: ./images/runner.png
:align: center
Clicking any runner button will launch the corresponding tests in the
bundled QUnit_ runner:
.. image:: ./images/tests.png
:align: center
Writing a test case
-------------------
The first step is to list the test file(s). This is done through the
``test`` key of the openerp manifest, by adding javascript files to it
(next to the usual YAML files, if any):
.. code-block:: python
{
'name': "Demonstration of web/javascript tests",
'category': 'Hidden',
'depends': ['web'],
'test': ['static/test/demo.js'],
}
and to create the corresponding test file(s)
.. note::
Test files which do not exist will be ignored, if all test files
of a module are ignored (can not be found), the test runner will
consider that the module has no javascript tests.
After that, refreshing the runner selector will display the new module
and allow running all of its (0 so far) tests:
.. image:: ./images/runner2.png
:align: center
The next step is to create a test case::
openerp.testing.section('basic section', function (test) {
test('my first test', function () {
ok(false, "this test has run");
});
});
All testing helpers and structures live in the ``openerp.testing``
module. OpenERP tests live in a :js:func:`~openerp.testing.section`,
which is itself part of a module. The first argument to a section is
the name of the section, the second one is the section body.
:js:func:`test <openerp.testing.case>`, provided by the
:js:func:`~openerp.testing.section` to the callback, is used to
register a given test case which will be run whenever the test runner
actually does its job. OpenERP Web test case use standard `QUnit
assertions`_ within them.
Launching the test runner at this point will run the test and display
the corresponding assertion message, with red colors indicating the
test failed:
.. image:: ./images/tests2.png
:align: center
Fixing the test (by replacing ``false`` to ``true`` in the assertion)
will make it pass:
.. image:: ./images/tests3.png
:align: center
Assertions
----------
As noted above, OpenERP Web's tests use `qunit assertions`_. They are
available globally (so they can just be called without references to
anything). The following list is available:
.. js:function:: ok(state[, message])
checks that ``state`` is truthy (in the javascript sense)
.. js:function:: strictEqual(actual, expected[, message])
checks that the actual (produced by a method being tested) and
expected values are identical (roughly equivalent to ``ok(actual
=== expected, message)``)
.. js:function:: notStrictEqual(actual, expected[, message])
checks that the actual and expected values are *not* identical
(roughly equivalent to ``ok(actual !== expected, message)``)
.. js:function:: deepEqual(actual, expected[, message])
deep comparison between actual and expected: recurse into
containers (objects and arrays) to ensure that they have the same
keys/number of elements, and the values match.
.. js:function:: notDeepEqual(actual, expected[, message])
inverse operation to :js:func:`deepEqual`
.. js:function:: throws(block[, expected][, message])
checks that, when called, the ``block`` throws an
error. Optionally validates that error against ``expected``.
:param Function block:
:param expected: if a regexp, checks that the thrown error's
message matches the regular expression. If an
error type, checks that the thrown error is of
that type.
:type expected: Error | RegExp
.. js:function:: equal(actual, expected[, message])
checks that ``actual`` and ``expected`` are loosely equal, using
the ``==`` operator and its coercion rules.
.. js:function:: notEqual(actual, expected[, message])
inverse operation to :js:func:`equal`
Getting an OpenERP instance
---------------------------
The OpenERP instance is the base through which most OpenERP Web
modules behaviors (functions, objects, …) are accessed. As a result,
the test framework automatically builds one, and loads the module
being tested and all of its dependencies inside it. This new instance
is provided as the first positional parameter to your test
cases. Let's observe by adding javascript code (not test code) to the
test module:
.. code-block:: python
{
'name': "Demonstration of web/javascript tests",
'category': 'Hidden',
'depends': ['web'],
'js': ['static/src/js/demo.js'],
'test': ['static/test/demo.js'],
}
::
// src/js/demo.js
openerp.web_tests_demo = function (instance) {
instance.web_tests_demo = {
value_true: true,
SomeType: instance.web.Class.extend({
init: function (value) {
this.value = value;
}
})
};
};
and then adding a new test case, which simply checks that the
``instance`` contains all the expected stuff we created in the
module::
// test/demo.js
test('module content', function (instance) {
ok(instance.web_tests_demo.value_true, "should have a true value");
var type_instance = new instance.web_tests_demo.SomeType(42);
strictEqual(type_instance.value, 42, "should have provided value");
});
DOM Scratchpad
--------------
As in the wider client, arbitrarily accessing document content is
strongly discouraged during tests. But DOM access is still needed to
e.g. fully initialize :js:class:`widgets <~openerp.web.Widget>` before
testing them.
Thus, a test case gets a DOM scratchpad as its second positional
parameter, in a jQuery instance. That scratchpad is fully cleaned up
before each test, and as long as it doesn't do anything outside the
scratchpad your code can do whatever it wants::
// test/demo.js
test('DOM content', function (instance, $scratchpad) {
$scratchpad.html('<div><span class="foo bar">ok</span></div>');
ok($scratchpad.find('span').hasClass('foo'),
"should have provided class");
});
test('clean scratchpad', function (instance, $scratchpad) {
ok(!$scratchpad.children().length, "should have no content");
ok(!$scratchpad.text(), "should have no text");
});
.. note::
The top-level element of the scratchpad is not cleaned up, test
cases can add text or DOM children but shoud not alter
``$scratchpad`` itself.
Loading templates
-----------------
To avoid the corresponding processing costs, by default templates are
not loaded into QWeb. If you need to render e.g. widgets making use of
QWeb templates, you can request their loading through the
:js:attr:`~TestOptions.templates` option to the :js:func:`test case
function <openerp.testing.case>`.
This will automatically load all relevant templates in the instance's
qweb before running the test case:
.. code-block:: python
{
'name': "Demonstration of web/javascript tests",
'category': 'Hidden',
'depends': ['web'],
'js': ['static/src/js/demo.js'],
'test': ['static/test/demo.js'],
'qweb': ['static/src/xml/demo.xml'],
}
.. code-block:: xml
<!-- src/xml/demo.xml -->
<templates id="template" xml:space="preserve">
<t t-name="DemoTemplate">
<t t-foreach="5" t-as="value">
<p><t t-esc="value"/></p>
</t>
</t>
</templates>
::
// test/demo.js
test('templates', {templates: true}, function (instance) {
var s = instance.web.qweb.render('DemoTemplate');
var texts = $(s).find('p').map(function () {
return $(this).text();
}).get();
deepEqual(texts, ['0', '1', '2', '3', '4']);
});
Asynchronous cases
------------------
The test case examples so far are all synchronous, they execute from
the first to the last line and once the last line has executed the
test is done. But the web client is full of :doc:`asynchronous code
</async>`, and thus test cases need to be async-aware.
This is done by returning a :js:class:`deferred <Deferred>` from the
case callback::
// test/demo.js
test('asynchronous', {
asserts: 1
}, function () {
var d = $.Deferred();
setTimeout(function () {
ok(true);
d.resolve();
}, 100);
return d;
});
This example also uses the :js:class:`options parameter <TestOptions>`
to specify the number of assertions the case should expect, if less or
more assertions are specified the case will count as failed.
Asynchronous test cases *must* specify the number of assertions they
will run. This allows more easily catching situations where e.g. the
test architecture was not warned about asynchronous operations.
.. note::
Asynchronous test cases also have a 2 seconds timeout: if the test
does not finish within 2 seconds, it will be considered
failed. This pretty much always means the test will not
resolve. This timeout *only* applies to the test itself, not to
the setup and teardown processes.
.. note::
If the returned deferred is rejected, the test will be failed
unless :js:attr:`~TestOptions.fail_on_rejection` is set to
``false``.
RPC
---
An important subset of asynchronous test cases is test cases which
need to perform (and chain, to an extent) RPC calls.
.. note::
Because they are a subset of asynchronous cases, RPC cases must
also provide a valid :js:attr:`assertions count
<TestOptions.asserts>`.
By default, test cases will fail when trying to perform an RPC
call. The ability to perform RPC calls must be explicitly requested by
a test case (or its containing test suite) through
:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or
``rpc``.
Mock RPC
++++++++
The preferred (and fastest from a setup and execution time point of
view) way to do RPC during tests is to mock the RPC calls: while
setting up the test case, provide what the RPC responses "should" be,
and only test the code between the "user" (the test itself) and the
RPC call, before the call is effectively done.
To do this, set the :js:attr:`rpc option <TestOptions.rpc>` to
``mock``. This will add a third parameter to the test case callback:
.. js:function:: mock(rpc_spec, handler)
Can be used in two different ways depending on the shape of the
first parameter:
* If it matches the pattern ``model:method`` (if it contains a
colon, essentially) the call will set up the mocking of an RPC
call straight to the OpenERP server (through XMLRPC) as
performed via e.g. :js:func:`openerp.web.Model.call`.
In that case, ``handler`` should be a function taking two
arguments ``args`` and ``kwargs``, matching the corresponding
arguments on the server side and should simply return the value
as if it were returned by the Python XMLRPC handler::
test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) {
// set up mocking
mock('people.famous:name_search', function (args, kwargs) {
strictEqual(kwargs.name, 'bob');
return [
[1, "Microsoft Bob"],
[2, "Bob the Builder"],
[3, "Silent Bob"]
];
});
// actual test code
return new instance.web.Model('people.famous')
.call('name_search', {name: 'bob'}).then(function (result) {
strictEqual(result.length, 3, "shoud return 3 people");
strictEqual(result[0][1], "Microsoft Bob",
"the most famous bob should be Microsoft Bob");
});
});
* Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it
will mock a JSON-RPC call to a web client controller, such as
``/web/webclient/translations``. In that case, the handler takes
a single ``params`` argument holding all of the parameters
provided over JSON-RPC.
As previously, the handler should simply return the result value
as if returned by the original JSON-RPC handler::
test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) {
var fetched_dbs = false, fetched_langs = false;
mock('/web/database/get_list', function () {
fetched_dbs = true;
return ['foo', 'bar', 'baz'];
});
mock('/web/session/get_lang_list', function () {
fetched_langs = true;
return [['vo_IS', 'Hopelandic / Vonlenska']];
});
// widget needs that or it blows up
instance.webclient = {toggle_bars: openerp.testing.noop};
var dbm = new instance.web.DatabaseManager({});
return dbm.appendTo($s).then(function () {
ok(fetched_dbs, "should have fetched databases");
ok(fetched_langs, "should have fetched languages");
deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
});
});
.. note::
Mock handlers can contain assertions, these assertions should be
part of the assertions count (and if multiple calls are made to a
handler containing assertions, it multiplies the effective number
of assertions).
.. _testing-rpc-rpc:
Actual RPC
++++++++++
A more realistic (but significantly slower and more expensive) way to
perform RPC calls is to perform actual calls to an actually running
OpenERP server. To do this, set the :js:attr:`rpc option
<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter
but will enable actual RPC, and the automatic creation and destruction
of databases (from a specified source) around tests.
First, create a basic model we can test stuff with:
.. code-block:: javascript
from openerp.osv import orm, fields
class TestObject(orm.Model):
_name = 'web_tests_demo.model'
_columns = {
'name': fields.char("Name", required=True),
'thing': fields.char("Thing"),
'other': fields.char("Other", required=True)
}
_defaults = {
'other': "bob"
}
then the actual test::
test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) {
var Model = new instance.web.Model('web_tests_demo.model');
return Model.call('create', [{name: "Bob"}])
.then(function (id) {
return Model.call('read', [[id]]);
}).then(function (records) {
strictEqual(records.length, 1);
var record = records[0];
strictEqual(record.name, "Bob");
strictEqual(record.thing, false);
// default value
strictEqual(record.other, 'bob');
});
});
This test looks like a "mock" RPC test but for the lack of mock
response (and the different ``rpc`` type), however it has further
ranging consequences in that it will copy an existing database to a
new one, run the test in full on that temporary database and destroy
the database, to simulate an isolated and transactional context and
avoid affecting other tests. One of the consequences is that it takes
a *long* time to run (5~10s, most of that time being spent waiting for
a database duplication).
Furthermore, as the test needs to clone a database, it also has to ask
which database to clone, the database/super-admin password and the
password of the ``admin`` user (in order to authenticate as said
user). As a result, the first time the test runner encounters an
``rpc: "rpc"`` test configuration it will produce the following
prompt:
.. image:: ./images/db-query.png
:align: center
and stop the testing process until the necessary information has been
provided.
The prompt will only appear once per test run, all tests will use the
same "source" database.
.. note::
The handling of that information is currently rather brittle and
unchecked, incorrect values will likely crash the runner.
.. note::
The runner does not currently store this information (for any
longer than a test run that is), the prompt will have to be filled
every time.
Testing API
-----------
.. js:function:: openerp.testing.section(name[, options], body)
A test section, serves as shared namespace for related tests (for
constants or values to only set up once). The ``body`` function
should contain the tests themselves.
Note that the order in which tests are run is essentially
undefined, do *not* rely on it.
:param String name:
:param TestOptions options:
:param body:
:type body: Function<:js:func:`~openerp.testing.case`, void>
.. js:function:: openerp.testing.case(name[, options], callback)
Registers a test case callback in the test runner, the callback
will only be run once the runner is started (or maybe not at all,
if the test is filtered out).
:param String name:
:param TestOptions options:
:param callback:
:type callback: Function<instance, $, Function<String, Function, void>>
.. js:class:: TestOptions
the various options which can be passed to
:js:func:`~openerp.testing.section` or
:js:func:`~openerp.testing.case`. Except for
:js:attr:`~TestOptions.setup` and
:js:attr:`~TestOptions.teardown`, an option on
:js:func:`~openerp.testing.case` will overwrite the corresponding
option on :js:func:`~openerp.testing.section` so
e.g. :js:attr:`~TestOptions.rpc` can be set for a
:js:func:`~openerp.testing.section` and then differently set for
some :js:func:`~openerp.testing.case` of that
:js:func:`~openerp.testing.section`
.. js:attribute:: TestOptions.asserts
An integer, the number of assertions which should run during a
normal execution of the test. Mandatory for asynchronous tests.
.. js:attribute:: TestOptions.setup
Test case setup, run right before each test case. A section's
:js:func:`~TestOptions.setup` is run before the case's own, if
both are specified.
.. js:attribute:: TestOptions.teardown
Test case teardown, a case's :js:func:`~TestOptions.teardown`
is run before the corresponding section if both are present.
.. js:attribute:: TestOptions.fail_on_rejection
If the test is asynchronous and its resulting promise is
rejected, fail the test. Defaults to ``true``, set to
``false`` to not fail the test in case of rejection::
// test/demo.js
test('unfail rejection', {
asserts: 1,
fail_on_rejection: false
}, function () {
var d = $.Deferred();
setTimeout(function () {
ok(true);
d.reject();
}, 100);
return d;
});
.. js:attribute:: TestOptions.rpc
RPC method to use during tests, one of ``"mock"`` or
``"rpc"``. Any other value will disable RPC for the test (if
they were enabled by the suite for instance).
.. js:attribute:: TestOptions.templates
Whether the current module (and its dependencies)'s templates
should be loaded into QWeb before starting the test. A
boolean, ``false`` by default.
The test runner can also use two global configuration values set
directly on the ``window`` object:
* ``oe_all_dependencies`` is an ``Array`` of all modules with a web
component, ordered by dependency (for a module ``A`` with
dependencies ``A'``, any module of ``A'`` must come before ``A`` in
the array)
* ``oe_db_info`` is an object with 3 keys ``source``, ``supadmin`` and
``password``. It is used to pre-configure :ref:`actual RPC
<testing-rpc-rpc>` tests, to avoid a prompt being displayed
(especially for headless situations).
Running through Python
----------------------
The web client includes the means to run these tests on the
command-line (or in a CI system), but while actually running it is
pretty simple the setup of the pre-requisite parts has some
complexities.
1. Install unittest2_ and QUnitSuite_ in your Python environment. Both
can trivially be installed via `pip <http://pip-installer.org>`_ or
`easy_install
<http://packages.python.org/distribute/easy_install.html>`_.
The former is the unit-testing framework used by OpenERP, the
latter is an adapter module to run qunit_ test suites and convert
their result into something unittest2_ can understand and report.
2. Install PhantomJS_. It is a headless
browser which allows automating running and testing web
pages. QUnitSuite_ uses it to actually run the qunit_ test suite.
The PhantomJS_ website provides pre-built binaries for some
platforms, and your OS's package management probably provides it as
well.
If you're building PhantomJS_ from source, I recommend preparing
for some knitting time as it's not exactly fast (it needs to
compile both `Qt <http://qt-project.org/>`_ and `Webkit
<http://www.webkit.org/>`_, both being pretty big projects).
.. note::
Because PhantomJS_ is webkit-based, it will not be able to test
if Firefox, Opera or Internet Explorer can correctly run the
test suite (and it is only an approximation for Safari and
Chrome). It is therefore recommended to *also* run the test
suites in actual browsers once in a while.
.. note::
The version of PhantomJS_ this was build through is 1.7,
previous versions *should* work but are not actually supported
(and tend to just segfault when something goes wrong in
PhantomJS_ itself so they're a pain to debug).
3. Set up :ref:`OpenERP Command <openerpcommand:openerp-command>`,
which will be used to actually run the tests: running the qunit_
test suite requires a running server, so at this point OpenERP
Server isn't able to do it on its own during the building/testing
process.
4. Install a new database with all relevant modules (all modules with
a web component at least), then restart the server
.. note::
For some tests, a source database needs to be duplicated. This
operation requires that there be no connection to the database
being duplicated, but OpenERP doesn't currently break
existing/outstanding connections, so restarting the server is
the simplest way to ensure everything is in the right state.
5. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct
addons-path specified (and replacing ``$DATABASE`` by the source
database you created above)
.. note::
If you leave out ``-mweb``, the runner will attempt to run all
the tests in all the modules, which may or may not work.
If everything went correctly, you should now see a list of tests with
(hopefully) ``ok`` next to their names, closing with a report of the
number of tests run and the time it took:
.. literalinclude:: test-report.txt
:language: text
Congratulation, you have just performed a successful "offline" run of
the OpenERP Web test suite.
.. note::
Note that this runs all the Python tests for the ``web`` module,
but all the web tests for all of OpenERP. This can be surprising.
.. _qunit: http://qunitjs.com/
.. _qunit assertions: http://api.qunitjs.com/category/assert/
.. _unittest2: http://pypi.python.org/pypi/unittest2
.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/
.. _PhantomJS: http://phantomjs.org/

View File

@ -11,6 +11,7 @@ import logging
import mimetypes import mimetypes
import os import os
import pprint import pprint
import random
import sys import sys
import tempfile import tempfile
import threading import threading
@ -21,6 +22,7 @@ import urlparse
import uuid import uuid
import xmlrpclib import xmlrpclib
import babel.core
import simplejson import simplejson
import werkzeug.contrib.sessions import werkzeug.contrib.sessions
import werkzeug.datastructures import werkzeug.datastructures
@ -91,9 +93,25 @@ class WebRequest(object):
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
self.session = self.httpsession.get(self.session_id) self.session = self.httpsession.get(self.session_id)
if not self.session: if not self.session:
self.httpsession[self.session_id] = self.session = session.OpenERPSession() self.session = session.OpenERPSession()
self.httpsession[self.session_id] = self.session
self.context = self.params.pop('context', None) self.context = self.params.pop('context', None)
self.debug = self.params.pop('debug', False) != False 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')
if lang is None:
lang = self.httprequest.cookies.get('lang')
if lang is None:
lang = self.httprequest.accept_languages.best
if lang is None:
lang = 'en_US'
# tranform 2 letters lang like 'en' into 5 letters like 'en_US'
lang = babel.core.LOCALE_ALIASES.get(lang, lang)
# we use _ as seprator where RFC2616 uses '-'
self.lang = lang.replace('-', '_')
class JsonRequest(WebRequest): class JsonRequest(WebRequest):
""" JSON-RPC2 over HTTP. """ JSON-RPC2 over HTTP.
@ -210,6 +228,10 @@ class JsonRequest(WebRequest):
_logger.debug("<--\n%s", pprint.pformat(response)) _logger.debug("<--\n%s", pprint.pformat(response))
if jsonp: if jsonp:
# If we use jsonp, that's mean we are called from another host
# Some browser (IE and Safari) do no allow third party cookies
# We need then to manage http sessions manually.
response['httpsessionid'] = self.httpsession.sid
mime = 'application/javascript' mime = 'application/javascript'
body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),) body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),)
else: else:
@ -336,23 +358,13 @@ class Controller(object):
#---------------------------------------------------------- #----------------------------------------------------------
# Session context manager # Session context manager
#---------------------------------------------------------- #----------------------------------------------------------
STORES = {}
@contextlib.contextmanager @contextlib.contextmanager
def session_context(request, storage_path, session_cookie='httpsessionid'): def session_context(request, session_store, session_lock, sid):
session_store, session_lock = STORES.get(storage_path, (None, None))
if not session_store:
session_store = werkzeug.contrib.sessions.FilesystemSessionStore( storage_path)
session_lock = threading.Lock()
STORES[storage_path] = session_store, session_lock
sid = request.cookies.get(session_cookie)
with session_lock: with session_lock:
if sid: if sid:
request.session = session_store.get(sid) request.session = session_store.get(sid)
else: else:
request.session = session_store.new() request.session = session_store.new()
try: try:
yield request.session yield request.session
finally: finally:
@ -404,6 +416,18 @@ def session_context(request, storage_path, session_cookie='httpsessionid'):
session_store.save(request.session) session_store.save(request.session)
def session_gc(session_store):
if random.random() < 0.001:
# we keep session one week
last_week = time.time() - 60*60*24*7
for fname in os.listdir(session_store.path):
path = os.path.join(session_store.path, fname)
try:
if os.path.getmtime(path) < last_week:
os.unlink(path)
except OSError:
pass
#---------------------------------------------------------- #----------------------------------------------------------
# WSGI Application # WSGI Application
#---------------------------------------------------------- #----------------------------------------------------------
@ -434,26 +458,31 @@ class DisableCacheMiddleware(object):
start_response(status, new_headers) start_response(status, new_headers)
return self.app(environ, start_wrapped) return self.app(environ, start_wrapped)
def session_path():
try:
username = getpass.getuser()
except Exception:
username = "unknown"
path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
if not os.path.exists(path):
os.mkdir(path, 0700)
return path
class Root(object): class Root(object):
"""Root WSGI application for the OpenERP Web Client. """Root WSGI application for the OpenERP Web Client.
""" """
def __init__(self): def __init__(self):
self.httpsession_cookie = 'httpsessionid'
self.addons = {} self.addons = {}
static_dirs = self._load_addons() static_dirs = self._load_addons()
app = werkzeug.wsgi.SharedDataMiddleware( self.dispatch, static_dirs) app = werkzeug.wsgi.SharedDataMiddleware( self.dispatch, static_dirs)
self.dispatch = DisableCacheMiddleware(app) self.dispatch = DisableCacheMiddleware(app)
try: # Setup http sessions
username = getpass.getuser() path = session_path()
except Exception: self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
username = "unknown" self.session_lock = threading.Lock()
self.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username) _logger.debug('HTTP sessions stored in: %s', path)
if not os.path.exists(self.session_storage):
os.mkdir(self.session_storage, 0700)
_logger.debug('HTTP sessions stored in: %s', self.session_storage)
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
""" Handle a WSGI request """ Handle a WSGI request
@ -476,8 +505,14 @@ class Root(object):
if not handler: if not handler:
response = werkzeug.exceptions.NotFound() response = werkzeug.exceptions.NotFound()
else: else:
with session_context(request, self.session_storage, self.httpsession_cookie) as session: sid = request.cookies.get('sid')
result = handler( request) if not sid:
sid = request.args.get('sid')
session_gc(self.session_store)
with session_context(request, self.session_store, self.session_lock, sid) as session:
result = handler(request)
if isinstance(result, basestring): if isinstance(result, basestring):
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))] headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
@ -486,7 +521,7 @@ class Root(object):
response = result response = result
if hasattr(response, 'set_cookie'): if hasattr(response, 'set_cookie'):
response.set_cookie(self.httpsession_cookie, session.sid) response.set_cookie('sid', session.sid)
return response(environ, start_response) return response(environ, start_response)
@ -530,7 +565,7 @@ class Root(object):
:rtype: ``Controller | None`` :rtype: ``Controller | None``
""" """
if l: if l:
ps = '/' + '/'.join(l) ps = '/' + '/'.join(filter(None, l))
method_name = 'index' method_name = 'index'
while ps: while ps:
c = controllers_path.get(ps) c = controllers_path.get(ps)

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:07+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:07+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:07+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:07+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
"X-Poedit-Language: Czech\n" "X-Poedit-Language: Czech\n"
#. openerp-web #. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
"Language: es\n" "Language: es\n"
#. openerp-web #. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:07+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -9,13 +9,13 @@ msgstr ""
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-07-02 09:06+0200\n" "POT-Creation-Date: 2012-07-02 09:06+0200\n"
"PO-Revision-Date: 2012-10-26 12:17+0000\n" "PO-Revision-Date: 2012-10-26 12:17+0000\n"
"Last-Translator: Herczeg Péter <herczegp@gmail.com>\n" "Last-Translator: Herczeg Péter <hp@erp-cloud.hu>\n"
"Language-Team: Hungarian <hu@li.org>\n" "Language-Team: Hungarian <hu@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-27 05:15+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16194)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-23 05:05+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16179)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-22 04:46+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176
@ -66,7 +66,7 @@ msgstr ""
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:499 #: addons/web/static/src/js/chrome.js:499
msgid "Restored" msgid "Restored"
msgstr "" msgstr "Atkurta"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:499 #: addons/web/static/src/js/chrome.js:499
@ -253,7 +253,7 @@ msgstr ""
#: addons/web/static/src/js/formats.js:139 #: addons/web/static/src/js/formats.js:139
#, python-format #, python-format
msgid "(%d records)" msgid "(%d records)"
msgstr "" msgstr "(%d įrašai(-ų))"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/formats.js:325 #: addons/web/static/src/js/formats.js:325
@ -265,7 +265,7 @@ msgstr "Atsisiųsti"
#: addons/web/static/src/js/formats.js:330 #: addons/web/static/src/js/formats.js:330
#, python-format #, python-format
msgid "Download \"%s\"" msgid "Download \"%s\""
msgstr "" msgstr "Parsisiųsti \"%s\""
#. openerp-web #. openerp-web
#: addons/web/static/src/js/search.js:437 #: addons/web/static/src/js/search.js:437
@ -527,12 +527,12 @@ msgstr "Forma"
#: addons/web/static/src/xml/base.xml:763 #: addons/web/static/src/xml/base.xml:763
#: addons/web/static/src/xml/base.xml:1714 #: addons/web/static/src/xml/base.xml:1714
msgid "Delete" msgid "Delete"
msgstr "" msgstr "Ištrinti"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:762 #: addons/web/static/src/xml/base.xml:762
msgid "Duplicate" msgid "Duplicate"
msgstr "" msgstr "Sukurti kopiją"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/view_form.js:133 #: addons/web/static/src/js/view_form.js:133
@ -589,12 +589,12 @@ msgstr ""
#: addons/web/static/src/js/view_form.js:2238 #: addons/web/static/src/js/view_form.js:2238
#, python-format #, python-format
msgid "<em>   Create \"<strong>%s</strong>\"</em>" msgid "<em>   Create \"<strong>%s</strong>\"</em>"
msgstr "" msgstr "<em>   Sukurti \"<strong>%s</strong>\"</em>"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/view_form.js:2244 #: addons/web/static/src/js/view_form.js:2244
msgid "<em>   Create and Edit...</em>" msgid "<em>   Create and Edit...</em>"
msgstr "" msgstr "<em>   Sukurti ir redaguoti...</em>"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/view_form.js:2277 #: addons/web/static/src/js/view_form.js:2277
@ -606,7 +606,7 @@ msgstr "Paieška: "
#: addons/web/static/src/js/view_form.js:2277 #: addons/web/static/src/js/view_form.js:2277
#: addons/web/static/src/js/view_form.js:2765 #: addons/web/static/src/js/view_form.js:2765
msgid "Create: " msgid "Create: "
msgstr "" msgstr "Sukurti "
#. openerp-web #. openerp-web
#: addons/web/static/src/js/view_form.js:2062 #: addons/web/static/src/js/view_form.js:2062
@ -668,7 +668,7 @@ msgstr "Grupė"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/view_list.js:549 #: addons/web/static/src/js/view_list.js:549
msgid "Do you really want to remove these records?" msgid "Do you really want to remove these records?"
msgstr "" msgstr "Ar tikrai norite ištrinti šiuos įrašus?"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/views.js:925 #: addons/web/static/src/js/views.js:925
@ -813,25 +813,25 @@ msgstr ""
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:297 #: addons/web/static/src/xml/base.xml:297
msgid "Invalid username or password" msgid "Invalid username or password"
msgstr "" msgstr "Neteisingas naudotojo vardas arba slaptažodis"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:116 #: addons/web/static/src/xml/base.xml:116
#: addons/web/static/src/xml/base.xml:150 #: addons/web/static/src/xml/base.xml:150
#: addons/web/static/src/xml/base.xml:301 #: addons/web/static/src/xml/base.xml:301
msgid "Database:" msgid "Database:"
msgstr "" msgstr "Duomenų bazė:"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:306 #: addons/web/static/src/xml/base.xml:306
msgid "Username" msgid "Username"
msgstr "" msgstr "Naudotojas"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:308 #: addons/web/static/src/xml/base.xml:308
#: addons/web/static/src/xml/base.xml:331 #: addons/web/static/src/xml/base.xml:331
msgid "Password" msgid "Password"
msgstr "" msgstr "Slaptažodis"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:310 #: addons/web/static/src/xml/base.xml:310
@ -870,17 +870,17 @@ msgstr ""
#: addons/web/static/src/xml/base.xml:195 #: addons/web/static/src/xml/base.xml:195
#: addons/web/static/src/xml/base.xml:330 #: addons/web/static/src/xml/base.xml:330
msgid "Restore" msgid "Restore"
msgstr "" msgstr "Atkurti"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:332 #: addons/web/static/src/xml/base.xml:332
msgid "Back to Login" msgid "Back to Login"
msgstr "" msgstr "Grįžti į prisijungimo langą"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:61 #: addons/web/static/src/xml/base.xml:61
msgid "CREATE DATABASE" msgid "CREATE DATABASE"
msgstr "" msgstr "SUKURTI DUOMENŲ BAZĘ"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:68 addons/web/static/src/xml/base.xml:211 #: addons/web/static/src/xml/base.xml:68 addons/web/static/src/xml/base.xml:211
@ -922,7 +922,7 @@ msgstr ""
#: addons/web/static/src/xml/base.xml:162 #: addons/web/static/src/xml/base.xml:162
#: addons/web/static/src/xml/base.xml:187 #: addons/web/static/src/xml/base.xml:187
msgid "Master Password:" msgid "Master Password:"
msgstr "" msgstr "Pagrindinis slaptažodis"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:143 #: addons/web/static/src/xml/base.xml:143
@ -932,7 +932,7 @@ msgstr ""
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:175 #: addons/web/static/src/xml/base.xml:175
msgid "RESTORE DATABASE" msgid "RESTORE DATABASE"
msgstr "" msgstr "ATKURTI DUOMENŲ BAZĘ"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:182 #: addons/web/static/src/xml/base.xml:182
@ -1012,17 +1012,17 @@ msgstr "OpenERP.com"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:1720 #: addons/web/static/src/xml/base.xml:1720
msgid "Old Password:" msgid "Old Password:"
msgstr "" msgstr "Esamas slaptažodis:"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:1725 #: addons/web/static/src/xml/base.xml:1725
msgid "New Password:" msgid "New Password:"
msgstr "" msgstr "Naujas slaptažodis:"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:1730 #: addons/web/static/src/xml/base.xml:1730
msgid "Confirm Password:" msgid "Confirm Password:"
msgstr "" msgstr "Patvirtinti slaptažodį:"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:390 #: addons/web/static/src/xml/base.xml:390
@ -1254,7 +1254,7 @@ msgstr ""
#: addons/web/static/src/xml/base.xml:1222 #: addons/web/static/src/xml/base.xml:1222
#: addons/web/static/src/xml/base.xml:1279 #: addons/web/static/src/xml/base.xml:1279
msgid "Clear" msgid "Clear"
msgstr "" msgstr "Išvalyti"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:1179 #: addons/web/static/src/xml/base.xml:1179
@ -1408,12 +1408,12 @@ msgstr ""
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:1509 #: addons/web/static/src/xml/base.xml:1509
msgid "Save & New" msgid "Save & New"
msgstr "" msgstr "Išsaugoti ir sukurti naują"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:1510 #: addons/web/static/src/xml/base.xml:1510
msgid "Save & Close" msgid "Save & Close"
msgstr "" msgstr "Išsaugoti ir uždaryti"
#. openerp-web #. openerp-web
#: addons/web/static/src/xml/base.xml:1617 #: addons/web/static/src/xml/base.xml:1617
@ -1572,9 +1572,6 @@ msgstr ""
#~ msgid "Advanced Filters" #~ msgid "Advanced Filters"
#~ msgstr "Išplėstiniai filtrai" #~ msgstr "Išplėstiniai filtrai"
#~ msgid "Other Options"
#~ msgstr "Kiti nustatymai"
#~ msgid "OK" #~ msgid "OK"
#~ msgstr "Gerai" #~ msgstr "Gerai"
@ -1602,3 +1599,22 @@ msgstr ""
#~ msgid "Reports" #~ msgid "Reports"
#~ msgstr "Ataskaitos" #~ msgstr "Ataskaitos"
#~ msgid "Other Options"
#~ msgstr "Kitos parinktys"
#~ msgid "LOGOUT"
#~ msgstr "ATSIJUNGTI"
#~ msgid "Create..."
#~ msgstr "Sukurti..."
#~ msgid "Search"
#~ msgstr "Ieškoti"
#~ msgid "Search..."
#~ msgstr "Ieškoti..."
#, python-format
#~ msgid "[%(first_record)d to %(last_record)d] of %(records_count)d"
#~ msgstr "[%(first_record)d iki %(last_record)d] iš %(records_count)d"

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176
@ -243,17 +243,19 @@ msgid ""
"Destination fields should only be selected once, some fields are selected " "Destination fields should only be selected once, some fields are selected "
"more than once:" "more than once:"
msgstr "" msgstr ""
"Pola docelowe muszą być wybierane tylko raz. niektóre inne pola mogą być "
"wybierane więcej niż raz:"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/data_import.js:386 #: addons/web/static/src/js/data_import.js:386
msgid "*Required Fields are not selected :" msgid "*Required Fields are not selected :"
msgstr "" msgstr "*Wymagane pola nie zostały wybrane :"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/formats.js:139 #: addons/web/static/src/js/formats.js:139
#, python-format #, python-format
msgid "(%d records)" msgid "(%d records)"
msgstr "" msgstr "(%d rekordów)"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/formats.js:325 #: addons/web/static/src/js/formats.js:325

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -15,8 +15,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-02 05:20+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16218)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-02 05:20+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16218)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:02+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:07+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-11-07 04:55+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16232)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-21 05:03+0000\n" "X-Launchpad-Export-Date: 2012-11-15 05:08+0000\n"
"X-Generator: Launchpad (build 16165)\n" "X-Generator: Launchpad (build 16265)\n"
#. openerp-web #. openerp-web
#: addons/web/static/src/js/chrome.js:176 #: addons/web/static/src/js/chrome.js:176

View File

@ -27,7 +27,8 @@
var $tip = this.tip(); var $tip = this.tip();
$tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
$tip[0].className = 'tipsy openerp oe_tooltip '; // reset classname in case of dynamic gravity $tip[0].className = 'tipsy '; // reset classname in case of dynamic gravity
$tip.openerpClass('oe_tooltip');
$tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body);
var pos = $.extend({}, this.$element.offset(), { var pos = $.extend({}, this.$element.offset(), {

View File

@ -1115,7 +1115,7 @@ body .ui-tooltip { border-width:2px; }
/*** Input field styling from Bootstrap **/ /*** Input field styling from Bootstrap **/
input, textarea { /* input, textarea {
-webkit-transition: border linear 0.2s, box-shadow linear 0.2s; -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-moz-transition: border linear 0.2s, box-shadow linear 0.2s; -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
-ms-transition: border linear 0.2s, box-shadow linear 0.2s; -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
@ -1137,12 +1137,12 @@ input[type=file]:focus, input[type=checkbox]:focus, select:focus {
-moz-box-shadow: none; -moz-box-shadow: none;
box-shadow: none; box-shadow: none;
outline: 1px dotted #666; outline: 1px dotted #666;
} }*/
input[type="text"], /*input[type="text"],
input[type="password"], input[type="password"],*/
.ui-autocomplete-input, .ui-autocomplete-input,
textarea, /*textarea,*/
.uneditable-input { .uneditable-input {
display: inline-block; display: inline-block;
padding: 4px; padding: 4px;

View File

@ -20,20 +20,6 @@
font-style: normal; font-style: normal;
} }
@media print {
.oe_topbar, .oe_leftbar, .oe_loading {
display: none !important;
}
}
.openerp.openerp_webclient_container {
height: 100%;
}
.text-tag .text-button {
height: auto !important;
min-height: 16px;
}
.openerp { .openerp {
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -46,6 +32,9 @@
* http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image
*/ */
} }
.openerp.openerp_webclient_container {
height: 100%;
}
.openerp :-moz-placeholder { .openerp :-moz-placeholder {
color: #afafb6 !important; color: #afafb6 !important;
font-style: italic !important; font-style: italic !important;
@ -197,6 +186,10 @@
.openerp .oe_bounce_container { .openerp .oe_bounce_container {
display: inline-block; display: inline-block;
} }
.openerp .text-tag .text-button {
height: auto !important;
min-height: 16px;
}
.openerp .ui-tabs { .openerp .ui-tabs {
position: static; position: static;
} }
@ -1136,6 +1129,7 @@
height: 40px; height: 40px;
width: 157px; width: 157px;
margin: 14px 0; margin: 14px 0;
border: 0;
} }
.openerp .oe_footer { .openerp .oe_footer {
position: fixed; position: fixed;
@ -2306,16 +2300,16 @@
text-align: justify; text-align: justify;
} }
.openerp .oe_form_editable .oe_form .oe_form_field_integer input { .openerp .oe_form_editable .oe_form .oe_form_field_integer input {
width: 6em !important; width: 6em;
} }
.openerp .oe_form_editable .oe_form .oe_form_field_float input { .openerp .oe_form_editable .oe_form .oe_form_field_float input {
width: 7em !important; width: 7em;
} }
.openerp .oe_form_editable .oe_form .oe_form_field_date input { .openerp .oe_form_editable .oe_form .oe_form_field_date input {
width: 7.5em !important; width: 7.5em;
} }
.openerp .oe_form_editable .oe_form .oe_form_field_datetime input { .openerp .oe_form_editable .oe_form .oe_form_field_datetime input {
width: 11.5em !important; width: 11.5em;
} }
.openerp .oe_hidden_input_file { .openerp .oe_hidden_input_file {
position: relative; position: relative;
@ -2361,6 +2355,77 @@
.openerp .oe_form .oe_form_field_image:hover .oe_form_field_image_controls { .openerp .oe_form .oe_form_field_image:hover .oe_form_field_image_controls {
display: block; display: block;
} }
.openerp .oe_fileupload {
display: inline-block;
clear: both;
width: 100%;
}
.openerp .oe_fileupload .oe_add {
float: left;
position: relative;
width: 100%;
left: 2px;
top: 7px;
}
.openerp .oe_fileupload .oe_add button {
display: inline;
height: 24px;
font-size: 12px;
line-height: 12px;
vertical-align: middle;
}
.openerp .oe_fileupload .oe_add button.oe_attach {
width: 24px;
overflow: hidden;
width: 24px;
overflow: hidden;
background: transparent;
color: #7c7bad;
box-shadow: none;
border: none;
text-shadow: none;
}
.openerp .oe_fileupload .oe_add button.oe_attach .oe_e {
position: relative;
top: -1px;
left: -9px;
}
.openerp .oe_fileupload .oe_add input.oe_form_binary_file {
display: inline-block;
margin-left: -5px;
height: 28px;
width: 52px;
margin-top: -26px;
}
.openerp .oe_fileupload .oe_add .oe_attach_label {
color: #7c7bad;
margin-left: -3px;
}
.openerp .oe_fileupload .oe_attachments {
margin-bottom: 4px;
margin-right: 0px;
font-size: 12px;
border-radius: 2px;
border: solid 1px rgba(124, 123, 173, 0.14);
}
.openerp .oe_fileupload .oe_attachments .oe_attachment {
padding: 2px;
padding-left: 4px;
padding-right: 4px;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e {
font-size: 23px;
margin-top: -5px;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e:hover {
text-decoration: none;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(odd) {
background: white;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) {
background: #f4f5fa;
}
.openerp .oe_form_field_many2one td:first-child { .openerp .oe_form_field_many2one td:first-child {
position: relative; position: relative;
} }
@ -2374,6 +2439,9 @@
float: right; float: right;
padding-left: 2px; padding-left: 2px;
} }
.openerp .oe_form_field_many2one input {
padding-right: 13px;
}
.openerp.ui-autocomplete li.oe_m2o_dropdown_option a { .openerp.ui-autocomplete li.oe_m2o_dropdown_option a {
font-style: italic; font-style: italic;
padding-left: 2em; padding-left: 2em;
@ -2578,32 +2646,33 @@
top: 5px; top: 5px;
} }
.openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_cm_button { .openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_cm_button {
display: none; line-height: 19px;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_input_icon {
margin-top: 5px;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field {
min-width: 0;
max-width: none;
} }
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea { .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea {
height: 27px; height: 27px;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea {
-moz-border-radius: 0; -moz-border-radius: 0;
-webkit-border-radius: 0; -webkit-border-radius: 0;
border-radius: 0; border-radius: 0;
border: 1px solid #aaaaff; border: 1px solid #aaaaff;
margin: 0; margin: 0;
} }
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field select {
min-width: 0;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input { .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input {
text-align: right; text-align: right;
width: 100% !important; width: 100% !important;
} }
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime > span, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date > span {
width: 100% !important;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master { .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master {
width: 100% !important; width: 100% !important;
} }
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_field_float, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_view_integer, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date {
min-width: 0 !important;
max-width: none !important;
}
.openerp .oe_list_group_name { .openerp .oe_list_group_name {
white-space: nowrap; white-space: nowrap;
} }
@ -2668,6 +2737,9 @@
text-align: right !important; text-align: right !important;
max-width: 100px; max-width: 100px;
} }
.openerp .oe_list_content td.oe_list_field_date, .openerp .oe_list_content th.oe_list_header_date {
min-width: 6em;
}
.openerp .oe_list_content > thead { .openerp .oe_list_content > thead {
border-bottom: 2px solid #cacaca; border-bottom: 2px solid #cacaca;
background: #eeeeee; background: #eeeeee;
@ -2735,6 +2807,10 @@
.openerp .oe_list_content > tbody > tr > td.oe_list_checkbox:first-child:after, .openerp .oe_list_content > tbody > tr th.oe_list_checkbox:first-child:after { .openerp .oe_list_content > tbody > tr > td.oe_list_checkbox:first-child:after, .openerp .oe_list_content > tbody > tr th.oe_list_checkbox:first-child:after {
border-width: 0; border-width: 0;
} }
.openerp .oe_list_content > tbody > tr > td.oe_list_field_boolean input {
filter: alpha(opacity=50);
opacity: 0.5;
}
.openerp .oe_list_content > tbody > tr:nth-child(odd) { .openerp .oe_list_content > tbody > tr:nth-child(odd) {
background-color: #f0f0fa; background-color: #f0f0fa;
background-color: #f0f0fa; background-color: #f0f0fa;
@ -2882,78 +2958,6 @@
color: #333333; color: #333333;
} }
.openerp .oe_fileupload {
display: inline-block;
clear: both;
width: 100%;
}
.openerp .oe_fileupload .oe_add {
float: left;
position: relative;
width: 100%;
left: 2px;
top: 7px;
}
.openerp .oe_fileupload .oe_add button {
display: inline;
height: 24px;
font-size: 12px;
line-height: 12px;
vertical-align: middle;
}
.openerp .oe_fileupload .oe_add button.oe_attach {
width: 24px;
overflow: hidden;
width: 24px;
overflow: hidden;
background: transparent;
color: #7c7bad;
box-shadow: none;
border: none;
text-shadow: none;
}
.openerp .oe_fileupload .oe_add button.oe_attach .oe_e {
position: relative;
top: -1px;
left: -9px;
}
.openerp .oe_fileupload .oe_add input.oe_form_binary_file {
display: inline-block;
margin-left: -5px;
height: 28px;
width: 52px;
margin-top: -26px;
}
.openerp .oe_fileupload .oe_add .oe_attach_label {
color: #7c7bad;
margin-left: -3px;
}
.openerp .oe_fileupload .oe_attachments {
margin-bottom: 4px;
margin-right: 0px;
font-size: 12px;
border-radius: 2px;
border: solid 1px rgba(124, 123, 173, 0.14);
}
.openerp .oe_fileupload .oe_attachments .oe_attachment {
padding: 2px;
padding-left: 4px;
padding-right: 4px;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e {
font-size: 23px;
margin-top: -5px;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e:hover {
text-decoration: none;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(odd) {
background: white;
}
.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) {
background: #f4f5fa;
}
.kitten-mode-activated { .kitten-mode-activated {
background-image: url(http://placekitten.com/g/1365/769); background-image: url(http://placekitten.com/g/1365/769);
background-size: cover; background-size: cover;
@ -3012,8 +3016,8 @@ div.ui-widget-overlay {
.openerp { .openerp {
text-shadow: none; text-shadow: none;
} }
.openerp .oe_header_row, .openerp ul.oe_header, .openerp div.oe_mail_thread_action, .openerp .oe_mail_recthread_actions, .openerp .oe_button_box, .openerp .oe_form button, .openerp button.oe_invite, .openerp .oe_form header, .openerp .openerp .oe_notebook > li.ui-state-default { .openerp .oe_header_row, .openerp ul.oe_header, .openerp div.oe_mail_thread_action, .openerp .oe_mail_recthread_actions, .openerp .oe_button_box, .openerp .oe_form button, .openerp button.oe_invite, .openerp .oe_form header, .openerp .openerp .oe_notebook > li.ui-state-default, .openerp .oe_topbar, .openerp .oe_leftbar, .openerp .oe_loading {
display: none; display: none !important;
} }
.openerp .oe_list_content button, .openerp .oe_list_content input[type=checkbox] { .openerp .oe_list_content button, .openerp .oe_list_content input[type=checkbox] {
visibility: hidden; visibility: hidden;

View File

@ -140,18 +140,6 @@ $sheet-padding: 16px
// }}} // }}}
@media print
.oe_topbar, .oe_leftbar, .oe_loading
display: none !important
.openerp.openerp_webclient_container
height: 100%
// jQueryUI css bug fixing
.text-tag .text-button
height: auto !important
min-height: 16px
.openerp .openerp
// Global style {{{ // Global style {{{
padding: 0 padding: 0
@ -161,6 +149,8 @@ $sheet-padding: 16px
font-size: 13px font-size: 13px
background: white background: white
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5) text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5)
&.openerp_webclient_container
height: 100%
// }}} // }}}
//Placeholder style{{{ //Placeholder style{{{
\:-moz-placeholder \:-moz-placeholder
@ -254,6 +244,11 @@ $sheet-padding: 16px
.oe_bounce_container .oe_bounce_container
display: inline-block display: inline-block
// Bug lp:1051746
.text-tag .text-button
height: auto !important
min-height: 16px
// bug noted in jquery ui CSS doesn't seem to occur in IE9, // bug noted in jquery ui CSS doesn't seem to occur in IE9,
// so remove position:relative // so remove position:relative
.ui-tabs .ui-tabs
@ -928,6 +923,7 @@ $sheet-padding: 16px
height: 40px height: 40px
width: 157px width: 157px
margin: 14px 0 margin: 14px 0
border: 0
.oe_footer .oe_footer
position: fixed position: fixed
bottom: 0 bottom: 0
@ -1837,13 +1833,13 @@ $sheet-padding: 16px
.oe_form_editable .oe_form_editable
.oe_form .oe_form
.oe_form_field_integer input .oe_form_field_integer input
width: 6em !important width: 6em
.oe_form_field_float input .oe_form_field_float input
width: 7em !important width: 7em
.oe_form_field_date input .oe_form_field_date input
width: 7.5em !important width: 7.5em
.oe_form_field_datetime input .oe_form_field_datetime input
width: 11.5em !important width: 11.5em
// }}} // }}}
// FormView.fields_binary {{{ // FormView.fields_binary {{{
/* http://www.quirksmode.org/dom/inputfile.html /* http://www.quirksmode.org/dom/inputfile.html
@ -1886,6 +1882,64 @@ $sheet-padding: 16px
@include box-sizing(border) @include box-sizing(border)
&:hover .oe_form_field_image_controls &:hover .oe_form_field_image_controls
display: block display: block
.oe_fileupload
display: inline-block
clear: both
width: 100%
.oe_add
float: left
position: relative
width: 100%
left: +2px
top: +7px
button
display: inline
height: 24px
font-size: 12px
line-height: 12px
vertical-align: middle
button.oe_attach
width: 24px
overflow: hidden
width: 24px
overflow: hidden
background: transparent
color: #7C7BAD
box-shadow: none
border: none
text-shadow: none
.oe_e
position: relative
top: -1px
left: -9px
input.oe_form_binary_file
display: inline-block
margin-left: -5px
height: 28px
width: 52px
margin-top: -26px
.oe_attach_label
color: #7C7BAD
margin-left: -3px
.oe_attachments
margin-bottom: 4px
margin-right: 0px
font-size: 12px
border-radius: 2px
border: solid 1px rgba(124,123,173,0.14)
.oe_attachment
padding: 2px
padding-left: 4px
padding-right: 4px
.oe_e
font-size: 23px
margin-top: -5px
.oe_e:hover
text-decoration: none
.oe_attachment:nth-child(odd)
background: white
.oe_attachment:nth-child(even)
background: #F4F5FA
// }}} // }}}
// FormView.many2one {{{ // FormView.many2one {{{
.oe_form_field_many2one .oe_form_field_many2one
@ -1899,6 +1953,8 @@ $sheet-padding: 16px
line-height: 14px line-height: 14px
float: right float: right
padding-left: 2px padding-left: 2px
input
padding-right: 13px
&.ui-autocomplete &.ui-autocomplete
li.oe_m2o_dropdown_option a li.oe_m2o_dropdown_option a
font-style: italic font-style: italic
@ -2041,26 +2097,26 @@ $sheet-padding: 16px
.oe_m2o_drop_down_button .oe_m2o_drop_down_button
top: 5px top: 5px
.oe_m2o_cm_button .oe_m2o_cm_button
display: none line-height: 19px
.oe_input_icon
margin-top: 5px
.oe_form_field .oe_form_field
min-width: 0
max-width: none
input, textarea input, textarea
height: $row-height height: $row-height
input, textarea
@include radius(0) @include radius(0)
border: 1px solid #aaf border: 1px solid #aaf
margin: 0 margin: 0
input, textarea, select
min-width: 0
&.oe_form_field_float,&.oe_form_view_integer &.oe_form_field_float,&.oe_form_view_integer
input input
text-align: right text-align: right
width: 100% !important width: 100% !important
&.oe_form_field_datetime,&.oe_form_field_date &.oe_form_field_datetime,&.oe_form_field_date
> span
width: 100% !important
input.oe_datepicker_master input.oe_datepicker_master
width: 100% !important width: 100% !important
.oe_form_field_float,.oe_form_view_integer,&.oe_form_field_datetime,&.oe_form_field_date
min-width: 0 !important
max-width: none !important
.oe_list_group_name .oe_list_group_name
white-space: nowrap white-space: nowrap
// }}} // }}}
@ -2119,6 +2175,8 @@ $sheet-padding: 16px
td.oe_number td.oe_number
text-align: right !important text-align: right !important
max-width: 100px max-width: 100px
td.oe_list_field_date, th.oe_list_header_date
min-width: 6em
> thead > thead
border-bottom: 2px solid #cacaca border-bottom: 2px solid #cacaca
background: #eee background: #eee
@ -2168,6 +2226,8 @@ $sheet-padding: 16px
width: 17px width: 17px
&:after &:after
border-width: 0 border-width: 0
> td.oe_list_field_boolean input
@include opacity()
> tr:nth-child(odd) > tr:nth-child(odd)
background-color: #f0f0fa background-color: #f0f0fa
@include vertical-gradient(#f0f0fa, #eeeef6) @include vertical-gradient(#f0f0fa, #eeeef6)
@ -2282,67 +2342,6 @@ $sheet-padding: 16px
float: right float: right
color: #333 color: #333
// }}} // }}}
.openerp
.oe_fileupload
display: inline-block
clear: both
width: 100%
.oe_add
float: left
position: relative
width: 100%
left: +2px
top: +7px
button
display: inline
height: 24px
font-size: 12px
line-height: 12px
vertical-align: middle
button.oe_attach
width: 24px
overflow: hidden
width: 24px
overflow: hidden
background: transparent
color: #7C7BAD
box-shadow: none
border: none
text-shadow: none
.oe_e
position: relative
top: -1px
left: -9px
input.oe_form_binary_file
display: inline-block
margin-left: -5px
height: 28px
width: 52px
margin-top: -26px
.oe_attach_label
color: #7C7BAD
margin-left: -3px
.oe_attachments
margin-bottom: 4px
margin-right: 0px
font-size: 12px
border-radius: 2px
border: solid 1px rgba(124,123,173,0.14)
.oe_attachment
padding: 2px
padding-left: 4px
padding-right: 4px
.oe_e
font-size: 23px
margin-top: -5px
.oe_e:hover
text-decoration: none
.oe_attachment:nth-child(odd)
background: white
.oe_attachment:nth-child(even)
background: #F4F5FA
// Kitten Mode {{{ // Kitten Mode {{{
.kitten-mode-activated .kitten-mode-activated
background-image: url(http://placekitten.com/g/1365/769) background-image: url(http://placekitten.com/g/1365/769)
@ -2352,11 +2351,13 @@ $sheet-padding: 16px
opacity: 0.70 opacity: 0.70
// }}} // }}}
// jQueryUI top level {{{
// The jQuery-ui overlay and Autocomplete are outside the .openerp div, please don't add indentation !!! // The jQuery-ui overlay and Autocomplete are outside the .openerp div, please don't add indentation !!!
div.ui-widget-overlay div.ui-widget-overlay
background: black background: black
@include opacity(0.3) @include opacity(0.3)
// TODO: I think only the overlay is problematic, the other top level widgets should use $.fn.openerpClass()
// eg: $el.autocomplete().openerpClass();
.ui-widget .ui-widget
font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif
color: #4c4c4c color: #4c4c4c
@ -2383,11 +2384,14 @@ div.ui-widget-overlay
.ui-corner-all .ui-corner-all
@include radius(3px) @include radius(3px)
// }}}
// @media print {{{
@media print @media print
.openerp .openerp
.oe_header_row, ul.oe_header, div.oe_mail_thread_action, .oe_mail_recthread_actions, .oe_button_box, .oe_form button, button.oe_invite, .oe_form header, .openerp .oe_notebook > li.ui-state-default .oe_header_row, ul.oe_header, div.oe_mail_thread_action, .oe_mail_recthread_actions, .oe_button_box, .oe_form button, button.oe_invite, .oe_form header, .openerp .oe_notebook > li.ui-state-default, .oe_topbar, .oe_leftbar, .oe_loading
display: none // We use !important here because jQuery adds @style = display: block on elements when using $.fn.show()
display: none !important
.oe_list_content .oe_list_content
button, input[type=checkbox] button, input[type=checkbox]
visibility: hidden visibility: hidden
@ -2413,5 +2417,7 @@ div.ui-widget-overlay
background: none background: none
.openerp div.oe_mail_wall .openerp div.oe_mail_wall
overflow: hidden !important overflow: hidden !important
// }}}
// au BufWritePost,FileWritePost *.sass :!sass --style expanded --line-numbers <afile> > "%:p:r.css" // au BufWritePost,FileWritePost *.sass :!sass --style expanded --line-numbers <afile> > "%:p:r.css"
// vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker: // vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker:

View File

@ -19,12 +19,14 @@
/** /**
* OpenERP instance constructor * OpenERP instance constructor
* *
* @param {Array} modules list of modules to initialize * @param {Array|String} modules list of modules to initialize
*/ */
init: function(modules) { init: function(modules) {
// By default only web will be loaded, the rest will be by loaded if (modules === "fuck your shit, don't load anything you cunt") {
// by openerp.web.Session on the first session_authenticate modules = [];
modules = _.union(['web'], modules || []); } else {
modules = _.union(['web'], modules || []);
}
var new_instance = { var new_instance = {
// links to the global openerp // links to the global openerp
_openerp: openerp, _openerp: openerp,

View File

@ -48,7 +48,7 @@ instance.web.Notification = instance.web.Widget.extend({
*/ */
instance.web.dialog = function(element) { instance.web.dialog = function(element) {
var result = element.dialog.apply(element, _.rest(_.toArray(arguments))); var result = element.dialog.apply(element, _.rest(_.toArray(arguments)));
result.dialog("widget").addClass("openerp"); result.dialog("widget").openerpClass();
return result; return result;
}; };
@ -190,7 +190,14 @@ instance.web.Dialog = instance.web.Widget.extend({
}); });
instance.web.CrashManager = instance.web.Class.extend({ instance.web.CrashManager = instance.web.Class.extend({
init: function() {
this.active = true;
},
rpc_error: function(error) { rpc_error: function(error) {
if (!this.active) {
return;
}
if (error.data.fault_code) { if (error.data.fault_code) {
var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); var split = ("" + error.data.fault_code).split('\n')[0].split(' -- ');
if (split.length > 1) { if (split.length > 1) {
@ -205,6 +212,9 @@ instance.web.CrashManager = instance.web.Class.extend({
} }
}, },
show_warning: function(error) { show_warning: function(error) {
if (!this.active) {
return;
}
instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), { instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
title: "OpenERP " + _.str.capitalize(error.type), title: "OpenERP " + _.str.capitalize(error.type),
buttons: [ buttons: [
@ -213,7 +223,9 @@ instance.web.CrashManager = instance.web.Class.extend({
}); });
}, },
show_error: function(error) { show_error: function(error) {
var self = this; if (!this.active) {
return;
}
var buttons = {}; var buttons = {};
buttons[_t("Ok")] = function() { buttons[_t("Ok")] = function() {
$(this).dialog("close"); $(this).dialog("close");
@ -310,7 +322,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.db_list = null; self.db_list = null;
}); });
var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) { var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
self.lang_list = result.lang_list; self.lang_list = result;
}); });
return $.when(fetch_db, fetch_langs).done(self.do_render); return $.when(fetch_db, fetch_langs).done(self.do_render);
}, },
@ -333,6 +345,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label"); self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
self.$el.find("label").addClass("oe_form_label"); self.$el.find("label").addClass("oe_form_label");
self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
@ -418,6 +431,18 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.do_action(client_action); self.do_action(client_action);
}); });
}, },
do_duplicate: function(form) {
var self = this;
var fields = $(form).serializeArray();
self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
if (result.error) {
self.display_error(result);
return;
}
self.do_notify("Duplicating database", "The database has been duplicated.");
self.start();
});
},
do_drop: function(form) { do_drop: function(form) {
var self = this; var self = this;
var $form = $(form), var $form = $(form),
@ -518,6 +543,9 @@ instance.web.Login = instance.web.Widget.extend({
this.selected_db = null; this.selected_db = null;
this.selected_login = null; this.selected_login = null;
this.params = action.params || {}; this.params = action.params || {};
if (_.isEmpty(this.params)) {
this.params = $.bbq.getState(true);
}
if (this.params.login_successful) { if (this.params.login_successful) {
this.on('login_successful', this, this.params.login_successful); this.on('login_successful', this, this.params.login_successful);
@ -619,9 +647,35 @@ instance.web.Login = instance.web.Widget.extend({
}); });
instance.web.client_actions.add("login", "instance.web.Login"); instance.web.client_actions.add("login", "instance.web.Login");
/**
* Redirect to url by replacing window.location
* If wait is true, sleep 1s and wait for the server i.e. after a restart.
*/
instance.web.redirect = function(url, wait) {
// Dont display a dialog if some xmlhttprequest are in progress
if (instance.client && instance.client.crashmanager) {
instance.client.crashmanager.active = false;
}
var wait_server = function() {
instance.session.rpc("/web/webclient/version_info", {}).done(function() {
window.location = url;
}).fail(function() {
setTimeout(wait_server, 250);
});
};
if (wait) {
setTimeout(wait_server, 1000);
} else {
window.location = url;
}
};
/** /**
* Client action to reload the whole interface. * Client action to reload the whole interface.
* If params has an entry 'menu_id', it opens the given menu entry. * If params.menu_id, it opens the given menu entry.
* If params.wait, reload will wait the openerp server to be reachable before reloading
*/ */
instance.web.Reload = function(parent, action) { instance.web.Reload = function(parent, action) {
var params = action.params || {}; var params = action.params || {};
@ -637,8 +691,8 @@ instance.web.Reload = function(parent, action) {
hash = "#menu_id=" + menu_id; hash = "#menu_id=" + menu_id;
} }
var url = l.protocol + "//" + l.host + l.pathname + search + hash; var url = l.protocol + "//" + l.host + l.pathname + search + hash;
window.onerror = function() {};
window.location = url; instance.web.redirect(url, params.wait);
}; };
instance.web.client_actions.add("reload", "instance.web.Reload"); instance.web.client_actions.add("reload", "instance.web.Reload");
@ -648,7 +702,7 @@ instance.web.client_actions.add("reload", "instance.web.Reload");
*/ */
instance.web.HistoryBack = function(parent) { instance.web.HistoryBack = function(parent) {
if (!parent.history_back()) { if (!parent.history_back()) {
window.location = '/' + (window.location.search || ''); instance.web.Home(parent);
} }
}; };
instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
@ -656,11 +710,10 @@ instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
/** /**
* Client action to go back home. * Client action to go back home.
*/ */
instance.web.Home = instance.web.Widget.extend({ instance.web.Home = function(parent, action) {
init: function(parent) { var url = '/' + (window.location.search || '');
window.location = '/' + (window.location.search || ''); instance.web.redirect(url, action.params && action.params.wait);
} };
});
instance.web.client_actions.add("home", "instance.web.Home"); instance.web.client_actions.add("home", "instance.web.Home");
instance.web.ChangePassword = instance.web.Widget.extend({ instance.web.ChangePassword = instance.web.Widget.extend({
@ -887,7 +940,7 @@ instance.web.UserMenu = instance.web.Widget.extend({
if(res.company_id[0] > 1) if(res.company_id[0] > 1)
topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]); topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
self.$el.find('.oe_topbar_name').text(topbar_name); self.$el.find('.oe_topbar_name').text(topbar_name);
var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=image_small&id=%s', self.session.prefix, self.session.session_id, self.session.uid); var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
$avatar.attr('src', avatar_src); $avatar.attr('src', avatar_src);
}); });
}; };
@ -899,7 +952,7 @@ instance.web.UserMenu = instance.web.Widget.extend({
on_menu_settings: function() { on_menu_settings: function() {
var self = this; var self = this;
if (!this.getParent().has_uncommitted_changes()) { if (!this.getParent().has_uncommitted_changes()) {
self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }, function(result) { self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
result.res_id = instance.session.uid; result.res_id = instance.session.uid;
self.getParent().action_manager.do_action(result); self.getParent().action_manager.do_action(result);
}); });
@ -930,6 +983,7 @@ instance.web.Client = instance.web.Widget.extend({
return instance.session.session_bind(this.origin).then(function() { return instance.session.session_bind(this.origin).then(function() {
var $e = $(QWeb.render(self._template, {})); var $e = $(QWeb.render(self._template, {}));
self.replaceElement($e); self.replaceElement($e);
$e.openerpClass();
self.bind_events(); self.bind_events();
return self.show_common(); return self.show_common();
}); });
@ -1106,10 +1160,10 @@ instance.web.WebClient = instance.web.Client.extend({
var self = this; var self = this;
var state = event.getState(true); var state = event.getState(true);
if (!_.isEqual(this._current_state, state)) { if (!_.isEqual(this._current_state, state)) {
if(state.action_id === undefined && state.menu_id) { if(!state.action && state.menu_id) {
self.menu.has_been_loaded.done(function() { self.menu.has_been_loaded.done(function() {
self.menu.do_reload().done(function() { self.menu.do_reload().done(function() {
self.menu.menu_click(state.menu_id) self.menu.menu_click(state.menu_id);
}); });
}); });
} else { } else {

View File

@ -774,10 +774,10 @@ instance.web.Widget = instance.web.Class.extend(instance.web.PropertiesMixin, {
} }
return false; return false;
}, },
rpc: function(url, data, success, error) { rpc: function(url, data, options) {
var def = $.Deferred().done(success).fail(error); var def = $.Deferred();
var self = this; var self = this;
instance.session.rpc(url, data).done(function() { instance.session.rpc(url, data, options).done(function() {
if (!self.isDestroyed()) if (!self.isDestroyed())
def.resolve.apply(def, arguments); def.resolve.apply(def, arguments);
}).fail(function() { }).fail(function() {
@ -1231,12 +1231,14 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
* *
* @param {String} url RPC endpoint * @param {String} url RPC endpoint
* @param {Object} params call parameters * @param {Object} params call parameters
* @param {Object} options additional options for rpc call
* @param {Function} success_callback function to execute on RPC call success * @param {Function} success_callback function to execute on RPC call success
* @param {Function} error_callback function to execute on RPC call failure * @param {Function} error_callback function to execute on RPC call failure
* @returns {jQuery.Deferred} jquery-provided ajax deferred * @returns {jQuery.Deferred} jquery-provided ajax deferred
*/ */
rpc: function(url, params) { rpc: function(url, params, options) {
var self = this; var self = this;
options = options || {};
// url can be an $.ajax option object // url can be an $.ajax option object
if (_.isString(url)) { if (_.isString(url)) {
url = { url: url }; url = { url: url };
@ -1251,10 +1253,12 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
id: _.uniqueId('r') id: _.uniqueId('r')
}; };
var deferred = $.Deferred(); var deferred = $.Deferred();
this.trigger('request', url, payload); if (! options.shadow)
this.trigger('request', url, payload);
var request = this.rpc_function(url, payload).done( var request = this.rpc_function(url, payload).done(
function (response, textStatus, jqXHR) { function (response, textStatus, jqXHR) {
self.trigger('response', response); if (! options.shadow)
self.trigger('response', response);
if (!response.error) { if (!response.error) {
if (url.url === '/web/session/eval_domain_and_context') { if (url.url === '/web/session/eval_domain_and_context') {
self.test_eval(params, response.result); self.test_eval(params, response.result);
@ -1268,7 +1272,8 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
} }
).fail( ).fail(
function(jqXHR, textStatus, errorThrown) { function(jqXHR, textStatus, errorThrown) {
self.trigger('response_failed', jqXHR); if (! options.shadow)
self.trigger('response_failed', jqXHR);
var error = { var error = {
code: -32098, code: -32098,
message: "XmlHttpRequestError " + errorThrown, message: "XmlHttpRequestError " + errorThrown,
@ -1276,7 +1281,7 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
}; };
deferred.reject(error, $.Event()); deferred.reject(error, $.Event());
}); });
// Allow deferred user to disable on_rpc_error in fail // Allow deferred user to disable rpc_error call in fail
deferred.fail(function() { deferred.fail(function() {
deferred.fail(function(error, event) { deferred.fail(function(error, event) {
if (!event.isDefaultPrevented()) { if (!event.isDefaultPrevented()) {
@ -1309,9 +1314,18 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
// extracted from payload to set on the url // extracted from payload to set on the url
var data = { var data = {
session_id: this.session_id, session_id: this.session_id,
id: payload.id id: payload.id,
sid: this.httpsessionid,
}; };
url.url = this.get_url(url.url);
var set_sid = function (response, textStatus, jqXHR) {
// If response give us the http session id, we store it for next requests...
if (response.httpsessionid) {
self.httpsessionid = response.httpsessionid;
}
};
url.url = this.url(url.url, null);
var ajax = _.extend({ var ajax = _.extend({
type: "GET", type: "GET",
dataType: 'jsonp', dataType: 'jsonp',
@ -1326,7 +1340,7 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
if(payload_url.length < 2000) { if(payload_url.length < 2000) {
// Direct jsonp request // Direct jsonp request
ajax.data.r = payload_str; ajax.data.r = payload_str;
return $.ajax(ajax); return $.ajax(ajax).done(set_sid);
} else { } else {
// Indirect jsonp request // Indirect jsonp request
var ifid = _.uniqueId('oe_rpc_iframe'); var ifid = _.uniqueId('oe_rpc_iframe');
@ -1364,11 +1378,20 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
}); });
// append the iframe to the DOM (will trigger the first load) // append the iframe to the DOM (will trigger the first load)
$form.after($iframe); $form.after($iframe);
return deferred; return deferred.done(set_sid);
} }
}, },
get_url: function (file) {
return this.prefix + file; url: function(path, params) {
var qs = '';
if (!_.isNull(params)) {
params = _.extend(params || {}, {session_id: this.session_id});
if (this.httpsessionid) {
params.sid = this.httpsessionid;
}
qs = '?' + $.param(params);
}
return this.prefix + path + qs;
}, },
}); });

View File

@ -3,10 +3,13 @@
*--------------------------------------------------------*/ *--------------------------------------------------------*/
var console; var console;
if (!console) { if (!console) {
console = {log: function () {}}; // Even IE9 only exposes console object if debug window opened
} console = {};
if (!console.debug) { ('log error debug info warn assert clear dir dirxml trace group'
console.debug = console.log; + ' groupCollapsed groupEnd time timeEnd profile profileEnd count'
+ ' exception').split(/\s+/).forEach(function(property) {
console[property] = _.identity;
});
} }
openerp.web.coresetup = function(instance) { openerp.web.coresetup = function(instance) {
@ -19,9 +22,9 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
this.name = instance._session_id; this.name = instance._session_id;
this.qweb_mutex = new $.Mutex(); this.qweb_mutex = new $.Mutex();
}, },
rpc: function(url, params) { rpc: function(url, params, options) {
params.session_id = this.session_id; params.session_id = this.session_id;
return this._super(url, params); return this._super(url, params, options);
}, },
/** /**
* Setup a sessionm * Setup a sessionm
@ -198,7 +201,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
var self = this; var self = this;
_.each(files, function (file) { _.each(files, function (file) {
$('head').append($('<link>', { $('head').append($('<link>', {
'href': self.get_url(file), 'href': self.url(file, null),
'rel': 'stylesheet', 'rel': 'stylesheet',
'type': 'text/css' 'type': 'text/css'
})); }));
@ -207,11 +210,11 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
load_js: function(files) { load_js: function(files) {
var self = this; var self = this;
var d = $.Deferred(); var d = $.Deferred();
if(files.length != 0) { if(files.length !== 0) {
var file = files.shift(); var file = files.shift();
var tag = document.createElement('script'); var tag = document.createElement('script');
tag.type = 'text/javascript'; tag.type = 'text/javascript';
tag.src = self.get_url(file); tag.src = self.url(file, null);
tag.onload = tag.onreadystatechange = function() { tag.onload = tag.onreadystatechange = function() {
if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done ) if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
return; return;
@ -457,6 +460,16 @@ $.fn.getAttributes = function() {
} }
return o; return o;
} }
$.fn.openerpClass = function(additionalClass) {
// This plugin should be applied on top level elements
additionalClass = additionalClass || '';
if (!!$.browser.msie) {
additionalClass += ' openerp_ie';
}
return this.each(function() {
$(this).addClass('openerp ' + additionalClass);
});
};
/** Jquery extentions */ /** Jquery extentions */
$.Mutex = (function() { $.Mutex = (function() {

View File

@ -278,9 +278,10 @@ instance.web.Model = instance.web.Class.extend({
* @param {String} method name of the method to call * @param {String} method name of the method to call
* @param {Array} [args] positional arguments * @param {Array} [args] positional arguments
* @param {Object} [kwargs] keyword arguments * @param {Object} [kwargs] keyword arguments
* @param {Object} [options] additional options for the rpc() method
* @returns {jQuery.Deferred<>} call result * @returns {jQuery.Deferred<>} call result
*/ */
call: function (method, args, kwargs) { call: function (method, args, kwargs, options) {
args = args || []; args = args || [];
kwargs = kwargs || {}; kwargs = kwargs || {};
if (!_.isArray(args)) { if (!_.isArray(args)) {
@ -294,7 +295,7 @@ instance.web.Model = instance.web.Class.extend({
method: method, method: method,
args: args, args: args,
kwargs: kwargs kwargs: kwargs
}); }, options);
}, },
/** /**
* Fetches a Query instance bound to this model, for searching * Fetches a Query instance bound to this model, for searching
@ -721,10 +722,10 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
this.last_default_get = {}; this.last_default_get = {};
}, },
default_get: function(fields, options) { default_get: function(fields, options) {
return this._super(fields, options).done(this.on_default_get); var self = this;
}, return this._super(fields, options).done(function(res) {
on_default_get: function(res) { self.last_default_get = res;
this.last_default_get = res; });
}, },
create: function(data) { create: function(data) {
var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,

View File

@ -83,6 +83,42 @@ instance.web.strip_raw_chars = function (value) {
var normalize_format = function (format) { var normalize_format = function (format) {
return Date.normalizeFormat(instance.web.strip_raw_chars(format)); return Date.normalizeFormat(instance.web.strip_raw_chars(format));
}; };
/**
* Check with a scary heuristic if the value is a bin_size or not.
* If not, compute an approximate size out of the base64 encoded string.
*
* @param {String} value original format
*/
instance.web.binary_to_binsize = function (value) {
if (!value) {
return instance.web.human_size(0);
}
if (value.substr(0, 10).indexOf(' ') == -1) {
// Computing approximate size out of base64 encoded string
// http://en.wikipedia.org/wiki/Base64#MIME
return instance.web.human_size(value.length / 1.37);
} else {
// already bin_size
return value;
}
};
/**
* Returns a human readable size
*
* @param {Number} numner of bytes
*/
instance.web.human_size = function(size) {
var units = _t("Bytes,Kb,Mb,Gb,Tb,Pb,Eb,Zb,Yb").split(',');
var i = 0;
while (size >= 1024) {
size /= 1024;
++i;
}
return size.toFixed(2) + ' ' + units[i];
};
/** /**
* Formats a single atomic value based on a field descriptor * Formats a single atomic value based on a field descriptor
* *

View File

@ -349,7 +349,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
$.when(load_view) $.when(load_view)
.then(function(r) { .then(function(r) {
self.search_view_loaded(r) self.search_view_loaded(r)
}).fail(function () { }, function () {
self.ready.reject.apply(null, arguments); self.ready.reject.apply(null, arguments);
}); });
} }
@ -459,7 +459,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
complete_global_search: function (req, resp) { complete_global_search: function (req, resp) {
$.when.apply(null, _(this.inputs).chain() $.when.apply(null, _(this.inputs).chain()
.invoke('complete', req.term) .invoke('complete', req.term)
.value()).done(function () { .value()).then(function () {
resp(_(_(arguments).compact()).flatten(true)); resp(_(_(arguments).compact()).flatten(true));
}); });
}, },
@ -527,7 +527,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
childView.on('blurred', self, self.proxy('childBlurred')); childView.on('blurred', self, self.proxy('childBlurred'));
}); });
$.when.apply(null, started).done(function () { $.when.apply(null, started).then(function () {
var input_to_focus; var input_to_focus;
// options.at: facet inserted at given index, focus next input // options.at: facet inserted at given index, focus next input
// otherwise just focus last input // otherwise just focus last input
@ -608,7 +608,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
// add Filters to this.inputs, need view.controls filled // add Filters to this.inputs, need view.controls filled
(new instance.web.search.Filters(this)); (new instance.web.search.Filters(this));
// add custom filters to this.inputs // add custom filters to this.inputs
(new instance.web.search.CustomFilters(this)); this.custom_filters = new instance.web.search.CustomFilters(this);
// add Advanced to this.inputs // add Advanced to this.inputs
(new instance.web.search.Advanced(this)); (new instance.web.search.Advanced(this));
}, },
@ -635,16 +635,41 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
// load defaults // load defaults
var defaults_fetched = $.when.apply(null, _(this.inputs).invoke( var defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
'facet_for_defaults', this.defaults)).done(function () { 'facet_for_defaults', this.defaults))
self.query.reset(_(arguments).compact(), {preventSearch: true}); .then(this.proxy('setup_default_query'));
});
return $.when(drawer_started, defaults_fetched) return $.when(drawer_started, defaults_fetched)
.done(function () { .done(function () {
self.trigger("search_view_loaded", data); self.trigger("search_view_loaded", data);
self.ready.resolve(); self.ready.resolve();
}); });
}, },
setup_default_query: function () {
// Hacky implementation of CustomFilters#facet_for_defaults ensure
// CustomFilters will be ready (and CustomFilters#filters will be
// correctly filled) by the time this method executes.
var custom_filters = this.custom_filters.filters;
if (!_(custom_filters).isEmpty()) {
// Check for any is_default custom filter
var personal_filter = _(custom_filters).find(function (filter) {
return filter.user_id && filter.is_default;
});
if (personal_filter) {
this.custom_filters.enable_filter(personal_filter, true);
return;
}
var global_filter = _(custom_filters).find(function (filter) {
return !filter.user_id && filter.is_default;
});
if (global_filter) {
this.custom_filters.enable_filter(global_filter, true);
return;
}
}
// No custom filter, or no is_default custom filter, apply view defaults
this.query.reset(_(arguments).compact(), {preventSearch: true});
},
/** /**
* Extract search data from the view's facets. * Extract search data from the view's facets.
* *
@ -1460,10 +1485,15 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
instance.web.search.CustomFilters = instance.web.search.Input.extend({ instance.web.search.CustomFilters = instance.web.search.Input.extend({
template: 'SearchView.CustomFilters', template: 'SearchView.CustomFilters',
_in_drawer: true, _in_drawer: true,
init: function () {
this.is_ready = $.Deferred();
this._super.apply(this, arguments);
},
start: function () { start: function () {
var self = this; var self = this;
this.model = new instance.web.Model('ir.filters'); this.model = new instance.web.Model('ir.filters');
this.filters = {}; this.filters = {};
this.$filters = {};
this.view.query this.view.query
.on('remove', function (facet) { .on('remove', function (facet) {
if (!facet.get('is_custom_filter')) { if (!facet.get('is_custom_filter')) {
@ -1479,24 +1509,78 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
// FIXME: local eval of domain and context to get rid of special endpoint // FIXME: local eval of domain and context to get rid of special endpoint
return this.rpc('/web/searchview/get_filters', { return this.rpc('/web/searchview/get_filters', {
model: this.view.model model: this.view.model
}).then(this.proxy('set_filters')); })
.then(this.proxy('set_filters'))
.then(function () {
self.is_ready.resolve(null);
}, function () {
self.is_ready.reject();
});
},
/**
* Special implementation delaying defaults until CustomFilters is loaded
*/
facet_for_defaults: function () {
return this.is_ready;
},
/**
* Generates a mapping key (in the filters and $filter mappings) for the
* filter descriptor object provided (as returned by ``get_filters``).
*
* The mapping key is guaranteed to be unique for a given (user_id, name)
* pair.
*
* @param {Object} filter
* @param {String} filter.name
* @param {Number|Pair<Number, String>} [filter.user_id]
* @return {String} mapping key corresponding to the filter
*/
key_for: function (filter) {
var user_id = filter.user_id;
var uid = (user_id instanceof Array) ? user_id[0] : user_id;
return _.str.sprintf('(%s)%s', uid, filter.name);
},
/**
* Generates a :js:class:`~instance.web.search.Facet` descriptor from a
* filter descriptor
*
* @param {Object} filter
* @param {String} filter.name
* @param {Object} [filter.context]
* @param {Array} [filter.domain]
* @return {Object}
*/
facet_for: function (filter) {
return {
category: _t("Custom Filter"),
icon: 'M',
field: {
get_context: function () { return filter.context; },
get_groupby: function () { return [filter.context]; },
get_domain: function () { return filter.domain; }
},
is_custom_filter: true,
values: [{label: filter.name, value: null}]
};
}, },
clear_selection: function () { clear_selection: function () {
this.$('li.oe_selected').removeClass('oe_selected'); this.$('li.oe_selected').removeClass('oe_selected');
}, },
append_filter: function (filter) { append_filter: function (filter) {
var self = this; var self = this;
var key = _.str.sprintf('(%s)%s', filter.user_id, filter.name); var key = this.key_for(filter);
var $filter; var $filter;
if (key in this.filters) { if (key in this.$filters) {
$filter = this.filters[key]; $filter = this.$filters[key];
} else { } else {
var id = filter.id; var id = filter.id;
$filter = this.filters[key] = $('<li></li>') this.filters[key] = filter;
$filter = this.$filters[key] = $('<li></li>')
.appendTo(this.$('.oe_searchview_custom_list')) .appendTo(this.$('.oe_searchview_custom_list'))
.addClass(filter.user_id ? 'oe_searchview_custom_private' .addClass(filter.user_id ? 'oe_searchview_custom_private'
: 'oe_searchview_custom_public') : 'oe_searchview_custom_public')
.toggleClass('oe_searchview_custom_default', filter.is_default)
.text(filter.name); .text(filter.name);
$('<a class="oe_searchview_custom_delete">x</a>') $('<a class="oe_searchview_custom_delete">x</a>')
@ -1504,33 +1588,30 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
e.stopPropagation(); e.stopPropagation();
self.model.call('unlink', [id]).done(function () { self.model.call('unlink', [id]).done(function () {
$filter.remove(); $filter.remove();
delete self.$filters[key];
delete self.filters[key];
}); });
}) })
.appendTo($filter); .appendTo($filter);
} }
$filter.unbind('click').click(function () { $filter.unbind('click').click(function () {
self.view.query.reset([{ self.enable_filter(filter);
category: _t("Custom Filter"),
icon: 'M',
field: {
get_context: function () { return filter.context; },
get_groupby: function () { return [filter.context]; },
get_domain: function () { return filter.domain; }
},
is_custom_filter: true,
values: [{label: filter.name, value: null}]
}]);
$filter.addClass('oe_selected');
}); });
}, },
enable_filter: function (filter, preventSearch) {
this.view.query.reset([this.facet_for(filter)], {
preventSearch: preventSearch || false});
this.$filters[this.key_for(filter)].addClass('oe_selected');
},
set_filters: function (filters) { set_filters: function (filters) {
_(filters).map(_.bind(this.append_filter, this)); _(filters).map(_.bind(this.append_filter, this));
}, },
save_current: function () { save_current: function () {
var self = this; var self = this;
var $name = this.$('input:first'); var $name = this.$('input:first');
var private_filter = !this.$('input:last').prop('checked'); var private_filter = !this.$('#oe_searchview_custom_public').prop('checked');
var set_as_default = this.$('#oe_searchview_custom_default').prop('checked');
var search = this.view.build_search_data(); var search = this.view.build_search_data();
this.rpc('/web/session/eval_domain_and_context', { this.rpc('/web/session/eval_domain_and_context', {
@ -1546,7 +1627,8 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
user_id: private_filter ? instance.session.uid : false, user_id: private_filter ? instance.session.uid : false,
model_id: self.view.model, model_id: self.view.model,
context: results.context, context: results.context,
domain: results.domain domain: results.domain,
is_default: set_as_default
}; };
// FIXME: current context? // FIXME: current context?
return self.model.call('create_or_replace', [filter]).done(function (id) { return self.model.call('create_or_replace', [filter]).done(function (id) {

View File

@ -0,0 +1,393 @@
// Test support structures and methods for OpenERP
openerp.testing = {};
(function (testing) {
var dependencies = {
corelib: [],
coresetup: ['corelib'],
data: ['corelib', 'coresetup'],
dates: [],
formats: ['coresetup', 'dates'],
chrome: ['corelib', 'coresetup'],
views: ['corelib', 'coresetup', 'data', 'chrome'],
search: ['data', 'coresetup', 'formats'],
list: ['views', 'data'],
form: ['data', 'views', 'list', 'formats'],
list_editable: ['list', 'form', 'data'],
};
testing.dependencies = window['oe_all_dependencies'] || [];
testing.current_module = null;
testing.templates = { };
testing.add_template = function (name) {
var xhr = QWeb2.Engine.prototype.get_xhr();
xhr.open('GET', name, false);
xhr.send(null);
(testing.templates[testing.current_module] =
testing.templates[testing.current_module] || [])
.push(xhr.responseXML);
};
/**
* Function which does not do anything
*/
testing.noop = function () { };
/**
* Alter provided instance's ``session`` attribute to make response
* mockable:
*
* * The ``responses`` parameter can be used to provide a map of (RPC)
* paths (e.g. ``/web/view/load``) to a function returning a response
* to the query.
* * ``instance.session`` grows a ``responses`` attribute which is
* a map of the same (and is in fact initialized to the ``responses``
* parameter if one is provided)
*
* Note that RPC requests to un-mocked URLs will be rejected with an
* error message: only explicitly specified urls will get a response.
*
* Mocked sessions will *never* perform an actual RPC connection.
*
* @param instance openerp instance being initialized
* @param {Object} [responses]
*/
testing.mockifyRPC = function (instance, responses) {
var session = instance.session;
session.responses = responses || {};
session.rpc_function = function (url, payload) {
var fn, params;
var needle = payload.params.model + ':' + payload.params.method;
if (url.url === '/web/dataset/call_kw'
&& needle in this.responses) {
fn = this.responses[needle];
params = [
payload.params.args || [],
payload.params.kwargs || {}
];
} else {
fn = this.responses[url.url];
params = [payload];
}
if (!fn) {
return $.Deferred().reject({}, 'failed',
_.str.sprintf("Url %s not found in mock responses, with arguments %s",
url.url, JSON.stringify(payload.params))
).promise();
}
try {
return $.when(fn.apply(null, params)).then(function (result) {
// Wrap for RPC layer unwrapper thingy
return {result: result};
});
} catch (e) {
// not sure why this looks like that
return $.Deferred().reject({}, 'failed', String(e));
}
};
};
var StackProto = {
execute: function (fn) {
var args = [].slice.call(arguments, 1);
// Warning: here be dragons
var i = 0, setups = this.setups, teardowns = this.teardowns;
var d = $.Deferred();
var succeeded, failed;
var success = function () {
succeeded = _.toArray(arguments);
return teardown();
};
var failure = function () {
// save first failure
if (!failed) {
failed = _.toArray(arguments);
}
// chain onto next teardown
return teardown();
};
var setup = function () {
// if setup to execute
if (i < setups.length) {
var f = setups[i] || testing.noop;
$.when(f.apply(null, args)).then(function () {
++i;
setup();
}, failure);
} else {
$.when(fn.apply(null, args)).then(success, failure);
}
};
var teardown = function () {
// if teardown to execute
if (i > 0) {
var f = teardowns[--i] || testing.noop;
$.when(f.apply(null, args)).then(teardown, failure);
} else {
if (failed) {
d.reject.apply(d, failed);
} else if (succeeded) {
d.resolve.apply(d, succeeded);
} else {
throw new Error("Didn't succeed or fail?");
}
}
};
setup();
return d;
},
push: function (setup, teardown) {
return _.extend(Object.create(StackProto), {
setups: this.setups.concat([setup]),
teardowns: this.teardowns.concat([teardown])
});
},
unshift: function (setup, teardown) {
return _.extend(Object.create(StackProto), {
setups: [setup].concat(this.setups),
teardowns: [teardown].concat(this.teardowns)
});
}
};
/**
*
* @param {Function} [setup]
* @param {Function} [teardown]
* @return {*}
*/
testing.Stack = function (setup, teardown) {
return _.extend(Object.create(StackProto), {
setups: setup ? [setup] : [],
teardowns: teardown ? [teardown] : []
});
};
var db = window['oe_db_info'];
testing.section = function (name, options, body) {
if (_.isFunction(options)) {
body = options;
options = {};
}
_.defaults(options, {
setup: testing.noop,
teardown: testing.noop
});
QUnit.module(testing.current_module + '.' + name, {_oe: options});
body(testing.case);
};
testing.case = function (name, options, callback) {
if (_.isFunction(options)) {
callback = options;
options = {};
}
var module = testing.current_module;
var module_index = _.indexOf(testing.dependencies, module);
var module_deps = testing.dependencies.slice(
// If module not in deps (because only tests, no JS) -> indexOf
// returns -1 -> index becomes 0 -> replace with ``undefined`` so
// Array#slice returns a full copy
0, module_index + 1 || undefined);
// Serialize options for this precise test case
// WARNING: typo is from jquery, do not fix!
var env = QUnit.config.currentModuleTestEnviroment;
// section setup
// case setup
// test
// case teardown
// section teardown
var case_stack = testing.Stack()
.push(env._oe.setup, env._oe.teardown)
.push(options.setup, options.teardown);
var opts = _.defaults({}, options, env._oe);
// FIXME: if this test is ignored, will still query
if (opts.rpc === 'rpc' && !db) {
QUnit.config.autostart = false;
db = {
source: null,
supadmin: null,
password: null
};
var $msg = $('<form style="margin: 0 1em 1em;">')
.append('<h3>A test needs to clone a database</h3>')
.append('<h4>Please provide the source clone information</h4>')
.append(' Source DB: ').append('<input name="source">').append('<br>')
.append(' DB Password: ').append('<input name="supadmin">').append('<br>')
.append('Admin Password: ').append('<input name="password">').append('<br>')
.append('<input type="submit" value="OK"/>')
.submit(function (e) {
e.preventDefault();
e.stopPropagation();
db.source = $msg.find('input[name=source]').val();
db.supadmin = $msg.find('input[name=supadmin]').val();
db.password = $msg.find('input[name=password]').val();
QUnit.start();
$.unblockUI();
});
$.blockUI({
message: $msg,
css: {
fontFamily: 'monospace',
textAlign: 'left',
whiteSpace: 'pre-wrap',
cursor: 'default'
}
});
}
QUnit.test(name, function () {
var instance;
if (!opts.dependencies) {
instance = openerp.init(module_deps);
} else {
// empty-but-specified dependencies actually allow running
// without loading any module into the instance
// TODO: clean up this mess
var d = opts.dependencies.slice();
// dependencies list should be in deps order, reverse to make
// loading order from last
d.reverse();
var di = 0;
while (di < d.length) {
var m = /^web\.(\w+)$/.exec(d[di]);
if (m) {
d[di] = m[1];
}
d.splice.apply(d, [di+1, 0].concat(
_(dependencies[d[di]]).reverse()));
++di;
}
instance = openerp.init("fuck your shit, don't load anything you cunt");
_(d).chain()
.reverse()
.uniq()
.each(function (module) {
openerp.web[module](instance);
});
}
if (_.isNumber(opts.asserts)) {
expect(opts.asserts);
}
if (opts.templates) {
for(var i=0; i<module_deps.length; ++i) {
var dep = module_deps[i];
var templates = testing.templates[dep];
if (_.isEmpty(templates)) { continue; }
for (var j=0; j < templates.length; ++j) {
instance.web.qweb.add_template(templates[j]);
}
}
}
var $fixture = $('#qunit-fixture');
var mock, async = false;
switch (opts.rpc) {
case 'mock':
async = true;
testing.mockifyRPC(instance);
mock = function (spec, handler) {
instance.session.responses[spec] = handler;
};
break;
case 'rpc':
async = true;
(function () {
// Bunch of random base36 characters
var dbname = 'test_' + Math.random().toString(36).slice(2);
// Add db setup/teardown at the start of the stack
case_stack = case_stack.unshift(function (instance) {
// FIXME hack: don't want the session to go through shitty loading process of everything
instance.session.session_init = testing.noop;
instance.session.load_modules = testing.noop;
instance.session.session_bind();
return instance.session.rpc('/web/database/duplicate', {
fields: [
{name: 'super_admin_pwd', value: db.supadmin},
{name: 'db_original_name', value: db.source},
{name: 'db_name', value: dbname}
]
}).then(function (result) {
if (result.error) {
return $.Deferred().reject(result.error).promise();
}
return instance.session.session_authenticate(
dbname, 'admin', db.password, true);
});
}, function (instance) {
return instance.session.rpc('/web/database/drop', {
fields: [
{name: 'drop_pwd', value: db.supadmin},
{name: 'drop_db', value: dbname}
]
}).then(function (result) {
if (result.error) {
return $.Deferred().reject(result.error).promise();
}
return result;
});
});
})();
}
// Always execute tests asynchronously
stop();
var timeout;
case_stack.execute(function () {
var result = callback.apply(null, arguments);
if (!(result && _.isFunction(result.then))) {
if (async) {
ok(false, "asynchronous test cases must return a promise");
}
} else {
if (!_.isNumber(opts.asserts)) {
ok(false, "asynchronous test cases must specify the "
+ "number of assertions they expect");
}
}
return $.Deferred(function (d) {
$.when(result).then(function () {
d.resolve.apply(d, arguments)
}, function () {
d.reject.apply(d, arguments);
});
if (async || (result && result.then)) {
// async test can be either implicit async (rpc) or
// promise-returning
timeout = setTimeout(function () {
QUnit.config.semaphore = 1;
d.reject({message: "Test timed out"});
}, 2000);
}
});
}, instance, $fixture, mock).always(function () {
if (timeout) { clearTimeout(timeout); }
start();
}).fail(function (error) {
if (options.fail_on_rejection === false) {
return;
}
var message;
if (typeof error !== 'object'
|| typeof error.message !== 'string') {
message = JSON.stringify([].slice.apply(arguments));
} else {
message = error.message;
if (error.data && error.data.debug) {
message += '\n\n' + error.data.debug;
}
}
ok(false, message);
});
});
};
})(openerp.testing);

View File

@ -173,10 +173,14 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
} else { } else {
this.$el.find('.oe_form_buttons').replaceWith(this.$buttons); this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
} }
this.$buttons.on('click', '.oe_form_button_create', this.on_button_create); this.$buttons.on('click', '.oe_form_button_create',
this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit); this.guard_active(this.on_button_create));
this.$buttons.on('click', '.oe_form_button_save', this.on_button_save); this.$buttons.on('click', '.oe_form_button_edit',
this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel); this.guard_active(this.on_button_edit));
this.$buttons.on('click', '.oe_form_button_save',
this.guard_active(this.on_button_save));
this.$buttons.on('click', '.oe_form_button_cancel',
this.guard_active(this.on_button_cancel));
if (this.options.footer_to_buttons) { if (this.options.footer_to_buttons) {
this.$el.find('footer').appendTo(this.$buttons); this.$el.find('footer').appendTo(this.$buttons);
} }
@ -618,6 +622,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return self._process_save(save_obj).then(function() { return self._process_save(save_obj).then(function() {
save_obj.ret = _.toArray(arguments); save_obj.ret = _.toArray(arguments);
return iterate(); return iterate();
}, function() {
save_obj.error = true;
}); });
} }
return $.when(); return $.when();
@ -801,6 +807,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var save_obj = {prepend_on_create: prepend_on_create, ret: null}; var save_obj = {prepend_on_create: prepend_on_create, ret: null};
this.save_list.push(save_obj); this.save_list.push(save_obj);
return this._process_operations().then(function() { return this._process_operations().then(function() {
if (save_obj.error)
return $.Deferred().reject();
return $.when.apply($, save_obj.ret); return $.when.apply($, save_obj.ret);
}); });
}, },
@ -1187,14 +1195,36 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
}); });
} }
}, },
view_arch_to_dom_node: function(arch) {
// Historic mess for views arch
//
// server:
// -> got xml as string
// -> parse to xml and manipulate domains and contexts
// -> convert to json
// client:
// -> got view as json
// -> convert back to xml as string
// -> parse it as xml doc (manipulate button@type for IE)
// -> convert back to string
// -> parse it as dom element with jquery
// -> for each widget, convert node to json
//
// Wow !!!
var xml = instance.web.json_node_to_xml(arch);
var doc = $.parseXML('<div class="oe_form">' + xml + '</div>');
$('button', doc).each(function() {
$(this).attr('data-button-type', $(this).attr('type'));
});
xml = instance.web.xml_to_str(doc);
return $(xml);
},
render_to: function($target) { render_to: function($target) {
var self = this; var self = this;
this.$target = $target; this.$target = $target;
// TODO: I know this will save the world and all the kitten for a moment, this.$form = this.view_arch_to_dom_node(this.fvg.arch);
// but one day, we will have to get rid of xml2json
var xml = instance.web.json_node_to_xml(this.fvg.arch);
this.$form = $('<div class="oe_form">' + xml + '</div>');
this.process_version(); this.process_version();
@ -1863,6 +1893,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi
instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
template: 'WidgetButton', template: 'WidgetButton',
init: function(field_manager, node) { init: function(field_manager, node) {
node.attrs.type = node.attrs['data-button-type'];
this._super(field_manager, node); this._super(field_manager, node);
this.force_disabled = false; this.force_disabled = false;
this.string = (this.node.attrs.string || '').replace(/_/g, ''); this.string = (this.node.attrs.string || '').replace(/_/g, '');
@ -2152,17 +2183,10 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
}, },
set_dimensions: function (height, width) { set_dimensions: function (height, width) {
// remove width css property this.$el.css({
this.$el.css('width', ''); width: width,
// extract style (without width) minHeight: height
var old_style = this.$el.attr('style'); });
// jQuery doesn't understand/use !important
var style = 'width:' + width + 'px !important;';
if (old_style) {
style += old_style
}
this.$el.attr('style', style);
this.$el.css('minHeight', height);
}, },
commit_value: function() { commit_value: function() {
return $.when(); return $.when();
@ -2386,7 +2410,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
self.$input.focus(); self.$input.focus();
return; return;
} }
self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date()); self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
self.$input_picker.show(); self.$input_picker.show();
self.picker('show'); self.picker('show');
self.$input_picker.hide(); self.$input_picker.hide();
@ -3102,7 +3126,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
minLength: 0, minLength: 0,
delay: 0 delay: 0
}); });
this.$input.autocomplete("widget").addClass("openerp"); this.$input.autocomplete("widget").openerpClass();
// used to correct a bug when selecting an element by pushing 'enter' in an editable list // used to correct a bug when selecting an element by pushing 'enter' in an editable list
this.$input.keyup(function(e) { this.$input.keyup(function(e) {
if (e.which === 13) { // ENTER if (e.which === 13) { // ENTER
@ -3813,26 +3837,13 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
this._super.apply(this, arguments); this._super.apply(this, arguments);
}, },
do_delete: function (ids) { do_delete: function (ids) {
var self = this; var confirm = window.confirm;
var next = $.when(); window.confirm = function () { return true; };
var _super = this._super; try {
// handle deletion of an item which does not exist return this._super(ids);
// TODO: better handle that in the editable list? } finally {
var false_id_index = _(ids).indexOf(false); window.confirm = confirm;
if (false_id_index !== -1) {
ids.splice(false_id_index, 1);
next = this.cancel_edition(true);
} }
return next.then(function () {
// wheeee
var confirm = window.confirm;
window.confirm = function () { return true; };
try {
return _super.call(self, ids);
} finally {
window.confirm = confirm;
}
});
} }
}); });
instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({ instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
@ -3995,13 +4006,18 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
self._drop_shown = true; self._drop_shown = true;
}); });
self.tags = self.$text.textext()[0].tags(); self.tags = self.$text.textext()[0].tags();
self.$text.focusout(function() { self.$text
self.$text.trigger("setInputData", ""); .focusin(function () {
}).keydown(function(e) { self.trigger('focused');
if (e.which === $.ui.keyCode.TAB && self._drop_shown) { })
self.$text.textext()[0].autocomplete().selectFromDropdown(); .focusout(function() {
} self.$text.trigger("setInputData", "");
}); self.trigger('blurred');
}).keydown(function(e) {
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
self.$text.textext()[0].autocomplete().selectFromDropdown();
}
});
}, },
set_value: function(value_) { set_value: function(value_) {
value_ = value_ || []; value_ = value_ || [];
@ -4790,15 +4806,6 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as); this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
this.$el.find('.oe_form_binary_file_clear').click(this.on_clear); this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
}, },
human_filesize : function(size) {
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var i = 0;
while (size >= 1024) {
size /= 1024;
++i;
}
return size.toFixed(2) + ' ' + units[i];
},
on_file_change: function(e) { on_file_change: function(e) {
var self = this; var self = this;
var file_node = e.target; var file_node = e.target;
@ -4846,6 +4853,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
link.href = "data:application/octet-stream;base64," + value; link.href = "data:application/octet-stream;base64," + value;
} else { } else {
instance.web.blockUI(); instance.web.blockUI();
var c = instance.webclient.crashmanager;
this.session.get_file({ this.session.get_file({
url: '/web/binary/saveas_ajax', url: '/web/binary/saveas_ajax',
data: {data: JSON.stringify({ data: {data: JSON.stringify({
@ -4856,7 +4864,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
context: this.view.dataset.get_context() context: this.view.dataset.get_context()
})}, })},
complete: instance.web.unblockUI, complete: instance.web.unblockUI,
error: instance.webclient.crashmanager.on_rpc_error error: c.rpc_error.bind(c)
}); });
ev.stopPropagation(); ev.stopPropagation();
return false; return false;
@ -4915,7 +4923,7 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
this.binary_value = true; this.binary_value = true;
this.internal_set_value(file_base64); this.internal_set_value(file_base64);
var show_value = name + " (" + this.human_filesize(size) + ")"; var show_value = name + " (" + instance.web.human_size(size) + ")";
this.$el.find('input').eq(0).val(show_value); this.$el.find('input').eq(0).val(show_value);
this.set_filename(name); this.set_filename(name);
}, },
@ -4935,12 +4943,16 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
if (this.get('value') && ! /^\d+(\.\d*)? \w+$/.test(this.get('value'))) { if (this.get('value') && ! /^\d+(\.\d*)? \w+$/.test(this.get('value'))) {
url = 'data:image/png;base64,' + this.get('value'); url = 'data:image/png;base64,' + this.get('value');
} else if (this.get('value')) { } else if (this.get('value')) {
var id = escape(JSON.stringify(this.view.datarecord.id || null)); var id = JSON.stringify(this.view.datarecord.id || null);
var field = this.name; var field = this.name;
if (this.options.preview_image) if (this.options.preview_image)
field = this.options.preview_image; field = this.options.preview_image;
url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + url = this.session.url('/web/binary/image', {
this.view.dataset.model +'&id=' + id + '&field=' + field + '&t=' + (new Date().getTime()); model: this.view.dataset.model,
id: id,
field: field,
t: (new Date().getTime()),
});
} else { } else {
url = this.placeholder; url = this.placeholder;
} }
@ -4979,14 +4991,14 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
* Options on attribute ; "blockui" {Boolean} block the UI or not * Options on attribute ; "blockui" {Boolean} block the UI or not
* during the file is uploading * during the file is uploading
*/ */
instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
template: "FieldBinaryFileUploader", template: "FieldBinaryFileUploader",
init: function(field_manager, node) { init: function(field_manager, node) {
this._super(field_manager, node); this._super(field_manager, node);
this.field_manager = field_manager; this.field_manager = field_manager;
this.node = node; this.node = node;
if(this.field.type != "one2many" || this.field.relation != 'ir.attachment') { if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
throw "The type of the field '"+this.field.string+"' must be a one2many field with a relation to 'ir.attachment' model."; throw "The type of the field '"+this.field.string+"' must be a many2many field with a relation to 'ir.attachment' model.";
} }
this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
this.fileupload_id = _.uniqueId('oe_fileupload_temp'); this.fileupload_id = _.uniqueId('oe_fileupload_temp');
@ -4996,21 +5008,85 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel
this._super(this); this._super(this);
this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
}, },
set_value: function(value_) {
var value_ = value_ || [];
var self = this;
var ids = [];
_.each(value_, function(command) {
if (isNaN(command) && command.id == undefined) {
switch (command[0]) {
case commands.CREATE:
ids = ids.concat(command[2]);
return;
case commands.REPLACE_WITH:
ids = ids.concat(command[2]);
return;
case commands.UPDATE:
ids = ids.concat(command[2]);
return;
case commands.LINK_TO:
ids = ids.concat(command[1]);
return;
case commands.DELETE:
ids = _.filter(ids, function (id) { return id != command[1];});
return;
case commands.DELETE_ALL:
ids = [];
return;
}
} else {
ids.push(command);
}
});
this._super( ids );
},
get_value: function() { get_value: function() {
return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); }); return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); });
}, },
get_file_url: function (attachment) { get_file_url: function (attachment) {
return instance.origin + '/web/binary/saveas?session_id=' + this.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id']; return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
},
read_name_values : function () {
var self = this;
// select the list of id for a get_name
var values = [];
_.each(this.get('value'), function (val) {
if (typeof val != 'object') {
values.push(val);
}
});
// send request for get_name
if (values.length) {
return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
_.each(datas, function (data) {
data.no_unlink = true;
data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
_.each(self.get('value'), function (val, key) {
if(val == data.id) {
self.get('value')[key] = data;
}
});
});
});
} else {
return $.when(this.get('value'));
}
}, },
render_value: function () { render_value: function () {
var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': this})); var self = this;
render.on('click', '.oe_delete', _.bind(this.on_file_delete, this)); this.read_name_values().then(function (datas) {
this.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
// reinit input type file var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
var $input = this.$('input.oe_form_binary_file'); render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
$input.after($input.clone(true)).remove(); self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
this.$(".oe_fileupload").show();
// reinit input type file
var $input = self.$('input.oe_form_binary_file');
$input.after($input.clone(true)).remove();
self.$(".oe_fileupload").show();
});
}, },
on_file_change: function (event) { on_file_change: function (event) {
event.stopPropagation(); event.stopPropagation();
@ -5026,7 +5102,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel
} }
// block UI or not // block UI or not
if(this.node.attrs.blockui) { if(this.node.attrs.blockui>0) {
instance.web.blockUI(); instance.web.blockUI();
} }
@ -5060,7 +5136,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel
}, },
on_file_loaded: function (event, result) { on_file_loaded: function (event, result) {
// unblock UI // unblock UI
if(this.node.attrs.blockui) { if(this.node.attrs.blockui>0) {
instance.web.unblockUI(); instance.web.unblockUI();
} }
@ -5090,7 +5166,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel
if(file_id != this.get('value')[i].id){ if(file_id != this.get('value')[i].id){
files.push(this.get('value')[i]); files.push(this.get('value')[i]);
} }
else { else if(!this.get('value')[i].no_unlink) {
this.ds_file.unlink([file_id]); this.ds_file.unlink([file_id]);
} }
} }
@ -5252,7 +5328,7 @@ instance.web.form.widgets = new instance.web.Registry({
'progressbar': 'instance.web.form.FieldProgressBar', 'progressbar': 'instance.web.form.FieldProgressBar',
'image': 'instance.web.form.FieldBinaryImage', 'image': 'instance.web.form.FieldBinaryImage',
'binary': 'instance.web.form.FieldBinaryFile', 'binary': 'instance.web.form.FieldBinaryFile',
'one2many_binary': 'instance.web.form.FieldOne2ManyBinaryMultiFiles', 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
'statusbar': 'instance.web.form.FieldStatus', 'statusbar': 'instance.web.form.FieldStatus',
'monetary': 'instance.web.form.FieldMonetary', 'monetary': 'instance.web.form.FieldMonetary',
}); });

View File

@ -1,6 +1,6 @@
openerp.web.list = function (instance) { openerp.web.list = function (instance) {
var _t = instance.web._t, var _t = instance.web._t,
_lt = instance.web._lt; _lt = instance.web._lt;
var QWeb = instance.web.qweb; var QWeb = instance.web.qweb;
instance.web.views.add('list', 'instance.web.ListView'); instance.web.views.add('list', 'instance.web.ListView');
instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ { instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ {
@ -503,12 +503,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
return this.reload_content(); return this.reload_content();
}, },
reload_record: function (record) { reload_record: function (record) {
var self = this;
return this.dataset.read_ids( return this.dataset.read_ids(
[record.get('id')], [record.get('id')],
_.pluck(_(this.columns).filter(function (r) { _.pluck(_(this.columns).filter(function (r) {
return r.tag === 'field'; return r.tag === 'field';
}), 'name') }), 'name')
).done(function (records) { ).done(function (records) {
if (!records[0]) {
self.records.remove(record);
return;
}
_(records[0]).each(function (value, key) { _(records[0]).each(function (value, key) {
record.set(key, value, {silent: true}); record.set(key, value, {silent: true});
}); });
@ -2151,7 +2156,7 @@ instance.web.list.Boolean = instance.web.list.Column.extend({
* @private * @private
*/ */
_format: function (row_data, options) { _format: function (row_data, options) {
return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>', return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>',
row_data[this.id].value ? 'checked="checked"' : ''); row_data[this.id].value ? 'checked="checked"' : '');
} }
}); });
@ -2163,20 +2168,24 @@ instance.web.list.Binary = instance.web.list.Column.extend({
*/ */
_format: function (row_data, options) { _format: function (row_data, options) {
var text = _t("Download"); var text = _t("Download");
var download_url = _.str.sprintf( var value = row_data[this.id].value;
'/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', var download_url;
instance.session.session_id, options.model, this.id, options.id); if (value && value.substr(0, 10).indexOf(' ') == -1) {
if (this.filename) { download_url = "data:application/octet-stream;base64," + value;
download_url += '&filename_field=' + this.filename; } else {
if (row_data[this.filename]) { download_url = this.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( if (this.filename) {
row_data[this.filename].value, {type: 'char'})); download_url += '&filename_field=' + this.filename;
} }
} }
return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', { if (this.filename && row_data[this.filename]) {
text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
row_data[this.filename].value, {type: 'char'}));
}
return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', {
text: text, text: text,
href: download_url, href: download_url,
size: row_data[this.id].value size: instance.web.binary_to_binsize(value),
}); });
} }
}); });

View File

@ -77,6 +77,15 @@ openerp.web.list_editable = function (instance) {
do_edit: function (index, id, dataset) { do_edit: function (index, id, dataset) {
_.extend(this.dataset, dataset); _.extend(this.dataset, dataset);
}, },
do_delete: function (ids) {
var _super = this._super.bind(this);
var next = this.editor.is_editing()
? this.cancel_edition(true)
: $.when();
return next.then(function () {
return _super(ids);
});
},
editable: function () { editable: function () {
return this.fields_view.arch.attrs.editable return this.fields_view.arch.attrs.editable
|| this._context_editable || this._context_editable
@ -117,13 +126,6 @@ openerp.web.list_editable = function (instance) {
e.preventDefault(); e.preventDefault();
self.cancel_edition(); self.cancel_edition();
}); });
this.$el
.off('click', 'tbody td:not(.oe_list_field_cell)')
.on('click', 'tbody td:not(.oe_list_field_cell)', function () {
if (!self.editor.is_editing()) {
self.start_edition();
}
});
this.editor.destroy(); this.editor.destroy();
// Editor is not restartable due to formview not being // Editor is not restartable due to formview not being
// restartable // restartable
@ -164,13 +166,23 @@ openerp.web.list_editable = function (instance) {
* @returns {$.Deferred} * @returns {$.Deferred}
*/ */
ensure_saved: function () { ensure_saved: function () {
var self = this; return this.save_edition();
return this.saving_mutex.exec(function() { },
if (!self.editor.is_editing()) { /**
return $.when(); * Builds a record with the provided id (``false`` for a creation),
} * setting all columns with ``false`` value so code which relies on
return self.save_edition(); * having an actual value behaves correctly
}); *
* @param {*} id
* @return {instance.web.list.Record}
*/
make_empty_record: function (id) {
var attrs = {id: id};
_(this.columns).chain()
.filter(function (x) { return x.tag === 'field'})
.pluck('name')
.each(function (field) { attrs[field] = false; });
return new instance.web.list.Record(attrs);
}, },
/** /**
* Set up the edition of a record of the list view "inline" * Set up the edition of a record of the list view "inline"
@ -186,12 +198,7 @@ openerp.web.list_editable = function (instance) {
if (record) { if (record) {
item = record.attributes; item = record.attributes;
} else { } else {
var attrs = {id: false}; record = this.make_empty_record(false);
_(this.columns).chain()
.filter(function (x) { return x.tag === 'field'})
.pluck('name')
.each(function (field) { attrs[field] = false; });
record = new instance.web.list.Record(attrs);
this.records.add(record, { this.records.add(record, {
at: this.prepends_on_create() ? 0 : null}); at: this.prepends_on_create() ? 0 : null});
} }
@ -269,29 +276,34 @@ openerp.web.list_editable = function (instance) {
*/ */
save_edition: function () { save_edition: function () {
var self = this; var self = this;
return this.with_event('save', { return self.saving_mutex.exec(function() {
editor: this.editor, if (!self.editor.is_editing()) {
form: this.editor.form, return $.when();
cancel: false }
}, function () { return self.with_event('save', {
return this.editor.save().then(function (attrs) { editor: self.editor,
var created = false; form: self.editor.form,
var record = self.records.get(attrs.id); cancel: false
if (!record) { }, function () {
// new record return self.editor.save().then(function (attrs) {
created = true; var created = false;
record = self.records.find(function (r) { var record = self.records.get(attrs.id);
return !r.get('id'); if (!record) {
}).set('id', attrs.id); // new record
} created = true;
// onwrite callback could be altering & reloading the record = self.records.find(function (r) {
// record which has *just* been saved, so first perform all return !r.get('id');
// onwrites then do a final reload of the record }).set('id', attrs.id);
return self.handle_onwrite(record) }
.then(function () { // onwrite callback could be altering & reloading the
return self.reload_record(record); }) // record which has *just* been saved, so first perform all
.then(function () { // onwrites then do a final reload of the record
return { created: created, record: record }; }); return self.handle_onwrite(record)
.then(function () {
return self.reload_record(record); })
.then(function () {
return { created: created, record: record }; });
});
}); });
}); });
}, },
@ -391,7 +403,7 @@ openerp.web.list_editable = function (instance) {
if (!record) { if (!record) {
// insert after the source record // insert after the source record
var index = this.records.indexOf(source_record) + 1; var index = this.records.indexOf(source_record) + 1;
record = new instance.web.list.Record({id: id}); record = this.make_empty_record(id);
this.records.add(record, {at: index}); this.records.add(record, {at: index});
this.dataset.ids.splice(index, 0, id); this.dataset.ids.splice(index, 0, id);
} }

View File

@ -213,8 +213,12 @@ instance.web.ActionManager = instance.web.Widget.extend({
if (run_action) { if (run_action) {
this.null_action(); this.null_action();
action_loaded = this.do_action(state.action); action_loaded = this.do_action(state.action);
instance.webclient.menu.has_been_loaded.done(function() { $.when(action_loaded || null).done(function() {
instance.webclient.menu.open_action(state.action); instance.webclient.menu.has_been_loaded.done(function() {
if (self.inner_action && self.inner_action.id) {
instance.webclient.menu.open_action(self.inner_action.id);
}
});
}); });
} }
} }
@ -306,15 +310,15 @@ instance.web.ActionManager = instance.web.Widget.extend({
} }
var widget = executor.widget(); var widget = executor.widget();
if (executor.action.target === 'new') { if (executor.action.target === 'new') {
if (this.dialog_widget && ! this.dialog_widget.isDestroyed()) if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
this.dialog_widget.destroy(); this.dialog_widget.destroy();
if (this.dialog === null || this.dialog.isDestroyed()) {
this.dialog = new instance.web.Dialog(this, {
dialogClass: executor.klass,
});
this.dialog.on("closing", null, options.on_close);
this.dialog.init_dialog();
} }
this.dialog_stop();
this.dialog = new instance.web.Dialog(this, {
dialogClass: executor.klass,
});
this.dialog.on("closing", null, options.on_close);
this.dialog.init_dialog();
this.dialog.dialog_title = executor.action.name; this.dialog.dialog_title = executor.action.name;
if (widget instanceof instance.web.ViewManager) { if (widget instanceof instance.web.ViewManager) {
_.extend(widget.flags, { _.extend(widget.flags, {
@ -397,6 +401,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
}).done(function(res) { }).done(function(res) {
action = _.clone(action); action = _.clone(action);
action.context = res.context; action.context = res.context;
var c = instance.webclient.crashmanager;
self.session.get_file({ self.session.get_file({
url: '/web/report', url: '/web/report',
data: {action: JSON.stringify(action)}, data: {action: JSON.stringify(action)},
@ -407,7 +412,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
} }
self.dialog_stop(); self.dialog_stop();
}, },
error: instance.webclient.crashmanager.on_rpc_error error: c.rpc_error.bind(c)
}) })
}); });
}, },
@ -504,7 +509,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
.find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]') .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
.parent().addClass('active'); .parent().addClass('active');
r = $.when(view_promise).done(function () { return $.when(view_promise).done(function () {
_.each(_.keys(self.views), function(view_name) { _.each(_.keys(self.views), function(view_name) {
var controller = self.views[view_name].controller; var controller = self.views[view_name].controller;
if (controller) { if (controller) {
@ -520,7 +525,6 @@ instance.web.ViewManager = instance.web.Widget.extend({
}); });
self.trigger('switch_mode', view_type, no_store, view_options); self.trigger('switch_mode', view_type, no_store, view_options);
}); });
return r;
}, },
do_create_view: function(view_type) { do_create_view: function(view_type) {
// Lazy loading of views // Lazy loading of views
@ -785,8 +789,8 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
name: "JS Tests", name: "JS Tests",
target: 'new', target: 'new',
type : 'ir.actions.act_url', type : 'ir.actions.act_url',
url: '/web/static/test/test.html' url: '/web/tests?mod=*'
}) });
break; break;
case 'perm_read': case 'perm_read':
var ids = current_view.get_selected_ids(); var ids = current_view.get_selected_ids();
@ -1098,11 +1102,11 @@ instance.web.Sidebar = instance.web.Widget.extend({
on_attachments_loaded: function(attachments) { on_attachments_loaded: function(attachments) {
var self = this; var self = this;
var items = []; var items = [];
var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
_.each(attachments,function(a) { _.each(attachments,function(a) {
a.label = a.name; a.label = a.name;
if(a.type === "binary") { if(a.type === "binary") {
a.url = prefix + a.id + '&t=' + (new Date().getTime()); a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime());
} }
}); });
self.items['files'] = attachments; self.items['files'] = attachments;
@ -1235,11 +1239,12 @@ instance.web.View = instance.web.Widget.extend({
}); });
}, null); }, null);
} else { } else {
self.do_action({"type":"ir.actions.act_window_close"});
return result_handler(); return result_handler();
} }
}; };
if (action_data.special) { if (action_data.special === 'cancel') {
return handler({"type":"ir.actions.act_window_close"}); return handler({"type":"ir.actions.act_window_close"});
} else if (action_data.type=="object") { } else if (action_data.type=="object") {
var args = [[record_id]], additional_args = []; var args = [[record_id]], additional_args = [];
@ -1277,6 +1282,27 @@ instance.web.View = instance.web.Widget.extend({
do_hide: function () { do_hide: function () {
this.$el.hide(); this.$el.hide();
}, },
is_active: function () {
var manager = this.getParent();
return !manager || !manager.active_view
|| manager.views[manager.active_view].controller === this;
}, /**
* Wraps fn to only call it if the current view is the active one. If the
* current view is not active, doesn't call fn.
*
* fn can not return anything, as a non-call to fn can't return anything
* either
*
* @param {Function} fn function to wrap in the active guard
*/
guard_active: function (fn) {
var self = this;
return function () {
if (self.is_active()) {
fn.apply(self, arguments);
}
}
},
do_push_state: function(state) { do_push_state: function(state) {
if (this.getParent() && this.getParent().do_push_state) { if (this.getParent() && this.getParent().do_push_state) {
this.getParent().do_push_state(state); this.getParent().do_push_state(state);
@ -1384,14 +1410,16 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
} else { } else {
return r + '/>'; return r + '/>';
} }
} };
instance.web.xml_to_str = function(node) { instance.web.xml_to_str = function(node) {
if (window.ActiveXObject) { if (window.XMLSerializer) {
return (new XMLSerializer()).serializeToString(node);
} else if (window.ActiveXObject) {
return node.xml; return node.xml;
} else { } else {
return (new XMLSerializer()).serializeToString(node); throw new Error("Could not serialize XML");
} }
} };
instance.web.str_to_xml = function(s) { instance.web.str_to_xml = function(s) {
if (window.DOMParser) { if (window.DOMParser) {
var dp = new DOMParser(); var dp = new DOMParser();

View File

@ -152,6 +152,32 @@
</tr> </tr>
</table> </table>
</form> </form>
<form id="db_duplicate" name="duplicate_db_form" style="display: none;">
<div class="oe_view_manager oe_view_manager_current">
<div class="oe_view_manager_header" style="padding: 8px;">
<div class="oe_header_row">
<h2 class="oe_view_title">
<span class="oe_view_title_text oe_breadcrumb_title">Duplicate Database</span>
</h2>
<button type="submit" class="oe_button oe_highlight db_duplicate">Duplicate</button>
</div>
</div>
</div>
<table align="center" class="db_option_table">
<tr>
<td><label for="super_admin_pwd">Master password:</label></td>
<td><input type="password" name="super_admin_pwd" class="required" value="admin" /></td>
</tr>
<tr>
<td><label for="db_original_name">Original database name:</label></td>
<td><input type="text" name="db_original_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" autofocus="true"/></td>
</tr>
<tr>
<td><label for="db_name">New database name:</label></td>
<td><input type="text" name="db_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" /></td>
</tr>
</table>
</form>
<form id="db_drop" name="drop_db_form" style="display: none; "> <form id="db_drop" name="drop_db_form" style="display: none; ">
<div class="oe_view_manager oe_view_manager_current"> <div class="oe_view_manager oe_view_manager_current">
<div class="oe_view_manager_header" style="padding: 8px;"> <div class="oe_view_manager_header" style="padding: 8px;">
@ -279,6 +305,7 @@
<div class="oe_secondary_menu_section">Database Management</div> <div class="oe_secondary_menu_section">Database Management</div>
<ul class="oe_secondary_submenu"> <ul class="oe_secondary_submenu">
<li><a href="#db_create">Create</a></li> <li><a href="#db_create">Create</a></li>
<li><a href="#db_duplicate">Duplicate</a></li>
<li><a href="#db_drop">Drop</a></li> <li><a href="#db_drop">Drop</a></li>
<li><a href="#db_backup">Backup</a></li> <li><a href="#db_backup">Backup</a></li>
<li><a href="#db_restore">Restore</a></li> <li><a href="#db_restore">Restore</a></li>
@ -1182,26 +1209,28 @@
</t> </t>
<t t-name="FieldBinaryFileUploader.files"> <t t-name="FieldBinaryFileUploader.files">
<div class="oe_attachments"> <div class="oe_attachments">
<t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> <t t-if="widget.get('value')">
<div class="oe_attachment"> <t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
<span t-if="(file.upload or file.percent_loaded&lt;100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}"> <div class="oe_attachment">
<span class="oe_fileuploader_in_process">...Upload in progress...</span> <span t-if="(file.upload or file.percent_loaded&lt;100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}">
<t t-raw="file.name || file.filename"/> <span class="oe_fileuploader_in_process">...Upload in progress...</span>
</span> <t t-raw="file.name || file.filename"/>
<a t-if="(!file.upload or file.percent_loaded&gt;=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> </span>
<t t-raw="file.name || file.filename"/> <a t-if="(!file.upload or file.percent_loaded&gt;=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
</a> <t t-raw="file.name || file.filename"/>
<t t-if="(!file.upload or file.percent_loaded&gt;=100)"> </a>
<a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a> <t t-if="(!file.upload or file.percent_loaded&gt;=100)">
</t> <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a>
</div> </t>
</t> </div>
<t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> </t>
<div> <t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
<a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> <div>
<t t-raw="file.name || file.filename"/> <a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
</a> <t t-raw="file.name || file.filename"/>
</div> </a>
</div>
</t>
</t> </t>
</div> </div>
</t> </t>
@ -1512,17 +1541,19 @@
<h3><span class="oe_i">M</span> Custom Filters</h3> <h3><span class="oe_i">M</span> Custom Filters</h3>
<ul class="oe_searchview_custom_list"/> <ul class="oe_searchview_custom_list"/>
<div class="oe_searchview_custom"> <div class="oe_searchview_custom">
<h4>Save current filter</h4> <h4>Save current filter</h4>
<form> <form>
<p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p> <p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p>
<p><input id="oe_searchview_custom_public" type="checkbox"/> <p>
<label for="oe_searchview_custom_public">Share with all users</label></p> <input id="oe_searchview_custom_public" type="checkbox"/>
<button>Save</button> <label for="oe_searchview_custom_public">Share with all users</label>
</form> <input id="oe_searchview_custom_default" type="checkbox"/>
<label for="oe_searchview_custom_default">Use by default</label>
</p>
<button>Save</button>
</form>
</div> </div>
</div> </div>
<div>
</div>
</div> </div>
<div t-name="SearchView.advanced" class="oe_searchview_advanced"> <div t-name="SearchView.advanced" class="oe_searchview_advanced">

View File

@ -1,32 +1,7 @@
$(document).ready(function () { openerp.testing.section('Widget.proxy', {
var $fix = $('#qunit-fixture'); dependencies: ['web.corelib']
var mod = { }, function (test) {
setup: function () { test('(String)', function (instance) {
instance = window.openerp.init([]);
window.openerp.web.corelib(instance);
instance.web.qweb = new QWeb2.Engine();
instance.web.qweb.add_template(
'<no>' +
'<t t-name="test.widget.template">' +
'<ol>' +
'<li t-foreach="5" t-as="counter" ' +
't-attf-class="class-#{counter}">' +
'<input/>' +
'<t t-esc="counter"/>' +
'</li>' +
'</ol>' +
'</t>' +
'<t t-name="test.widget.template-value">' +
'<p><t t-esc="widget.value"/></p>' +
'</t>' +
'</no>');
}
};
var instance;
module('Widget.proxy', mod);
test('(String)', function () {
var W = instance.web.Widget.extend({ var W = instance.web.Widget.extend({
exec: function () { exec: function () {
this.executed = true; this.executed = true;
@ -37,7 +12,7 @@ $(document).ready(function () {
fn(); fn();
ok(w.executed, 'should execute the named method in the right context'); ok(w.executed, 'should execute the named method in the right context');
}); });
test('(String)(*args)', function () { test('(String)(*args)', function (instance) {
var W = instance.web.Widget.extend({ var W = instance.web.Widget.extend({
exec: function (arg) { exec: function (arg) {
this.executed = arg; this.executed = arg;
@ -49,7 +24,7 @@ $(document).ready(function () {
ok(w.executed, "should execute the named method in the right context"); ok(w.executed, "should execute the named method in the right context");
equal(w.executed, 42, "should be passed the proxy's arguments"); equal(w.executed, 42, "should be passed the proxy's arguments");
}); });
test('(String), include', function () { test('(String), include', function (instance) {
// the proxy function should handle methods being changed on the class // the proxy function should handle methods being changed on the class
// and should always proxy "by name", to the most recent one // and should always proxy "by name", to the most recent one
var W = instance.web.Widget.extend({ var W = instance.web.Widget.extend({
@ -67,23 +42,43 @@ $(document).ready(function () {
equal(w.executed, 2, "should be lazily resolved"); equal(w.executed, 2, "should be lazily resolved");
}); });
test('(Function)', function () { test('(Function)', function (instance) {
var w = new (instance.web.Widget.extend({ })); var w = new (instance.web.Widget.extend({ }));
var fn = w.proxy(function () { this.executed = true; }); var fn = w.proxy(function () { this.executed = true; });
fn(); fn();
ok(w.executed, "should set the function's context (like Function#bind)"); ok(w.executed, "should set the function's context (like Function#bind)");
}); });
test('(Function)(*args)', function () { test('(Function)(*args)', function (instance) {
var w = new (instance.web.Widget.extend({ })); var w = new (instance.web.Widget.extend({ }));
var fn = w.proxy(function (arg) { this.executed = arg; }); var fn = w.proxy(function (arg) { this.executed = arg; });
fn(42); fn(42);
equal(w.executed, 42, "should be passed the proxy's arguments"); equal(w.executed, 42, "should be passed the proxy's arguments");
}); });
});
module('Widget.renderElement', mod); openerp.testing.section('Widget.renderElement', {
test('no template, default', function () { dependencies: ['web.corelib'],
setup: function (instance) {
instance.web.qweb = new QWeb2.Engine();
instance.web.qweb.add_template(
'<no>' +
'<t t-name="test.widget.template">' +
'<ol>' +
'<li t-foreach="5" t-as="counter" ' +
't-attf-class="class-#{counter}">' +
'<input/>' +
'<t t-esc="counter"/>' +
'</li>' +
'</ol>' +
'</t>' +
'<t t-name="test.widget.template-value">' +
'<p><t t-esc="widget.value"/></p>' +
'</t>' +
'</no>');
}
}, function (test) {
test('no template, default', function (instance) {
var w = new (instance.web.Widget.extend({ })); var w = new (instance.web.Widget.extend({ }));
var $original = w.$el; var $original = w.$el;
@ -98,7 +93,7 @@ $(document).ready(function () {
equal(w.el.attributes.length, 0, "should not have generated any attribute"); equal(w.el.attributes.length, 0, "should not have generated any attribute");
ok(_.isEmpty(w.$el.html(), "should not have generated any content")); ok(_.isEmpty(w.$el.html(), "should not have generated any content"));
}); });
test('no template, custom tag', function () { test('no template, custom tag', function (instance) {
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
tagName: 'ul' tagName: 'ul'
})); }));
@ -106,7 +101,7 @@ $(document).ready(function () {
equal(w.el.nodeName, 'UL', "should have generated the custom element tag"); equal(w.el.nodeName, 'UL', "should have generated the custom element tag");
}); });
test('no template, @id', function () { test('no template, @id', function (instance) {
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
id: 'foo' id: 'foo'
})); }));
@ -116,7 +111,7 @@ $(document).ready(function () {
equal(w.$el.attr('id'), 'foo', "should have generated the id attribute"); equal(w.$el.attr('id'), 'foo', "should have generated the id attribute");
equal(w.el.id, 'foo', "should also be available via property"); equal(w.el.id, 'foo', "should also be available via property");
}); });
test('no template, @className', function () { test('no template, @className', function (instance) {
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
className: 'oe_some_class' className: 'oe_some_class'
})); }));
@ -125,7 +120,7 @@ $(document).ready(function () {
equal(w.el.className, 'oe_some_class', "should have the right property"); equal(w.el.className, 'oe_some_class', "should have the right property");
equal(w.$el.attr('class'), 'oe_some_class', "should have the right attribute"); equal(w.$el.attr('class'), 'oe_some_class', "should have the right attribute");
}); });
test('no template, bunch of attributes', function () { test('no template, bunch of attributes', function (instance) {
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
attributes: { attributes: {
'id': 'some_id', 'id': 'some_id',
@ -152,7 +147,7 @@ $(document).ready(function () {
equal(w.$el.attr('spoiler'), 'snape kills dumbledore'); equal(w.$el.attr('spoiler'), 'snape kills dumbledore');
}); });
test('template', function () { test('template', function (instance) {
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
template: 'test.widget.template' template: 'test.widget.template'
})); }));
@ -162,9 +157,41 @@ $(document).ready(function () {
equal(w.$el.children().length, 5); equal(w.$el.children().length, 5);
equal(w.el.textContent, '01234'); equal(w.el.textContent, '01234');
}); });
test('repeated', { asserts: 4 }, function (instance, $fix) {
module('Widget.$', mod); var w = new (instance.web.Widget.extend({
test('basic-alias', function () { template: 'test.widget.template-value'
}));
w.value = 42;
return w.appendTo($fix)
.done(function () {
equal($fix.find('p').text(), '42', "DOM fixture should contain initial value");
equal(w.$el.text(), '42', "should set initial value");
w.value = 36;
w.renderElement();
equal($fix.find('p').text(), '36', "DOM fixture should use new value");
equal(w.$el.text(), '36', "should set new value");
});
});
});
openerp.testing.section('Widget.$', {
dependencies: ['web.corelib'],
setup: function (instance) {
instance.web.qweb = new QWeb2.Engine();
instance.web.qweb.add_template(
'<no>' +
'<t t-name="test.widget.template">' +
'<ol>' +
'<li t-foreach="5" t-as="counter" ' +
't-attf-class="class-#{counter}">' +
'<input/>' +
'<t t-esc="counter"/>' +
'</li>' +
'</ol>' +
'</t>' +
'</no>');
}
}, function (test) {
test('basic-alias', function (instance) {
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
template: 'test.widget.template' template: 'test.widget.template'
})); }));
@ -173,9 +200,26 @@ $(document).ready(function () {
ok(w.$('li:eq(3)').is(w.$el.find('li:eq(3)')), ok(w.$('li:eq(3)').is(w.$el.find('li:eq(3)')),
"should do the same thing as calling find on the widget root"); "should do the same thing as calling find on the widget root");
}); });
});
module('Widget.events', mod); openerp.testing.section('Widget.events', {
test('delegate', function () { dependencies: ['web.corelib'],
setup: function (instance) {
instance.web.qweb = new QWeb2.Engine();
instance.web.qweb.add_template(
'<no>' +
'<t t-name="test.widget.template">' +
'<ol>' +
'<li t-foreach="5" t-as="counter" ' +
't-attf-class="class-#{counter}">' +
'<input/>' +
'<t t-esc="counter"/>' +
'</li>' +
'</ol>' +
'</t>' +
'</no>');
}
}, function (test) {
test('delegate', function (instance) {
var a = []; var a = [];
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
template: 'test.widget.template', template: 'test.widget.template',
@ -199,7 +243,7 @@ $(document).ready(function () {
ok(a[i], "should pass test " + i); ok(a[i], "should pass test " + i);
} }
}); });
test('undelegate', function () { test('undelegate', function (instance) {
var clicked = false, newclicked = false; var clicked = false, newclicked = false;
var w = new (instance.web.Widget.extend({ var w = new (instance.web.Widget.extend({
template: 'test.widget.template', template: 'test.widget.template',
@ -218,22 +262,4 @@ $(document).ready(function () {
ok(!clicked, "undelegate should unbind events delegated"); ok(!clicked, "undelegate should unbind events delegated");
ok(newclicked, "undelegate should only unbind events it created"); ok(newclicked, "undelegate should only unbind events it created");
}); });
module('Widget.renderElement', mod);
asyncTest('repeated', 4, function () {
var w = new (instance.web.Widget.extend({
template: 'test.widget.template-value'
}));
w.value = 42;
w.appendTo($fix)
.always(start)
.done(function () {
equal($fix.find('p').text(), '42', "DOM fixture should contain initial value");
equal(w.$el.text(), '42', "should set initial value");
w.value = 36;
w.renderElement();
equal($fix.find('p').text(), '36', "DOM fixture should use new value");
equal(w.$el.text(), '36', "should set new value");
});
});
}); });

View File

@ -1,30 +1,25 @@
$(document).ready(function () { openerp.testing.section('class', {
var openerp; dependencies: ['web.corelib']
module('web-class', { }, function (test) {
setup: function () { test('Basic class creation', function (instance) {
openerp = window.openerp.init([]); var C = instance.web.Class.extend({
window.openerp.web.corelib(openerp);
}
});
test('Basic class creation', function () {
var C = openerp.web.Class.extend({
foo: function () { foo: function () {
return this.somevar; return this.somevar;
} }
}); });
var instance = new C(); var i = new C();
instance.somevar = 3; i.somevar = 3;
ok(instance instanceof C); ok(i instanceof C);
strictEqual(instance.foo(), 3); strictEqual(i.foo(), 3);
}); });
test('Class initialization', function () { test('Class initialization', function (instance) {
var C1 = openerp.web.Class.extend({ var C1 = instance.web.Class.extend({
init: function () { init: function () {
this.foo = 3; this.foo = 3;
} }
}); });
var C2 = openerp.web.Class.extend({ var C2 = instance.web.Class.extend({
init: function (arg) { init: function (arg) {
this.foo = arg; this.foo = arg;
} }
@ -36,8 +31,8 @@ $(document).ready(function () {
strictEqual(i1.foo, 3); strictEqual(i1.foo, 3);
strictEqual(i2.foo, 42); strictEqual(i2.foo, 42);
}); });
test('Inheritance', function () { test('Inheritance', function (instance) {
var C0 = openerp.web.Class.extend({ var C0 = instance.web.Class.extend({
foo: function () { foo: function () {
return 1; return 1;
} }
@ -57,8 +52,8 @@ $(document).ready(function () {
strictEqual(new C1().foo(), 2); strictEqual(new C1().foo(), 2);
strictEqual(new C2().foo(), 3); strictEqual(new C2().foo(), 3);
}); });
test('In-place extension', function () { test('In-place extension', function (instance) {
var C0 = openerp.web.Class.extend({ var C0 = instance.web.Class.extend({
foo: function () { foo: function () {
return 3; return 3;
}, },
@ -83,8 +78,8 @@ $(document).ready(function () {
strictEqual(new C0().foo(), 5); strictEqual(new C0().foo(), 5);
strictEqual(new C0().qux(), 5); strictEqual(new C0().qux(), 5);
}); });
test('In-place extension and inheritance', function () { test('In-place extension and inheritance', function (instance) {
var C0 = openerp.web.Class.extend({ var C0 = instance.web.Class.extend({
foo: function () { return 1; }, foo: function () { return 1; },
bar: function () { return 1; } bar: function () { return 1; }
}); });
@ -101,24 +96,24 @@ $(document).ready(function () {
strictEqual(new C1().foo(), 4); strictEqual(new C1().foo(), 4);
strictEqual(new C1().bar(), 2); strictEqual(new C1().bar(), 2);
}); });
test('In-place extensions alter existing instances', function () { test('In-place extensions alter existing instances', function (instance) {
var C0 = openerp.web.Class.extend({ var C0 = instance.web.Class.extend({
foo: function () { return 1; }, foo: function () { return 1; },
bar: function () { return 1; } bar: function () { return 1; }
}); });
var instance = new C0(); var i = new C0();
strictEqual(instance.foo(), 1); strictEqual(i.foo(), 1);
strictEqual(instance.bar(), 1); strictEqual(i.bar(), 1);
C0.include({ C0.include({
foo: function () { return 2; }, foo: function () { return 2; },
bar: function () { return 2 + this._super(); } bar: function () { return 2 + this._super(); }
}); });
strictEqual(instance.foo(), 2); strictEqual(i.foo(), 2);
strictEqual(instance.bar(), 3); strictEqual(i.bar(), 3);
}); });
test('In-place extension of subclassed types', function () { test('In-place extension of subclassed types', function (instance) {
var C0 = openerp.web.Class.extend({ var C0 = instance.web.Class.extend({
foo: function () { return 1; }, foo: function () { return 1; },
bar: function () { return 1; } bar: function () { return 1; }
}); });
@ -126,13 +121,13 @@ $(document).ready(function () {
foo: function () { return 1 + this._super(); }, foo: function () { return 1 + this._super(); },
bar: function () { return 1 + this._super(); } bar: function () { return 1 + this._super(); }
}); });
var instance = new C1(); var i = new C1();
strictEqual(instance.foo(), 2); strictEqual(i.foo(), 2);
C0.include({ C0.include({
foo: function () { return 2; }, foo: function () { return 2; },
bar: function () { return 2 + this._super(); } bar: function () { return 2 + this._super(); }
}); });
strictEqual(instance.foo(), 3); strictEqual(i.foo(), 3);
strictEqual(instance.bar(), 4); strictEqual(i.bar(), 4);
}); });
}); });

View File

@ -1,18 +1,11 @@
$(document).ready(function () { openerp.testing.section('eval.contexts', {
var openerp; dependencies: ['web.coresetup']
}, function (test) {
module("eval.contexts", { test('context_sequences', function (instance) {
setup: function () {
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
}
});
test('context_sequences', function () {
// Context n should have base evaluation context + all of contexts // Context n should have base evaluation context + all of contexts
// 0..n-1 in its own evaluation context // 0..n-1 in its own evaluation context
var active_id = 4; var active_id = 4;
var result = openerp.session.test_eval_contexts([ var result = instance.session.test_eval_contexts([
{ {
"__contexts": [ "__contexts": [
{ {
@ -55,8 +48,8 @@ $(document).ready(function () {
record_id: active_id record_id: active_id
}); });
}); });
test('non-literal_eval_contexts', function () { test('non-literal_eval_contexts', function (instance) {
var result = openerp.session.test_eval_contexts([{ var result = instance.session.test_eval_contexts([{
"__ref": "compound_context", "__ref": "compound_context",
"__contexts": [ "__contexts": [
{"__ref": "context", "__debug": "{'type':parent.type}", {"__ref": "context", "__debug": "{'type':parent.type}",
@ -133,17 +126,15 @@ $(document).ready(function () {
}]); }]);
deepEqual(result, {type: 'out_invoice'}); deepEqual(result, {type: 'out_invoice'});
}); });
module('eval.domains', { });
setup: function () { openerp.testing.section('eval.contexts', {
openerp = window.openerp.testing.instanceFor('coresetup'); dependencies: ['web.coresetup', 'web.dates']
window.openerp.web.dates(openerp); }, function (test) {
} test('current_date', function (instance) {
}); var current_date = instance.web.date_to_str(new Date());
test('current_date', function () { var result = instance.session.test_eval_domains(
var current_date = openerp.web.date_to_str(new Date());
var result = openerp.session.test_eval_domains(
[[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}], [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}],
openerp.session.test_eval_get_context()); instance.session.test_eval_get_context());
deepEqual(result, [ deepEqual(result, [
['name', '>=', current_date], ['name', '>=', current_date],
['name', '<=', current_date] ['name', '<=', current_date]

View File

@ -1,33 +1,21 @@
$(document).ready(function () { openerp.testing.section('compute_domain', {
var openerp; dependencies: ['web.form']
module("form.widget", { }, function (test) {
setup: function () { test("basic", function (instance) {
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
window.openerp.web.form(openerp);
}
});
test("compute_domain", function () {
var fields = { var fields = {
'a': {value: 3}, 'a': {value: 3},
'group_method': {value: 'line'}, 'group_method': {value: 'line'},
'select1': {value: 'day'}, 'select1': {value: 'day'},
'rrule_type': {value: 'monthly'} 'rrule_type': {value: 'monthly'}
}; };
ok(openerp.web.form.compute_domain( ok(instance.web.form.compute_domain(
[['a', '=', 3]], fields)); [['a', '=', 3]], fields));
ok(openerp.web.form.compute_domain( ok(instance.web.form.compute_domain(
[['group_method','!=','count']], fields)); [['group_method','!=','count']], fields));
ok(openerp.web.form.compute_domain( ok(instance.web.form.compute_domain(
[['select1','=','day'], ['rrule_type','=','monthly']], fields)); [['select1','=','day'], ['rrule_type','=','monthly']], fields));
}); });
test("compute_domain or", function () { test("or", function (instance) {
var web = { var web = {
'section_id': {value: null}, 'section_id': {value: null},
'user_id': {value: null}, 'user_id': {value: null},
@ -38,22 +26,22 @@ $(document).ready(function () {
'|', ['user_id','=',3], '|', ['user_id','=',3],
['member_ids', 'in', [3]]]; ['member_ids', 'in', [3]]];
ok(openerp.web.form.compute_domain(domain, _.extend( ok(instance.web.form.compute_domain(domain, _.extend(
{}, web, {'section_id': {value: 42}}))); {}, web, {'section_id': {value: 42}})));
ok(openerp.web.form.compute_domain(domain, _.extend( ok(instance.web.form.compute_domain(domain, _.extend(
{}, web, {'user_id': {value: 3}}))); {}, web, {'user_id': {value: 3}})));
ok(openerp.web.form.compute_domain(domain, _.extend( ok(instance.web.form.compute_domain(domain, _.extend(
{}, web, {'member_ids': {value: 3}}))); {}, web, {'member_ids': {value: 3}})));
}); });
test("compute_domain not", function () { test("not", function (instance) {
var fields = { var fields = {
'a': {value: 5}, 'a': {value: 5},
'group_method': {value: 'line'} 'group_method': {value: 'line'}
}; };
ok(openerp.web.form.compute_domain( ok(instance.web.form.compute_domain(
['!', ['a', '=', 3]], fields)); ['!', ['a', '=', 3]], fields));
ok(openerp.web.form.compute_domain( ok(instance.web.form.compute_domain(
['!', ['group_method','=','count']], fields)); ['!', ['group_method','=','count']], fields));
}); });
}); });

View File

@ -1,16 +1,8 @@
$(document).ready(function () { openerp.testing.section('server-formats', {
var openerp; dependencies: ['web.coresetup', 'web.dates']
}, function (test) {
module('server-formats', { test('Parse server datetime', function (instance) {
setup: function () { var date = instance.web.str_to_datetime("2009-05-04 12:34:23");
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.dates(openerp);
}
});
test('Parse server datetime', function () {
var date = openerp.web.str_to_datetime("2009-05-04 12:34:23");
deepEqual( deepEqual(
[date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()], date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()],
@ -20,92 +12,86 @@ $(document).ready(function () {
date.getHours(), date.getMinutes(), date.getSeconds()], date.getHours(), date.getMinutes(), date.getSeconds()],
[2009, 5 - 1, 4, 12 - (date.getTimezoneOffset() / 60), 34, 23]); [2009, 5 - 1, 4, 12 - (date.getTimezoneOffset() / 60), 34, 23]);
var date2 = openerp.web.str_to_datetime('2011-12-10 00:00:00'); var date2 = instance.web.str_to_datetime('2011-12-10 00:00:00');
deepEqual( deepEqual(
[date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(), [date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(),
date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()], date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()],
[2011, 12 - 1, 10, 0, 0, 0]); [2011, 12 - 1, 10, 0, 0, 0]);
}); });
test('Parse server date', function () { test('Parse server date', function (instance) {
var date = openerp.web.str_to_date("2009-05-04"); var date = instance.web.str_to_date("2009-05-04");
deepEqual( deepEqual(
[date.getFullYear(), date.getMonth(), date.getDate()], [date.getFullYear(), date.getMonth(), date.getDate()],
[2009, 5 - 1, 4]); [2009, 5 - 1, 4]);
}); });
test('Parse server time', function () { test('Parse server time', function (instance) {
var date = openerp.web.str_to_time("12:34:23"); var date = instance.web.str_to_time("12:34:23");
deepEqual( deepEqual(
[date.getHours(), date.getMinutes(), date.getSeconds()], [date.getHours(), date.getMinutes(), date.getSeconds()],
[12, 34, 23]); [12, 34, 23]);
}); });
});
module('web-formats', { openerp.testing.section('web-formats', {
setup: function () { dependencies: ['web.formats']
openerp = window.openerp.init([]); }, function (test) {
window.openerp.web.corelib(openerp); test("format_datetime", function (instance) {
window.openerp.web.coresetup(openerp); var date = instance.web.str_to_datetime("2009-05-04 12:34:23");
window.openerp.web.dates(openerp); var str = instance.web.format_value(date, {type:"datetime"});
window.openerp.web.formats(openerp);
}
});
test("format_datetime", function () {
var date = openerp.web.str_to_datetime("2009-05-04 12:34:23");
var str = openerp.web.format_value(date, {type:"datetime"});
equal(str, date.toString("MM/dd/yyyy HH:mm:ss")); equal(str, date.toString("MM/dd/yyyy HH:mm:ss"));
}); });
test("format_date", function () { test("format_date", function (instance) {
var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); var date = instance.web.str_to_datetime("2009-05-04 12:34:23");
var str = openerp.web.format_value(date, {type:"date"}); var str = instance.web.format_value(date, {type:"date"});
equal(str, date.toString("MM/dd/yyyy")); equal(str, date.toString("MM/dd/yyyy"));
}); });
test("format_time", function () { test("format_time", function (instance) {
var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); var date = instance.web.str_to_datetime("2009-05-04 12:34:23");
var str = openerp.web.format_value(date, {type:"time"}); var str = instance.web.format_value(date, {type:"time"});
equal(str, date.toString("HH:mm:ss")); equal(str, date.toString("HH:mm:ss"));
}); });
test("format_float_time", function () { test("format_float_time", function (instance) {
strictEqual( strictEqual(
openerp.web.format_value(1.0, {type:'float', widget:'float_time'}), instance.web.format_value(1.0, {type:'float', widget:'float_time'}),
'01:00'); '01:00');
strictEqual( strictEqual(
openerp.web.format_value(0.9853, {type:'float', widget:'float_time'}), instance.web.format_value(0.9853, {type:'float', widget:'float_time'}),
'00:59'); '00:59');
strictEqual( strictEqual(
openerp.web.format_value(0.0085, {type:'float', widget:'float_time'}), instance.web.format_value(0.0085, {type:'float', widget:'float_time'}),
'00:01'); '00:01');
strictEqual( strictEqual(
openerp.web.format_value(-1.0, {type:'float', widget:'float_time'}), instance.web.format_value(-1.0, {type:'float', widget:'float_time'}),
'-01:00'); '-01:00');
strictEqual( strictEqual(
openerp.web.format_value(-0.9853, {type:'float', widget:'float_time'}), instance.web.format_value(-0.9853, {type:'float', widget:'float_time'}),
'-00:59'); '-00:59');
strictEqual( strictEqual(
openerp.web.format_value(-0.0085, {type:'float', widget:'float_time'}), instance.web.format_value(-0.0085, {type:'float', widget:'float_time'}),
'-00:01'); '-00:01');
}); });
test("format_float", function () { test("format_float", function (instance) {
var fl = 12.1234; var fl = 12.1234;
var str = openerp.web.format_value(fl, {type:"float"}); var str = instance.web.format_value(fl, {type:"float"});
equal(str, "12.12"); equal(str, "12.12");
equal(openerp.web.format_value(12.02, {type: 'float'}), equal(instance.web.format_value(12.02, {type: 'float'}),
'12.02'); '12.02');
equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 3]}), equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 3]}),
'0.000'); '0.000');
equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 4]}), equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 4]}),
'0.0002'); '0.0002');
equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 6]}), equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 6]}),
'0.000200'); '0.000200');
equal(openerp.web.format_value(1, {type: 'float', digits: [1, 6]}), equal(instance.web.format_value(1, {type: 'float', digits: [1, 6]}),
'1.000000'); '1.000000');
equal(openerp.web.format_value(1, {type: 'float'}), equal(instance.web.format_value(1, {type: 'float'}),
'1.00'); '1.00');
equal(openerp.web.format_value(-11.25, {type: 'float'}), equal(instance.web.format_value(-11.25, {type: 'float'}),
"-11.25"); "-11.25");
openerp.web._t.database.parameters.grouping = [1, 2, -1]; instance.web._t.database.parameters.grouping = [1, 2, -1];
equal(openerp.web.format_value(1111111.25, {type: 'float'}), equal(instance.web.format_value(1111111.25, {type: 'float'}),
"1111,11,1.25"); "1111,11,1.25");
openerp.web._t.database.parameters.grouping = [1, 0]; instance.web._t.database.parameters.grouping = [1, 0];
equal(openerp.web.format_value(-11.25, {type: 'float'}), equal(instance.web.format_value(-11.25, {type: 'float'}),
"-1,1.25"); "-1,1.25");
}); });
// test("parse_datetime", function () { // test("parse_datetime", function () {
@ -123,29 +109,29 @@ $(document).ready(function () {
// var res = openerp.web.parse_value(val.toString("HH:mm:ss"), {type:"time"}); // var res = openerp.web.parse_value(val.toString("HH:mm:ss"), {type:"time"});
// equal(val.toString("HH:mm:ss"), res.toString("HH:mm:ss")); // equal(val.toString("HH:mm:ss"), res.toString("HH:mm:ss"));
// }); // });
test('parse_integer', function () { test('parse_integer', function (instance) {
var val = openerp.web.parse_value('123,456', {type: 'integer'}); var val = instance.web.parse_value('123,456', {type: 'integer'});
equal(val, 123456); equal(val, 123456);
openerp.web._t.database.parameters.thousands_sep = '|'; instance.web._t.database.parameters.thousands_sep = '|';
var val2 = openerp.web.parse_value('123|456', {type: 'integer'}); var val2 = instance.web.parse_value('123|456', {type: 'integer'});
equal(val2, 123456); equal(val2, 123456);
}); });
test("parse_float", function () { test("parse_float", function (instance) {
var str = "134,112.1234"; var str = "134,112.1234";
var val = openerp.web.parse_value(str, {type:"float"}); var val = instance.web.parse_value(str, {type:"float"});
equal(val, 134112.1234); equal(val, 134112.1234);
var str = "-134,112.1234"; var str = "-134,112.1234";
var val = openerp.web.parse_value(str, {type:"float"}); var val = instance.web.parse_value(str, {type:"float"});
equal(val, -134112.1234); equal(val, -134112.1234);
_.extend(openerp.web._t.database.parameters, { _.extend(instance.web._t.database.parameters, {
decimal_point: ',', decimal_point: ',',
thousands_sep: '.' thousands_sep: '.'
}); });
var val3 = openerp.web.parse_value('123.456,789', {type: 'float'}); var val3 = instance.web.parse_value('123.456,789', {type: 'float'});
equal(val3, 123456.789); equal(val3, 123456.789);
}); });
test('intersperse', function () { test('intersperse', function (instance) {
var g = openerp.web.intersperse; var g = instance.web.intersperse;
equal(g("", []), ""); equal(g("", []), "");
equal(g("0", []), "0"); equal(g("0", []), "0");
equal(g("012", []), "012"); equal(g("012", []), "012");
@ -176,60 +162,61 @@ $(document).ready(function () {
equal(g("12345678", [3,3,3,3], '.'), '12.345.678'); equal(g("12345678", [3,3,3,3], '.'), '12.345.678');
equal(g("12345678", [3,0], '.'), '12.345.678'); equal(g("12345678", [3,0], '.'), '12.345.678');
}); });
test('format_integer', function () { test('format_integer', function (instance) {
openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; instance.web._t.database.parameters.grouping = [3, 3, 3, 3];
equal(openerp.web.format_value(1000000, {type: 'integer'}), equal(instance.web.format_value(1000000, {type: 'integer'}),
'1,000,000'); '1,000,000');
openerp.web._t.database.parameters.grouping = [3, 2, -1]; instance.web._t.database.parameters.grouping = [3, 2, -1];
equal(openerp.web.format_value(106500, {type: 'integer'}), equal(instance.web.format_value(106500, {type: 'integer'}),
'1,06,500'); '1,06,500');
openerp.web._t.database.parameters.grouping = [1, 2, -1]; instance.web._t.database.parameters.grouping = [1, 2, -1];
equal(openerp.web.format_value(106500, {type: 'integer'}), equal(instance.web.format_value(106500, {type: 'integer'}),
'106,50,0'); '106,50,0');
}); });
test('format_float', function () { test('format_float', function (instance) {
openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; instance.web._t.database.parameters.grouping = [3, 3, 3, 3];
equal(openerp.web.format_value(1000000, {type: 'float'}), equal(instance.web.format_value(1000000, {type: 'float'}),
'1,000,000.00'); '1,000,000.00');
openerp.web._t.database.parameters.grouping = [3, 2, -1]; instance.web._t.database.parameters.grouping = [3, 2, -1];
equal(openerp.web.format_value(106500, {type: 'float'}), equal(instance.web.format_value(106500, {type: 'float'}),
'1,06,500.00'); '1,06,500.00');
openerp.web._t.database.parameters.grouping = [1, 2, -1]; instance.web._t.database.parameters.grouping = [1, 2, -1];
equal(openerp.web.format_value(106500, {type: 'float'}), equal(instance.web.format_value(106500, {type: 'float'}),
'106,50,0.00'); '106,50,0.00');
_.extend(openerp.web._t.database.parameters, { _.extend(instance.web._t.database.parameters, {
grouping: [3, 0], grouping: [3, 0],
decimal_point: ',', decimal_point: ',',
thousands_sep: '.' thousands_sep: '.'
}); });
equal(openerp.web.format_value(6000, {type: 'float'}), equal(instance.web.format_value(6000, {type: 'float'}),
'6.000,00'); '6.000,00');
}); });
module('custom-date-formats', { });
setup: function () { openerp.testing.section('web-formats', {
openerp = window.openerp.init([]); dependencies: ['web.formats']
window.openerp.web.corelib(openerp); }, function (test) {
window.openerp.web.coresetup(openerp); test('format stripper', function (instance) {
window.openerp.web.dates(openerp); strictEqual(instance.web.strip_raw_chars('%a, %Y %b %d'),
window.openerp.web.formats(openerp); '%a, %Y %b %d');
} strictEqual(instance.web.strip_raw_chars('%a, %Y.eko %bren %da'),
'%a, %Y. %b %d');
}); });
test('format stripper', function () { test('ES date format', function (instance) {
strictEqual(openerp.web.strip_raw_chars('%a, %Y %b %d'), '%a, %Y %b %d'); instance.web._t.database.parameters.date_format = '%a, %Y %b %d';
strictEqual(openerp.web.strip_raw_chars('%a, %Y.eko %bren %da'), '%a, %Y. %b %d'); var date = instance.web.str_to_date("2009-05-04");
strictEqual(instance.web.format_value(date, {type:"date"}),
'Mon, 2009 May 04');
strictEqual(instance.web.parse_value('Mon, 2009 May 04', {type: 'date'}),
'2009-05-04');
}); });
test('ES date format', function () { test('extended ES date format', function (instance) {
openerp.web._t.database.parameters.date_format = '%a, %Y %b %d'; instance.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da';
var date = openerp.web.str_to_date("2009-05-04"); var date = instance.web.str_to_date("2009-05-04");
strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009 May 04'); strictEqual(instance.web.format_value(date, {type:"date"}),
strictEqual(openerp.web.parse_value('Mon, 2009 May 04', {type: 'date'}), '2009-05-04'); 'Mon, 2009. May 04');
}); strictEqual(instance.web.parse_value('Mon, 2009. May 04', {type: 'date'}),
test('extended ES date format', function () { '2009-05-04');
openerp.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da';
var date = openerp.web.str_to_date("2009-05-04");
strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009. May 04');
strictEqual(openerp.web.parse_value('Mon, 2009. May 04', {type: 'date'}), '2009-05-04');
}); });
}); });

View File

@ -1,16 +1,13 @@
$(document).ready(function () { openerp.testing.section('editor', {
var $fix = $('#qunit-fixture'); dependencies: ['web.list_editable'],
rpc: 'mock',
var instance; templates: true,
var baseSetup = function () { setup: function (instance, $s, mock) {
instance = openerp.testing.instanceFor('list_editable'); mock('test.model:create', function () {
return 42;
openerp.testing.loadTemplate(instance); });
}
openerp.testing.mockifyRPC(instance); }, function (test) {
};
/** /**
* *
* @param {String} name * @param {String} name
@ -30,7 +27,7 @@ $(document).ready(function () {
} }
/** /**
* @param {Array} fields * @param {Array} [fields]
* @return {Object} * @return {Object}
*/ */
function makeFormView(fields) { function makeFormView(fields) {
@ -67,46 +64,37 @@ $(document).ready(function () {
}; };
} }
module('editor', { test('base-state', {asserts: 2}, function (instance, $fix) {
setup: baseSetup
});
asyncTest('base-state', 2, function () {
var e = new instance.web.list.Editor({ var e = new instance.web.list.Editor({
dataset: {ids: []}, dataset: {ids: []},
edition_view: function () { edition_view: function () {
return makeFormView(); return makeFormView();
} }
}); });
e.appendTo($fix) return e.appendTo($fix)
.always(start)
.fail(function (error) { ok(false, error && error.message); })
.done(function () { .done(function () {
ok(!e.is_editing(), "should not be editing"); ok(!e.is_editing(), "should not be editing");
ok(e.form instanceof instance.web.FormView, ok(e.form instanceof instance.web.FormView,
"should use default form type"); "should use default form type");
}); });
}); });
asyncTest('toggle-edition-save', 4, function () { test('toggle-edition-save', {
instance.session.responses['/web/dataset/call_kw:create'] = function () { asserts: 4,
return { result: 42 }; setup: function (instance, $s, mock) {
}; mock('test.model:read', function () {
instance.session.responses['/web/dataset/call_kw:read'] = function () { return [{id: 42, a: false, b: false, c: false}];
return { result: [{ });
id: 42, }
a: false, }, function (instance, $fix) {
b: false,
c: false
}]};
};
var e = new instance.web.list.Editor({ var e = new instance.web.list.Editor({
dataset: new instance.web.DataSetSearch(), dataset: new instance.web.DataSetSearch(null, 'test.model'),
prepends_on_create: function () { return false; }, prepends_on_create: function () { return false; },
edition_view: function () { edition_view: function () {
return makeFormView([ field('a'), field('b'), field('c') ]); return makeFormView([ field('a'), field('b'), field('c') ]);
} }
}); });
var counter = 0; var counter = 0;
e.appendTo($fix) return e.appendTo($fix)
.then(function () { .then(function () {
return e.edit({}, function () { return e.edit({}, function () {
++counter; ++counter;
@ -117,26 +105,21 @@ $(document).ready(function () {
equal(counter, 3, "should have configured all fields"); equal(counter, 3, "should have configured all fields");
return e.save(); return e.save();
}) })
.always(start)
.fail(function (error) { ok(false, error && error.message); })
.done(function (record) { .done(function (record) {
ok(!e.is_editing(), "should have stopped editing"); ok(!e.is_editing(), "should have stopped editing");
equal(record.id, 42, "should have newly created id"); equal(record.id, 42, "should have newly created id");
}) })
}); });
asyncTest('toggle-edition-cancel', 2, function () { test('toggle-edition-cancel', { asserts: 2 }, function (instance, $fix) {
instance.session.responses['/web/dataset/call_kw:create'] = function () {
return { result: 42 };
};
var e = new instance.web.list.Editor({ var e = new instance.web.list.Editor({
dataset: new instance.web.DataSetSearch(), dataset: new instance.web.DataSetSearch(null, 'test.model'),
prepends_on_create: function () { return false; }, prepends_on_create: function () { return false; },
edition_view: function () { edition_view: function () {
return makeFormView([ field('a'), field('b'), field('c') ]); return makeFormView([ field('a'), field('b'), field('c') ]);
} }
}); });
var counter = 0; var counter = 0;
e.appendTo($fix) return e.appendTo($fix)
.then(function () { .then(function () {
return e.edit({}, function () { return e.edit({}, function () {
++counter; ++counter;
@ -145,22 +128,20 @@ $(document).ready(function () {
.then(function (form) { .then(function (form) {
return e.cancel(); return e.cancel();
}) })
.always(start)
.fail(function (error) { ok(false, error && error.message); })
.done(function (record) { .done(function (record) {
ok(!e.is_editing(), "should have stopped editing"); ok(!e.is_editing(), "should have stopped editing");
ok(!record.id, "should have no id"); ok(!record.id, "should have no id");
}) })
}); });
asyncTest('toggle-save-required', 2, function () { test('toggle-save-required', {
instance.session.responses['/web/dataset/call_kw:create'] = function () { asserts: 2,
return { result: 42 }; fail_on_rejection: false
}; }, function (instance, $fix) {
var e = new instance.web.list.Editor({ var e = new instance.web.list.Editor({
do_warn: function () { do_warn: function () {
warnings++; warnings++;
}, },
dataset: new instance.web.DataSetSearch(), dataset: new instance.web.DataSetSearch(null, 'test.model'),
prepends_on_create: function () { return false; }, prepends_on_create: function () { return false; },
edition_view: function () { edition_view: function () {
return makeFormView([ return makeFormView([
@ -169,7 +150,7 @@ $(document).ready(function () {
}); });
var counter = 0; var counter = 0;
var warnings = 0; var warnings = 0;
e.appendTo($fix) return e.appendTo($fix)
.then(function () { .then(function () {
return e.edit({}, function () { return e.edit({}, function () {
++counter; ++counter;
@ -178,78 +159,73 @@ $(document).ready(function () {
.then(function (form) { .then(function (form) {
return e.save(); return e.save();
}) })
.always(start)
.done(function () { ok(false, "cancel should not succeed"); }) .done(function () { ok(false, "cancel should not succeed"); })
.fail(function () { .fail(function () {
equal(warnings, 1, "should have been warned"); equal(warnings, 1, "should have been warned");
ok(e.is_editing(), "should have kept editing"); ok(e.is_editing(), "should have kept editing");
}) });
}); });
});
module('list-edition', { openerp.testing.section('list.edition', {
setup: function () { dependencies: ['web.list_editable'],
baseSetup(); rpc: 'mock',
templates: true,
var records = {}; setup: function (instance, $s, mock) {
_.extend(instance.session.responses, { var records = {};
'/web/view/load': function () { mock('demo:create', function (args) {
return {result: { records[42] = _.extend({}, args[0]);
type: 'tree', return 42;
fields: { });
a: {type: 'char', string: "A"}, mock('demo:read', function (args) {
b: {type: 'char', string: "B"}, var id = args[0][0];
c: {type: 'char', string: "C"} if (id in records) {
}, return [records[id]];
arch: { }
tag: 'tree', return [];
attrs: {}, });
children: [ mock('/web/view/load', function () {
{tag: 'field', attrs: {name: 'a'}}, return {
{tag: 'field', attrs: {name: 'b'}}, type: 'tree',
{tag: 'field', attrs: {name: 'c'}} fields: {
] a: {type: 'char', string: "A"},
} b: {type: 'char', string: "B"},
}}; c: {type: 'char', string: "C"}
}, },
'/web/dataset/call_kw:create': function (params) { arch: {
records[42] = _.extend({}, params.params.args[0]); tag: 'tree',
return {result: 42}; attrs: {},
}, children: [
'/web/dataset/call_kw:read': function (params) { {tag: 'field', attrs: {name: 'a'}},
var id = params.params.args[0][0]; {tag: 'field', attrs: {name: 'b'}},
if (id in records) { {tag: 'field', attrs: {name: 'c'}}
return {result: [records[id]]}; ]
}
return {result: []};
} }
}) };
} });
}); }
asyncTest('newrecord', 6, function () { }, function (test) {
test('newrecord', {asserts: 6}, function (instance, $fix, mock) {
var got_defaults = false; var got_defaults = false;
instance.session.responses['/web/dataset/call_kw:default_get'] = function (params) { mock('demo:default_get', function (args) {
var fields = params.params.args[0]; var fields = args[0];
deepEqual( deepEqual(
fields, ['a', 'b', 'c'], fields, ['a', 'b', 'c'],
"should ask defaults for all fields"); "should ask defaults for all fields");
got_defaults = true; got_defaults = true;
return {result: { return { a: "qux", b: "quux" };
a: "qux", });
b: "quux"
}};
};
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
l.appendTo($fix) return l.appendTo($fix)
.then(l.proxy('reload_content')) .then(l.proxy('reload_content'))
.then(function () { .then(function () {
return l.start_edition(); return l.start_edition();
}) })
.always(start)
.then(function () { .then(function () {
ok(got_defaults, "should have fetched default values for form"); ok(got_defaults, "should have fetched default values for form");
return l.save_edition(); return l.save_edition();
}) })
.then(function (result) { .then(function (result) {
@ -260,45 +236,39 @@ $(document).ready(function () {
"should have used default values"); "should have used default values");
ok(!result.record.get('c'), ok(!result.record.get('c'),
"should have no value if there was no default"); "should have no value if there was no default");
})
.fail(function (e) { ok(false, e && e.message || e); });
});
module('list-edition-events', {
setup: function () {
baseSetup();
_.extend(instance.session.responses, {
'/web/view/load': function () {
return {result: {
type: 'tree',
fields: {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
},
arch: {
tag: 'tree',
attrs: {},
children: [
{tag: 'field', attrs: {name: 'a'}},
{tag: 'field', attrs: {name: 'b'}},
{tag: 'field', attrs: {name: 'c'}}
]
}
}};
},
'/web/dataset/call_kw:read': function (params) {
return {result: [{
id: 1,
a: 'foo',
b: 'bar',
c: 'baz'
}]};
}
}); });
}
}); });
asyncTest('edition events', 4, function () { });
openerp.testing.section('list.edition.events', {
dependencies: ['web.list_editable'],
rpc: 'mock',
templates: true,
setup: function (instance, $s, mock) {
mock('demo:read', function () {
return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }];
});
mock('/web/view/load', function () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
},
arch: {
tag: 'tree',
attrs: {},
children: [
{tag: 'field', attrs: {name: 'a'}},
{tag: 'field', attrs: {name: 'b'}},
{tag: 'field', attrs: {name: 'c'}}
]
}
};
});
}
}, function (test) {
test('edition events', {asserts: 4}, function (instance, $fix) {
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
var o = { var o = {
counter: 0, counter: 0,
@ -306,9 +276,8 @@ $(document).ready(function () {
}; };
var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
l.on('edit:before edit:after', o, o.onEvent); l.on('edit:before edit:after', o, o.onEvent);
l.appendTo($fix) return l.appendTo($fix)
.then(l.proxy('reload_content')) .then(l.proxy('reload_content'))
.always(start)
.then(function () { .then(function () {
ok(l.options.editable, "should be editable"); ok(l.options.editable, "should be editable");
equal(o.counter, 0, "should have seen no event yet"); equal(o.counter, 0, "should have seen no event yet");
@ -317,11 +286,10 @@ $(document).ready(function () {
.then(function () { .then(function () {
ok(l.editor.is_editing(), "should be editing"); ok(l.editor.is_editing(), "should be editing");
equal(o.counter, 2, "should have seen two edition events"); equal(o.counter, 2, "should have seen two edition events");
}) });
.fail(function (e) { ok(false, e && e.message); });
}); });
asyncTest('edition events: cancelling', 3, function () { test('edition events: cancelling', {asserts: 3}, function (instance, $fix) {
var edit_after = false; var edit_after = false;
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
@ -331,9 +299,8 @@ $(document).ready(function () {
l.on('edit:after', {}, function () { l.on('edit:after', {}, function () {
edit_after = true; edit_after = true;
}); });
l.appendTo($fix) return l.appendTo($fix)
.then(l.proxy('reload_content')) .then(l.proxy('reload_content'))
.always(start)
.then(function () { .then(function () {
ok(l.options.editable, "should be editable"); ok(l.options.editable, "should be editable");
return l.start_edition(); return l.start_edition();
@ -343,7 +310,71 @@ $(document).ready(function () {
ok(!l.editor.is_editing(), "should not be editing"); ok(!l.editor.is_editing(), "should not be editing");
ok(!edit_after, "should not have fired the edit:after event"); ok(!edit_after, "should not have fired the edit:after event");
return $.when(); return $.when();
}) });
.fail(function (e) { ok(false, e && e.message || e); }); });
});
openerp.testing.section('list.edition.onwrite', {
dependencies: ['web.list_editable'],
rpc: 'mock',
templates: true,
}, function (test) {
test('record-to-read', {asserts: 4}, function (instance, $fix, mock) {
mock('/web/view/load', function () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: {
tag: 'tree',
attrs: { on_write: 'on_write', colors: 'red:a == "foo"' },
children: [
{tag: 'field', attrs: {name: 'a'}}
]
}
};
});
mock('demo:read', function (args, kwargs) {
if (_.isEmpty(args[0])) {
return [];
} else if (_.isEqual(args[0], [1])) {
return [
{id: 1, a: 'some value'}
];
} else if (_.isEqual(args[0], [42])) {
return [ {id: 42, a: 'foo'} ];
}
throw new Error(JSON.stringify(_.toArray(arguments)));
});
mock('demo:default_get', function () { return {}; });
mock('demo:create', function () { return 1; });
mock('demo:on_write', function () { return [42]; });
var ds = new instance.web.DataSetStatic(null, 'demo', null, []);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
return l.appendTo($fix)
.then(l.proxy('reload_content'))
.then(function () {
return l.start_edition();
})
.then(function () {
$fix.find('.oe_form_field input').val("some value").change();
})
.then(function () {
return l.save_edition();
})
.then(function () {
strictEqual(ds.ids.length, 2,
'should have id of created + on_write');
strictEqual(l.records.length, 2,
'should have record of created + on_write');
strictEqual(
$fix.find('tbody tr:eq(1)').css('color'), 'rgb(255, 0, 0)',
'shoud have color applied');
strictEqual(
$fix.find('tbody tr:eq(2)').css('color'), 'rgb(0, 0, 0)',
'should have default color applied');
});
}); });
}); });

View File

@ -1,45 +1,34 @@
$(document).ready(function () { openerp.testing.section('list.events', {
var openerp, dependencies: ['web.list']
create = function (o) { }, function (test) {
if (typeof Object.create === 'function') { var create = function (o) {
return Object.create(o); if (typeof Object.create === 'function') {
} return Object.create(o);
function Cls() {}
Cls.prototype = o;
return new Cls;
};
module('list-events', {
setup: function () {
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
} }
}); function Cls() {}
test('Simple event triggering', function () { Cls.prototype = o;
var e = create(openerp.web.list.Events), passed = false; return new Cls;
};
test('Simple event triggering', function (instance) {
var e = create(instance.web.list.Events), passed = false;
e.bind('foo', function () { passed = true; }); e.bind('foo', function () { passed = true; });
e.trigger('foo'); e.trigger('foo');
ok(passed); ok(passed);
}); });
test('Bind all', function () { test('Bind all', function (instance) {
var e = create(openerp.web.list.Events), event = null; var e = create(instance.web.list.Events), event = null;
e.bind(null, function (ev) { event = ev; }); e.bind(null, function (ev) { event = ev; });
e.trigger('foo'); e.trigger('foo');
strictEqual(event, 'foo'); strictEqual(event, 'foo');
}); });
test('Propagate trigger params', function () { test('Propagate trigger params', function (instance) {
var e = create(openerp.web.list.Events), p = false; var e = create(instance.web.list.Events), p = false;
e.bind(null, function (_, param) { p = param }); e.bind(null, function (_, param) { p = param });
e.trigger('foo', true); e.trigger('foo', true);
strictEqual(p, true) strictEqual(p, true)
}); });
test('Bind multiple callbacks', function () { test('Bind multiple callbacks', function (instance) {
var e = create(openerp.web.list.Events), count; var e = create(instance.web.list.Events), count;
e.bind('foo', function () { count++; }) e.bind('foo', function () { count++; })
.bind('bar', function () { count++; }) .bind('bar', function () { count++; })
.bind(null, function () { count++; }) .bind(null, function () { count++; })
@ -59,20 +48,20 @@ $(document).ready(function () {
e.trigger('baz'); e.trigger('baz');
strictEqual(count, 3); strictEqual(count, 3);
}); });
test('Mixin events', function () { test('Mixin events', function (instance) {
var cls = openerp.web.Class.extend({ var cls = instance.web.Class.extend({
method: function () { this.trigger('e'); } method: function () { this.trigger('e'); }
}); });
cls.include(openerp.web.list.Events); cls.include(instance.web.list.Events);
var instance = new cls, triggered = false; var i = new cls, triggered = false;
instance.bind('e', function () { triggered = true; }); i.bind('e', function () { triggered = true; });
instance.method(); i.method();
ok(triggered); ok(triggered);
}); });
test('Unbind all handlers', function () { test('Unbind all handlers', function (instance) {
var e = create(openerp.web.list.Events), passed = 0; var e = create(instance.web.list.Events), passed = 0;
e.bind('foo', function () { passed++; }); e.bind('foo', function () { passed++; });
e.trigger('foo'); e.trigger('foo');
strictEqual(passed, 1); strictEqual(passed, 1);
@ -80,8 +69,8 @@ $(document).ready(function () {
e.trigger('foo'); e.trigger('foo');
strictEqual(passed, 1); strictEqual(passed, 1);
}); });
test('Unbind one handler', function () { test('Unbind one handler', function (instance) {
var e = create(openerp.web.list.Events), p1 = 0, p2 = 0, var e = create(instance.web.list.Events), p1 = 0, p2 = 0,
h1 = function () { p1++; }, h2 = function () { p2++; }; h1 = function () { p1++; }, h2 = function () { p2++; };
e.bind('foo', h1); e.bind('foo', h1);
e.bind('foo', h2); e.bind('foo', h2);
@ -93,29 +82,20 @@ $(document).ready(function () {
strictEqual(p1, 1); strictEqual(p1, 1);
strictEqual(p2, 2); strictEqual(p2, 2);
}); });
});
module('list-records', { openerp.testing.section('list.records', {
setup: function () { dependencies: ['web.list']
openerp = window.openerp.init([]); }, function (test) {
window.openerp.web.corelib(openerp); test('Basic record initialization', function (instance) {
window.openerp.web.coresetup(openerp); var r = new instance.web.list.Record({qux: 3});
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});
test('Basic record initialization', function () {
var r = new openerp.web.list.Record({qux: 3});
r.set('foo', 1); r.set('foo', 1);
r.set('bar', 2); r.set('bar', 2);
strictEqual(r.get('foo'), 1); strictEqual(r.get('foo'), 1);
strictEqual(r.get('bar'), 2); strictEqual(r.get('bar'), 2);
strictEqual(r.get('qux'), 3); strictEqual(r.get('qux'), 3);
}); });
test('Change all the things', function () { test('Change all the things', function (instance) {
var r = new openerp.web.list.Record(), changed = false, field; var r = new instance.web.list.Record(), changed = false, field;
r.bind('change', function () { changed = true; }); r.bind('change', function () { changed = true; });
r.bind(null, function (e) { field = field || e.split(':')[1]}); r.bind(null, function (e) { field = field || e.split(':')[1]});
r.set('foo', 1); r.set('foo', 1);
@ -123,8 +103,8 @@ $(document).ready(function () {
ok(changed); ok(changed);
strictEqual(field, 'foo'); strictEqual(field, 'foo');
}); });
test('Change single field', function () { test('Change single field', function (instance) {
var r = new openerp.web.list.Record(), changed = 0; var r = new instance.web.list.Record(), changed = 0;
r.bind('change:foo', function () { changed++; }); r.bind('change:foo', function () { changed++; });
r.set('foo', 1); r.set('foo', 1);
r.set('bar', 1); r.set('bar', 1);
@ -132,21 +112,12 @@ $(document).ready(function () {
strictEqual(r.get('bar'), 1); strictEqual(r.get('bar'), 1);
strictEqual(changed, 1); strictEqual(changed, 1);
}); });
});
module('list-collections', { openerp.testing.section('list.collections', {
setup: function () { dependencies: ['web.list']
openerp = window.openerp.init([]); }, function (test) {
window.openerp.web.corelib(openerp); test('degenerate-fetch', function (instance) {
window.openerp.web.coresetup(openerp); var c = new instance.web.list.Collection();
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});
test('degenerate-fetch', function () {
var c = new openerp.web.list.Collection();
strictEqual(c.length, 0); strictEqual(c.length, 0);
c.add({id: 1, value: 2}); c.add({id: 1, value: 2});
c.add({id: 2, value: 3}); c.add({id: 2, value: 3});
@ -155,16 +126,16 @@ $(document).ready(function () {
strictEqual(c.length, 4); strictEqual(c.length, 4);
var r = c.at(2), r2 = c.get(1); var r = c.at(2), r2 = c.get(1);
ok(r instanceof openerp.web.list.Record); ok(r instanceof instance.web.list.Record);
strictEqual(r.get('id'), 3); strictEqual(r.get('id'), 3);
strictEqual(r.get('value'), 5); strictEqual(r.get('value'), 5);
ok(r2 instanceof openerp.web.list.Record); ok(r2 instanceof instance.web.list.Record);
strictEqual(r2.get('id'), 1); strictEqual(r2.get('id'), 1);
strictEqual(r2.get('value'), 2); strictEqual(r2.get('value'), 2);
}); });
test('degenerate-indexed-add', function () { test('degenerate-indexed-add', function (instance) {
var c = new openerp.web.list.Collection([ var c = new instance.web.list.Collection([
{id: 1, value: 5}, {id: 1, value: 5},
{id: 2, value: 10}, {id: 2, value: 10},
{id: 3, value: 20} {id: 3, value: 20}
@ -175,8 +146,8 @@ $(document).ready(function () {
strictEqual(c.at(1).get('value'), 55); strictEqual(c.at(1).get('value'), 55);
strictEqual(c.at(3).get('value'), 20); strictEqual(c.at(3).get('value'), 20);
}); });
test('degenerate-remove', function () { test('degenerate-remove', function (instance) {
var c = new openerp.web.list.Collection([ var c = new instance.web.list.Collection([
{id: 1, value: 5}, {id: 1, value: 5},
{id: 2, value: 10}, {id: 2, value: 10},
{id: 3, value: 20} {id: 3, value: 20}
@ -188,9 +159,9 @@ $(document).ready(function () {
equal(c.get(2), undefined); equal(c.get(2), undefined);
strictEqual(c.at(1).get('value'), 20); strictEqual(c.at(1).get('value'), 20);
}); });
test('degenerate-remove-bound', function () { test('degenerate-remove-bound', function (instance) {
var changed = false, var changed = false,
c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); c = new instance.web.list.Collection([ {id: 1, value: 5} ]);
c.bind('change', function () { changed = true; }); c.bind('change', function () { changed = true; });
var record = c.get(1); var record = c.get(1);
c.remove(record); c.remove(record);
@ -198,8 +169,8 @@ $(document).ready(function () {
ok(!changed, 'removed records should not trigger events in their ' + ok(!changed, 'removed records should not trigger events in their ' +
'parent collection'); 'parent collection');
}); });
test('degenerate-reset', function () { test('degenerate-reset', function (instance) {
var event, obj, c = new openerp.web.list.Collection([ var event, obj, c = new instance.web.list.Collection([
{id: 1, value: 5}, {id: 1, value: 5},
{id: 2, value: 10}, {id: 2, value: 10},
{id: 3, value: 20} {id: 3, value: 20}
@ -218,9 +189,9 @@ $(document).ready(function () {
strictEqual(c.length, 1); strictEqual(c.length, 1);
strictEqual(c.get(42).get('value'), 55); strictEqual(c.get(42).get('value'), 55);
}); });
test('degenerate-reset-bound', function () { test('degenerate-reset-bound', function (instance) {
var changed = false, var changed = false,
c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); c = new instance.web.list.Collection([ {id: 1, value: 5} ]);
c.bind('change', function () { changed = true; }); c.bind('change', function () { changed = true; });
var record = c.get(1); var record = c.get(1);
c.reset(); c.reset();
@ -229,9 +200,9 @@ $(document).ready(function () {
'parent collection'); 'parent collection');
}); });
test('degenerate-propagations', function () { test('degenerate-propagations', function (instance) {
var values = []; var values = [];
var c = new openerp.web.list.Collection([ var c = new instance.web.list.Collection([
{id: 1, value: 5}, {id: 1, value: 5},
{id: 2, value: 10}, {id: 2, value: 10},
{id: 3, value: 20} {id: 3, value: 20}
@ -244,8 +215,8 @@ $(document).ready(function () {
c.get(3).set('value', 21); c.get(3).set('value', 21);
deepEqual(values, [6, 11, 21]); deepEqual(values, [6, 11, 21]);
}); });
test('BTree', function () { test('BTree', function (instance) {
var root = new openerp.web.list.Collection(), var root = new instance.web.list.Collection(),
c = root.proxy('admin'), c = root.proxy('admin'),
total = 0; total = 0;
c.add({id: 1, name: "Administrator", login: 'admin'}); c.add({id: 1, name: "Administrator", login: 'admin'});
@ -260,8 +231,8 @@ $(document).ready(function () {
c.at(1).set('wealth', 5); c.at(1).set('wealth', 5);
strictEqual(total, 47); strictEqual(total, 47);
}); });
test('degenerate-successor', function () { test('degenerate-successor', function (instance) {
var root = new openerp.web.list.Collection([ var root = new instance.web.list.Collection([
{id: 1, value: 1}, {id: 1, value: 1},
{id: 2, value: 2}, {id: 2, value: 2},
{id: 3, value: 3}, {id: 3, value: 3},
@ -282,8 +253,8 @@ $(document).ready(function () {
root.at(3).attributes, root.at(3).attributes,
"wraparound should have no effect if not succ(last_record)"); "wraparound should have no effect if not succ(last_record)");
}); });
test('successor', function () { test('successor', function (instance) {
var root = new openerp.web.list.Collection(); var root = new instance.web.list.Collection();
root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]);
root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]);
root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]);
@ -298,8 +269,8 @@ $(document).ready(function () {
root.get(3).attributes, root.get(3).attributes,
"should wraparound within a collection"); "should wraparound within a collection");
}); });
test('degenerate-predecessor', function () { test('degenerate-predecessor', function (instance) {
var root = new openerp.web.list.Collection([ var root = new instance.web.list.Collection([
{id: 1, value: 1}, {id: 1, value: 1},
{id: 2, value: 2}, {id: 2, value: 2},
{id: 3, value: 3}, {id: 3, value: 3},
@ -320,8 +291,8 @@ $(document).ready(function () {
root.at(0).attributes, root.at(0).attributes,
"wraparound should have no effect if not pred(first_record)"); "wraparound should have no effect if not pred(first_record)");
}); });
test('predecessor', function () { test('predecessor', function (instance) {
var root = new openerp.web.list.Collection(); var root = new instance.web.list.Collection();
root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]);
root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]);
root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]);
@ -336,21 +307,12 @@ $(document).ready(function () {
root.get(4).attributes, root.get(4).attributes,
"should wraparound within a collection"); "should wraparound within a collection");
}); });
});
module('list-hofs', { openerp.testing.section('list.collections.hom', {
setup: function () { dependencies: ['web.list']
openerp = window.openerp.init([]); }, function (test) {
window.openerp.web.corelib(openerp); test('each, degenerate', function (instance) {
window.openerp.web.coresetup(openerp); var c = new instance.web.list.Collection([
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});
test('each, degenerate', function () {
var c = new openerp.web.list.Collection([
{id: 1, value: 5}, {id: 1, value: 5},
{id: 2, value: 10}, {id: 2, value: 10},
{id: 3, value: 20} {id: 3, value: 20}
@ -362,8 +324,8 @@ $(document).ready(function () {
ids, [1, 2, 3], ids, [1, 2, 3],
'degenerate collections should be iterated in record order'); 'degenerate collections should be iterated in record order');
}); });
test('each, deep', function () { test('each, deep', function (instance) {
var root = new openerp.web.list.Collection(), var root = new instance.web.list.Collection(),
ids = []; ids = [];
root.proxy('foo').add([ root.proxy('foo').add([
{id: 1, value: 5}, {id: 1, value: 5},
@ -382,8 +344,8 @@ $(document).ready(function () {
ids, [1, 2, 3, 10, 20, 30], ids, [1, 2, 3, 10, 20, 30],
'tree collections should be deeply iterated'); 'tree collections should be deeply iterated');
}); });
test('map, degenerate', function () { test('map, degenerate', function (instance) {
var c = new openerp.web.list.Collection([ var c = new instance.web.list.Collection([
{id: 1, value: 5}, {id: 1, value: 5},
{id: 2, value: 10}, {id: 2, value: 10},
{id: 3, value: 20} {id: 3, value: 20}
@ -395,8 +357,8 @@ $(document).ready(function () {
ids, [1, 2, 3], ids, [1, 2, 3],
'degenerate collections should be iterated in record order'); 'degenerate collections should be iterated in record order');
}); });
test('map, deep', function () { test('map, deep', function (instance) {
var root = new openerp.web.list.Collection(); var root = new instance.web.list.Collection();
root.proxy('foo').add([ root.proxy('foo').add([
{id: 1, value: 5}, {id: 1, value: 5},
{id: 2, value: 10}, {id: 2, value: 10},
@ -414,29 +376,20 @@ $(document).ready(function () {
ids, [1, 2, 3, 10, 20, 30], ids, [1, 2, 3, 10, 20, 30],
'tree collections should be deeply iterated'); 'tree collections should be deeply iterated');
}); });
});
module("list-weirds", { openerp.testing.section('list.collection.weirdoes', {
setup: function () { dependencies: ['web.list']
openerp = window.openerp.init([]); }, function (test) {
window.openerp.web.corelib(openerp); test('set-from-noid', function (instance) {
window.openerp.web.coresetup(openerp); var root = new instance.web.list.Collection();
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});
test('set-from-noid', function () {
var root = new openerp.web.list.Collection();
root.add({v: 3}); root.add({v: 3});
root.at(0).set('id', 42); root.at(0).set('id', 42);
var record = root.get(42); var record = root.get(42);
equal(root.length, 1); equal(root.length, 1);
equal(record.get('v'), 3, "should have fetched the original record"); equal(record.get('v'), 3, "should have fetched the original record");
}); });
test('set-from-previd', function () { test('set-from-previd', function (instance) {
var root = new openerp.web.list.Collection(); var root = new instance.web.list.Collection();
root.add({id: 1, v: 2}); root.add({id: 1, v: 2});
root.get(1).set('id', 42); root.get(1).set('id', 42);
var record = root.get(42); var record = root.get(42);

View File

@ -0,0 +1,56 @@
openerp.testing.section('list.buttons', {
dependencies: ['web.list', 'web.form'],
rpc: 'mock',
templates: true
}, function (test) {
test('record-deletion', {asserts: 2}, function (instance, $fix, mock) {
mock('/web/view/load', function () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: {
tag: 'tree',
attrs: { },
children: [
{tag: 'field', attrs: {name: 'a'}},
{tag: 'button', attrs: {type: 'object', name: 'foo'}}
]
}
};
});
mock('demo:read', function (args, kwargs) {
if (_.isEqual(args[0], [1, 2, 3])) {
return [
{id: 1, a: 'foo'}, {id: 2, a: 'bar'}, {id: 3, a: 'baz'}
];
} else if (_.isEqual(args[0], [2])) {
// button action virtually removed record
return [];
}
throw new Error(JSON.stringify(_.toArray(arguments)));
});
mock('/web/dataset/call_button', function () { return false; });
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1, 2, 3]);
var l = new instance.web.ListView({
do_action: openerp.testing.noop
}, ds, false, {editable: 'top'});
return l.appendTo($fix)
.then(l.proxy('reload_content'))
.then(function () {
var d = $.Deferred();
l.records.bind('remove', function () {
d.resolve();
});
$fix.find('table tbody tr:eq(1) button').click();
return d.promise();
})
.then(function () {
strictEqual(l.records.length, 2,
"should have 2 records left");
strictEqual($fix.find('table tbody tr[data-id]').length, 2,
"should have 2 rows left");
});
});
});

View File

@ -0,0 +1,60 @@
openerp.testing.section('mutex', {
dependencies: ['web.coresetup'],
setup: function (instance) {
}
}, function (test) {
test('simpleScheduling', function (instance) {
var m = new $.Mutex();
var def1 = $.Deferred();
var def2 = $.Deferred();
var p1 = m.exec(function() { return def1; });
var p2 = m.exec(function() { return def2; });
equal(p1.state(), "pending");
equal(p2.state(), "pending");
def1.resolve();
equal(p1.state(), "resolved");
equal(p2.state(), "pending");
def2.resolve();
equal(p1.state(), "resolved");
equal(p2.state(), "resolved");
});
test('simpleScheduling2', function (instance) {
var m = new $.Mutex();
var def1 = $.Deferred();
var def2 = $.Deferred();
var p1 = m.exec(function() { return def1; });
var p2 = m.exec(function() { return def2; });
equal(p1.state(), "pending");
equal(p2.state(), "pending");
def2.resolve();
equal(p1.state(), "pending");
equal(p2.state(), "pending");
def1.resolve();
equal(p1.state(), "resolved");
equal(p2.state(), "resolved");
});
test('reject', function (instance) {
var m = new $.Mutex();
var def1 = $.Deferred();
var def2 = $.Deferred();
var def3 = $.Deferred();
var p1 = m.exec(function() {return def1;});
var p2 = m.exec(function() {return def2;});
var p3 = m.exec(function() {return def3;});
equal(p1.state(), "pending");
equal(p2.state(), "pending");
equal(p3.state(), "pending");
def1.resolve();
equal(p1.state(), "resolved");
equal(p2.state(), "pending");
equal(p3.state(), "pending");
def2.reject();
equal(p1.state(), "resolved");
equal(p2.state(), "rejected");
equal(p3.state(), "pending");
def3.resolve();
equal(p1.state(), "resolved");
equal(p2.state(), "rejected");
equal(p3.state(), "resolved");
});
});

View File

@ -1,58 +1,55 @@
$(document).ready(function () { openerp.testing.section('registry', {
var openerp; dependencies: ['web.corelib'],
module('Registry', { setup: function (instance) {
setup: function () { instance.web.Foo = {};
openerp = window.openerp.init([]); instance.web.Bar = {};
window.openerp.web.corelib(openerp); instance.web.Foo2 = {};
openerp.web.Foo = {}; }
openerp.web.Bar = {}; }, function (test) {
openerp.web.Foo2 = {}; test('key set', function (instance) {
} var reg = new instance.web.Registry();
});
test('key set', function () {
var reg = new openerp.web.Registry();
reg.add('foo', 'openerp.web.Foo') reg.add('foo', 'instance.web.Foo')
.add('bar', 'openerp.web.Bar'); .add('bar', 'instance.web.Bar');
strictEqual(reg.get_object('bar'), openerp.web.Bar); strictEqual(reg.get_object('bar'), instance.web.Bar);
}); });
test('extension', function () { test('extension', function (instance) {
var reg = new openerp.web.Registry({ var reg = new instance.web.Registry({
foo: 'openerp.web.Foo', foo: 'instance.web.Foo',
bar: 'openerp.web.Bar' bar: 'instance.web.Bar'
}); });
var reg2 = reg.extend({ 'foo': 'openerp.web.Foo2' }); var reg2 = reg.extend({ 'foo': 'instance.web.Foo2' });
strictEqual(reg.get_object('foo'), openerp.web.Foo); strictEqual(reg.get_object('foo'), instance.web.Foo);
strictEqual(reg2.get_object('foo'), openerp.web.Foo2); strictEqual(reg2.get_object('foo'), instance.web.Foo2);
}); });
test('remain-linked', function () { test('remain-linked', function (instance) {
var reg = new openerp.web.Registry({ var reg = new instance.web.Registry({
foo: 'openerp.web.Foo', foo: 'instance.web.Foo',
bar: 'openerp.web.Bar' bar: 'instance.web.Bar'
}); });
var reg2 = reg.extend(); var reg2 = reg.extend();
reg.add('foo2', 'openerp.web.Foo2'); reg.add('foo2', 'instance.web.Foo2');
strictEqual(reg.get_object('foo2'), openerp.web.Foo2); strictEqual(reg.get_object('foo2'), instance.web.Foo2);
strictEqual(reg2.get_object('foo2'), openerp.web.Foo2); strictEqual(reg2.get_object('foo2'), instance.web.Foo2);
}); });
test('multiget', function () { test('multiget', function (instance) {
var reg = new openerp.web.Registry({ var reg = new instance.web.Registry({
foo: 'openerp.web.Foo', foo: 'instance.web.Foo',
bar: 'openerp.web.Bar' bar: 'instance.web.Bar'
}); });
strictEqual(reg.get_any(['qux', 'grault', 'bar', 'foo']), strictEqual(reg.get_any(['qux', 'grault', 'bar', 'foo']),
openerp.web.Bar); instance.web.Bar);
}); });
test('extended-multiget', function () { test('extended-multiget', function (instance) {
var reg = new openerp.web.Registry({ var reg = new instance.web.Registry({
foo: 'openerp.web.Foo', foo: 'instance.web.Foo',
bar: 'openerp.web.Bar' bar: 'instance.web.Bar'
}); });
var reg2 = reg.extend(); var reg2 = reg.extend();
strictEqual(reg2.get_any(['qux', 'grault', 'bar', 'foo']), strictEqual(reg2.get_any(['qux', 'grault', 'bar', 'foo']),
openerp.web.Bar); instance.web.Bar);
}); });
}); });

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