summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorsma (Tiny) <sma@tinyerp.com>2011-10-03 16:57:31 +0530
committersma (Tiny) <sma@tinyerp.com>2011-10-03 16:57:31 +0530
commit7e9e542418f791b024de401236eeb8e006be42d4 (patch)
tree91e49927b0de50c8c690a2eb608b425a12b4ac7c /addons
parent3e11ca62990fde34e537e233e3b099d22f8c8373 (diff)
parent2dae9519d7c3d0a4c2217b63939bb4eb1d83d39d (diff)
[MERGE]
bzr revid: sma@tinyerp.com-20110926105300-72fwa60uvvdf1qgg bzr revid: sma@tinyerp.com-20111003112731-uobxur1ryqb5laxy
Diffstat (limited to 'addons')
-rw-r--r--addons/web/__init__.py17
-rw-r--r--addons/web/__openerp__.py6
-rw-r--r--addons/web/common/dispatch.py69
-rw-r--r--addons/web/common/http.py7
-rw-r--r--addons/web/common/nonliterals.py11
-rw-r--r--addons/web/common/openerplib/main.py46
-rw-r--r--addons/web/common/session.py33
-rw-r--r--addons/web/controllers/main.py193
-rw-r--r--addons/web/static/lib/jquery.deferred-queue/LICENSE21
-rw-r--r--addons/web/static/lib/jquery.deferred-queue/README59
-rw-r--r--addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js34
-rw-r--r--addons/web/static/lib/py.parse/.hg_archival.txt5
-rw-r--r--addons/web/static/lib/py.parse/README1
-rw-r--r--addons/web/static/lib/py.parse/TODO.rst14
-rw-r--r--addons/web/static/lib/py.parse/lib/py.js546
-rw-r--r--addons/web/static/lib/py.parse/test/test.js90
-rw-r--r--addons/web/static/lib/underscore/underscore.string.js699
-rw-r--r--addons/web/static/src/css/base.css140
-rw-r--r--addons/web/static/src/css/data_import.css30
-rw-r--r--addons/web/static/src/img/gradientlinebg.gifbin0 -> 182 bytes
-rw-r--r--addons/web/static/src/js/boot.js9
-rw-r--r--addons/web/static/src/js/chrome.js239
-rw-r--r--addons/web/static/src/js/core.js12
-rw-r--r--addons/web/static/src/js/data.js43
-rw-r--r--addons/web/static/src/js/data_import.js289
-rw-r--r--addons/web/static/src/js/formats.js19
-rw-r--r--addons/web/static/src/js/search.js66
-rw-r--r--addons/web/static/src/js/view_form.js509
-rw-r--r--addons/web/static/src/js/view_list.js54
-rw-r--r--addons/web/static/src/js/view_list_editable.js11
-rw-r--r--addons/web/static/src/js/views.js35
-rw-r--r--addons/web/static/src/xml/base.xml291
-rw-r--r--addons/web_calendar/static/src/js/calendar.js1
-rw-r--r--addons/web_dashboard/static/src/js/dashboard.js2
-rw-r--r--addons/web_default_home/static/src/js/home.js18
-rw-r--r--addons/web_graph/__init__.py2
-rw-r--r--addons/web_graph/__openerp__.py2
-rw-r--r--addons/web_graph/static/src/js/graph.js533
-rw-r--r--addons/web_graph/static/src/xml/web_graph.xml2
-rw-r--r--addons/web_hello/__openerp__.py1
-rw-r--r--addons/web_hello/static/openerp/base_hello.js7
-rw-r--r--addons/web_kanban/static/src/css/kanban.css20
-rw-r--r--addons/web_kanban/static/src/img/minus-icon.pngbin0 -> 1390 bytes
-rw-r--r--addons/web_kanban/static/src/img/plus-icon.pngbin0 -> 761 bytes
-rw-r--r--addons/web_kanban/static/src/js/kanban.js181
-rw-r--r--addons/web_kanban/static/src/xml/web_kanban.xml19
-rw-r--r--addons/web_tests/__init__.py1
-rw-r--r--addons/web_tests/__openerp__.py8
-rw-r--r--addons/web_tests/static/src/css/web_tests.css3
-rw-r--r--addons/web_tests/static/src/js/web_tests.js37
50 files changed, 3159 insertions, 1276 deletions
diff --git a/addons/web/__init__.py b/addons/web/__init__.py
index 2478edb81b7..8802cb548c3 100644
--- a/addons/web/__init__.py
+++ b/addons/web/__init__.py
@@ -2,27 +2,26 @@ import common
import controllers
import common.dispatch
import logging
+import optparse
_logger = logging.getLogger(__name__)
+class Options(object):
+ pass
+
def wsgi_postload():
- import openerp.wsgi
+ import openerp
import os
import tempfile
_logger.info("embedded mode")
- class Options(object):
- pass
o = Options()
- o.dbfilter = '.*'
+ o.dbfilter = openerp.tools.config['dbfilter']
+ o.server_wide_modules = openerp.conf.server_wide_modules or ['web']
o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions")
- o.addons_path = os.path.dirname(os.path.dirname(__file__))
+ o.addons_path = openerp.modules.module.ad_paths
o.serve_static = True
o.backend = 'local'
app = common.dispatch.Root(o)
- #import openerp.wsgi
openerp.wsgi.register_wsgi_handler(app)
-# TODO
-# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point
-
diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py
index 85464e750cb..990f0b445a0 100644
--- a/addons/web/__openerp__.py
+++ b/addons/web/__openerp__.py
@@ -2,6 +2,7 @@
"name" : "web",
"depends" : [],
'active': True,
+ 'post_load' : 'wsgi_postload',
'js' : [
"static/lib/datejs/globalization/en-US.js",
"static/lib/datejs/core.js",
@@ -19,11 +20,13 @@
"static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js",
"static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js",
"static/lib/jquery.ui.notify/js/jquery.notify.js",
+ "static/lib/jquery.deferred-queue/jquery.deferred-queue.js",
"static/lib/json/json2.js",
"static/lib/qweb/qweb2.js",
"static/lib/underscore/underscore.js",
"static/lib/underscore/underscore.string.js",
"static/lib/labjs/LAB.src.js",
+ "static/lib/py.parse/lib/py.js",
"static/src/js/boot.js",
"static/src/js/core.js",
"static/src/js/dates.js",
@@ -32,6 +35,7 @@
"static/src/js/views.js",
"static/src/js/data.js",
"static/src/js/data_export.js",
+ "static/src/js/data_import.js",
"static/src/js/search.js",
"static/src/js/view_form.js",
"static/src/js/view_list.js",
@@ -45,6 +49,6 @@
"static/lib/jquery.ui.notify/css/ui.notify.css",
"static/src/css/base.css",
"static/src/css/data_export.css",
+ "static/src/css/data_import.css",
],
- 'post_load' : 'wsgi_postload',
}
diff --git a/addons/web/common/dispatch.py b/addons/web/common/dispatch.py
index 98b948704fa..cb998caf891 100644
--- a/addons/web/common/dispatch.py
+++ b/addons/web/common/dispatch.py
@@ -3,6 +3,7 @@ from __future__ import with_statement
import functools
import logging
+import urllib
import os
import pprint
import sys
@@ -13,7 +14,6 @@ import xmlrpclib
import simplejson
import werkzeug.datastructures
import werkzeug.exceptions
-import werkzeug.urls
import werkzeug.utils
import werkzeug.wrappers
import werkzeug.wsgi
@@ -21,8 +21,7 @@ import werkzeug.wsgi
import ast
import nonliterals
import http
-# import backendlocal as backend
-import session as backend
+import session
import openerplib
__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
@@ -34,7 +33,6 @@ _logger = logging.getLogger(__name__)
# Globals (wont move into a pool)
#-----------------------------------------------------------
-applicationsession = {}
addons_module = {}
addons_manifest = {}
controllers_class = {}
@@ -53,10 +51,6 @@ class WebRequest(object):
:type request: :class:`werkzeug.wrappers.BaseRequest`
:param config: configuration object
- .. attribute:: applicationsession
-
- an application-wide :class:`~collections.Mapping`
-
.. attribute:: httprequest
the original :class:`werkzeug.wrappers.Request` object provided to the
@@ -79,12 +73,12 @@ class WebRequest(object):
.. attribute:: session_id
- opaque identifier for the :class:`backend.OpenERPSession` instance of
+ opaque identifier for the :class:`session.OpenERPSession` instance of
the current request
.. attribute:: session
- :class:`~backend.OpenERPSession` instance for the current request
+ :class:`~session.OpenERPSession` instance for the current request
.. attribute:: context
@@ -95,18 +89,17 @@ class WebRequest(object):
``bool``, indicates whether the debug mode is active on the client
"""
def __init__(self, request, config):
- self.applicationsession = applicationsession
self.httprequest = request
self.httpresponse = None
self.httpsession = request.session
self.config = config
+
def init(self, params):
self.params = dict(params)
# OpenERP session setup
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
- self.session = self.httpsession.setdefault(
- self.session_id, backend.OpenERPSession(
- self.config.server_host, self.config.server_port))
+ self.session = self.httpsession.setdefault(self.session_id, session.OpenERPSession())
+ self.session.config = self.config
self.context = self.params.pop('context', None)
self.debug = self.params.pop('debug', False) != False
@@ -317,9 +310,16 @@ class Root(object):
by the server, will be filtered by this pattern
"""
def __init__(self, options):
- self.root = werkzeug.urls.Href('/web/webclient/home')
+ self.root = '/web/webclient/home'
self.config = options
+ if self.config.backend == 'local':
+ conn = openerplib.get_connector(protocol='local')
+ else:
+ conn = openerplib.get_connector(hostname=self.config.server_host,
+ port=self.config.server_port)
+ self.config.connector = conn
+
self.session_cookie = 'sessionid'
self.addons = {}
@@ -349,13 +349,12 @@ class Root(object):
request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
if request.path == '/':
- return werkzeug.utils.redirect(
- self.root(dict(request.args, debug='')), 301)(
- environ, start_response)
+ params = urllib.urlencode(dict(request.args, debug=''))
+ return werkzeug.utils.redirect(self.root + '?' + params, 301)(
+ environ, start_response)
elif request.path == '/mobile':
return werkzeug.utils.redirect(
- '/web_mobile/static/src/web_mobile.html', 301)(
- environ, start_response)
+ '/web_mobile/static/src/web_mobile.html', 301)(environ, start_response)
handler = self.find_handler(*(request.path.split('/')[1:]))
@@ -383,21 +382,21 @@ class Root(object):
static URLs to the corresponding directories
"""
statics = {}
- addons_path = self.config.addons_path
- if addons_path not in sys.path:
- sys.path.insert(0, addons_path)
- for module in os.listdir(addons_path):
- if module not in addons_module:
- manifest_path = os.path.join(addons_path, module, '__openerp__.py')
- if os.path.isfile(manifest_path):
- manifest = ast.literal_eval(open(manifest_path).read())
- _logger.info("Loading %s", module)
- m = __import__(module)
- addons_module[module] = m
- addons_manifest[module] = manifest
-
- statics['/%s/static' % module] = \
- os.path.join(addons_path, module, 'static')
+ for addons_path in self.config.addons_path:
+ if addons_path not in sys.path:
+ sys.path.insert(0, addons_path)
+ for module in os.listdir(addons_path):
+ if module not in addons_module:
+ manifest_path = os.path.join(addons_path, module, '__openerp__.py')
+ path_static = os.path.join(addons_path, module, 'static')
+ if os.path.isfile(manifest_path) and os.path.isdir(path_static):
+ manifest = ast.literal_eval(open(manifest_path).read())
+ manifest['addons_path'] = addons_path
+ _logger.info("Loading %s", module)
+ m = __import__(module)
+ addons_module[module] = m
+ addons_manifest[module] = manifest
+ statics['/%s/static' % module] = path_static
for k, v in controllers_class.items():
if k not in controllers_object:
o = v()
diff --git a/addons/web/common/http.py b/addons/web/common/http.py
index 3f6c96d1f42..0186fd985cc 100644
--- a/addons/web/common/http.py
+++ b/addons/web/common/http.py
@@ -20,6 +20,7 @@ def session(request, storage_path, session_cookie='sessionid'):
else:
request.session = session_store.new()
- yield request.session
-
- session_store.save(request.session)
+ try:
+ yield request.session
+ finally:
+ session_store.save(request.session)
diff --git a/addons/web/common/nonliterals.py b/addons/web/common/nonliterals.py
index 937da06ad9e..4db333a5769 100644
--- a/addons/web/common/nonliterals.py
+++ b/addons/web/common/nonliterals.py
@@ -41,6 +41,9 @@ class NonLiteralEncoder(simplejson.encoder.JSONEncoder):
'__eval_context': object.get_eval_context()
}
raise TypeError('Could not encode unknown non-literal %s' % object)
+
+_ALLOWED_KEYS = frozenset(['__ref', "__id", '__domains',
+ '__contexts', '__eval_context', 'own_values'])
def non_literal_decoder(dct):
""" Decodes JSON dicts into :class:`Domain` and :class:`Context` based on
@@ -50,6 +53,9 @@ def non_literal_decoder(dct):
``own_values`` dict key.
"""
if '__ref' in dct:
+ for x in dct.keys():
+ if not x in _ALLOWED_KEYS:
+ raise ValueError("'%s' key not allowed in non literal domain/context" % x)
if dct['__ref'] == 'domain':
domain = Domain(None, key=dct['__id'])
if 'own_values' in dct:
@@ -231,7 +237,10 @@ class CompoundContext(BaseContext):
def evaluate(self, context=None):
ctx = dict(context or {})
- ctx.update(self.get_eval_context() or {})
+ eval_context = self.get_eval_context()
+ if eval_context:
+ eval_context = self.session.eval_context(eval_context)
+ ctx.update(eval_context)
final_context = {}
for context_to_eval in self.contexts:
if not isinstance(context_to_eval, (dict, BaseContext)):
diff --git a/addons/web/common/openerplib/main.py b/addons/web/common/openerplib/main.py
index 21f8d48ff63..b09e9f9d72f 100644
--- a/addons/web/common/openerplib/main.py
+++ b/addons/web/common/openerplib/main.py
@@ -38,6 +38,7 @@ Code repository: https://code.launchpad.net/~niv-openerp/openerp-client-lib/trun
import xmlrpclib
import logging
import socket
+import sys
try:
import cPickle as pickle
@@ -70,6 +71,14 @@ class Connector(object):
self.hostname = hostname
self.port = port
+ def get_service(self, service_name):
+ """
+ Returns a Service instance to allow easy manipulation of one of the services offered by the remote server.
+
+ :param service_name: The name of the service.
+ """
+ return Service(self, service_name)
+
class XmlRPCConnector(Connector):
"""
A type of connector that uses the XMLRPC protocol.
@@ -190,6 +199,31 @@ class NetRPCConnector(Connector):
socket.disconnect()
return result
+class LocalConnector(Connector):
+ """
+ A type of connector that uses the XMLRPC protocol.
+ """
+ PROTOCOL = 'local'
+
+ __logger = _getChildLogger(_logger, 'connector.local')
+
+ def __init__(self):
+ pass
+
+ def send(self, service_name, method, *args):
+ import openerp
+ # TODO Exception handling
+ # This will be changed to be xmlrpc compatible
+ # OpenERPWarning code 1
+ # OpenERPException code 2
+ try:
+ result = openerp.netsvc.dispatch_rpc(service_name, method, args)
+ except:
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value))
+ raise fault
+ return result
+
class Service(object):
"""
A class to execute RPC calls on a specific service of the remote server.
@@ -295,7 +329,7 @@ class Connection(object):
:param service_name: The name of the service.
"""
- return Service(self.connector, service_name)
+ return self.connector.get_service(service_name)
class AuthenticationError(Exception):
"""
@@ -342,7 +376,7 @@ class Model(object):
index = {}
for r in result:
index[r['id']] = r
- result = [index[x] for x in args[0]]
+ result = [index[x] for x in args[0] if x in index]
self.__logger.debug('result: %r', result)
return result
return proxy
@@ -363,7 +397,7 @@ class Model(object):
records = self.read(record_ids, fields or [], context or {})
return records
-def get_connector(hostname, protocol="xmlrpc", port="auto"):
+def get_connector(hostname=None, protocol="xmlrpc", port="auto"):
"""
A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC.
@@ -377,10 +411,12 @@ def get_connector(hostname, protocol="xmlrpc", port="auto"):
return XmlRPCConnector(hostname, port)
elif protocol == "netrpc":
return NetRPCConnector(hostname, port)
+ elif protocol == "local":
+ return LocalConnector()
else:
- raise ValueError("You must choose xmlrpc or netrpc")
+ raise ValueError("You must choose xmlrpc or netrpc or local")
-def get_connection(hostname, protocol="xmlrpc", port='auto', database=None,
+def get_connection(hostname=None, protocol="xmlrpc", port='auto', database=None,
login=None, password=None, user_id=None):
"""
A shortcut method to easily create a connection to a remote OpenERP server.
diff --git a/addons/web/common/session.py b/addons/web/common/session.py
index d1997a3f903..2ed50bd6131 100644
--- a/addons/web/common/session.py
+++ b/addons/web/common/session.py
@@ -5,6 +5,9 @@ import time
import openerplib
import nonliterals
+
+import logging
+_logger = logging.getLogger(__name__)
#----------------------------------------------------------
# OpenERPSession RPC openerp backend access
#----------------------------------------------------------
@@ -26,25 +29,26 @@ class OpenERPSession(object):
Used to store references to non-literal domains which need to be
round-tripped to the client browser.
"""
- def __init__(self, server='127.0.0.1', port=8069):
- self._server = server
- self._port = port
+ def __init__(self):
+ self.config = None
self._db = False
self._uid = False
self._login = False
self._password = False
- self._locale = 'en_US'
self.context = {}
self.contexts_store = {}
self.domains_store = {}
- self._lang = {}
- self.remote_timezone = 'utc'
- self.client_timezone = False
+ def __getstate__(self):
+ state = dict(self.__dict__)
+ if "config" in state:
+ del state['config']
+ return state
+
def build_connection(self):
- return openerplib.get_connection(hostname=self._server, port=self._port,
- database=self._db, login=self._login,
- user_id=self._uid, password=self._password)
+ conn = openerplib.Connection(self.config.connector, database=self._db, login=self._login,
+ user_id=self._uid, password=self._password)
+ return conn
def proxy(self, service):
return self.build_connection().get_service(service)
@@ -99,15 +103,6 @@ class OpenERPSession(object):
self.context = self.model('res.users').context_get(self.context)
self.context = self.context or {}
- self.client_timezone = self.context.get("tz", False)
- # invalid code, anyway we decided the server will be in UTC
- #if self.client_timezone:
- # self.remote_timezone = self.execute('common', 'timezone_get')
-
- self._locale = self.context.get('lang','en_US')
- lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
- if lang_ids:
- self._lang = self.execute('res.lang', 'read',lang_ids[0], [])
return self.context
@property
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index d0c8950f9e1..9be0794a8bb 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -68,15 +68,7 @@ class Xml2Json:
# OpenERP Web web Controllers
#----------------------------------------------------------
-def manifest_glob(addons_path, addons, key):
- files = []
- for addon in addons:
- globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
- for pattern in globlist:
- for path in glob.glob(os.path.join(addons_path, addon, pattern)):
- files.append(path[len(addons_path):])
- return files
-
+# TODO change into concat_file(addons,key) taking care of addons_path
def concat_files(addons_path, file_list):
""" Concatenate file content
return (concat,timestamp)
@@ -104,7 +96,7 @@ home_template = textwrap.dedent("""<!DOCTYPE html>
%(javascript)s
<script type="text/javascript">
$(function() {
- var c = new openerp.init();
+ var c = new openerp.init(%(modules)s);
var wc = new c.web.WebClient("oe");
wc.start();
});
@@ -116,24 +108,45 @@ home_template = textwrap.dedent("""<!DOCTYPE html>
class WebClient(openerpweb.Controller):
_cp_path = "/web/webclient"
+ def server_wide_modules(self, req):
+ addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
+ return addons
+
+ def manifest_glob(self, req, addons, key):
+ if addons==None:
+ addons = self.server_wide_modules(req)
+ else:
+ addons = addons.split(',')
+ files = []
+ for addon in addons:
+ manifest = openerpweb.addons_manifest.get(addon, None)
+ if not manifest:
+ continue
+ addons_path = manifest['addons_path']
+ globlist = manifest.get(key, [])
+ for pattern in globlist:
+ for path in glob.glob(os.path.join(addons_path, addon, pattern)):
+ files.append(path[len(addons_path):])
+ return files
+
@openerpweb.jsonrequest
- def csslist(self, req, mods='web'):
- return manifest_glob(req.config.addons_path, mods.split(','), 'css')
+ def csslist(self, req, mods=None):
+ return self.manifest_glob(req, mods, 'css')
@openerpweb.jsonrequest
- def jslist(self, req, mods='web'):
- return manifest_glob(req.config.addons_path, mods.split(','), 'js')
+ def jslist(self, req, mods=None):
+ return self.manifest_glob(req, mods, 'js')
@openerpweb.httprequest
- def css(self, req, mods='web'):
- files = manifest_glob(req.config.addons_path, mods.split(','), 'css')
+ def css(self, req, mods=None):
+ files = self.manifest_glob(req, mods, 'css')
content,timestamp = concat_files(req.config.addons_path, files)
# TODO request set the Date of last modif and Etag
return req.make_response(content, [('Content-Type', 'text/css')])
@openerpweb.httprequest
- def js(self, req, mods='web'):
- files = manifest_glob(req.config.addons_path, mods.split(','), 'js')
+ def js(self, req, mods=None):
+ files = self.manifest_glob(req, mods, 'js')
content,timestamp = concat_files(req.config.addons_path, files)
# TODO request set the Date of last modif and Etag
return req.make_response(content, [('Content-Type', 'application/javascript')])
@@ -143,17 +156,19 @@ class WebClient(openerpweb.Controller):
# script tags
jslist = ['/web/webclient/js']
if req.debug:
- jslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'js')]
+ jslist = [i + '?debug=' + str(time.time()) for i in self.manifest_glob(req, None, 'js')]
js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
# css tags
csslist = ['/web/webclient/css']
if req.debug:
- csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'css')]
+ csslist = [i + '?debug=' + str(time.time()) for i in self.manifest_glob(req, None, 'css')]
css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
+
r = home_template % {
'javascript': js,
- 'css': css
+ 'css': css,
+ 'modules': simplejson.dumps(self.server_wide_modules(req)),
}
return r
@@ -166,20 +181,21 @@ class WebClient(openerpweb.Controller):
"grouping", "decimal_point", "thousands_sep"])
else:
lang_obj = None
-
+
if lang.count("_") > 0:
separator = "_"
else:
separator = "@"
langs = lang.split(separator)
langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
-
+
transs = {}
for addon_name in mods:
transl = {"messages":[]}
transs[addon_name] = transl
for l in langs:
- f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po")
+ addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
+ f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
if not os.path.exists(f_name):
continue
try:
@@ -240,7 +256,7 @@ class Database(openerpweb.Controller):
password, db = operator.itemgetter(
'drop_pwd', 'drop_db')(
dict(map(operator.itemgetter('name', 'value'), fields)))
-
+
try:
return req.session.proxy("db").drop(password, db)
except xmlrpclib.Fault, e:
@@ -291,7 +307,7 @@ class Session(openerpweb.Controller):
@openerpweb.jsonrequest
def login(self, req, db, login, password):
req.session.login(db, login, password)
- ctx = req.session.get_context()
+ ctx = req.session.get_context() if req.session._uid else {}
return {
"session_id": req.session_id,
@@ -299,6 +315,7 @@ class Session(openerpweb.Controller):
"context": ctx,
"db": req.session._db
}
+
@openerpweb.jsonrequest
def get_session_info(self, req):
req.session.assert_valid(force=True)
@@ -307,6 +324,7 @@ class Session(openerpweb.Controller):
"context": req.session.get_context() if req.session._uid else False,
"db": req.session._db
}
+
@openerpweb.jsonrequest
def change_password (self,req,fields):
old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
@@ -322,6 +340,7 @@ class Session(openerpweb.Controller):
except:
return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
return {'error': 'Error, password not changed !', 'title': 'Change Password'}
+
@openerpweb.jsonrequest
def sc_list(self, req):
return req.session.model('ir.ui.view_sc').get_sc(
@@ -336,13 +355,14 @@ class Session(openerpweb.Controller):
}
except Exception, e:
return {"error": e, "title": "Languages"}
-
+
@openerpweb.jsonrequest
def modules(self, req):
# TODO query server for installed web modules
mods = []
for name, manifest in openerpweb.addons_manifest.items():
- if name != 'web' and manifest.get('active', True):
+ # TODO replace by ir.module.module installed web
+ if name not in req.config.server_wide_modules and manifest.get('active', True):
mods.append(name)
return mods
@@ -472,6 +492,9 @@ def clean_action(req, action):
if isinstance(action.get('domain'), basestring):
action['domain'] = eval( action['domain'], eval_ctx ) or []
+ if 'type' not in action:
+ action['type'] = 'ir.actions.act_window_close'
+
if action['type'] == 'ir.actions.act_window':
return fix_view_modes(action)
return action
@@ -715,7 +738,7 @@ class DataSet(openerpweb.Controller):
args[domain_id] = d
if context_id and len(args) - 1 >= context_id:
args[context_id] = c
-
+
for i in xrange(len(args)):
if isinstance(args[i], web.common.nonliterals.BaseContext):
args[i] = req.session.eval_context(args[i])
@@ -963,7 +986,7 @@ class SearchView(View):
if field.get('context'):
field["context"] = self.parse_domain(field["context"], req.session)
return {'fields': fields}
-
+
@openerpweb.jsonrequest
def get_filters(self, req, model):
Model = req.session.model("ir.filters")
@@ -972,7 +995,7 @@ class SearchView(View):
filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
return filters
-
+
@openerpweb.jsonrequest
def save_filter(self, req, model, name, context_to_save, domain):
Model = req.session.model("ir.filters")
@@ -1382,7 +1405,7 @@ class Reports(View):
report_data['form'] = action['datas']['form']
if 'ids' in action['datas']:
report_ids = action['datas']['ids']
-
+
report_id = report_srv.report(
req.session._db, req.session._uid, req.session._password,
action["report_name"], report_ids,
@@ -1394,6 +1417,7 @@ class Reports(View):
req.session._db, req.session._uid, req.session._password, report_id)
if report_struct["state"]:
break
+
time.sleep(self.POLLING_DELAY)
report = base64.b64decode(report_struct['result'])
@@ -1407,3 +1431,108 @@ class Reports(View):
('Content-Type', report_mimetype),
('Content-Length', len(report))],
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, jsonp):
+ 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
+ data_record = 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, data_record)
+ # 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}}))
diff --git a/addons/web/static/lib/jquery.deferred-queue/LICENSE b/addons/web/static/lib/jquery.deferred-queue/LICENSE
new file mode 100644
index 00000000000..ee8cf44578c
--- /dev/null
+++ b/addons/web/static/lib/jquery.deferred-queue/LICENSE
@@ -0,0 +1,21 @@
+Copyright 2011 Xavier Morel. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this list of
+ conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list
+ of conditions and the following disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY XAVIER MOREL ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/addons/web/static/lib/jquery.deferred-queue/README b/addons/web/static/lib/jquery.deferred-queue/README
new file mode 100644
index 00000000000..71d980199c9
--- /dev/null
+++ b/addons/web/static/lib/jquery.deferred-queue/README
@@ -0,0 +1,59 @@
+.. -*- restructuredtext -*-
+
+In jQuery 1.5, jQuery has introduced a Deferred object in order to
+better handle callbacks.
+
+Along with Deferred, it introduced ``jQuery.when`` which allows, among
+other things, for multiplexing deferreds (waiting on multiple
+deferreds at the same time).
+
+While this is very nice if all deferreds are available at the same
+point, it doesn't really work if the resolution of a deferred can
+generate more deferreds, or for collections of deferreds coming from
+multiple sources (which may not be aware of one another).
+
+Deferred.queue tries to be a solution to this. It is based on the
+principle of FIFO multiple-producers multiple-consumers tasks queues,
+such as Python's `queue.Queue`_. Any code with a reference to the
+queue can add a promise to the queue, and the queue (which is a
+promise itself) will only be resolved once all promises within are
+resolved.
+
+Quickstart
+----------
+
+Deferred.queue has a very simple life cycle: it is dormant when
+created, it starts working as soon as promises get added to it (via
+``.push``, or by providing them directly to its constructor), and as
+soon as the last promise in the queue is resolved it resolves itself.
+
+If any promise in the queue fails, the queue itself will be rejected
+(without waiting for further promises).
+
+Once a queue has been resolved or rejected, adding new promises to it
+results in an error.
+
+API
+---
+
+``jQuery.Deferred.queue([promises...])``
+ Creates a new deferred queue. Can be primed with a series of promise
+ objects.
+
+``.push(promise...)``
+ Adds promises to the queue. Returns the queue itself (can be
+ chained).
+
+ If the queue has already been resolved or rejected, raises an error.
+
+``.then([doneCallbacks][, failCallbacks])``
+ Promise/A ``then`` method.
+
+``.done(doneCallbacks)``
+ jQuery ``done`` extension to promise objects
+
+``.fail(failCallbacks)``
+ jQuery ``fail`` extension to promise objects
+
+.. _queue.Queue:
+ http://docs.python.org/dev/library/queue.html
diff --git a/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js b/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js
new file mode 100644
index 00000000000..7ef83342f77
--- /dev/null
+++ b/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js
@@ -0,0 +1,34 @@
+(function ($) {
+ "use strict";
+ $.extend($.Deferred, {
+ queue: function () {
+ var queueDeferred = $.Deferred();
+ var promises = 0;
+
+ function resolve() {
+ if (--promises > 0) {
+ return;
+ }
+ setTimeout($.proxy(queueDeferred, 'resolve'), 0);
+ }
+
+ var promise = $.extend(queueDeferred.promise(), {
+ push: function () {
+ if (this.isResolved() || this.isRejected()) {
+ throw new Error("Can not add promises to a resolved "
+ + "or rejected promise queue");
+ }
+
+ promises += 1;
+ $.when.apply(null, arguments).then(
+ resolve, $.proxy(queueDeferred, 'reject'));
+ return this;
+ }
+ });
+ if (arguments.length) {
+ promise.push.apply(promise, arguments);
+ }
+ return promise;
+ }
+ });
+})(jQuery)
diff --git a/addons/web/static/lib/py.parse/.hg_archival.txt b/addons/web/static/lib/py.parse/.hg_archival.txt
new file mode 100644
index 00000000000..43d5d80a9e4
--- /dev/null
+++ b/addons/web/static/lib/py.parse/.hg_archival.txt
@@ -0,0 +1,5 @@
+repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
+node: 87fb1b67d6a13f10a1a328104ee4d4b2c36801ec
+branch: default
+latesttag: 0.2
+latesttagdistance: 1
diff --git a/addons/web/static/lib/py.parse/README b/addons/web/static/lib/py.parse/README
new file mode 100644
index 00000000000..453f001ee15
--- /dev/null
+++ b/addons/web/static/lib/py.parse/README
@@ -0,0 +1 @@
+Parser and evaluator of Python expressions
diff --git a/addons/web/static/lib/py.parse/TODO.rst b/addons/web/static/lib/py.parse/TODO.rst
new file mode 100644
index 00000000000..3638b681ddc
--- /dev/null
+++ b/addons/web/static/lib/py.parse/TODO.rst
@@ -0,0 +1,14 @@
+* Parser
+ since parsing expressions, try with a pratt parser
+ http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
+ http://effbot.org/zone/simple-top-down-parsing.htm
+
+Evaluator
+---------
+
+* Stop busyworking trivial binary operator
+* Make it *trivial* to build Python type-wrappers
+* Implement Python's `data model
+ protocols<http://docs.python.org/reference/datamodel.html#basic-customization>`_
+ for *all* supported operations, optimizations can come later
+* Automatically type-wrap everything (for now anyway)
diff --git a/addons/web/static/lib/py.parse/lib/py.js b/addons/web/static/lib/py.parse/lib/py.js
new file mode 100644
index 00000000000..0f2415f1252
--- /dev/null
+++ b/addons/web/static/lib/py.parse/lib/py.js
@@ -0,0 +1,546 @@
+var py = {};
+(function (exports) {
+ var NUMBER = /^\d$/,
+ NAME_FIRST = /^[a-zA-Z_]$/,
+ NAME = /^[a-zA-Z0-9_]$/;
+
+ var create = function (o, props) {
+ function F() {}
+ F.prototype = o;
+ var inst = new F;
+ for(var name in props) {
+ if(!props.hasOwnProperty(name)) { continue; }
+ inst[name] = props[name];
+ }
+ return inst;
+ };
+
+ var symbols = {};
+ var comparators = {};
+ var Base = {
+ nud: function () { throw new Error(this.id + " undefined as prefix"); },
+ led: function (led) { throw new Error(this.id + " undefined as infix"); },
+ toString: function () {
+ if (this.id === '(constant)' || this.id === '(number)' || this.id === '(name)' || this.id === '(string)') {
+ return [this.id.slice(0, this.id.length-1), ' ', this.value, ')'].join('');
+ } else if (this.id === '(end)') {
+ return '(end)';
+ } else if (this.id === '(comparator)' ) {
+ var repr = ['(comparator', this.expressions[0]];
+ for (var i=0;i<this.operators.length; ++i) {
+ repr.push(this.operators[i], this.expressions[i+1]);
+ }
+ return repr.join(' ') + ')';
+ }
+ var out = [this.id, this.first, this.second, this.third]
+ .filter(function (r){return r}).join(' ');
+ return '(' + out + ')';
+ }
+ };
+ function symbol(id, bp) {
+ bp = bp || 0;
+ var s = symbols[id];
+ if (s) {
+ if (bp > s.lbp) {
+ s.lbp = bp;
+ }
+ return s;
+ }
+ return symbols[id] = create(Base, {
+ id: id,
+ lbp: bp
+ });
+ }
+ function constant(id) {
+ symbol(id).nud = function () {
+ this.id = "(constant)";
+ this.value = id;
+ return this;
+ };
+ }
+ function prefix(id, bp, nud) {
+ symbol(id).nud = nud || function () {
+ this.first = expression(bp);
+ return this
+ }
+ }
+ function infix(id, bp, led) {
+ symbol(id, bp).led = led || function (left) {
+ this.first = left;
+ this.second = expression(bp);
+ return this;
+ }
+ }
+ function infixr(id, bp) {
+ symbol(id, bp).led = function (left) {
+ this.first = left;
+ this.second = expression(bp - 1);
+ return this;
+ }
+ }
+ function comparator(id) {
+ comparators[id] = true;
+ var bp = 60;
+ infix(id, bp, function (left) {
+ this.id = '(comparator)';
+ this.operators = [id];
+ this.expressions = [left, expression(bp)];
+ while (token.id in comparators) {
+ this.operators.push(token.id);
+ advance();
+ this.expressions.push(
+ expression(bp));
+ }
+ return this;
+ });
+ }
+
+ constant('None'); constant('False'); constant('True');
+
+ symbol('(number)').nud = function () { return this; };
+ symbol('(name)').nud = function () { return this; };
+ symbol('(string)').nud = function () { return this; };
+ symbol('(end)');
+
+ symbol(':'); symbol(')'); symbol(']'); symbol('}'); symbol(',');
+ symbol('else');
+
+ symbol('lambda', 20).nud = function () {
+ this.first = [];
+ if (token.id !== ':') {
+ for(;;) {
+ if (token.id !== '(name)') {
+ throw new Error('Excepted an argument name');
+ }
+ this.first.push(token);
+ advance();
+ if (token.id !== ',') {
+ break;
+ }
+ advance(',');
+ }
+ }
+ advance(':');
+ this.second = expression();
+ return this;
+ };
+ infix('if', 20, function (left) {
+ this.first = left;
+ this.second = expression();
+ advance('else');
+ this.third = expression();
+ return this;
+ });
+
+ infixr('or', 30); infixr('and', 40); prefix('not', 50);
+
+ comparator('in'); comparator('not in');
+ comparator('is'); comparator('is not');
+ comparator('<'); comparator('<=');
+ comparator('>'); comparator('>=');
+ comparator('<>'); comparator('!='); comparator('==');
+
+ infix('|', 70); infix('^', 80), infix('&', 90);
+
+ infix('<<', 100); infix('>>', 100);
+
+ infix('+', 110); infix('-', 110);
+
+ infix('*', 120); infix('/', 120);
+ infix('//', 120), infix('%', 120);
+
+ prefix('-', 130); prefix('+', 130); prefix('~', 130);
+
+ infixr('**', 140);
+
+ infix('.', 150, function (left) {
+ if (token.id !== '(name)') {
+ throw new Error('Expected attribute name, got ', token.id);
+ }
+ this.first = left;
+ this.second = token;
+ advance();
+ return this;
+ });
+ symbol('(', 150).nud = function () {
+ this.first = [];
+ var comma = false;
+ if (token.id !== ')') {
+ while (true) {
+ if (token.id === ')') {
+ break;
+ }
+ this.first.push(expression());
+ if (token.id !== ',') {
+ break;
+ }
+ comma = true;
+ advance(',');
+ }
+ }
+ advance(')');
+ if (!this.first.length || comma) {
+ return this;
+ } else {
+ return this.first[0];
+ }
+ };
+ symbol('(').led = function (left) {
+ this.first = left;
+ this.second = [];
+ if (token.id !== ")") {
+ for(;;) {
+ this.second.push(expression());
+ if (token.id !== ',') {
+ break;
+ }
+ advance(',');
+ }
+ }
+ advance(")");
+ return this;
+
+ };
+ infix('[', 150, function (left) {
+ this.first = left;
+ this.second = expression();
+ advance("]");
+ return this;
+ });
+ symbol('[').nud = function () {
+ this.first = [];
+ if (token.id !== ']') {
+ for (;;) {
+ if (token.id === ']') {
+ break;
+ }
+ this.first.push(expression());
+ if (token.id !== ',') {
+ break;
+ }
+ advance(',');
+ }
+ }
+ advance(']');
+ return this;
+ };
+
+ symbol('{').nud = function () {
+ this.first = [];
+ if (token.id !== '}') {
+ for(;;) {
+ if (token.id === '}') {
+ break;
+ }
+ var key = expression();
+ advance(':');
+ var value = expression();
+ this.first.push([key, value]);
+ if (token.id !== ',') {
+ break;
+ }
+ advance(',');
+ }
+ }
+ advance('}');
+ return this;
+ };
+
+ var longops = {
+ '*': ['*'],
+ '<': ['<', '=', '>'],
+ '>': ['=', '>'],
+ '!': ['='],
+ '=': ['='],
+ '/': ['/']
+ };
+ function Tokenizer() {
+ this.states = ['initial'];
+ this.tokens = [];
+ }
+ Tokenizer.prototype = {
+ builder: function (empty) {
+ var key = this.states[0] + '_builder';
+ if (empty) {
+ var value = this[key];
+ delete this[key];
+ return value;
+ } else {
+ return this[key] = this[key] || [];
+ }
+ },
+ simple: function (type) {
+ this.tokens.push({type: type});
+ },
+ push: function (new_state) {
+ this.states.push(new_state);
+ },
+ pop: function () {
+ this.states.pop();
+ },
+
+ feed: function (str, index) {
+ var s = this.states;
+ return this[s[s.length - 1]](str, index);
+ },
+
+ initial: function (str, index) {
+ var character = str[index];
+
+ if (character in longops) {
+ var follow = longops[character];
+ for(var i=0, len=follow.length; i<len; ++i) {
+ if (str[index+1] === follow[i]) {
+ character += follow[i];
+ index++;
+ break;
+ }
+ }
+ }
+
+ if (character === ' ') {
+ return index+1;
+ } else if (character === '\0') {
+ this.tokens.push(symbols['(end)']);
+ return index + 1
+ } else if (character === '"' || character === "'") {
+ this.push('string');
+ return index + 1;
+ } else if (NUMBER.test(character)) {
+ this.push('number');
+ return index;
+ } else if (NAME_FIRST.test(character)) {
+ this.push('name');
+ return index;
+ } else if (character in symbols) {
+ this.tokens.push(create(symbols[character]));
+ return index + 1;
+ }
+ throw new Error("Tokenizing failure of <<" + str + ">> at index " + index
+ + ", character [[" + character + "]]"
+ + "; parsed so far: " + this.tokens);
+ },
+ string: function (str, index) {
+ var character = str[index];
+ if (character === '"' || character === "'") {
+ this.tokens.push(create(symbols['(string)'], {
+ value: this.builder(true).join('')
+ }));
+ this.pop();
+ return index + 1;
+ }
+ this.builder().push(character);
+ return index + 1;
+ },
+ number: function (str, index) {
+ var character = str[index];
+ if (!NUMBER.test(character)) {
+ this.tokens.push(create(symbols['(number)'], {
+ value: parseFloat(this.builder(true).join(''))
+ }));
+ this.pop();
+ return index;
+ }
+ this.builder().push(character);
+ return index + 1;
+ },
+ name: function (str, index) {
+ var character = str[index];
+ if (!NAME.test(character)) {
+ var name = this.builder(true).join('');
+ var symbol = symbols[name];
+ if (symbol) {
+ if (name === 'in' && this.tokens[this.tokens.length-1].id === 'not') {
+ symbol = symbols['not in'];
+ this.tokens.pop();
+ } else if (name === 'not' && this.tokens[this.tokens.length-1].id === 'is') {
+ symbol = symbols['is not'];
+ this.tokens.pop();
+ }
+ this.tokens.push(create(symbol));
+ } else {
+ this.tokens.push(create(symbols['(name)'], {
+ value: name
+ }));
+ }
+ this.pop();
+ return index;
+ }
+ this.builder().push(character);
+ return index + 1;
+ }
+ };
+
+ exports.tokenize = function tokenize(str) {
+ var index = 0,
+ tokenizer = new Tokenizer(str);
+ str += '\0';
+
+ do {
+ index = tokenizer.feed(str, index);
+ } while (index !== str.length);
+ return tokenizer.tokens;
+ };
+
+ var token, next;
+ function expression(rbp) {
+ rbp = rbp || 0;
+ var t = token;
+ token = next();
+ var left = t.nud();
+ while (rbp < token.lbp) {
+ t = token;
+ token = next();
+ left = t.led(left);
+ }
+ return left;
+ }
+ function advance(id) {
+ if (id && token.id !== id) {
+ throw new Error(
+ 'Expected "' + id + '", got "' + token.id + '"');
+ }
+ token = next();
+ }
+
+ exports.object = create({}, {});
+ exports.bool = function (arg) { return !!arg; };
+ exports.tuple = create(exports.object, {
+ __contains__: function (value) {
+ for(var i=0, len=this.values.length; i<len; ++i) {
+ if (this.values[i] === value) {
+ return true;
+ }
+ }
+ return false;
+ },
+ toJSON: function () {
+ return this.values;
+ }
+ });
+ exports.list = exports.tuple;
+ exports.dict = create(exports.object, {
+ toJSON: function () {
+ return this.values;
+ }
+ });
+
+ exports.parse = function (toks) {
+ var index = 0;
+ token = toks[0];
+ next = function () { return toks[++index]; };
+ return expression();
+ };
+ var evaluate_operator = function (operator, a, b) {
+ switch (operator) {
+ case '==': case 'is': return a === b;
+ case '!=': case 'is not': return a !== b;
+ case '<': return a < b;
+ case '<=': return a <= b;
+ case '>': return a > b;
+ case '>=': return a >= b;
+ case 'in':
+ if (typeof b === 'string') {
+ return b.indexOf(a) !== -1;
+ }
+ return b.__contains__(a);
+ case 'not in':
+ if (typeof b === 'string') {
+ return b.indexOf(a) === -1;
+ }
+ return !b.__contains__(a);
+ }
+ throw new Error('SyntaxError: unknown comparator [[' + operator + ']]');
+ };
+ exports.evaluate = function (expr, context) {
+ switch (expr.id) {
+ case '(name)':
+ var val = context[expr.value];
+ if (val === undefined) {
+ throw new Error("NameError: name '" + expr.value + "' is not defined");
+ }
+ return val;
+ case '(string)':
+ case '(number)':
+ return expr.value;
+ case '(constant)':
+ if (expr.value === 'None')
+ return null;
+ else if (expr.value === 'False')
+ return false;
+ else if (expr.value === 'True')
+ return true;
+ throw new Error("SyntaxError: unknown constant '" + expr.value + "'");
+ case '(comparator)':
+ var result, left = exports.evaluate(expr.expressions[0], context);
+ for(var i=0; i<expr.operators.length; ++i) {
+ result = evaluate_operator(
+ expr.operators[i],
+ left,
+ left = exports.evaluate(expr.expressions[i+1], context));
+ if (!result) { return false; }
+ }
+ return true;
+ case '-':
+ if (expr.second) {
+ throw new Error('SyntaxError: binary [-] not implemented yet');
+ }
+ return -(exports.evaluate(expr.first, context));
+ case 'not':
+ return !(exports.evaluate(expr.first, context));
+ case 'and':
+ return (exports.evaluate(expr.first, context)
+ && exports.evaluate(expr.second, context));
+ case 'or':
+ return (exports.evaluate(expr.first, context)
+ || exports.evaluate(expr.second, context));
+ case '(':
+ if (expr.second) {
+ var fn = exports.evaluate(expr.first, context), args=[];
+ for (var jj=0; jj<expr.second.length; ++jj) {
+ args.push(exports.evaluate(
+ expr.second[jj], context));
+ }
+ return fn.apply(null, args);
+ }
+ var tuple_exprs = expr.first,
+ tuple_values = [];
+ for (var j=0, len=tuple_exprs.length; j<len; ++j) {
+ tuple_values.push(exports.evaluate(
+ tuple_exprs[j], context));
+ }
+ return create(exports.tuple, {values: tuple_values});
+ case '[':
+ if (expr.second) {
+ throw new Error('SyntaxError: indexing not implemented yet');
+ }
+ var list_exprs = expr.first, list_values = [];
+ for (var k=0; k<list_exprs.length; ++k) {
+ list_values.push(exports.evaluate(
+ list_exprs[k], context));
+ }
+ return create(exports.list, {values: list_values});
+ case '{':
+ var dict_exprs = expr.first, dict_values = {};
+ for(var l=0; l<dict_exprs.length; ++l) {
+ dict_values[exports.evaluate(dict_exprs[l][0], context)] =
+ exports.evaluate(dict_exprs[l][1], context);
+ }
+ return create(exports.dict, {values: dict_values});
+ case '.':
+ if (expr.second.id !== '(name)') {
+ throw new Error('SyntaxError: ' + expr);
+ }
+ return exports.evaluate(expr.first, context)[expr.second.value];
+ default:
+ throw new Error('SyntaxError: Unknown node [[' + expr.id + ']]');
+ }
+ };
+ exports.eval = function (str, context) {
+ return exports.evaluate(
+ exports.parse(
+ exports.tokenize(
+ str)),
+ context);
+ }
+})(typeof exports === 'undefined' ? py : exports);
diff --git a/addons/web/static/lib/py.parse/test/test.js b/addons/web/static/lib/py.parse/test/test.js
new file mode 100644
index 00000000000..0b4ddc1b623
--- /dev/null
+++ b/addons/web/static/lib/py.parse/test/test.js
@@ -0,0 +1,90 @@
+var py = require('../lib/py.js'),
+ assert = require('assert');
+
+// Literals
+assert.strictEqual(py.eval('1'), 1);
+assert.strictEqual(py.eval('None'), null);
+assert.strictEqual(py.eval('False'), false);
+assert.strictEqual(py.eval('True'), true);
+assert.strictEqual(py.eval('"somestring"'), 'somestring');
+assert.strictEqual(py.eval("'somestring'"), 'somestring');
+assert.deepEqual(py.eval("()").toJSON(), []);
+assert.deepEqual(py.eval("[]").toJSON(), []);
+assert.deepEqual(py.eval("{}").toJSON(), {});
+assert.deepEqual(py.eval("(None, True, False, 0, 1, 'foo')").toJSON(),
+ [null, true, false, 0, 1, 'foo']);
+assert.deepEqual(py.eval("[None, True, False, 0, 1, 'foo']").toJSON(),
+ [null, true, false, 0, 1, 'foo']);
+assert.deepEqual(py.eval("{'foo': 1, foo: 2}", {foo: 'bar'}).toJSON(),
+ {foo: 1, bar: 2});
+
+// Equality tests
+assert.ok(py.eval(
+ "foo == 'foo'", {foo: 'foo'}));
+// Inequality
+assert.ok(py.eval(
+ "foo != bar", {foo: 'foo', bar: 'bar'}));
+
+// Comparisons
+assert.ok(py.eval('3 < 5'));
+assert.ok(py.eval('5 >= 3'));
+assert.ok(py.eval('3 >= 3'));
+assert.ok(!py.eval('5 < 3'));
+assert.ok(py.eval('1 < 3 < 5'));
+assert.ok(py.eval('5 > 3 > 1'));
+assert.ok(py.eval('1 < 3 > 2 == 2 > -2 not in (0, 1, 2)'));
+// string rich comparisons
+assert.ok(py.eval(
+ 'date >= current', {date: '2010-06-08', current: '2010-06-05'}));
+
+// Boolean operators
+assert.ok(py.eval(
+ "foo == 'foo' or foo == 'bar'", {foo: 'bar'}));
+assert.ok(py.eval(
+ "foo == 'foo' and bar == 'bar'", {foo: 'foo', bar: 'bar'}));
+// - lazyness, second clauses NameError if not short-circuited
+assert.ok(py.eval(
+ "foo == 'foo' or bar == 'bar'", {foo: 'foo'}));
+assert.ok(!py.eval(
+ "foo == 'foo' and bar == 'bar'", {foo: 'bar'}));
+
+// contains (in)
+assert.ok(py.eval(
+ "foo in ('foo', 'bar')", {foo: 'bar'}));
+assert.ok(py.eval('1 in (1, 2, 3, 4)'));
+assert.ok(!py.eval('1 in (2, 3, 4)'));
+assert.ok(py.eval('type in ("url",)', {type: 'url'}));
+assert.ok(!py.eval('type in ("url",)', {type: 'ur'}));
+assert.ok(py.eval('1 not in (2, 3, 4)'));
+assert.ok(py.eval('type not in ("url",)', {type: 'ur'}));
+
+assert.ok(py.eval(
+ "foo in ['foo', 'bar']", {foo: 'bar'}));
+// string contains
+assert.ok(py.eval('type in "view"', {type: 'view'}));
+assert.ok(!py.eval('type in "view"', {type: 'bob'}));
+assert.ok(py.eval('type in "url"', {type: 'ur'}));
+
+// Literals
+assert.strictEqual(py.eval('False'), false);
+assert.strictEqual(py.eval('True'), true);
+assert.strictEqual(py.eval('None'), null);
+assert.ok(py.eval('foo == False', {foo: false}));
+assert.ok(!py.eval('foo == False', {foo: true}));
+
+// conversions
+assert.strictEqual(
+ py.eval('bool(date_deadline)', {bool: py.bool, date_deadline: '2008'}),
+ true);
+
+// getattr
+assert.ok(py.eval('foo.bar', {foo: {bar: true}}));
+assert.ok(!py.eval('foo.bar', {foo: {bar: false}}));
+
+// complex expressions
+assert.ok(py.eval(
+ "state=='pending' and not(date_deadline and (date_deadline < current_date))",
+ {state: 'pending', date_deadline: false}));
+assert.ok(py.eval(
+ "state=='pending' and not(date_deadline and (date_deadline < current_date))",
+ {state: 'pending', date_deadline: '2010-05-08', current_date: '2010-05-08'}));;
diff --git a/addons/web/static/lib/underscore/underscore.string.js b/addons/web/static/lib/underscore/underscore.string.js
index 4da2b26ec1c..d15c1f1f7ce 100644
--- a/addons/web/static/lib/underscore/underscore.string.js
+++ b/addons/web/static/lib/underscore/underscore.string.js
@@ -4,301 +4,436 @@
// Documentation: https://github.com/edtsech/underscore.string
// Some code is borrowed from MooTools and Alexandru Marasteanu.
-// Version 1.1.4
+// Version 1.1.6
-(function(){
- // ------------------------- Baseline setup ---------------------------------
- // Establish the root object, "window" in the browser, or "global" on the server.
- var root = this;
+(function(root){
+ 'use strict';
- var nativeTrim = String.prototype.trim;
+ if (typeof _ != 'undefined') {
+ var _reverse = _().reverse,
+ _include = _.include;
+ }
- function str_repeat(i, m) {
- for (var o = []; m > 0; o[--m] = i);
- return o.join('');
- }
+ // Defining helper functions.
- function defaultToWhiteSpace(characters){
- if (characters) {
- return _s.escapeRegExp(characters);
- }
- return '\\s';
- }
+ var nativeTrim = String.prototype.trim;
- var _s = {
+ var parseNumber = function(source) { return source * 1 || 0; };
- isBlank: function(str){
- return !!str.match(/^\s*$/);
- },
+ var strRepeat = function(i, m) {
+ for (var o = []; m > 0; o[--m] = i);
+ return o.join('');
+ };
- capitalize : function(str) {
- return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase();
- },
+ var slice = function(a){
+ return Array.prototype.slice.call(a);
+ };
- chop: function(str, step){
- step = step || str.length;
- var arr = [];
- for (var i = 0; i < str.length;) {
- arr.push(str.slice(i,i + step));
- i = i + step;
- }
- return arr;
- },
-
- clean: function(str){
- return _s.strip(str.replace(/\s+/g, ' '));
- },
-
- count: function(str, substr){
- var count = 0, index;
- for (var i=0; i < str.length;) {
- index = str.indexOf(substr, i);
- index >= 0 && count++;
- i = i + (index >= 0 ? index : 0) + substr.length;
- }
- return count;
- },
-
- chars: function(str) {
- return str.split('');
- },
-
- escapeHTML: function(str) {
- return String(str||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
- .replace(/"/g, '&quot;').replace(/'/g, "&apos;");
- },
-
- unescapeHTML: function(str) {
- return String(str||'').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
- .replace(/&quot;/g, '"').replace(/&apos;/g, "'");
- },
-
- escapeRegExp: function(str){
- // From MooTools core 1.2.4
- return String(str||'').replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
- },
-
- insert: function(str, i, substr){
- var arr = str.split('');
- arr.splice(i, 0, substr);
- return arr.join('');
- },
-
- includes: function(str, needle){
- return str.indexOf(needle) !== -1;
- },
-
- join: function(sep) {
- // TODO: Could this be faster by converting
- // arguments to Array and using array.join(sep)?
- sep = String(sep);
- var str = "";
- for (var i=1; i < arguments.length; i += 1) {
- str += String(arguments[i]);
- if ( i !== arguments.length-1 ) {
- str += sep;
- }
- }
- return str;
- },
-
- lines: function(str) {
- return str.split("\n");
- },
-
-// reverse: function(str){
-// return Array.prototype.reverse.apply(str.split('')).join('');
-// },
-
- splice: function(str, i, howmany, substr){
- var arr = str.split('');
- arr.splice(i, howmany, substr);
- return arr.join('');
- },
-
- startsWith: function(str, starts){
- return str.length >= starts.length && str.substring(0, starts.length) === starts;
- },
-
- endsWith: function(str, ends){
- return str.length >= ends.length && str.substring(str.length - ends.length) === ends;
- },
-
- succ: function(str){
- var arr = str.split('');
- arr.splice(str.length-1, 1, String.fromCharCode(str.charCodeAt(str.length-1) + 1));
- return arr.join('');
- },
-
- titleize: function(str){
- var arr = str.split(' '),
- word;
- for (var i=0; i < arr.length; i++) {
- word = arr[i].split('');
- if(typeof word[0] !== 'undefined') word[0] = word[0].toUpperCase();
- i+1 === arr.length ? arr[i] = word.join('') : arr[i] = word.join('') + ' ';
- }
- return arr.join('');
- },
-
- camelize: function(str){
- return _s.trim(str).replace(/(\-|_|\s)+(.)?/g, function(match, separator, chr) {
- return chr ? chr.toUpperCase() : '';
- });
- },
-
- underscored: function(str){
- return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/\-|\s+/g, '_').toLowerCase();
- },
-
- dasherize: function(str){
- return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1-$2').replace(/^([A-Z]+)/, '-$1').replace(/\_|\s+/g, '-').toLowerCase();
- },
-
- trim: function(str, characters){
- if (!characters && nativeTrim) {
- return nativeTrim.call(str);
- }
- characters = defaultToWhiteSpace(characters);
- return str.replace(new RegExp('\^[' + characters + ']+|[' + characters + ']+$', 'g'), '');
- },
-
- ltrim: function(str, characters){
- characters = defaultToWhiteSpace(characters);
- return str.replace(new RegExp('\^[' + characters + ']+', 'g'), '');
- },
-
- rtrim: function(str, characters){
- characters = defaultToWhiteSpace(characters);
- return str.replace(new RegExp('[' + characters + ']+$', 'g'), '');
- },
-
- truncate: function(str, length, truncateStr){
- truncateStr = truncateStr || '...';
- return str.slice(0,length) + truncateStr;
- },
-
- words: function(str, delimiter) {
- delimiter = delimiter || " ";
- return str.split(delimiter);
- },
-
-
- pad: function(str, length, padStr, type) {
-
- var padding = '';
- var padlen = 0;
-
- if (!padStr) { padStr = ' '; }
- else if (padStr.length > 1) { padStr = padStr[0]; }
- switch(type) {
- case "right":
- padlen = (length - str.length);
- padding = str_repeat(padStr, padlen);
- str = str+padding;
- break;
- case "both":
- padlen = (length - str.length);
- padding = {
- 'left' : str_repeat(padStr, Math.ceil(padlen/2)),
- 'right': str_repeat(padStr, Math.floor(padlen/2))
- };
- str = padding.left+str+padding.right;
- break;
- default: // "left"
- padlen = (length - str.length);
- padding = str_repeat(padStr, padlen);;
- str = padding+str;
+ var defaultToWhiteSpace = function(characters){
+ if (characters) {
+ return _s.escapeRegExp(characters);
+ }
+ return '\\s';
+ };
+
+ var sArgs = function(method){
+ return function(){
+ var args = slice(arguments);
+ for(var i=0; i<args.length; i++)
+ args[i] = args[i] == null ? '' : '' + args[i];
+ return method.apply(null, args);
+ };
+ };
+
+ // sprintf() for JavaScript 0.7-beta1
+ // http://www.diveintojavascript.com/projects/javascript-sprintf
+ //
+ // Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com>
+ // All rights reserved.
+
+ var sprintf = (function() {
+ function get_type(variable) {
+ return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
+ }
+
+ var str_repeat = strRepeat;
+
+ var str_format = function() {
+ if (!str_format.cache.hasOwnProperty(arguments[0])) {
+ str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
+ }
+ return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
+ };
+
+ str_format.format = function(parse_tree, argv) {
+ var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
+ for (i = 0; i < tree_length; i++) {
+ node_type = get_type(parse_tree[i]);
+ if (node_type === 'string') {
+ output.push(parse_tree[i]);
+ }
+ else if (node_type === 'array') {
+ match = parse_tree[i]; // convenience purposes only
+ if (match[2]) { // keyword argument
+ arg = argv[cursor];
+ for (k = 0; k < match[2].length; k++) {
+ if (!arg.hasOwnProperty(match[2][k])) {
+ throw(sprintf('[_.sprintf] property "%s" does not exist', match[2][k]));
+ }
+ arg = arg[match[2][k]];
}
- return str;
- },
-
- lpad: function(str, length, padStr) {
- return _s.pad(str, length, padStr);
- },
-
- rpad: function(str, length, padStr) {
- return _s.pad(str, length, padStr, 'right');
- },
-
- lrpad: function(str, length, padStr) {
- return _s.pad(str, length, padStr, 'both');
- },
-
-
- /**
- * Credits for this function goes to
- * http://www.diveintojavascript.com/projects/sprintf-for-javascript
- *
- * Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com>
- * All rights reserved.
- * */
- sprintf: function(){
-
- var i = 0, a, f = arguments[i++], o = [], m, p, c, x, s = '';
- while (f) {
- if (m = /^[^\x25]+/.exec(f)) {
- o.push(m[0]);
- }
- else if (m = /^\x25{2}/.exec(f)) {
- o.push('%');
+ } else if (match[1]) { // positional argument (explicit)
+ arg = argv[match[1]];
+ }
+ else { // positional argument (implicit)
+ arg = argv[cursor++];
+ }
+
+ if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
+ throw(sprintf('[_.sprintf] expecting number but found %s', get_type(arg)));
+ }
+ switch (match[8]) {
+ case 'b': arg = arg.toString(2); break;
+ case 'c': arg = String.fromCharCode(arg); break;
+ case 'd': arg = parseInt(arg, 10); break;
+ case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
+ case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
+ case 'o': arg = arg.toString(8); break;
+ case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
+ case 'u': arg = Math.abs(arg); break;
+ case 'x': arg = arg.toString(16); break;
+ case 'X': arg = arg.toString(16).toUpperCase(); break;
+ }
+ arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
+ pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
+ pad_length = match[6] - String(arg).length;
+ pad = match[6] ? str_repeat(pad_character, pad_length) : '';
+ output.push(match[5] ? arg + pad : pad + arg);
+ }
+ }
+ return output.join('');
+ };
+
+ str_format.cache = {};
+
+ str_format.parse = function(fmt) {
+ var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
+ while (_fmt) {
+ if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
+ parse_tree.push(match[0]);
+ }
+ else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
+ parse_tree.push('%');
+ }
+ else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
+ if (match[2]) {
+ arg_names |= 1;
+ var field_list = [], replacement_field = match[2], field_match = [];
+ if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
+ field_list.push(field_match[1]);
+ while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+ if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
+ field_list.push(field_match[1]);
}
- else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(f)) {
- if (((a = arguments[m[1] || i++]) == null) || (a == undefined)) {
- throw('Too few arguments.');
- }
- if (/[^s]/.test(m[7]) && (typeof(a) != 'number')) {
- throw('Expecting number but found ' + typeof(a));
- }
- switch (m[7]) {
- case 'b': a = a.toString(2); break;
- case 'c': a = String.fromCharCode(a); break;
- case 'd': a = parseInt(a); break;
- case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break;
- case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break;
- case 'o': a = a.toString(8); break;
- case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break;
- case 'u': a = Math.abs(a); break;
- case 'x': a = a.toString(16); break;
- case 'X': a = a.toString(16).toUpperCase(); break;
- }
- a = (/[def]/.test(m[7]) && m[2] && a >= 0 ? '+'+ a : a);
- c = m[3] ? m[3] == '0' ? '0' : m[3].charAt(1) : ' ';
- x = m[5] - String(a).length - s.length;
- p = m[5] ? str_repeat(c, x) : '';
- o.push(s + (m[4] ? a + p : p + a));
+ else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
+ field_list.push(field_match[1]);
}
else {
- throw('Huh ?!');
+ throw('[_.sprintf] huh?');
}
- f = f.substring(m[0].length);
+ }
}
- return o.join('');
+ else {
+ throw('[_.sprintf] huh?');
+ }
+ match[2] = field_list;
+ }
+ else {
+ arg_names |= 2;
+ }
+ if (arg_names === 3) {
+ throw('[_.sprintf] mixing positional and named placeholders is not (yet) supported');
+ }
+ parse_tree.push(match);
}
- }
-
- // Aliases
-
- _s.strip = _s.trim;
- _s.lstrip = _s.ltrim;
- _s.rstrip = _s.rtrim;
- _s.center = _s.lrpad
- _s.ljust = _s.lpad
- _s.rjust = _s.rpad
-
- // CommonJS module is defined
- if (typeof window === 'undefined' && typeof module !== 'undefined') {
- // Export module
- module.exports = _s;
-
- // Integrate with Underscore.js
- } else if (typeof root._ !== 'undefined') {
- root._.mixin(_s);
-
- // Or define it
- } else {
- root._ = _s;
- }
-
-}());
+ else {
+ throw('[_.sprintf] huh?');
+ }
+ _fmt = _fmt.substring(match[0].length);
+ }
+ return parse_tree;
+ };
+
+ return str_format;
+ })();
+
+
+
+ // Defining underscore.string
+
+ var _s = {
+
+ isBlank: sArgs(function(str){
+ return (/^\s*$/).test(str);
+ }),
+
+ stripTags: sArgs(function(str){
+ return str.replace(/<\/?[^>]+>/ig, '');
+ }),
+
+ capitalize : sArgs(function(str) {
+ return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase();
+ }),
+
+ chop: sArgs(function(str, step){
+ step = parseNumber(step) || str.length;
+ var arr = [];
+ for (var i = 0; i < str.length;) {
+ arr.push(str.slice(i,i + step));
+ i = i + step;
+ }
+ return arr;
+ }),
+
+ clean: sArgs(function(str){
+ return _s.strip(str.replace(/\s+/g, ' '));
+ }),
+
+ count: sArgs(function(str, substr){
+ var count = 0, index;
+ for (var i=0; i < str.length;) {
+ index = str.indexOf(substr, i);
+ index >= 0 && count++;
+ i = i + (index >= 0 ? index : 0) + substr.length;
+ }
+ return count;
+ }),
+
+ chars: sArgs(function(str) {
+ return str.split('');
+ }),
+
+ escapeHTML: sArgs(function(str) {
+ return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
+ .replace(/"/g, '&quot;').replace(/'/g, "&apos;");
+ }),
+
+ unescapeHTML: sArgs(function(str) {
+ return str.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
+ .replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
+ }),
+
+ escapeRegExp: sArgs(function(str){
+ // From MooTools core 1.2.4
+ return str.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
+ }),
+
+ insert: sArgs(function(str, i, substr){
+ var arr = str.split('');
+ arr.splice(parseNumber(i), 0, substr);
+ return arr.join('');
+ }),
+
+ includes: sArgs(function(str, needle){
+ return str.indexOf(needle) !== -1;
+ }),
+
+ include: function(obj, needle) {
+ if (!_include || (/string|number/).test(typeof obj)) {
+ return this.includes(obj, needle);
+ } else {
+ return _include(obj, needle);
+ }
+ },
+
+ join: sArgs(function(sep) {
+ var args = slice(arguments);
+ return args.join(args.shift());
+ }),
+
+ lines: sArgs(function(str) {
+ return str.split("\n");
+ }),
+
+ reverse: function(obj){
+ if (!_reverse || (/string|number/).test(typeof obj)) {
+ return Array.prototype.reverse.apply(String(obj).split('')).join('');
+ } else {
+ return _reverse.call(_(obj));
+ }
+ },
+
+ splice: sArgs(function(str, i, howmany, substr){
+ var arr = str.split('');
+ arr.splice(parseNumber(i), parseNumber(howmany), substr);
+ return arr.join('');
+ }),
+
+ startsWith: sArgs(function(str, starts){
+ return str.length >= starts.length && str.substring(0, starts.length) === starts;
+ }),
+
+ endsWith: sArgs(function(str, ends){
+ return str.length >= ends.length && str.substring(str.length - ends.length) === ends;
+ }),
+
+ succ: sArgs(function(str){
+ var arr = str.split('');
+ arr.splice(str.length-1, 1, String.fromCharCode(str.charCodeAt(str.length-1) + 1));
+ return arr.join('');
+ }),
+
+ titleize: sArgs(function(str){
+ var arr = str.split(' '),
+ word;
+ for (var i=0; i < arr.length; i++) {
+ word = arr[i].split('');
+ if(typeof word[0] !== 'undefined') word[0] = word[0].toUpperCase();
+ i+1 === arr.length ? arr[i] = word.join('') : arr[i] = word.join('') + ' ';
+ }
+ return arr.join('');
+ }),
+
+ camelize: sArgs(function(str){
+ return _s.trim(str).replace(/(\-|_|\s)+(.)?/g, function(match, separator, chr) {
+ return chr ? chr.toUpperCase() : '';
+ });
+ }),
+
+ underscored: function(str){
+ return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/\-|\s+/g, '_').toLowerCase();
+ },
+
+ dasherize: function(str){
+ return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1-$2').replace(/^([A-Z]+)/, '-$1').replace(/\_|\s+/g, '-').toLowerCase();
+ },
+
+ trim: sArgs(function(str, characters){
+ if (!characters && nativeTrim) {
+ return nativeTrim.call(str);
+ }
+ characters = defaultToWhiteSpace(characters);
+ return str.replace(new RegExp('\^[' + characters + ']+|[' + characters + ']+$', 'g'), '');
+ }),
+
+ ltrim: sArgs(function(str, characters){
+ characters = defaultToWhiteSpace(characters);
+ return str.replace(new RegExp('\^[' + characters + ']+', 'g'), '');
+ }),
+
+ rtrim: sArgs(function(str, characters){
+ characters = defaultToWhiteSpace(characters);
+ return str.replace(new RegExp('[' + characters + ']+$', 'g'), '');
+ }),
+
+ truncate: sArgs(function(str, length, truncateStr){
+ truncateStr = truncateStr || '...';
+ length = parseNumber(length);
+ return str.length > length ? str.slice(0,length) + truncateStr : str;
+ }),
+
+ words: function(str, delimiter) {
+ return String(str).split(delimiter || " ");
+ },
+
+ pad: sArgs(function(str, length, padStr, type) {
+ var padding = '',
+ padlen = 0;
+
+ length = parseNumber(length);
+
+ if (!padStr) { padStr = ' '; }
+ else if (padStr.length > 1) { padStr = padStr.charAt(0); }
+ switch(type) {
+ case 'right':
+ padlen = (length - str.length);
+ padding = strRepeat(padStr, padlen);
+ str = str+padding;
+ break;
+ case 'both':
+ padlen = (length - str.length);
+ padding = {
+ 'left' : strRepeat(padStr, Math.ceil(padlen/2)),
+ 'right': strRepeat(padStr, Math.floor(padlen/2))
+ };
+ str = padding.left+str+padding.right;
+ break;
+ default: // 'left'
+ padlen = (length - str.length);
+ padding = strRepeat(padStr, padlen);;
+ str = padding+str;
+ }
+ return str;
+ }),
+
+ lpad: function(str, length, padStr) {
+ return _s.pad(str, length, padStr);
+ },
+
+ rpad: function(str, length, padStr) {
+ return _s.pad(str, length, padStr, 'right');
+ },
+
+ lrpad: function(str, length, padStr) {
+ return _s.pad(str, length, padStr, 'both');
+ },
+
+ sprintf: sprintf,
+
+ vsprintf: function(fmt, argv){
+ argv.unshift(fmt);
+ return sprintf.apply(null, argv);
+ },
+
+ toNumber: function(str, decimals) {
+ var num = parseNumber(parseNumber(str).toFixed(parseNumber(decimals)));
+ return (!(num === 0 && (str !== "0" && str !== 0))) ? num : Number.NaN;
+ },
+
+ strRight: sArgs(function(sourceStr, sep){
+ var pos = (!sep) ? -1 : sourceStr.indexOf(sep);
+ return (pos != -1) ? sourceStr.slice(pos+sep.length, sourceStr.length) : sourceStr;
+ }),
+
+ strRightBack: sArgs(function(sourceStr, sep){
+ var pos = (!sep) ? -1 : sourceStr.lastIndexOf(sep);
+ return (pos != -1) ? sourceStr.slice(pos+sep.length, sourceStr.length) : sourceStr;
+ }),
+
+ strLeft: sArgs(function(sourceStr, sep){
+ var pos = (!sep) ? -1 : sourceStr.indexOf(sep);
+ return (pos != -1) ? sourceStr.slice(0, pos) : sourceStr;
+ }),
+
+ strLeftBack: sArgs(function(sourceStr, sep){
+ var pos = sourceStr.lastIndexOf(sep);
+ return (pos != -1) ? sourceStr.slice(0, pos) : sourceStr;
+ })
+
+ };
+
+ // Aliases
+
+ _s.strip = _s.trim;
+ _s.lstrip = _s.ltrim;
+ _s.rstrip = _s.rtrim;
+ _s.center = _s.lrpad;
+ _s.ljust = _s.lpad;
+ _s.rjust = _s.rpad;
+
+ // CommonJS module is defined
+ if (typeof module !== 'undefined' && module.exports) {
+ // Export module
+ module.exports = _s;
+
+ // Integrate with Underscore.js
+ } else if (typeof root._ !== 'undefined') {
+ root._.mixin(_s);
+
+ // Or define it
+ } else {
+ root._ = _s;
+ }
+
+}(this || window));
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css
index 5d4da8175dd..7f5b7f5f41b 100644
--- a/addons/web/static/src/css/base.css
+++ b/addons/web/static/src/css/base.css
@@ -125,7 +125,7 @@ body.openerp, .openerp textarea, .openerp input, .openerp select, .openerp optio
margin-top: 5px;
text-align: center;
}
-.openerp .login.login_invalid .login_error_message {
+.openerp .login .login_invalid .login_error_message {
display: block;
}
@@ -221,17 +221,11 @@ label.error {
height: 100%;
background: #f0eeee;
}
-
-/* Menu */
-.openerp .sf-menu {
- margin-bottom: 0;
-}
-/*
-.sf-menu a {
- padding: 5px 5px;
+.openerp .oe-application-container {
+ height: 100%;
}
-*/
+/* Menu */
.openerp .menu {
height: 34px;
background: #cc4e45; /* Old browsers */
@@ -253,7 +247,7 @@ label.error {
height: 20px;
margin: 3px 2px;
padding: 0 8px;
-
+
background: #bd5e54; /* Old browsers */
background: -moz-linear-gradient(top, #bd5e54 0%, #90322a 60%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bd5e54), color-stop(60%,#90322a)); /* Chrome,Safari4+ */
@@ -291,13 +285,38 @@ label.error {
background: linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* W3C */
/* for ie */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#5c5c5c', endColorstr='#969595',GradientType=0 ); /* IE6-9 */
-
color: #fff;
}
-.openerp .oe-application-container {
- height: 100%;
-}
/* Secondary Menu */
+.openerp .secondary_menu .oe_toggle_secondary_menu {
+ position: absolute;
+ cursor: pointer;
+ border-left: 1px solid #282828;
+ width: 21px;
+ height: 21px;
+ z-index: 10;
+ background: transparent;
+ color: white;
+ text-shadow: 0 1px 0 #333;
+ text-align: center;
+ font-size: 18px;
+ line-height: 14px;
+ right: 0;
+}
+.openerp .secondary_menu.oe_folded .oe_toggle_secondary_menu {
+ position: static;
+ border-left: none;
+ border-bottom: 1px solid #282828;
+ width: 21px;
+ height: 21px;
+ background: #818181;
+}
+.openerp .secondary_menu.oe_folded .oe_toggle_secondary_menu span.oe_menu_fold {
+ display: none;
+}
+.openerp .secondary_menu.oe_unfolded .oe_toggle_secondary_menu span.oe_menu_unfold {
+ display: none;
+}
.openerp .secondary_menu {
width: 200px;
min-width: 200px;
@@ -306,15 +325,34 @@ label.error {
background: #5A5858;
vertical-align: top;
height: 100%;
+ position: relative;
}
-.openerp .secondary_menu .menu_content {
- padding: 0;
- border: none;
- background: none;
- overflow: hidden;
+.openerp .secondary_menu.oe_folded {
+ width: 20px;
+ min-width: 20px;
+ position: static;
+}
+.openerp .secondary_menu.oe_folded .oe_secondary_menu.active {
+ position: absolute;
+ z-index: 100;
+ border: 4px solid #585858;
+ border: 4px solid rgba(88, 88, 88, .5);
+ border-radius: 4px;
+ min-width: 200px;
}
-.openerp .secondary_menu h3 {
- padding: 0 0 2px;
+.openerp .secondary_menu a {
+ display: block;
+ padding: 0 5px 2px 5px;
+ line-height: 20px;
+ text-decoration: none;
+ white-space: nowrap;
+ color: white;
+ text-shadow: 0 1px 0 #333;
+}
+.openerp .oe_secondary_submenu {
+ background: #5A5858;
+}
+.openerp .secondary_menu a.oe_secondary_menu_item {
background: #949292; /* Old browsers */
background: -moz-linear-gradient(top, #949292 0%, #6d6b6b 87%, #282828 99%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#949292), color-stop(87%,#6d6b6b), color-stop(99%,#282828)); /* Chrome,Safari4+ */
@@ -325,34 +363,13 @@ label.error {
background: linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* W3C */
/* for ie9 */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#5B5A5A',GradientType=0 ); /* IE6-9 */
- border: none;
- /* overriding jquery ui */
- -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
-}
-.openerp .secondary_menu h4 {
- padding: 0 0 2px 10px;
- border: none;
- background: none;
-}
-.openerp .secondary_menu h3 span, .openerp .secondary_menu h4 span {
- left: 0 !important;
-}
-.openerp .secondary_menu a {
- display: block;
- height: 20px;
- padding: 0 5px;
- line-height: 20px;
white-space: nowrap;
color: white;
- text-decoration: none;
text-shadow: 0 1px 0 #333;
}
-.openerp .secondary_menu a.leaf:hover,
-.openerp .secondary_menu a.leaf:active,
-.openerp .secondary_menu a.leaf.active,
-.openerp .secondary_menu h4:hover,
-.openerp .secondary_menu h4:active,
-.openerp .secondary_menu h4.active {
+.openerp a.oe_secondary_submenu_item:hover,
+.openerp a.oe_secondary_submenu_item.leaf.active {
+ display: block;
background: #ffffff; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #d8d8d8 11%, #afafaf 86%, #333333 91%, #5a5858 96%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(11%,#d8d8d8), color-stop(86%,#afafaf), color-stop(91%,#333333), color-stop(96%,#5a5858)); /* Chrome,Safari4+ */
@@ -361,22 +378,17 @@ label.error {
background: -ms-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* IE10+ */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#5A5858',GradientType=0 ); /* IE6-9 */
background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* W3C */
- /* overriding jquery ui */
- -moz-border-radius-topright: 0; -webkit-border-top-right-radius: 0; border-top-right-radius: 0;
+ padding: 0 5px 2px 5px;
+ line-height: 20px;
color: #3f3d3d;
+ text-decoration: none;
text-shadow: #fff 0 1px 0;
- border: none !important;
}
-
-.openerp .secondary_menu h4:hover a,
-.openerp .secondary_menu h4:active a,
-.openerp .secondary_menu h4.active a {
- color: #3f3d3d;
- text-shadow: #fff 0 1px 0;
- border: none !important;
+.openerp a.oe_secondary_submenu_item.submenu.opened span:before {
+ content: "\25be";
}
-.openerp div.submenu_accordion div.menu_content a span {
- padding-left: 20px;
+.openerp a.oe_secondary_submenu_item.submenu span:before {
+ content: "\25b8";
}
/* Header */
@@ -880,6 +892,9 @@ label.error {
.openerp .oe_forms input.field_datetime {
min-width: 11em;
}
+.openerp .oe_forms.oe_frame .oe_datepicker_root {
+ width: 100%;
+}
.openerp .oe_forms .button {
color: #4c4c4c;
white-space: nowrap;
@@ -892,7 +907,14 @@ label.error {
position: absolute;
cursor: pointer;
right: 5px;
- top: 5px;
+ top: 3px;
+}
+.openerp .oe_datepicker_root {
+ position: relative;
+ display: inline-block;
+}
+.openerp .oe_datepicker_root input[type="text"] {
+ min-width: 160px;
}
.openerp .oe_input_icon_disabled {
position: absolute;
diff --git a/addons/web/static/src/css/data_import.css b/addons/web/static/src/css/data_import.css
new file mode 100644
index 00000000000..1fb3b22aa45
--- /dev/null
+++ b/addons/web/static/src/css/data_import.css
@@ -0,0 +1,30 @@
+.openerp .oe_import_grid {
+ border: none;
+ border-collapse: collapse;
+}
+.openerp .oe_import_grid-header .oe_import_grid-cell {
+ background: url(../img/gradientlinebg.gif) repeat-x #CCCCCC;
+ border-bottom: 1px solid #E3E3E3;
+ font-weight: bold;
+ text-align: left;
+}
+.openerp .oe_import_grid-row .oe_import_grid-cell {
+ border-bottom: 1px solid #E3E3E3;
+}
+.openerp .separator.horizontal {
+ font-weight: bold;
+ border-bottom-width: 1px;
+ margin: 6px 4px 6px 1px;
+ height: 20px;
+}
+.openerp .duplicate_fld{
+ background-color:#FF6666;
+}
+.openerp .select_fld{
+ background: none repeat scroll 0 0 white;
+}
+.openerp .ui-autocomplete {
+ max-height: 300px;
+ overflow-y: auto;
+ padding-right: 20px;
+}
diff --git a/addons/web/static/src/img/gradientlinebg.gif b/addons/web/static/src/img/gradientlinebg.gif
new file mode 100644
index 00000000000..f40bd9c0f6a
--- /dev/null
+++ b/addons/web/static/src/img/gradientlinebg.gif
Binary files differ
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js
index 38f8eaa1e7c..f951d160866 100644
--- a/addons/web/static/src/js/boot.js
+++ b/addons/web/static/src/js/boot.js
@@ -19,9 +19,9 @@
/**
* OpenERP instance constructor
*
- * @param {Boolean} skip_init if true, skips the built-in initialization
+ * @param {Array} modules list of modules to initialize
*/
- init: function(skip_init) {
+ init: function(modules) {
var new_instance = {
// links to the global openerp
_openerp: openerp,
@@ -35,8 +35,9 @@
web_mobile: {}
};
openerp.sessions[new_instance._session_id] = new_instance;
- if (!skip_init){
- openerp.web(new_instance);
+ modules = modules || ["web"];
+ for(var i=0; i < modules.length; i++) {
+ openerp[modules[i]](new_instance);
}
return new_instance;
}
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js
index d114f485c62..1955298ba27 100644
--- a/addons/web/static/src/js/chrome.js
+++ b/addons/web/static/src/js/chrome.js
@@ -1,7 +1,6 @@
/*---------------------------------------------------------
* OpenERP Web chrome
*---------------------------------------------------------*/
-
openerp.web.chrome = function(openerp) {
var QWeb = openerp.web.qweb;
@@ -129,6 +128,7 @@ openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog#
// Destroy widget
this.close();
this.$dialog.dialog('destroy');
+ this._super();
}
});
@@ -218,9 +218,8 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
this.$element.closest(".openerp")
.removeClass("login-mode")
.addClass("database_block");
-
+
var self = this;
-
var fetch_db = this.rpc("/web/database/get_list", {}, function(result) {
self.db_list = result.db_list;
});
@@ -232,7 +231,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
self.lang_list = result.lang_list;
});
$.when(fetch_db, fetch_langs).then(function () {self.do_create();});
-
+
this.$element.find('#db-create').click(this.do_create);
this.$element.find('#db-drop').click(this.do_drop);
this.$element.find('#db-backup').click(this.do_backup);
@@ -254,7 +253,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
.removeClass("database_block")
.end()
.empty();
-
+ this._super();
},
/**
* Converts a .serializeArray() result into a dict. Does not bother folding
@@ -300,9 +299,9 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
var admin = result[1][0];
setTimeout(function () {
- self.stop();
self.widget_parent.do_login(
info.db, admin.login, admin.password);
+ self.stop();
$.unblockUI();
});
});
@@ -399,7 +398,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
do_restore: function() {
var self = this;
self.$option_id.html(QWeb.render("RestoreDB", self));
-
+
self.$option_id.find("form[name=restore_db_form]").validate({
submitHandler: function (form) {
$.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
@@ -464,15 +463,19 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
remember_creditentials: true,
+
+ template: "Login",
+ identifier_prefix: 'oe-app-login-',
/**
* @constructs openerp.web.Login
* @extends openerp.web.Widget
- *
+ *
* @param parent
* @param element_id
*/
- init: function(parent, element_id) {
- this._super(parent, element_id);
+
+ init: function(parent) {
+ this._super(parent);
this.has_local_storage = typeof(localStorage) != 'undefined';
this.selected_db = null;
this.selected_login = null;
@@ -487,17 +490,6 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
},
start: function() {
var self = this;
- this.rpc("/web/database/get_list", {}, function(result) {
- self.db_list = result.db_list;
- self.display();
- }, function() {
- self.display();
- });
- },
- display: function() {
- var self = this;
-
- this.$element.html(QWeb.render("Login", this));
this.database = new openerp.web.Database(
this, "oe_database", "oe_db_options");
@@ -506,6 +498,17 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
});
this.$element.find("form").submit(this.on_submit);
+
+ this.rpc("/web/database/get_list", {}, function(result) {
+ var tpl = openerp.web.qweb.render('Login_dblist', {db_list: result.db_list, selected_db: self.selected_db});
+ self.$element.find("input[name=db]").replaceWith(tpl)
+ },
+ function(error, event) {
+ if (error.data.fault_code === 'AccessDenied') {
+ event.preventDefault();
+ }
+ });
+
},
on_login_invalid: function() {
this.$element.closest(".openerp").addClass("login-mode");
@@ -574,14 +577,13 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
/**
* @constructs openerp.web.Header
* @extends openerp.web.Widget
- *
+ *
* @param parent
*/
init: function(parent) {
this._super(parent);
this.qs = "?" + jQuery.param.querystring();
this.$content = $();
- console.debug("initializing header with id", this.element_id);
this.update_promise = $.Deferred().resolve();
},
start: function() {
@@ -666,7 +668,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
});
});
},
-
+
on_action: function(action) {
},
on_preferences: function(){
@@ -702,10 +704,11 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
},
Save: function(){
var inner_viewmanager = action_manager.inner_viewmanager;
- inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save(function(){
- inner_viewmanager.start();
+ inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save()
+ .then(function() {
+ self.dialog.stop();
+ window.location.reload();
});
- $(this).dialog('destroy')
}
}
});
@@ -713,7 +716,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
action_manager.appendTo(this.dialog);
action_manager.render(this.dialog);
},
-
+
change_password :function() {
var self = this;
this.dialog = new openerp.web.Dialog(this,{
@@ -728,21 +731,13 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
submitHandler: function (form) {
self.rpc("/web/session/change_password",{
'fields': $(form).serializeArray()
- }, function(result) {
- if (result.error) {
- self.display_error(result);
+ }, function(result) {
+ if (result.error) {
+ self.display_error(result);
return;
- }
- else {
- if (result.new_password) {
- self.session.password = result.new_password;
- var session = new openerp.web.Session(self.session.server, self.session.port);
- session.start();
- session.session_login(self.session.db, self.session.login, self.session.password)
- }
- }
- self.notification.notify("Changed Password", "Password has been changed successfully");
- self.dialog.close();
+ } else {
+ self.session.logout();
+ }
});
}
});
@@ -766,7 +761,7 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{
/**
* @constructs openerp.web.Menu
* @extends openerp.web.Widget
- *
+ *
* @param parent
* @param element_id
* @param secondary_menu_id
@@ -776,74 +771,132 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{
this.secondary_menu_id = secondary_menu_id;
this.$secondary_menu = $("#" + secondary_menu_id).hide();
this.menu = false;
+ this.folded = false;
+ if (window.localStorage) {
+ this.folded = localStorage.getItem('oe_menu_folded') === 'true';
+ }
+ this.float_timeout = 700;
},
start: function() {
+ this.$secondary_menu.addClass(this.folded ? 'oe_folded' : 'oe_unfolded');
+ this.reload();
+ },
+ reload: function() {
this.rpc("/web/menu/load", {}, this.on_loaded);
},
on_loaded: function(data) {
this.data = data;
- this.$element.html(QWeb.render("Menu", this.data));
- for (var i = 0; i < this.data.data.children.length; i++) {
- var v = { menu : this.data.data.children[i] };
- this.$secondary_menu.append(QWeb.render("Menu.secondary", v));
- }
- this.$secondary_menu.find("div.menu_accordion").accordion({
- animated : false,
- autoHeight : false,
- icons : false
- });
- this.$secondary_menu.find("div.submenu_accordion").accordion({
- animated : false,
- autoHeight : false,
- active: false,
- collapsible: true,
- header: 'h4'
- });
-
+ this.$element.html(QWeb.render("Menu", { widget : this }));
+ this.$secondary_menu.html(QWeb.render("Menu.secondary", { widget : this }));
this.$element.add(this.$secondary_menu).find("a").click(this.on_menu_click);
+ this.$secondary_menu.find('.oe_toggle_secondary_menu').click(this.on_toggle_fold);
+ },
+ on_toggle_fold: function() {
+ this.$secondary_menu.toggleClass('oe_folded').toggleClass('oe_unfolded');
+ if (this.folded) {
+ this.$secondary_menu.find('.oe_secondary_menu.active').show();
+ } else {
+ this.$secondary_menu.find('.oe_secondary_menu').hide();
+ }
+ this.folded = !this.folded;
+ if (window.localStorage) {
+ localStorage.setItem('oe_menu_folded', this.folded.toString());
+ }
},
on_menu_click: function(ev, id) {
id = id || 0;
- var $menu, $parent, $secondary;
+ var $clicked_menu, manual = false;
if (id) {
// We can manually activate a menu with it's id (for hash url mapping)
- $menu = this.$element.find('a[data-menu=' + id + ']');
- if (!$menu.length) {
- $menu = this.$secondary_menu.find('a[data-menu=' + id + ']');
+ manual = true;
+ $clicked_menu = this.$element.find('a[data-menu=' + id + ']');
+ if (!$clicked_menu.length) {
+ $clicked_menu = this.$secondary_menu.find('a[data-menu=' + id + ']');
}
} else {
- $menu = $(ev.currentTarget);
- id = $menu.data('menu');
- }
- if (this.$secondary_menu.has($menu).length) {
- $secondary = $menu.parents('.menu_accordion');
- $parent = this.$element.find('a[data-menu=' + $secondary.data('menu-parent') + ']');
- } else {
- $parent = $menu;
- $secondary = this.$secondary_menu.find('.menu_accordion[data-menu-parent=' + $menu.attr('data-menu') + ']');
+ $clicked_menu = $(ev.currentTarget);
+ id = $clicked_menu.data('menu');
}
- this.$secondary_menu.find('.menu_accordion').hide();
- // TODO: ui-accordion : collapse submenus and expand the good one
- $secondary.show();
-
- if (id) {
+ if (this.do_menu_click($clicked_menu, manual) && id) {
this.session.active_id = id;
- this.rpc('/web/menu/action', {'menu_id': id},
- this.on_menu_action_loaded);
+ this.rpc('/web/menu/action', {'menu_id': id}, this.on_menu_action_loaded);
}
+ ev.stopPropagation();
+ return false;
+ },
+ do_menu_click: function($clicked_menu, manual) {
+ var $sub_menu, $main_menu,
+ active = $clicked_menu.is('.active'),
+ sub_menu_visible = false;
+
+ if (this.$secondary_menu.has($clicked_menu).length) {
+ $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
+ $main_menu = this.$element.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
+ } else {
+ $sub_menu = this.$secondary_menu.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
+ $main_menu = $clicked_menu;
+ }
+
+ sub_menu_visible = $sub_menu.is(':visible');
+ this.$secondary_menu.find('.oe_secondary_menu').hide();
$('.active', this.$element.add(this.$secondary_menu.show())).removeClass('active');
- $parent.addClass('active');
- $menu.addClass('active');
- $menu.parent('h4').addClass('active');
+ $main_menu.add($clicked_menu).add($sub_menu).addClass('active');
- if (this.$secondary_menu.has($menu).length) {
- return !$menu.is(".leaf");
+ if (!(this.folded && manual)) {
+ this.do_show_secondary($sub_menu, $main_menu);
+ }
+
+ if ($main_menu != $clicked_menu) {
+ if ($clicked_menu.is('.submenu')) {
+ $sub_menu.find('.submenu.opened').each(function() {
+ if (!$(this).next().has($clicked_menu).length && !$(this).is($clicked_menu)) {
+ $(this).removeClass('opened').next().hide();
+ }
+ });
+ $clicked_menu.toggleClass('opened').next().toggle();
+ } else if ($clicked_menu.is('.leaf')) {
+ $sub_menu.toggle(!this.folded);
+ return true;
+ }
+ } else if (this.folded) {
+ if (active && sub_menu_visible) {
+ $sub_menu.hide();
+ return true;
+ }
} else {
- return false;
+ return true;
}
+ return false;
+ },
+ do_show_secondary: function($sub_menu, $main_menu) {
+ var self = this;
+ if (this.folded) {
+ var css = $main_menu.position(),
+ fold_width = this.$secondary_menu.width() + 2,
+ window_width = $(window).width();
+ css.top += 33;
+ css.left -= Math.round(($sub_menu.width() - $main_menu.width()) / 2);
+ css.left = css.left < fold_width ? fold_width : css.left;
+ if ((css.left + $sub_menu.width()) > window_width) {
+ delete(css.left);
+ css.right = 1;
+ }
+ $sub_menu.css(css);
+ $sub_menu.mouseenter(function() {
+ clearTimeout($sub_menu.data('timeoutId'));
+ }).mouseleave(function(evt) {
+ var timeoutId = setTimeout(function() {
+ if (self.folded) {
+ $sub_menu.hide();
+ }
+ }, self.float_timeout);
+ $sub_menu.data('timeoutId', timeoutId);
+ });
+ }
+ $sub_menu.show();
},
on_menu_action_loaded: function(data) {
var self = this;
@@ -860,7 +913,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
/**
* @constructs openerp.web.WebClient
* @extends openerp.web.Widget
- *
+ *
* @param element_id
*/
init: function(element_id) {
@@ -883,7 +936,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
openerp.web.Widget.prototype.notification = new openerp.web.Notification(this, "oe_notification");
this.header = new openerp.web.Header(this);
- this.login = new openerp.web.Login(this, "oe_login");
+ this.login = new openerp.web.Login(this);
this.header.on_logout.add(this.login.on_logout);
this.header.on_action.add(this.on_menu_action);
@@ -903,9 +956,8 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
start: function() {
this.header.appendTo($("#oe_header"));
this.session.start();
- this.login.start();
+ this.login.appendTo($('#oe_login'));
this.menu.start();
- console.debug("The openerp client has been initialized.");
},
on_logged: function() {
if(this.action_manager)
@@ -951,7 +1003,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
self.execute_home_action(home_action[0], ds);
})
},
- default_home: function () {
+ default_home: function () {
},
/**
* Bundles the execution of the home action
@@ -976,7 +1028,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
},
do_url_set_hash: function(url) {
if(!this.url_external_hashchange) {
- console.log("url set #hash to",url);
this.url_internal_hashchange = true;
jQuery.bbq.pushState(url);
}
@@ -984,10 +1035,8 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
on_url_hashchange: function() {
if(this.url_internal_hashchange) {
this.url_internal_hashchange = false;
- console.log("url jump to FLAG OFF");
} else {
var url = jQuery.deparam.fragment();
- console.log("url jump to",url);
this.url_external_hashchange = true;
this.action_manager.on_url_hashchange(url);
this.url_external_hashchange = false;
diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js
index dba47ee758c..9d4d2124008 100644
--- a/addons/web/static/src/js/core.js
+++ b/addons/web/static/src/js/core.js
@@ -1,7 +1,10 @@
/*---------------------------------------------------------
* OpenERP Web core
*--------------------------------------------------------*/
-
+var console;
+if (!console) {
+ console = {log: function () {}};
+}
if (!console.debug) {
console.debug = console.log;
}
@@ -483,7 +486,6 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
self.user_context = result.context;
self.db = result.db;
self.session_save();
- self.on_session_valid();
return true;
}).then(success_callback);
},
@@ -772,7 +774,6 @@ openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp
* // stuff that you want to init before the rendering
* },
* start: function() {
- * this._super();
* // stuff you want to make after the rendering, `this.$element` holds a correct value
* this.$element.find(".my_button").click(/* an example of event binding * /);
*
@@ -916,6 +917,8 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg
* @returns {jQuery.Deferred}
*/
start: function() {
+ /* The default implementation is only useful for retro-compatibility, it is
+ not necessary to call it using _super() when using Widget for new components. */
if (!this.$element) {
var tmp = document.getElementById(this.element_id);
this.$element = tmp ? $(tmp) : undefined;
@@ -923,7 +926,7 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg
return $.Deferred().done().promise();
},
/**
- * Destroys the current widget, also destory all its children before destroying itself.
+ * Destroys the current widget, also destroy all its children before destroying itself.
*/
stop: function() {
_.each(_.clone(this.widget_children), function(el) {
@@ -944,7 +947,6 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg
* If that's not the case this method will simply return `false`.
*/
do_action: function(action, on_finished) {
- console.log('Widget.do_action', action, on_finished);
if (this.widget_parent) {
return this.widget_parent.do_action(action, on_finished);
}
diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js
index e5d323c8a96..fcf6a65dcf5 100644
--- a/addons/web/static/src/js/data.js
+++ b/addons/web/static/src/js/data.js
@@ -233,6 +233,7 @@ openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends
});
openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet# */{
+ identifier_prefix: "dataset",
/**
* DateaManagement interface between views and the collection of selected
* OpenERP records (represents the view's state?)
@@ -242,16 +243,12 @@ openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet
*
* @param {String} model the OpenERP model this dataset will manage
*/
- init: function(source_controller, model, context) {
- // we don't want the dataset to be a child of anything!
- this._super(null);
- this.session = source_controller ? source_controller.session : undefined;
+ init: function(parent, model, context) {
+ this._super(parent);
this.model = model;
this.context = context || {};
this.index = null;
},
- start: function() {
- },
previous: function () {
this.index -= 1;
if (this.index < 0) {
@@ -549,13 +546,11 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D
sort: this.sort(),
offset: offset,
limit: options.limit || false
- }, function (result) {
+ }).pipe(function (result) {
self.ids = result.ids;
self.offset = offset;
- if (callback) {
- callback(result.records);
- }
- });
+ return result.records;
+ }).then(callback);
},
get_domain: function (other_domain) {
if (other_domain) {
@@ -603,20 +598,27 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D
});
openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({
virtual_id_prefix: "one2many_v_id_",
- virtual_id_regex: /one2many_v_id_.*/,
debug_mode: true,
init: function() {
this._super.apply(this, arguments);
this.reset_ids([]);
+ this.last_default_get = {};
+ },
+ default_get: function(fields, callback) {
+ return this._super(fields).then(this.on_default_get).then(callback);
+ },
+ on_default_get: function(res) {
+ this.last_default_get = res;
},
create: function(data, callback, error_callback) {
- var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data};
+ var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
+ defaults: this.last_default_get};
this.to_create.push(cached);
this.cache.push(cached);
this.on_change();
- var to_return = $.Deferred().then(callback);
- to_return.resolve({result: cached.id});
- return to_return.promise();
+ var prom = $.Deferred().then(callback);
+ setTimeout(function() {prom.resolve({result: cached.id});}, 0);
+ return prom.promise();
},
write: function (id, data, options, callback) {
var self = this;
@@ -676,7 +678,8 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({
var cached = _.detect(self.cache, function(x) {return x.id === id;});
var created = _.detect(self.to_create, function(x) {return x.id === id;});
if (created) {
- _.each(fields, function(x) {if (cached.values[x] === undefined) cached.values[x] = false;});
+ _.each(fields, function(x) {if (cached.values[x] === undefined)
+ cached.values[x] = created.defaults[x] || false;});
} else {
if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined}))
to_get.push(id);
@@ -715,7 +718,13 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({
return completion.promise();
}
});
+openerp.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
+
openerp.web.ReadOnlyDataSetSearch = openerp.web.DataSetSearch.extend({
+ default_get: function(fields, callback) {
+ return this._super(fields, callback).then(this.on_default_get);
+ },
+ on_default_get: function(result) {},
create: function(data, callback, error_callback) {
this.on_create(data);
var to_return = $.Deferred().then(callback);
diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js
new file mode 100644
index 00000000000..9c686846751
--- /dev/null
+++ b/addons/web/static/src/js/data_import.js
@@ -0,0 +1,289 @@
+openerp.web.data_import = function(openerp) {
+var QWeb = openerp.web.qweb;
+/**
+ * Safari does not deal well at all with raw JSON data being returned. As a
+ * result, we're going to cheat by using a pseudo-jsonp: instead of getting
+ * JSON data in the iframe, we're getting a ``script`` tag which consists of a
+ * function call and the returned data (the json dump).
+ *
+ * The function is an auto-generated name bound to ``window``, which calls
+ * back into the callback provided here.
+ *
+ * @param {Object} form the form element (DOM or jQuery) to use in the call
+ * @param {Object} attributes jquery.form attributes object
+ * @param {Function} callback function to call with the returned data
+ */
+function jsonp(form, attributes, callback) {
+ attributes = attributes || {};
+ var options = {jsonp: _.uniqueId('import_callback_')};
+ window[options.jsonp] = function () {
+ delete window[options.jsonp];
+ callback.apply(null, arguments);
+ };
+ if ('data' in attributes) {
+ _.extend(attributes.data, options);
+ } else {
+ _.extend(attributes, {data: options});
+ }
+ $(form).ajaxSubmit(attributes);
+}
+
+openerp.web.DataImport = openerp.web.Dialog.extend({
+ template: 'ImportDataView',
+ dialog_title: "Import Data",
+ init: function(parent, dataset){
+ var self = this;
+ this._super(parent, {});
+ this.model = parent.model;
+ this.fields = [];
+ this.all_fields = [];
+ this.required_fields = null;
+
+ var convert_fields = function (root, prefix) {
+ prefix = prefix || '';
+ _(root.fields).each(function (f) {
+ self.all_fields.push(prefix + f.name);
+ if (f.fields) {
+ convert_fields(f, prefix + f.id + '/');
+ }
+ });
+ };
+ this.ready = $.Deferred.queue().then(function () {
+ self.required_fields = _(self.fields).chain()
+ .filter(function (field) { return field.required; })
+ .pluck('name')
+ .value();
+ convert_fields(self);
+ self.all_fields.sort();
+ });
+ },
+ start: function() {
+ var self = this;
+ this._super();
+ this.open({
+ modal: true,
+ width: '70%',
+ height: 'auto',
+ position: 'top',
+ buttons: [
+ {text: "Close", click: function() { self.stop(); }},
+ {text: "Import File", click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'}
+ ],
+ close: function(event, ui) {
+ self.stop();
+ }
+ });
+ this.toggle_import_button(false);
+ this.$element.find('#csvfile').change(this.on_autodetect_data);
+ this.$element.find('fieldset').change(this.on_autodetect_data);
+ this.$element.find('fieldset legend').click(function() {
+ $(this).next().toggle();
+ });
+ this.ready.push(new openerp.web.DataSet(this, this.model).call(
+ 'fields_get', [], function (fields) {
+ self.graft_fields(fields);
+ }));
+ },
+ graft_fields: function (fields, parent, level) {
+ parent = parent || this;
+ level = level || 0;
+
+ var self = this;
+ _(fields).each(function (field, field_name) {
+ var f = {
+ id: field_name,
+ name: field_name,
+ string: field.string,
+ required: field.required
+ };
+
+ switch (field.type) {
+ case 'many2many':
+ case 'many2one':
+ f.name += '/id';
+ break;
+ case 'one2many':
+ f.name += '/id';
+ f.fields = [];
+ // only fetch sub-fields to a depth of 2 levels
+ if (level < 2) {
+ self.ready.push(new openerp.web.DataSet(self, field.relation).call(
+ 'fields_get', [], function (fields) {
+ self.graft_fields(fields, f, level+1);
+ }));
+ }
+ break;
+ }
+ parent.fields.push(f);
+ });
+ },
+ toggle_import_button: function (newstate) {
+ this.$dialog.dialog('widget')
+ .find('.oe-dialog-import-button')
+ .button('option', 'disabled', !newstate);
+ },
+ do_import: function() {
+ if(!this.$element.find('#csvfile').val()) { return; }
+ var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10);
+ var with_headers = this.$element.find('#file_has_headers').prop('checked');
+ if (!lines_to_skip && with_headers) {
+ lines_to_skip = 1;
+ }
+ var indices = [], fields = [];
+ this.$element.find(".sel_fields").each(function(index, element) {
+ var val = element.value;
+ if (!val) {
+ return;
+ }
+ indices.push(index);
+ fields.push(val);
+ });
+
+ jsonp(this.$element.find('#import_data'), {
+ url: '/web/import/import_data',
+ data: {
+ model: this.model,
+ meta: JSON.stringify({
+ skip: lines_to_skip,
+ indices: indices,
+ fields: fields
+ })
+ }
+ }, this.on_import_results);
+ },
+ on_autodetect_data: function() {
+ if(!this.$element.find('#csvfile').val()) { return; }
+ jsonp(this.$element.find('#import_data'), {
+ url: '/web/import/detect_data'
+ }, this.on_import_results);
+ },
+ on_import_results: function(results) {
+ this.$element.find('#result').empty();
+ var headers, result_node = this.$element.find("#result");
+
+ if (results['records']) {
+ var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10),
+ with_headers = this.$element.find('#file_has_headers').prop('checked');
+ headers = with_headers ? results.records[0] : null;
+
+ result_node.append(QWeb.render('ImportView.result', {
+ 'headers': headers,
+ 'records': lines_to_skip ? results.records.slice(lines_to_skip)
+ : with_headers ? results.records.slice(1)
+ : results.records
+ }));
+ } else if (results['error']) {
+ result_node.append(QWeb.render('ImportView.error', {
+ 'error': results['error']}));
+ } else if (results['success']) {
+ if (this.widget_parent.widget_parent.active_view == "list") {
+ this.widget_parent.reload_content();
+ }
+ this.stop();
+ return;
+ }
+
+ var self = this;
+ this.ready.then(function () {
+ var $fields = self.$element.find('.sel_fields').bind('blur', function () {
+ if (this.value && !_(self.all_fields).contains(this.value)) {
+ this.value = '';
+ }
+ }).autocomplete({
+ minLength: 0,
+ source: self.all_fields,
+ change: self.on_check_field_values
+ }).focus(function () {
+ $(this).autocomplete('search');
+ });
+ // Column auto-detection
+ _(headers).each(function (header, index) {
+ var f =_(self.fields).detect(function (field) {
+ // TODO: levenshtein between header and field.string
+ return field.name === header || field.string.toLowerCase() === header;
+ });
+ if (f) {
+ $fields.eq(index).val(f.name);
+ }
+ });
+ self.on_check_field_values();
+ });
+ },
+ /**
+ * Looks through all the field selections, and tries to find if two
+ * (or more) columns were matched to the same model field.
+ *
+ * Returns a map of the multiply-mapped fields to an array of offending
+ * columns (not actually columns, but the inputs containing the same field
+ * names).
+ *
+ * Also has the side-effect of marking the discovered inputs with the class
+ * ``duplicate_fld``.
+ *
+ * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
+ */
+ find_duplicate_fields: function() {
+ // Maps values to DOM nodes, in order to discover duplicates
+ var values = {}, duplicates = {};
+ this.$element.find(".sel_fields").each(function(index, element) {
+ var value = element.value;
+ var $element = $(element).removeClass('duplicate_fld');
+ if (!value) { return; }
+
+ if (!(value in values)) {
+ values[value] = element;
+ } else {
+ var same_valued_field = values[value];
+ if (value in duplicates) {
+ duplicates[value].push(element);
+ } else {
+ duplicates[value] = [same_valued_field, element];
+ }
+ $element.add(same_valued_field).addClass('duplicate_fld');
+ }
+ });
+ return duplicates;
+ },
+ on_check_field_values: function () {
+ this.$element.find("#message, #msg").remove();
+
+ var required_valid = this.check_required();
+
+ var duplicates = this.find_duplicate_fields();
+ if (_.isEmpty(duplicates)) {
+ this.toggle_import_button(required_valid);
+ } else {
+ var $err = $('<div id="msg" style="color: red;">Destination fields should only be selected once, some fields are selected more than once:</div>').insertBefore(this.$element.find('#result'));
+ var $dupes = $('<dl>').appendTo($err);
+ _(duplicates).each(function(elements, value) {
+ $('<dt>').text(value).appendTo($dupes);
+ _(elements).each(function(element) {
+ var cell = $(element).closest('td');
+ $('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes);
+ });
+ });
+ this.toggle_import_button(false);
+ }
+
+ },
+ check_required: function() {
+ if (!this.required_fields.length) { return true; }
+
+ var selected_fields = _(this.$element.find('.sel_fields').get()).chain()
+ .pluck('value')
+ .compact()
+ .value();
+
+ var missing_fields = _.difference(this.required_fields, selected_fields);
+ if (missing_fields.length) {
+ this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>');
+ return false;
+ }
+ return true;
+ },
+ stop: function() {
+ $(this.$dialog).remove();
+ this._super();
+ }
+});
+};
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
index b0dc4fcc289..e7feedf26a5 100644
--- a/addons/web/static/src/js/formats.js
+++ b/addons/web/static/src/js/formats.js
@@ -82,7 +82,12 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) {
}
switch (descriptor.widget || descriptor.type) {
case 'integer':
- var tmp = Number(value);
+ var tmp;
+ do {
+ tmp = value;
+ value = value.replace(openerp.web._t.database.parameters.thousands_sep, "");
+ } while(tmp !== value);
+ tmp = Number(value);
if (isNaN(tmp))
throw value + " is not a correct integer";
return tmp;
@@ -113,26 +118,26 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) {
var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate,
Date.CultureInfo.formatPatterns.longTime));
if (tmp !== null)
- return tmp;
+ return openerp.web.datetime_to_str(tmp);
tmp = Date.parse(value);
if (tmp !== null)
- return tmp;
+ return openerp.web.datetime_to_str(tmp);
throw value + " is not a valid datetime";
case 'date':
var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate);
if (tmp !== null)
- return tmp;
+ return openerp.web.date_to_str(tmp);
tmp = Date.parse(value);
if (tmp !== null)
- return tmp;
+ return openerp.web.date_to_str(tmp);
throw value + " is not a valid date";
case 'time':
var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime);
if (tmp !== null)
- return tmp;
+ return openerp.web.time_to_str(tmp);
tmp = Date.parse(value);
if (tmp !== null)
- return tmp;
+ return openerp.web.time_to_str(tmp);
throw value + " is not a valid time";
}
return value;
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
index 5db66ab7260..db91d6bfd27 100644
--- a/addons/web/static/src/js/search.js
+++ b/addons/web/static/src/js/search.js
@@ -327,7 +327,8 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
this.notification.notify("Invalid Search", "triggered from search view");
},
do_clear: function () {
- $('.filter_label').removeClass('enabled');
+ this.$element.find('.filter_label').removeClass('enabled');
+ this.enabled_filters.splice(0);
var string = $('a.searchview_group_string');
_.each(string, function(str){
$(str).closest('div.searchview_group').removeClass("expanded").addClass('folded');
@@ -542,6 +543,7 @@ openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends open
var domains = _(this.filters).chain()
.filter(function (filter) { return filter.is_enabled(); })
.map(function (filter) { return filter.attrs.domain; })
+ .reject(_.isEmpty)
.value();
if (!domains.length) { return; }
@@ -720,7 +722,11 @@ openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends open
openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{
error_message: "not a valid integer",
parse: function (value) {
- return parseInt(value, 10);
+ try {
+ return openerp.web.parse_value(value, {'widget': 'integer'});
+ } catch (e) {
+ return NaN;
+ }
}
});
/**
@@ -730,7 +736,11 @@ openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @len
openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{
error_message: "not a valid number",
parse: function (value) {
- return parseFloat(value);
+ try {
+ return openerp.web.parse_value(value, {'widget': 'float'});
+ } catch (e) {
+ return NaN;
+ }
}
});
/**
@@ -785,20 +795,18 @@ openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @
* @extends openerp.web.search.DateField
*/
openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{
- /**
- * enables date picker on the HTML widgets
- */
+ template: "SearchView.date",
start: function () {
this._super();
- this.$element.addClass('field_date').datepicker({
- dateFormat: 'yy-mm-dd'
- });
- },
- stop: function () {
- this.$element.datepicker('destroy');
+ this.datewidget = new openerp.web.DateWidget(this);
+ this.datewidget.prependTo(this.$element);
+ this.datewidget.$element.find("input").attr("size", 15);
+ this.datewidget.$element.find("input").attr("autofocus",
+ this.attrs.default_focus === '1' ? 'autofocus' : undefined);
+ this.datewidget.set_value(this.defaults[this.attrs.name] || false);
},
get_value: function () {
- return this.$element.val();
+ return this.datewidget.get_value() || null;
}
});
/**
@@ -1113,7 +1121,7 @@ openerp.web.search.ExtendedSearchProposition.Char = openerp.web.OldWidget.extend
}
});
openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.extend({
- template: 'SearchView.extended_search.proposition.datetime',
+ template: 'SearchView.extended_search.proposition.empty',
identifier_prefix: 'extended-search-proposition-datetime',
operators: [
{value: "=", text: "is equal to"},
@@ -1124,18 +1132,16 @@ openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.ex
{value: "<=", text: "less or equal than"}
],
get_value: function() {
- return this.$element.val();
+ return this.datewidget.get_value();
},
start: function() {
this._super();
- this.$element.datetimepicker({
- dateFormat: 'yy-mm-dd',
- timeFormat: 'hh:mm:ss'
- });
+ this.datewidget = new openerp.web.DateTimeWidget(this);
+ this.datewidget.prependTo(this.$element);
}
});
openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend({
- template: 'SearchView.extended_search.proposition.date',
+ template: 'SearchView.extended_search.proposition.empty',
identifier_prefix: 'extended-search-proposition-date',
operators: [
{value: "=", text: "is equal to"},
@@ -1146,14 +1152,12 @@ openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend
{value: "<=", text: "less or equal than"}
],
get_value: function() {
- return this.$element.val();
+ return this.datewidget.get_value();
},
start: function() {
this._super();
- this.$element.datepicker({
- dateFormat: 'yy-mm-dd',
- timeFormat: 'hh:mm:ss'
- });
+ this.datewidget = new openerp.web.DateWidget(this);
+ this.datewidget.prependTo(this.$element);
}
});
openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.extend({
@@ -1168,11 +1172,11 @@ openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.ext
{value: "<=", text: "less or equal than"}
],
get_value: function() {
- var value = parseFloat(this.$element.val());
- if(value != 0 && !value) {
+ try {
+ return openerp.web.parse_value(this.$element.val(), {'widget': 'integer'});
+ } catch (e) {
return "";
}
- return Math.round(value);
}
});
openerp.web.search.ExtendedSearchProposition.Float = openerp.web.OldWidget.extend({
@@ -1187,11 +1191,11 @@ openerp.web.search.ExtendedSearchProposition.Float = openerp.web.OldWidget.exten
{value: "<=", text: "less or equal than"}
],
get_value: function() {
- var value = parseFloat(this.$element.val());
- if(value != 0 && !value) {
+ try {
+ return openerp.web.parse_value(this.$element.val(), {'widget': 'float'});
+ } catch (e) {
return "";
}
- return value;
}
});
openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.OldWidget.extend({
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index d3293b4f2ad..11ebad8d093 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -26,7 +26,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
- this.view_id = view_id;
+ this.view_id = view_id || false;
this.fields_view = {};
this.widgets = {};
this.widgets_counter = 0;
@@ -41,7 +41,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.has_been_loaded = $.Deferred();
this.$form_header = null;
this.translatable_fields = [];
- _.defaults(this.options, {"always_show_new_button": true});
+ _.defaults(this.options, {"always_show_new_button": true,
+ "not_interactible_on_create": false});
},
start: function() {
this._super();
@@ -72,17 +73,25 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
_.each(this.widgets, function(w) {
w.stop();
});
+ this._super();
+ },
+ reposition: function ($e) {
+ this.$element = $e;
+ this.on_loaded();
},
on_loaded: function(data) {
var self = this;
- this.fields_view = data;
- var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
+ if (data) {
+ this.fields_view = data;
+ var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
- this.$element.html(QWeb.render(this.form_template, { 'frame': frame, 'view': this }));
+ this.rendered = QWeb.render(this.form_template, { 'frame': frame, 'view': this });
+ }
+ this.$element.html(this.rendered);
_.each(this.widgets, function(w) {
w.start();
});
- this.$form_header = this.$element.find('#' + this.element_id + '_header');
+ this.$form_header = this.$element.find('.oe_form_header:first');
this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() {
var action = $(this).data('pager-action');
self.on_pager_action(action);
@@ -93,6 +102,17 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel);
this.$form_header.find('button.oe_form_button_new').click(this.on_button_new);
this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate);
+ this.$form_header.find('button.oe_form_button_toggle').click(function () {
+ self.translatable_fields = [];
+ self.widgets = {};
+ self.fields = {};
+ self.$form_header.find('button').unbind('click');
+ self.registry = self.registry === openerp.web.form.widgets
+ ? openerp.web.form.readonly
+ : openerp.web.form.widgets;
+ self.on_loaded(self.fields_view);
+ self.reload();
+ });
if (this.options.sidebar && this.options.sidebar_id) {
this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
@@ -196,7 +216,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
}
},
do_update_pager: function(hide_index) {
- var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager');
+ var $pager = this.$form_header.find('div.oe_form_pager');
var index = hide_index ? '-' : this.dataset.index + 1;
$pager.find('span.oe_pager_index').html(index);
$pager.find('span.oe_pager_count').html(this.dataset.ids.length);
@@ -304,9 +324,15 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
var def = $.Deferred();
$.when(this.has_been_loaded).then(function() {
if (self.can_be_discarded()) {
- self.dataset.default_get(_.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() {
+ var keys = _.keys(self.fields_view.fields);
+ if (keys.length) {
+ self.dataset.default_get(keys).then(self.on_record_loaded).then(function() {
+ def.resolve();
+ });
+ } else {
+ self.on_record_loaded({});
def.resolve();
- });
+ }
}
});
return def.promise();
@@ -339,7 +365,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
do_save: function(success, prepend_on_create) {
var self = this;
if (!this.ready) {
- return false;
+ return $.Deferred().reject();
}
var form_dirty = false,
form_invalid = false,
@@ -361,23 +387,18 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
if (form_invalid) {
first_invalid_field.focus();
this.on_invalid();
- return false;
- } else if (form_dirty) {
+ return $.Deferred().reject();
+ } else {
console.log("About to save", values);
if (!this.datarecord.id) {
- return this.dataset.create(values, function(r) {
- self.on_created(r, success, prepend_on_create);
- });
+ return this.dataset.create(values).pipe(function(r) {
+ return self.on_created(r, undefined, prepend_on_create);
+ }).then(success);
} else {
- return this.dataset.write(this.datarecord.id, values, {}, function(r) {
- self.on_saved(r, success);
- });
+ return this.dataset.write(this.datarecord.id, values, {}).pipe(function(r) {
+ return self.on_saved(r);
+ }).then(success);
}
- } else {
- setTimeout(function() {
- self.on_saved({ result: true }, success);
- });
- return true;
}
},
do_save_edit: function() {
@@ -401,11 +422,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
on_saved: function(r, success) {
if (!r.result) {
// should not happen in the server, but may happen for internal purpose
+ return $.Deferred().reject();
} else {
- if (success) {
- success(r);
- }
this.reload();
+ return $.Deferred().then(success).resolve(r);
}
},
/**
@@ -424,6 +444,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
on_created: function(r, success, prepend_on_create) {
if (!r.result) {
// should not happen in the server, but may happen for internal purpose
+ return $.Deferred().reject();
} else {
this.datarecord.id = r.result;
if (!prepend_on_create) {
@@ -438,10 +459,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.sidebar.attachments.do_update();
}
console.debug("The record has been created with id #" + this.datarecord.id);
- if (success) {
- success(_.extend(r, {created: true}));
- }
this.reload();
+ return $.Deferred().then(success).resolve(_.extend(r, {created: true}));
}
},
do_search: function (domains, contexts, groupbys) {
@@ -471,6 +490,24 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
get_selected_ids: function() {
var id = this.dataset.ids[this.dataset.index];
return id ? [id] : [];
+ },
+ recursive_save: function() {
+ var self = this;
+ return $.when(this.do_save()).pipe(function(res) {
+ if (self.dataset.parent_view)
+ return self.dataset.parent_view.recursive_save();
+ });
+ },
+ is_interactible_record: function() {
+ var id = this.datarecord.id;
+ if (!id) {
+ if (this.options.not_interactible_on_create)
+ return false;
+ } else if (typeof(id) === "string") {
+ if(openerp.web.BufferedDataSet.virtual_id_regex.test(id))
+ return false;
+ }
+ return true;
}
});
openerp.web.FormDialog = openerp.web.Dialog.extend({
@@ -618,6 +655,7 @@ openerp.web.form.compute_domain = function(expr, fields) {
openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{
template: 'Widget',
+ identifier_prefix: 'formview-widget-',
/**
* @constructs openerp.web.form.Widget
* @extends openerp.web.Widget
@@ -631,11 +669,13 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.
this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
this.type = this.type || node.tag;
this.element_name = this.element_name || this.type;
- this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_");
+ this.element_class = [
+ 'formview', this.view.view_id, this.element_name,
+ this.view.widgets_counter++].join("_");
- this._super(view, this.element_id);
+ this._super(view);
- this.view.widgets[this.element_id] = this;
+ this.view.widgets[this.element_class] = this;
this.children = node.children;
this.colspan = parseInt(node.attrs.colspan || 1, 10);
this.decrease_max_width = 0;
@@ -648,12 +688,7 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.
this.width = this.node.attrs.width;
},
start: function() {
- this.$element = $('#' + this.element_id);
- },
- stop: function() {
- if (this.$element) {
- this.$element.remove();
- }
+ this.$element = this.view.$element.find('.' + this.element_class);
},
process_modifiers: function() {
var compute_domain = openerp.web.form.compute_domain;
@@ -757,13 +792,24 @@ openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
for (var i = 0; i < node.children.length; i++) {
var n = node.children[i];
if (n.tag == "page") {
- var page = new openerp.web.form.WidgetNotebookPage(this.view, n, this, this.pages.length);
+ var page = new openerp.web.form.WidgetNotebookPage(
+ this.view, n, this, this.pages.length);
this.pages.push(page);
}
}
},
start: function() {
+ var self = this;
this._super.apply(this, arguments);
+ this.$element.find('> ul > li').each(function (index, tab_li) {
+ var page = self.pages[index],
+ id = _.uniqueId(self.element_name + '-');
+ page.element_id = id;
+ $(tab_li).find('a').attr('href', '#' + id);
+ });
+ this.$element.find('> div').each(function (index, page) {
+ page.id = self.pages[index].element_id;
+ });
this.$element.tabs();
this.view.on_button_new.add_last(this.do_select_first_visible_tab);
},
@@ -785,11 +831,11 @@ openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
this.index = index;
this.element_name = 'page_' + index;
this._super(view, node);
- this.element_tab_id = this.element_id + '_tab';
},
start: function() {
this._super.apply(this, arguments);
- this.$element_tab = $('#' + this.element_tab_id);
+ this.$element_tab = this.notebook.$element.find(
+ '> ul > li:eq(' + this.index + ')');
},
update_dom: function() {
if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) {
@@ -801,9 +847,9 @@ openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
});
openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
+ template: 'WidgetSeparator',
init: function(view, node) {
this._super(view, node);
- this.template = "WidgetSeparator";
this.orientation = node.attrs.orientation || 'horizontal';
if (this.orientation === 'vertical') {
this.width = '1';
@@ -813,9 +859,10 @@ openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
});
openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
+ template: 'WidgetButton',
init: function(view, node) {
this._super(view, node);
- this.template = "WidgetButton";
+ this.force_disabled = false;
if (this.string) {
// We don't have button key bindings in the webclient
this.string = this.string.replace(/_/g, '');
@@ -827,45 +874,74 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
},
start: function() {
this._super.apply(this, arguments);
- this.$element.click(this.on_click);
+ this.$element.find("button").click(this.on_click);
},
- on_click: function(saved) {
+ on_click: function() {
var self = this;
- if ((!this.node.attrs.special && this.view.dirty_for_user && saved !== true) || !this.view.datarecord.id) {
- this.view.do_save(function() {
- self.on_click(true);
- });
- } else {
- if (this.node.attrs.confirm) {
- var dialog = $('<div>' + this.node.attrs.confirm + '</div>').dialog({
+ this.force_disabled = true;
+ this.check_disable();
+ this.execute_action().always(function() {
+ self.force_disabled = false;
+ self.check_disable();
+ });
+ },
+ execute_action: function() {
+ var self = this;
+ var exec_action = function() {
+ if (self.node.attrs.confirm) {
+ var def = $.Deferred();
+ var dialog = $('<div>' + self.node.attrs.confirm + '</div>').dialog({
title: 'Confirm',
modal: true,
buttons: {
Ok: function() {
- self.on_confirmed();
- $(this).dialog("close");
+ self.on_confirmed().then(function() {
+ def.resolve();
+ });
+ $(self).dialog("close");
},
Cancel: function() {
- $(this).dialog("close");
+ def.resolve();
+ $(self).dialog("close");
}
}
});
+ return def.promise();
} else {
- this.on_confirmed();
+ return self.on_confirmed();
}
+ };
+ if ((!this.node.attrs.special && this.view.dirty_for_user) || !this.view.datarecord.id) {
+ return this.view.recursive_save().pipe(exec_action);
+ } else {
+ return exec_action();
}
},
on_confirmed: function() {
var self = this;
- this.view.do_execute_action(
+ return this.view.do_execute_action(
this.node.attrs, this.view.dataset, this.view.datarecord.id, function () {
self.view.reload();
});
+ },
+ update_dom: function() {
+ this._super();
+ this.check_disable();
+ },
+ check_disable: function() {
+ if (this.force_disabled || !this.view.is_interactible_record()) {
+ this.$element.find("button").attr("disabled", "disabled");
+ this.$element.find("button").css("color", "grey");
+ } else {
+ this.$element.find("button").removeAttr("disabled");
+ this.$element.find("button").css("color", "");
+ }
}
});
openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
+ template: 'WidgetLabel',
init: function(view, node) {
this.element_name = 'label_' + node.attrs.name;
@@ -876,7 +952,6 @@ openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
this.template = "WidgetParagraph";
this.colspan = parseInt(this.node.attrs.colspan || 1, 10);
} else {
- this.template = "WidgetLabel";
this.colspan = 1;
this.width = '1%';
this.decrease_max_width = 1;
@@ -895,7 +970,7 @@ openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
var self = this;
this.$element.find("label").dblclick(function() {
var widget = self['for'] || self;
- console.log(widget.element_id , widget);
+ console.log(widget.element_class , widget);
window.w = widget;
});
}
@@ -960,7 +1035,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f
return !this.invalid;
},
is_dirty: function() {
- return this.dirty;
+ return this.dirty && !this.readonly;
},
get_on_change_value: function() {
return this.get_value();
@@ -995,37 +1070,40 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f
focus: function() {
},
_build_view_fields_values: function() {
- var a_dataset = this.view.dataset || {};
+ var a_dataset = this.view.dataset;
var fields_values = this.view.get_fields_values();
var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {};
fields_values.parent = parent_values;
return fields_values;
},
+ _build_eval_context: function() {
+ var a_dataset = this.view.dataset;
+ return new openerp.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
+ },
/**
* Builds a new context usable for operations related to fields by merging
* the fields'context with the action's context.
*/
build_context: function() {
- // I previously belevied contexts should be herrited, but now I doubt it
- //var a_context = this.view.dataset.get_context() || {};
var f_context = this.field.context || null;
// maybe the default_get should only be used when we do a default_get?
- var v_context1 = this.node.attrs.default_get || {};
- var v_context2 = this.node.attrs.context || {};
- var v_context = new openerp.web.CompoundContext(v_context1, v_context2);
- if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true
- var fields_values = this._build_view_fields_values();
+ var v_contexts = _.compact([this.node.attrs.default_get || null,
+ this.node.attrs.context || null]);
+ var v_context = new openerp.web.CompoundContext();
+ _.each(v_contexts, function(x) {v_context.add(x);});
+ if (_.detect(v_contexts, function(x) {return !!x.__ref;})) {
+ var fields_values = this._build_eval_context();
v_context.set_eval_context(fields_values);
}
// if there is a context on the node, overrides the model's context
- var ctx = f_context || v_context;
+ var ctx = v_contexts.length > 0 ? v_context : f_context;
return ctx;
},
build_domain: function() {
var f_domain = this.field.domain || null;
var v_domain = this.node.attrs.domain || [];
if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true
- var fields_values = this._build_view_fields_values();
+ var fields_values = this._build_eval_context();
v_domain = new openerp.web.CompoundDomain(v_domain).set_eval_context(fields_values);
}
// if there is a domain on the node, overrides the model's domain
@@ -1034,10 +1112,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f
});
openerp.web.form.FieldChar = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldChar";
- },
+ template: 'FieldChar',
start: function() {
this._super.apply(this, arguments);
this.$element.find('input').change(this.on_ui_change);
@@ -1046,6 +1121,7 @@ openerp.web.form.FieldChar = openerp.web.form.Field.extend({
this._super.apply(this, arguments);
var show_value = openerp.web.format_value(value, this, '');
this.$element.find('input').val(show_value);
+ return show_value;
},
update_dom: function() {
this._super.apply(this, arguments);
@@ -1070,10 +1146,7 @@ openerp.web.form.FieldChar = openerp.web.form.Field.extend({
});
openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldEmail";
- },
+ template: 'FieldEmail',
start: function() {
this._super.apply(this, arguments);
this.$element.find('button').click(this.on_button_clicked);
@@ -1084,18 +1157,11 @@ openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
} else {
location.href = 'mailto:' + this.value;
}
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val());
}
});
openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldUrl";
- },
+ template: 'FieldUrl',
start: function() {
this._super.apply(this, arguments);
this.$element.find('button').click(this.on_button_clicked);
@@ -1120,16 +1186,13 @@ openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
}
});
-openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldDate";
- this.jqueryui_object = 'datetimepicker';
- },
+openerp.web.DateTimeWidget = openerp.web.Widget.extend({
+ template: "web.datetimepicker",
+ jqueryui_object: 'datetimepicker',
+ type_of_date: "datetime",
start: function() {
var self = this;
- this._super.apply(this, arguments);
- this.$element.find('input').change(this.on_ui_change);
+ this.$element.find('input').change(this.on_change);
this.picker({
onSelect: this.on_picker_select,
changeMonth: true,
@@ -1147,6 +1210,8 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
this.$element.find('button.oe_datepicker_close').click(function() {
self.$element.find('.oe_datepicker').hide();
});
+ this.set_readonly(false);
+ this.value = false;
},
picker: function() {
return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments);
@@ -1156,69 +1221,99 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
this.$element.find('input').val(date ? this.format_client(date) : '').change();
},
set_value: function(value) {
- value = this.parse(value);
- this._super(value);
+ this.value = value;
this.$element.find('input').val(value ? this.format_client(value) : '');
},
get_value: function() {
- return this.format(this.value);
+ return this.value;
},
set_value_from_ui: function() {
var value = this.$element.find('input').val() || false;
this.value = this.parse_client(value);
- this._super();
},
- update_dom: function() {
- this._super.apply(this, arguments);
+ set_readonly: function(readonly) {
+ this.readonly = readonly;
this.$element.find('input').attr('disabled', this.readonly);
- this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', this.readonly);
+ this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
},
- validate: function() {
- this.invalid = false;
+ is_valid: function(required) {
var value = this.$element.find('input').val();
if (value === "") {
- this.invalid = this.required;
+ return !required;
} else {
try {
this.parse_client(value);
- this.invalid = false;
+ return true;
} catch(e) {
- this.invalid = true;
+ return false;
}
}
},
focus: function() {
this.$element.find('input').focus();
},
- parse: openerp.web.auto_str_to_date,
parse_client: function(v) {
- return openerp.web.parse_value(v, this.field);
- },
- format: function(val) {
- return openerp.web.auto_date_to_str(val, this.field.type);
+ return openerp.web.parse_value(v, {"widget": this.type_of_date});
},
format_client: function(v) {
- return openerp.web.format_value(v, this.field);
+ return openerp.web.format_value(v, {"widget": this.type_of_date});
+ },
+ on_change: function() {
+ if (this.is_valid()) {
+ this.set_value_from_ui();
+ }
}
});
-openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
- init: function(view, node) {
- this._super(view, node);
- this.jqueryui_object = 'datepicker';
- },
+openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({
+ jqueryui_object: 'datepicker',
+ type_of_date: "date",
on_picker_select: function(text, instance) {
this._super(text, instance);
this.$element.find('.oe_datepicker').hide();
}
});
-openerp.web.form.FieldText = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldText";
+openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
+ template: "EmptyComponent",
+ build_widget: function() {
+ return new openerp.web.DateTimeWidget(this);
},
start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.datewidget = this.build_widget();
+ this.datewidget.on_change.add(this.on_ui_change);
+ this.datewidget.appendTo(this.$element);
+ },
+ set_value: function(value) {
+ this._super(value);
+ this.datewidget.set_value(value);
+ },
+ get_value: function() {
+ return this.datewidget.get_value();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.datewidget.set_readonly(this.readonly);
+ },
+ validate: function() {
+ this.invalid = !this.datewidget.is_valid(this.required);
+ },
+ focus: function() {
+ this.datewidget.focus();
+ }
+});
+
+openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
+ build_widget: function() {
+ return new openerp.web.DateWidget(this);
+ }
+});
+
+openerp.web.form.FieldText = openerp.web.form.Field.extend({
+ template: 'FieldText',
+ start: function() {
this._super.apply(this, arguments);
this.$element.find('textarea').change(this.on_ui_change);
},
@@ -1250,10 +1345,7 @@ openerp.web.form.FieldText = openerp.web.form.Field.extend({
});
openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldBoolean";
- },
+ template: 'FieldBoolean',
start: function() {
var self = this;
this._super.apply(this, arguments);
@@ -1284,10 +1376,7 @@ openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
});
openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldProgressBar";
- },
+ template: 'FieldProgressBar',
start: function() {
this._super.apply(this, arguments);
this.$element.find('div').progressbar({
@@ -1310,10 +1399,10 @@ openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({
});
openerp.web.form.FieldSelection = openerp.web.form.Field.extend({
+ template: 'FieldSelection',
init: function(view, node) {
var self = this;
this._super(view, node);
- this.template = "FieldSelection";
this.values = this.field.selection;
_.each(this.values, function(v, i) {
if (v[0] === false && v[1] === '') {
@@ -1420,9 +1509,9 @@ openerp.web.form.dialog = function(content, options) {
};
openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
+ template: 'FieldMany2One',
init: function(view, node) {
this._super(view, node);
- this.template = "FieldMany2One";
this.limit = 7;
this.value = null;
this.cm_id = _.uniqueId('m2o_cm_');
@@ -1435,7 +1524,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
this.$input = this.$element.find("input");
this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
-
+
// context menu
var init_context_menu_def = $.Deferred().then(function(e) {
var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
@@ -1443,7 +1532,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
[[self.field.relation, false]], false, rdataset.get_context()], false, 0)
.then(function(result) {
self.related_entries = result;
-
+
var $cmenu = $("#" + self.cm_id);
$cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
var bindings = {};
@@ -1747,10 +1836,10 @@ var commands = {
}
};
openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
+ template: 'FieldOne2Many',
multi_selection: false,
init: function(view, node) {
this._super(view, node);
- this.template = "FieldOne2Many";
this.is_started = $.Deferred();
this.form_last_update = $.Deferred();
this.disable_utility_classes = true;
@@ -1781,6 +1870,8 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
}
if(view.view_type === "list") {
view.options.selectable = self.multi_selection;
+ } else if (view.view_type === "form") {
+ view.options.not_interactible_on_create = true;
}
views.push(view);
});
@@ -1840,6 +1931,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
switch (command[0]) {
case commands.CREATE:
obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
+ obj.defaults = {};
self.dataset.to_create.push(obj);
self.dataset.cache.push(_.clone(obj));
ids.push(obj.id);
@@ -1869,6 +1961,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
_.each(value, function(command) {
var obj = {values: command};
obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
+ obj.defaults = {};
self.dataset.to_create.push(obj);
self.dataset.cache.push(_.clone(obj));
ids.push(obj.id);
@@ -1913,11 +2006,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
var view = this.viewmanager.views[this.viewmanager.active_view].controller;
if (this.viewmanager.active_view === "form") {
var res = $.when(view.do_save());
- if (res === false) {
- // ignore
- } else if (res.isRejected()) {
- throw "Save or create on one2many dataset is not supposed to fail.";
- } else if (!res.isResolved()) {
+ if (!res.isResolved() && !res.isRejected()) {
throw "Asynchronous get_value() is not supported in form view.";
}
return res;
@@ -1931,9 +2020,8 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
},
validate: function() {
this.invalid = false;
- var self = this;
- var view = self.viewmanager.views[self.viewmanager.active_view].controller;
- if (self.viewmanager.active_view === "form") {
+ var view = this.viewmanager.views[this.viewmanager.active_view].controller;
+ if (this.viewmanager.active_view === "form") {
for (var f in view.fields) {
f = view.fields[f];
if (!f.is_valid()) {
@@ -1970,6 +2058,7 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
} else {
var self = this;
var pop = new openerp.web.form.SelectCreatePopup(this);
+ pop.on_default_get.add(self.dataset.on_default_get);
pop.select_element(self.o2m.field.relation,{
initial_view: "form",
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
@@ -1979,7 +2068,8 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
self.o2m.dataset.on_change();
});
},
- parent_view: self.o2m.view
+ parent_view: self.o2m.view,
+ form_view_options: {'not_interactible_on_create':true}
}, self.o2m.build_domain(), self.o2m.build_context());
pop.on_select_elements.add_last(function() {
self.o2m.reload_current_view();
@@ -1995,7 +2085,8 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
parent_view: self.o2m.view,
read_function: function() {
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
- }
+ },
+ form_view_options: {'not_interactible_on_create':true}
});
pop.on_write.add(function(id, data) {
self.o2m.dataset.write(id, data, {}, function(r) {
@@ -2006,10 +2097,10 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
});
openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
+ template: 'FieldMany2Many',
multi_selection: false,
init: function(view, node) {
this._super(view, node);
- this.template = "FieldMany2Many";
this.list_id = _.uniqueId("many2many");
this.is_started = $.Deferred();
},
@@ -2110,6 +2201,8 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
* - alternative_form_view
* - create_function (defaults to a naive saving behavior)
* - parent_view
+ * - form_view_options
+ * - list_view_options
*/
select_element: function(model, options, domain, context) {
var self = this;
@@ -2131,6 +2224,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
this.dataset = new openerp.web.ReadOnlyDataSetSearch(this, this.model,
this.context);
this.dataset.parent_view = this.options.parent_view;
+ this.dataset.on_default_get.add(this.on_default_get);
if (this.options.initial_view == "search") {
this.setup_search_view();
} else { // "form"
@@ -2173,7 +2267,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
});
self.view_list = new openerp.web.form.SelectCreateListView(self,
self.dataset, false,
- {'deletable': false});
+ _.extend({'deletable': false}, self.options.list_view_options || {}));
self.view_list.popup = self;
self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() {
self.view_list.do_show();
@@ -2209,7 +2303,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
this.view_list.$element.hide();
}
this.dataset.index = null;
- this.view_form = new openerp.web.FormView(this, this.dataset, false);
+ this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
if (this.options.alternative_form_view) {
this.view_form.set_embedded_view(this.options.alternative_form_view);
}
@@ -2253,7 +2347,8 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
this.on_select_elements(this.created_elements);
}
this.stop();
- }
+ },
+ on_default_get: function(res) {}
});
openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({
@@ -2283,6 +2378,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp
* - auto_write (default true)
* - read_function
* - parent_view
+ * - form_view_options
*/
show_element: function(model, row_id, context, options) {
this.model = model;
@@ -2318,7 +2414,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp
on_write_completed: function() {},
setup_form_view: function() {
var self = this;
- this.view_form = new openerp.web.FormView(this, this.dataset, false);
+ this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
if (this.options.alternative_form_view) {
this.view_form.set_embedded_view(this.options.alternative_form_view);
}
@@ -2351,9 +2447,9 @@ openerp.web.form.FormOpenDataset = openerp.web.ReadOnlyDataSetSearch.extend({
});
openerp.web.form.FieldReference = openerp.web.form.Field.extend({
+ template: 'FieldReference',
init: function(view, node) {
this._super(view, node);
- this.template = "FieldReference";
this.fields_view = {
fields: {
selection: {
@@ -2488,10 +2584,7 @@ openerp.web.form.FieldBinary = openerp.web.form.Field.extend({
});
openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldBinaryFile";
- },
+ template: 'FieldBinaryFile',
set_value: function(value) {
this._super.apply(this, arguments);
var show_value = (value != null && value !== false) ? value : '';
@@ -2519,10 +2612,7 @@ openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
});
openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldBinaryImage";
- },
+ template: 'FieldBinaryImage',
start: function() {
this._super.apply(this, arguments);
this.$image = this.$element.find('img.oe-binary-image');
@@ -2597,6 +2687,93 @@ openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
}
});
+openerp.web.form.WidgetNotebookReadonly = openerp.web.form.WidgetNotebook.extend({
+ template: 'WidgetNotebook.readonly'
+});
+openerp.web.form.FieldReadonly = openerp.web.form.Field.extend({
+
+});
+openerp.web.form.FieldCharReadonly = openerp.web.form.FieldReadonly.extend({
+ template: 'FieldChar.readonly',
+ set_value: function (value) {
+ this._super.apply(this, arguments);
+ var show_value = openerp.web.format_value(value, this, '');
+ this.$element.find('div').text(show_value);
+ return show_value;
+ }
+});
+openerp.web.form.FieldURIReadonly = openerp.web.form.FieldCharReadonly.extend({
+ template: 'FieldURI.readonly',
+ scheme: null,
+ set_value: function (value) {
+ var displayed = this._super.apply(this, arguments);
+ this.$element.find('a')
+ .attr('href', this.scheme + ':' + displayed)
+ .text(displayed);
+ }
+});
+openerp.web.form.FieldEmailReadonly = openerp.web.form.FieldURIReadonly.extend({
+ scheme: 'mailto'
+});
+openerp.web.form.FieldUrlReadonly = openerp.web.form.FieldURIReadonly.extend({
+ set_value: function (value) {
+ var s = /(\w+):(\.+)/.match(value);
+ if (!(s[0] === 'http' || s[0] === 'https')) { return; }
+ this.scheme = s[0];
+ this._super(s[1]);
+ }
+});
+openerp.web.form.FieldBooleanReadonly = openerp.web.form.FieldCharReadonly.extend({
+ set_value: function (value) {
+ this._super(value ? '\u2714' : '\u2718');
+ }
+});
+openerp.web.form.FieldSelectionReadonly = openerp.web.form.FieldReadonly.extend({
+ template: 'FieldChar.readonly',
+ init: function(view, node) {
+ // lifted straight from r/w version
+ var self = this;
+ this._super(view, node);
+ this.values = this.field.selection;
+ _.each(this.values, function(v, i) {
+ if (v[0] === false && v[1] === '') {
+ self.values.splice(i, 1);
+ }
+ });
+ this.values.unshift([false, '']);
+ },
+ set_value: function (value) {
+ value = value === null ? false : value;
+ value = value instanceof Array ? value[0] : value;
+ var option = _(this.values)
+ .detect(function (record) { return record[0] === value; });
+ this._super(value);
+ this.$element.find('div').text(option ? option[1] : this.values[0][1]);
+ }
+});
+openerp.web.form.FieldMany2OneReadonly = openerp.web.form.FieldCharReadonly.extend({
+ set_value: function (value) {
+ value = value || null;
+ this.invalid = false;
+ var self = this;
+ this.tmp_value = value;
+ self.update_dom();
+ self.on_value_changed();
+ var real_set_value = function(rval) {
+ self.$element.find('div').text(rval ? rval[1] : '');
+ };
+ if(typeof(value) === "number") {
+ var dataset = new openerp.web.DataSetStatic(
+ this, this.field.relation, self.build_context());
+ dataset.name_get([value], function(data) {
+ real_set_value(data[0]);
+ }).fail(function() {self.tmp_value = undefined;});
+ } else {
+ setTimeout(function() {real_set_value(value);}, 0);
+ }
+ }
+});
+
/**
* Registry of form widgets, called by :js:`openerp.web.FormView`
*/
@@ -2629,6 +2806,22 @@ openerp.web.form.widgets = new openerp.web.Registry({
'binary': 'openerp.web.form.FieldBinaryFile',
'statusbar': 'openerp.web.form.FieldStatus'
});
+openerp.web.form.readonly = openerp.web.form.widgets.clone({
+ 'notebook': 'openerp.web.form.WidgetNotebookReadonly',
+ 'char': 'openerp.web.form.FieldCharReadonly',
+ 'email': 'openerp.web.form.FieldEmailReadonly',
+ 'url': 'openerp.web.form.FieldUrlReadonly',
+ 'text': 'openerp.web.form.FieldCharReadonly',
+ 'text_wiki' : 'openerp.web.form.FieldCharReadonly',
+ 'date': 'openerp.web.form.FieldCharReadonly',
+ 'datetime': 'openerp.web.form.FieldCharReadonly',
+ 'selection' : 'openerp.web.form.FieldSelectionReadonly',
+ 'many2one': 'openerp.web.form.FieldMany2OneReadonly',
+ 'boolean': 'openerp.web.form.FieldBooleanReadonly',
+ 'float': 'openerp.web.form.FieldCharReadonly',
+ 'integer': 'openerp.web.form.FieldCharReadonly',
+ 'float_time': 'openerp.web.form.FieldCharReadonly'
+});
};
diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js
index f8a89cd34a1..8a9deaab228 100644
--- a/addons/web/static/src/js/view_list.js
+++ b/addons/web/static/src/js/view_list.js
@@ -49,6 +49,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
this.model = dataset.model;
this.view_id = view_id;
this.previous_colspan = null;
+ this.colors = null;
this.columns = [];
@@ -75,6 +76,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
}
self.compute_aggregates();
});
+
},
/**
* Retrieves the view's number of records per page (|| section)
@@ -132,6 +134,31 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
return this.reload_view(null, null, true);
},
/**
+ * Returns the color for the provided record in the current view (from the
+ * ``@colors`` attribute)
+ *
+ * @param {Record} record record for the current row
+ * @returns {String} CSS color declaration
+ */
+ color_for: function (record) {
+ if (!this.colors) { return ''; }
+ var context = _.extend({}, record.attributes, {
+ uid: this.session.uid,
+ current_date: new Date().toString('yyyy-MM-dd')
+ // TODO: time, datetime, relativedelta
+ });
+ for(var i=0, len=this.colors.length; i<len; ++i) {
+ var pair = this.colors[i],
+ color = pair[0],
+ expression = pair[1];
+ if (py.evaluate(expression, context)) {
+ return 'color: ' + color + ';';
+ }
+ // TODO: handle evaluation errors
+ }
+ return '';
+ },
+ /**
* Called after loading the list view's description, sets up such things
* as the view table's columns, renders the table itself and hooks up the
* various table-level and row-level DOM events (action buttons, deletion
@@ -159,6 +186,17 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
this.fields_view = data;
this.name = "" + this.fields_view.arch.attrs.string;
+ if (this.fields_view.arch.attrs.colors) {
+ this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain()
+ .compact()
+ .map(function(color_pair) {
+ var pair = color_pair.split(':'),
+ color = pair[0],
+ expr = pair[1];
+ return [color, py.parse(py.tokenize(expr)), expr];
+ }).value();
+ }
+
this.setup_columns(this.fields_view.fields, grouped);
this.$element.html(QWeb.render("ListView", this));
@@ -415,8 +453,8 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
*/
do_search: function (domains, contexts, groupbys) {
return this.rpc('/web/session/eval_domain_and_context', {
- domains: [this.dataset.get_domain()].concat(domains),
- contexts: [this.dataset.get_context()].concat(contexts),
+ domains: _([this.dataset.get_domain()].concat(domains)).compact(),
+ contexts: _([this.dataset.get_context()].concat(contexts)).compact(),
group_by_seq: groupbys
}, $.proxy(this, 'do_actual_search'));
},
@@ -541,6 +579,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
if (_.isEmpty(records)) {
records = this.groups.get_records();
}
+ records = _(records).compact();
var count = 0, sums = {};
_(columns).each(function (column) {
@@ -1235,6 +1274,9 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L
},
get_records: function () {
if (_(this.children).isEmpty()) {
+ if (!this.datagroup.length) {
+ return;
+ }
return {
count: this.datagroup.length,
values: this.datagroup.aggregates
@@ -1351,10 +1393,10 @@ var Record = openerp.web.Class.extend(/** @lends Record# */{
* @returns {Object} record displayable in a form view
*/
toForm: function () {
- var form_data = {};
- _(this.attributes).each(function (value, key) {
- form_data[key] = {value: value};
- });
+ var form_data = {}, attrs = this.attributes;
+ for(var k in attrs) {
+ form_data[k] = {value: attrs[k]};
+ }
return {data: form_data};
}
diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js
index b5a5a3d8c10..beb2c3ac0a9 100644
--- a/addons/web/static/src/js/view_list_editable.js
+++ b/addons/web/static/src/js/view_list_editable.js
@@ -182,9 +182,11 @@ openerp.web.list_editable = function (openerp) {
self.edition_id = record_id;
self.edition_form = _.extend(new openerp.web.ListEditableFormView(self, self.dataset, false), {
form_template: 'ListView.row.form',
- registry: openerp.web.list.form.widgets
+ registry: openerp.web.list.form.widgets,
+ $element: $new_row
});
- self.edition_form.appendTo($("#" + $new_row.attr('id')));
+ // HA HA
+ self.edition_form.appendTo();
$.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
// put in $.when just in case FormView.on_loaded becomes asynchronous
$new_row.find('td')
@@ -335,6 +337,9 @@ openerp.web.list_editable = function (openerp) {
});
openerp.web.ListEditableFormView = openerp.web.FormView.extend({
- init_view: function() {}
+ init_view: function() {},
+ _render_and_insert: function () {
+ return this.start();
+ }
});
};
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index fa27e59c0af..be47cfc3364 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -86,7 +86,7 @@ db.web.ActionManager = db.web.Widget.extend({
console.log("Action manager can't handle action of type " + action.type, action);
return;
}
- this[type](action, on_close);
+ return this[type](action, on_close);
},
ir_actions_act_window: function (action, on_close) {
if (action.target === 'new') {
@@ -163,7 +163,7 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{
*/
init: function(parent, dataset, views) {
this._super(parent);
- this.model = dataset.model;
+ this.model = dataset ? dataset.model : undefined;
this.dataset = dataset;
this.searchview = null;
this.active_view = null;
@@ -184,7 +184,6 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{
start: function() {
this._super();
var self = this;
- this.dataset.start();
this.$element.find('.oe_vm_switch button').click(function() {
self.on_mode_switch($(this).data('view-type'));
});
@@ -332,6 +331,7 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View
// dataset initialization will take the session from ``this``, so if we
// do not have it yet (and we don't, because we've not called our own
// ``_super()``) rpc requests will blow up.
+ this._super(parent, null, action.views);
this.session = parent.session;
this.action = action;
var dataset = new db.web.DataSetSearch(this, action.res_model, action.context, action.domain);
@@ -339,7 +339,7 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View
dataset.ids.push(action.res_id);
dataset.index = 0;
}
- this._super(parent, dataset, action.views);
+ this.dataset = dataset;
this.flags = this.action.flags || {};
if (action.res_model == 'board.board' && action.views.length == 1 && action.views) {
// Not elegant but allows to avoid form chrome (pager, save/new
@@ -470,7 +470,7 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View
* Intercept do_action resolution from children views
*/
on_action_executed: function () {
- new db.web.DataSet(this, 'res.log')
+ return new db.web.DataSet(this, 'res.log')
.call('get', [], this.do_display_log);
},
/**
@@ -770,28 +770,31 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{
var self = this;
var result_handler = function () {
if (on_closed) { on_closed.apply(null, arguments); }
- self.widget_parent.on_action_executed.apply(null, arguments);
+ return self.widget_parent.on_action_executed.apply(null, arguments);
};
var handler = function (r) {
var action = r.result;
if (action && action.constructor == Object) {
- action.context = action.context || {};
- _.extend(action.context, {
- active_id: record_id || false,
- active_ids: [record_id || false],
- active_model: dataset.model
+ return self.rpc('/web/session/eval_domain_and_context', {
+ contexts: [dataset.get_context(), action.context || {}, {
+ active_id: record_id || false,
+ active_ids: [record_id || false],
+ active_model: dataset.model
+ }],
+ domains: []
+ }).pipe(function (results) {
+ action.context = results.context
+ return self.do_action(action, result_handler);
});
- action.context = new db.web.CompoundContext(dataset.get_context(), action.context);
- self.do_action(action, result_handler);
} else {
- result_handler();
+ return result_handler();
}
};
var context = new db.web.CompoundContext(dataset.get_context(), action_data.context || {});
if (action_data.special) {
- handler({result: {"type":"ir.actions.act_window_close"}});
+ return handler({result: {"type":"ir.actions.act_window_close"}});
} else if (action_data.type=="object") {
return dataset.call_button(action_data.name, [[record_id], context], handler);
} else if (action_data.type=="action") {
@@ -863,6 +866,8 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{
console.log('Todo');
},
on_sidebar_import: function() {
+ var import_view = new db.web.DataImport(this, this.dataset);
+ import_view.start();
},
on_sidebar_export: function() {
var export_view = new db.web.DataExport(this, this.dataset);
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 4d4efbed2a3..3f06270e0e0 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -38,11 +38,17 @@
</td>
</tr>
<tr>
- <td valign="top" id="oe_secondary_menu" class="secondary_menu">
- </td>
- <td valign="top" class="oe-application-container">
- <div id="oe_app" class="oe-application">
- </div>
+ <td colspan="2" valign="top" height="100%">
+ <table cellspacing="0" cellpadding="0" border="0" height="100%" width="100%">
+ <tr>
+ <td valign="top" id="oe_secondary_menu" class="secondary_menu">
+ </td>
+ <td valign="top" class="oe-application-container">
+ <div id="oe_app" class="oe-application">
+ </div>
+ </td>
+ </tr>
+ </table>
</td>
</tr>
<tr>
@@ -236,7 +242,23 @@
</table>
</form>
</t>
+
+<t t-name="Login_dblist">
+ <select name="db">
+ <t t-foreach="db_list" t-as="db">
+ <t t-if="selected_db === db">
+ <option t-att-value="db" selected="true">
+ <t t-esc="db"/></option>
+ </t>
+ <t t-if="selected_db !== db">
+ <option t-att-value="db"><t t-esc="db"/></option>
+ </t>
+ </t>
+ </select>
+</t>
+
<t t-name="Login">
+ <div>
<form class="oe_forms">
<fieldset>
<legend style="">
@@ -247,33 +269,18 @@
<tr>
<td><label for="db">Database:</label></td>
<td>
- <t t-if="!db_list">
- <input type="text" name="db" t-att-value="selected_db || ''" autofocus="true"/>
- </t>
- <t t-if="db_list">
- <select name="db">
- <t t-foreach="db_list" t-as="db">
- <t t-if="selected_db === db">
- <option t-att-value="db" selected="true">
- <t t-esc="db"/></option>
- </t>
- <t t-if="selected_db !== db">
- <option t-att-value="db"><t t-esc="db"/></option>
- </t>
- </t>
- </select>
- </t>
+ <input type="text" name="db" t-att-value="widget.selected_db || ''" autofocus="true"/>
</td>
</tr>
<tr>
<td><label for="login">User:</label></td>
<td><input type="text" name="login"
- t-att-value="selected_login || ''" autofocus="true"/></td>
+ t-att-value="widget.selected_login || ''" autofocus="true"/></td>
</tr>
<tr>
<td><label for="password">Password:</label></td>
<td><input type="password" name="password"
- t-att-value="selected_password || ''"/></td>
+ t-att-value="widget.selected_password || ''"/></td>
</tr>
<tr>
<td></td>
@@ -324,6 +331,7 @@
</table>
</div>
+ </div>
</t>
<t t-name="Header">
<div>
@@ -342,22 +350,12 @@
<li>
<a t-att-href="'/' + widget.qs" title="Home" class="home"><img src="/web/static/src/img/header-home.png" width="16" height="16" border="0"/></a>
</li>
- <!--
- <li>
- <a href="#requests" title="Requests" class="requests"><img src="/web/static/src/img/header-requests.png" width="16" height="16" border="0"/><small>1</small></a>
- </li>
- -->
<li class="preferences">
- <a href="#preferences" title="Preferences" class="preferences"><img src="/web/static/src/img/header-preferences.png" width="16" height="16" border="0"/></a>
- </li>
- <li>
- <a href="#about" title="About" class="about"><img src="/web/static/src/img/header-about.png" width="16" height="16" border="0"/></a>
+ <a href="javascript:void(0)" title="Preferences" class="preferences"><img src="/web/static/src/img/header-preferences.png" width="16" height="16" border="0"/></a>
</li>
- <!--
<li>
- <a href="http://doc.openerp.com/v6.0/book?version=$version" title="Help" target="_blank" class="help"><img src="/web/static/src/img/header-help.png" width="16" height="16" border="0"/></a>
+ <a href="javascript:void(0)" title="About" class="about"><img src="/web/static/src/img/header-about.png" width="16" height="16" border="0"/></a>
</li>
- -->
</ul>
<div class="block">
<a href="#logout" class="logout">LOGOUT</a>
@@ -374,45 +372,39 @@
</ul>
<t t-name="Menu">
<table align="center">
- <tr>
- <t t-foreach="data.children" t-as="menu">
- <td>
- <a href="#" t-att-data-menu="menu.id">
- <t t-esc="menu.name"/>
- </a>
- </td>
- </t>
- </tr>
+ <tr>
+ <td t-foreach="widget.data.data.children" t-as="menu">
+ <a href="#" t-att-data-menu="menu.id">
+ <t t-esc="menu.name"/>
+ </a>
+ </td>
+ </tr>
</table>
</t>
<t t-name="Menu.secondary">
- <div style="display: none" class="menu_accordion" t-att-data-menu-parent="menu.id">
+ <div t-attf-class="oe_toggle_secondary_menu">
+ <span class="oe_menu_fold" title="Fold menu">&amp;laquo;</span>
+ <span class="oe_menu_unfold" title="Unfold menu">&amp;raquo;</span>
+ </div>
+ <div t-foreach="widget.data.data.children" t-as="menu" style="display: none" class="oe_secondary_menu" t-att-data-menu-parent="menu.id">
<t t-foreach="menu.children" t-as="menu">
- <t t-set="header">h3</t>
+ <t t-set="classname">oe_secondary_menu_item</t>
+ <t t-set="level" t-value="0"/>
<t t-call="Menu.secondary.children"/>
</t>
</div>
</t>
<t t-name="Menu.secondary.children">
- &lt;<t t-esc="header"/>&gt;
- <a href="#" t-attf-id="menu_#{menu.id}">
- <span><t t-esc="menu.name"/></span>
- </a>
- &lt;/<t t-esc="header"/>&gt;
- <div class="menu_content">
+ <t t-set="level" t-value="level + 1"/>
+ <a href="#" t-att-id="menu.children.length ? 'menu_' + menu.id : undefined"
+ t-att-class="classname + (menu.children.length ? ' submenu' : ' leaf') + (menu_first and level == 1 ? ' opened' : '')"
+ t-att-data-menu="menu.children.length ? undefined : menu.id">
+ <span t-attf-style="padding-left: #{(level - 2) * 20}px"> <t t-esc="menu.name"/></span>
+ </a>
+ <div t-attf-class="oe_secondary_submenu" t-if="menu.children.length" t-att-style="menu_first and level == 1 ? undefined : 'display: none'">
<t t-foreach="menu.children" t-as="menu">
- <t t-if="!menu.children.length">
- <a href="#" class="leaf" t-att-data-menu="menu.id">
- <span><t t-esc="menu.name"/></span>
- </a>
- </t>
- <t t-if="menu.children.length">
- <div class="submenu_accordion">
- <t t-call="Menu.secondary.children">
- <t t-set="header">h4</t>
- </t>
- </div>
- </t>
+ <t t-set="classname">oe_secondary_submenu_item</t>
+ <t t-call="Menu.secondary.children"/>
</t>
</div>
</t>
@@ -624,7 +616,8 @@
</t>
</t>
<tr t-name="ListView.row" t-att-class="row_parity"
- t-att-data-id="record.get('id')">
+ t-att-data-id="record.get('id')"
+ t-att-style="view.color_for(record)">
<t t-foreach="columns" t-as="column">
<td t-if="column.meta">
@@ -658,7 +651,7 @@
<t t-raw="frame.render()"/>
</t>
<t t-name="FormView">
- <div class="oe_form_header" t-att-id="view.element_id + '_header'">
+ <div class="oe_form_header">
<div class="oe_form_buttons" t-if="view.options.action_buttons !== false">
<!--<button type="button" class="oe_form_button_save">
<span class="oe_form_on_update">Save</span>
@@ -671,6 +664,7 @@
<!--<button type="button" class="oe_form_button_cancel">Cancel</button>-->
<button type="button" class="oe_form_button_new">New</button>
<button type="button" class="oe_form_button_duplicate oe_form_on_update">Duplicate</button>
+ <button type="button" class="oe_form_button_toggle">Readonly/Editable</button>
</div>
<div class="oe_form_pager" t-if="view.options.pager !== false">
<button type="button" data-pager-action="first">First</button>
@@ -730,8 +724,7 @@
t-att-width="td.width"
t-att-nowrap="td.nowrap or td.is_field_m2o? 'true' : undefined"
t-att-valign="td.table ? 'top' : undefined"
- t-att-id="td.element_id"
- t-attf-class="oe_form_frame_cell #{td.classname}"
+ t-attf-class="oe_form_frame_cell #{td.classname} #{td.element_class}"
>
<t t-raw="td.render()"/>
</td>
@@ -741,8 +734,8 @@
</t>
<t t-name="WidgetNotebook">
<ul>
- <li t-foreach="widget.pages" t-as="page" t-att-id="page.element_tab_id">
- <a t-att-href="'#' + page.element_id">
+ <li t-foreach="widget.pages" t-as="page">
+ <a href="#">
<t t-esc="page.string"/>
</a>
</li>
@@ -752,17 +745,23 @@
</t>
</t>
<t t-name="WidgetNotebookPage">
- <div t-att-id="widget.element_id">
+ <div>
<t t-call="WidgetFrame"/>
</div>
</t>
+<t t-name="WidgetNotebook.readonly">
+ <t t-foreach="widget.pages" t-as="page">
+ <h3><t t-esc="page.string"/></h3>
+ <t t-raw="page.render()"/>
+ </t>
+</t>
<t t-name="WidgetSeparator">
<div t-if="widget.orientation !== 'vertical'" t-att-class="'separator ' + widget.orientation">
<t t-esc="widget.string"/>
</div>
</t>
<t t-name="WidgetLabel">
- <label t-att-for="widget.element_id + '_field'"
+ <label t-att-for="widget.element_id"
t-att-class="'oe_label' + (widget.help ? '_help' : '')"
t-att-title="widget.help">
<t t-esc="widget.string"/>
@@ -776,12 +775,22 @@
<t t-name="FieldChar">
<input type="text" size="1"
t-att-name="widget.name"
- t-att-id="widget.element_id + '_field'"
- t-att-class="'field_' + widget.type"
+ t-att-id="widget.element_id"
+ t-attf-class="field_#{widget.type} #{widget.element_class}"
t-attf-style="width: #{widget.field.translate ? '99' : '100'}%"
/>
<img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>
</t>
+<t t-name="FieldChar.readonly">
+ <div
+ t-att-id="widget.element_id"
+ t-attf-class="field_#{widget.type} #{widget.element_class}"
+ t-attf-style="width: #{widget.field.translate ? '99' : '100'}%">
+ </div>
+</t>
+<t t-name="FieldURI.readonly">
+ <a href="#">#</a>
+</t>
<t t-name="FieldEmail">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
@@ -813,26 +822,28 @@
<t t-name="FieldText">
<textarea rows="6"
t-att-name="widget.name"
- t-att-id="widget.element_id + '_field'"
- t-att-class="'field_' + widget.type"
+ t-att-id="widget.element_id"
+ t-attf-class="field_#{widget.type} #{widget.element_class}"
t-attf-style="width: #{widget.field.translate ? '99' : '100'}%"
></textarea>
<img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>
</t>
-<t t-name="FieldDate">
- <t t-call="FieldChar"/>
- <img class="oe_input_icon oe_datepicker_trigger" src="/web/static/src/img/ui/field_calendar.png"
- title="Select date" width="16" height="16" border="0"/>
- <div class="oe_datepicker ui-widget-content ui-corner-all" style="display: none; position: absolute; z-index: 1;">
- <div class="oe_datepicker_container"/>
- <button type="button" class="oe_datepicker_close ui-state-default ui-priority-primary ui-corner-all" style="float: right;">Done</button>
+<t t-name="web.datetimepicker">
+ <div class="oe_datepicker_root">
+ <input type="text" size="1" style="width: 100%"/>
+ <img class="oe_input_icon oe_datepicker_trigger" src="/web/static/src/img/ui/field_calendar.png"
+ title="Select date" width="16" height="16" border="0"/>
+ <div class="oe_datepicker ui-widget-content ui-corner-all" style="display: none; position: absolute; z-index: 1;">
+ <div class="oe_datepicker_container"/>
+ <button type="button" class="oe_datepicker_close ui-state-default ui-priority-primary ui-corner-all" style="float: right;">Done</button>
+ </div>
</div>
</t>
<t t-name="FieldSelection">
<select
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
- t-att-class="'field_' + widget.type"
+ t-attf-class="field_#{widget.type} #{widget.element_class}"
style="width: 100%">
<t t-foreach="widget.values" t-as="option">
<option><t t-esc="option[1]"/></option>
@@ -840,9 +851,9 @@
</select>
</t>
<t t-name="FieldMany2One">
- <div t-att-id="widget.element_id" class="oe-m2o">
- <input t-att-id="widget.element_id + '_input'" type="text" size="1" style="width: 100%;"/>
- <span class="oe-m2o-drop-down-button" t-att-id="widget.element_id + '_drop_down'">
+ <div t-att-class="widget.element_class" class="oe-m2o">
+ <input type="text" size="1" style="width: 100%;"/>
+ <span class="oe-m2o-drop-down-button">
<img src="/web/static/src/img/down-arrow.png" /></span>
<span class="oe-m2o-cm-button" t-att-id="widget.name + '_open'">
<img src="/web/static/src/img/icons/gtk-index.png"/></span>
@@ -865,8 +876,6 @@
</ul>
</t>
<t t-name="FieldOne2Many">
- <div t-att-id="widget.element_id">
- </div>
</t>
<t t-name="FieldMany2Many">
<div t-att-id="widget.list_id"></div>
@@ -874,10 +883,10 @@
<t t-name="FieldReference">
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="oe_frame oe_forms">
<tr>
- <td t-att-id="widget.selection.element_id" class="oe_form_frame_cell oe_form_selection">
+ <td t-attf-class="oe_form_frame_cell oe_form_selection #{widget.selection.element_class}">
<t t-raw="widget.selection.render()"/>
</td>
- <td t-att-id="widget.m2o.element_id" class="oe_form_frame_cell oe_form_many2one" nowrap="true">
+ <td class="oe_form_frame_cell oe_form_many2one #{widget.selection.element_class}" nowrap="true">
<t t-raw="widget.m2o.render()"/>
</td>
</tr>
@@ -887,7 +896,7 @@
<input type="checkbox"
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
- t-att-class="'field_' + widget.type"/>
+ t-attf-class="field_#{widget.type} #{widget.element_class}"/>
</t>
<t t-name="FieldProgressBar">
<div t-opentag="true" class="oe-progressbar">
@@ -902,7 +911,7 @@
t-att-border="widget.readonly ? 0 : 1"
t-att-id="widget.element_id + '_field'"
t-att-name="widget.name"
- t-att-class="'field_' + widget.type"
+ t-attf-class="field_#{widget.type} #{widget.element_class}"
t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"
/>
@@ -950,7 +959,7 @@
<input type="text" size="1"
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
- t-att-class="'field_' + widget.type" style="width: 100%"
+ t-attf-class="field_#{widget.type} #{widget.element_class}" style="width: 100%"
/>
</td>
<td class="oe-binary" nowrap="true">
@@ -995,7 +1004,7 @@
</t>
<t t-name="WidgetButton">
<button type="button"
- t-att-id="widget.element_id + '_button'"
+ t-attf-class="#{widget.element_class}"
t-att-title="widget.help"
style="width: 100%" class="button">
<img t-if="widget.node.attrs.icon" t-att-src="'/web/static/src/img/icons/' + widget.node.attrs.icon + '.png'" width="16" height="16"/>
@@ -1070,6 +1079,18 @@
<t t-if="filters.length" t-raw="filters.render(defaults)"/>
</div>
</t>
+<t t-name="SearchView.date">
+ <label t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
+ t-att-title="attrs.help"
+ t-att-for="element_id">
+ <t t-esc="attrs.string || attrs.name"/>
+ <span t-if="attrs.help">?</span>
+ </label>
+ <div style="white-space: nowrap;">
+ <span t-att-id="element_id"></span>
+ <t t-if="filters.length" t-raw="filters.render(defaults)"/>
+ </div>
+</t>
<t t-name="SearchView.field.selection">
<label t-att-title="attrs.help"
t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
@@ -1161,11 +1182,8 @@
<t t-name="SearchView.extended_search.proposition.char">
<input t-att-id="element_id" class="field_char"/>
</t>
-<t t-name="SearchView.extended_search.proposition.datetime">
- <input t-att-id="element_id" class="field_datetime"/>
-</t>
-<t t-name="SearchView.extended_search.proposition.date">
- <input t-att-id="element_id" class="field_date"/>
+<t t-name="SearchView.extended_search.proposition.empty">
+ <span t-att-id="element_id"></span>
</t>
<t t-name="SearchView.extended_search.proposition.integer">
<input type="number" t-att-id="element_id" class="field_integer" step="1"/>
@@ -1437,6 +1455,79 @@
</table>
</form>
</t>
+
+<t t-name="ImportView">
+ <a id="importview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Import</a>
+</t>
+<t t-name="ImportDataView">
+<form name="import_data" id="import_data" action="" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="session_id" t-att-value="session.session_id"/>
+ <h2 class="separator horizontal">1. Import a .CSV file</h2>
+ <p>Select a .CSV file to import. If you need a sample of file to import,
+ you should use the export tool with the "Import Compatible" option.
+ </p>
+ <p>
+ <label for="csvfile">CSV File:</label>
+ <input type="file" id="csvfile" size="50" name="csvfile"/>
+ </p>
+ <h2 class="separator horizontal">2. Check your file format</h2>
+ <div id="result"></div>
+ <fieldset>
+ <legend style="cursor:pointer;">Import Options</legend>
+ <table style="display:none">
+ <tr>
+ <td colspan="4">
+ <label for="file_has_headers">Does your file have titles?</label>
+ <input type="checkbox" checked="checked"
+ id="file_has_headers"/>
+ </td>
+ </tr>
+ <tr>
+ <td><label for="csv_separator">Separator:</label></td>
+ <td><input type="text" name="csvsep" id="csv_separator" value=","/></td>
+ <td><label for="csv_delimiter">Delimiter:</label></td>
+ <td><input type="text" name="csvdel" id="csv_delimiter" value='"'/></td>
+ </tr>
+ <tr>
+ <td><label for="csv_encoding">Encoding:</label></td>
+ <td>
+ <select name="csvcode" id="csv_encoding">
+ <option value="utf-8">UTF-8</option>
+ <option value="latin1">Latin 1</option>
+ </select>
+ </td>
+ <td><label for="csv_skip" title="For use if CSV files have titles on multiple lines, skips more than a single line during import">
+ Lines to skip<sup>?</sup>:</label></td>
+ <td><input type="number" id="csv_skip" value="0" min="0"/></td>
+ </tr>
+ </table>
+ </fieldset>
+</form>
+</t>
+<table t-name="ImportView.result"
+ class="oe_import_grid" width="100%" style="margin: 5px 0;">
+ <tr t-if="headers" class="oe_import_grid-header">
+ <td t-foreach="headers" t-as="header" class="oe_import_grid-cell">
+ <t t-esc="header"/></td>
+ </tr>
+ <tr>
+ <td t-foreach="records[0]" t-as="column">
+ <input class="sel_fields"/>
+ </td>
+ </tr>
+ <tr t-foreach="records" t-as="record" class="oe_import_grid-row">
+ <td t-foreach="record" t-as="cell" class="oe_import_grid-cell">
+ <t t-esc="cell"/></td>
+ </tr>
+</table>
+<t t-name="ImportView.error">
+ <p style="white-space:pre-line;">The import failed due to:<t t-esc="error.message"/></p>
+ <t t-if="error.preview">
+ <p>Here is a preview of the file we could not import:</p>
+ <pre><t t-esc="error.preview"/></pre>
+ </t>
+</t>
+
<t t-name="About-Page">
<div>
<h1>OpenERP Web</h1>
diff --git a/addons/web_calendar/static/src/js/calendar.js b/addons/web_calendar/static/src/js/calendar.js
index e590280b933..5de601e0921 100644
--- a/addons/web_calendar/static/src/js/calendar.js
+++ b/addons/web_calendar/static/src/js/calendar.js
@@ -35,6 +35,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
},
stop: function() {
scheduler.clearAll();
+ this._super();
},
on_loaded: function(data) {
this.fields_view = data;
diff --git a/addons/web_dashboard/static/src/js/dashboard.js b/addons/web_dashboard/static/src/js/dashboard.js
index 008d03d1548..4672492821d 100644
--- a/addons/web_dashboard/static/src/js/dashboard.js
+++ b/addons/web_dashboard/static/src/js/dashboard.js
@@ -301,7 +301,7 @@ openerp.web_dashboard.ConfigOverview = openerp.web.View.extend({
.then(this.on_records_loaded);
},
on_records_loaded: function (read_response, progress_response) {
- var records = read_response[0].records,
+ var records = read_response,
progress = progress_response[0];
var grouped_todos = _(records).chain()
diff --git a/addons/web_default_home/static/src/js/home.js b/addons/web_default_home/static/src/js/home.js
index ed8561bc44d..67964351873 100644
--- a/addons/web_default_home/static/src/js/home.js
+++ b/addons/web_default_home/static/src/js/home.js
@@ -94,8 +94,10 @@ openerp.web_default_home = function (openerp) {
})
},
install_module: function (module_name) {
+ var self = this;
var Modules = new openerp.web.DataSetSearch(
- this, 'ir.module.module', null, [['name', '=', module_name], ['state', '=', 'uninstalled']]);
+ this, 'ir.module.module', null,
+ [['name', '=', module_name], ['state', '=', 'uninstalled']]);
var Upgrade = new openerp.web.DataSet(this, 'base.module.upgrade');
$.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
@@ -105,13 +107,21 @@ openerp.web_default_home = function (openerp) {
[_.pluck(records, 'id'), 'to install', ['uninstalled']],
function () {
Upgrade.call('upgrade_module', [[]], function () {
- $.unblockUI();
- // TODO: less brutal reloading
- window.location.reload(true);
+ self.run_configuration_wizards();
});
}
)
});
+ },
+ run_configuration_wizards: function () {
+ var self = this;
+ new openerp.web.DataSet(this, 'res.config').call('start', [[]], function (action) {
+ $.unblockUI();
+ self.do_action(action, function () {
+ // TODO: less brutal reloading
+ window.location.reload(true);
+ });
+ });
}
});
};
diff --git a/addons/web_graph/__init__.py b/addons/web_graph/__init__.py
index 53e20f29429..e69de29bb2d 100644
--- a/addons/web_graph/__init__.py
+++ b/addons/web_graph/__init__.py
@@ -1,2 +0,0 @@
-#!/usr/bin/python
-import controllers \ No newline at end of file
diff --git a/addons/web_graph/__openerp__.py b/addons/web_graph/__openerp__.py
index 4a1c4a6576f..03839744480 100644
--- a/addons/web_graph/__openerp__.py
+++ b/addons/web_graph/__openerp__.py
@@ -3,7 +3,7 @@
"version": "2.0",
"depends": ['web'],
"js": [
- "static/lib/dhtmlxGraph/codebase/dhtmlxchart.js",
+ "static/lib/dhtmlxGraph/codebase/dhtmlxchart_debug.js",
"static/src/js/graph.js"],
"css": ["static/lib/dhtmlxGraph/codebase/dhtmlxchart.css"],
"active": True
diff --git a/addons/web_graph/static/src/js/graph.js b/addons/web_graph/static/src/js/graph.js
index 2a493b89166..08ceeddc2d9 100644
--- a/addons/web_graph/static/src/js/graph.js
+++ b/addons/web_graph/static/src/js/graph.js
@@ -19,11 +19,14 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
init: function(parent, dataset, view_id) {
this._super(parent);
- this.view_manager = parent;
this.dataset = dataset;
- this.dataset_index = 0;
- this.model = this.dataset.model;
this.view_id = view_id;
+
+ this.first_field = null;
+ this.abscissa = null;
+ this.ordinate = null;
+ this.columns = [];
+ this.group_field = null;
},
do_show: function () {
// TODO: re-trigger search
@@ -33,348 +36,265 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
this.$element.hide();
},
start: function() {
+ var self = this;
this._super();
- return this.rpc("/web_graph/graphview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
+ return $.when(
+ this.dataset.call('fields_get', []),
+ this.rpc('/web/view/load', {
+ model: this.dataset.model,
+ view_id: this.view_id,
+ view_type: 'graph'
+ })).then(function (fields_result, view_result) {
+ self.fields = fields_result[0];
+ self.fields_view = view_result[0];
+ self.on_loaded();
+ });
+ },
+ /**
+ * Returns all object fields involved in the graph view
+ */
+ list_fields: function () {
+ var fs = [this.abscissa];
+ fs.push.apply(fs, _(this.columns).pluck('name'));
+ if (this.group_field) {
+ fs.push(this.group_field);
+ }
+ return fs;
},
- on_loaded: function(data) {
- this.all_fields = data.all_fields;
- this.fields_view = data.fields_view;
- this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
- this.view_id = this.fields_view.view_id;
+ on_loaded: function() {
this.chart = this.fields_view.arch.attrs.type || 'pie';
- this.fields = this.fields_view.fields;
- this.chart_info_fields = [];
- this.operator_field = '';
- this.operator_field_one = '';
- this.operator = [];
- this.group_field = [];
- this.orientation = this.fields_view.arch.attrs.orientation || '';
- this.elem_id = this.$element[0]['id'];
+ this.orientation = this.fields_view.arch.attrs.orientation || 'vertical';
_.each(this.fields_view.arch.children, function (field) {
- if (field.attrs.operator) {
- this.operator.push(field.attrs.name);
- }
- else if (field.attrs.group) {
- this.group_field.push(field.attrs.name);
- }
- else {
- this.chart_info_fields.push(field.attrs.name);
+ var attrs = field.attrs;
+ if (attrs.group) {
+ this.group_field = attrs.name;
+ } else if(!this.abscissa) {
+ this.first_field = this.abscissa = attrs.name;
+ } else {
+ this.columns.push({
+ name: attrs.name,
+ operator: attrs.operator || '+'
+ });
}
}, this);
+ this.ordinate = this.columns[0].name;
- this.operator_field = this.operator[0];
- if(this.operator.length > 1){
- this.operator_field_one = this.operator[1];
- }
- if(this.operator == ''){
- this.operator_field = this.chart_info_fields[1];
- }
- this.chart_info = this.chart_info_fields[0];
- this.x_title = this.fields[this.chart_info_fields[0]]['string'];
- this.y_title = this.fields[this.operator_field]['string'];
- this.load_chart();
+ this.dataset.read_slice(
+ this.list_fields(), {}, $.proxy(this, 'schedule_chart'));
},
-
- load_chart: function(data) {
- var self = this;
- var domain = false;
- if(data){
- this.x_title = this.all_fields[this.chart_info_fields]['string'];
- this.y_title = this.all_fields[this.operator_field]['string'];
- self.schedule_chart(data);
- }else{
- if(! _.isEmpty(this.view_manager.dataset.domain)){
- domain = this.view_manager.dataset.domain;
- }else if(! _.isEmpty(this.view_manager.action.domain)){
- domain = this.view_manager.action.domain;
- }
- this.dataset.domain = domain;
- this.dataset.context = this.view_manager.dataset.context;
- this.dataset.read_slice(_(this.fields).keys(),{}, function(res) {
- self.schedule_chart(res);
- });
- }
- },
-
schedule_chart: function(results) {
- this.$element.html(QWeb.render("GraphView", {"fields_view": this.fields_view, "chart": this.chart,'elem_id': this.elem_id}));
+ var self = this;
+ this.$element.html(QWeb.render("GraphView", {
+ "fields_view": this.fields_view,
+ "chart": this.chart,
+ 'element_id': this.element_id
+ }));
- _.each(results, function (result) {
- _.each(result, function (field_value, field_name) {
- if (typeof field_value == 'object') {
- result[field_name] = field_value[field_value.length - 1];
+ var fields = _(this.columns).pluck('name').concat([this.abscissa]);
+ if (this.group_field) { fields.push(this.group_field); }
+ // transform search result into usable records (convert from OpenERP
+ // value shapes to usable atomic types
+ var records = _(results).map(function (result) {
+ var point = {};
+ _(result).each(function (value, field) {
+ if (!_(fields).contains(field)) { return; }
+ if (value === false) { point[field] = false; return; }
+ switch (self.fields[field].type) {
+ case 'selection':
+ point[field] = _(self.fields[field].selection).detect(function (choice) {
+ return choice[0] === value;
+ })[1];
+ break;
+ case 'many2one':
+ point[field] = value[1];
+ break;
+ case 'integer': case 'float': case 'char':
+ case 'date': case 'datetime':
+ point[field] = value;
+ break;
+ default:
+ throw new Error(
+ "Unknown field type " + self.fields[field].type
+ + "for field " + field + " (" + value + ")");
}
- if (typeof field_value == 'string') {
- var choices = this.all_fields[field_name]['selection'];
- _.each(choices, function (choice) {
- if (field_value == choice[0]) {
- result[field_name] = choice;
- }
- });
- }
- }, this);
- }, this);
+ });
+ return point;
+ });
+ // aggregate data, because dhtmlx is crap. Aggregate on abscissa field,
+ // leave split on group field => max m*n records where m is the # of
+ // values for the abscissa and n is the # of values for the group field
+ var graph_data = [];
+ _(records).each(function (record) {
+ var abscissa = record[self.abscissa],
+ group = record[self.group_field];
+ var r = _(graph_data).detect(function (potential) {
+ return potential[self.abscissa] === abscissa
+ && (!self.group_field
+ || potential[self.group_field] === group);
+ });
+ var datapoint = r || {};
- var graph_data = {};
- _.each(results, function (result) {
- var group_key = [];
- if(this.group_field.length){
- _.each(this.group_field, function (res) {
- result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
- group_key.push(result[res]);
- });
- }else{
- group_key.push(result[this.group_field]);
- }
- var column_key = result[this.chart_info_fields] + "_" + group_key;
- var column_descriptor = {};
- if (graph_data[column_key] == undefined) {
- column_descriptor[this.operator_field] = result[this.operator_field];
- if (this.operator.length > 1) {
- column_descriptor[this.operator_field_one] = result[this.operator_field_one];
- }
- column_descriptor[this.chart_info_fields] = result[this.chart_info_fields];
- if(this.group_field.length){
- _.each(this.group_field, function (res) {
- column_descriptor[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
- });
+ datapoint[self.abscissa] = abscissa;
+ if (self.group_field) { datapoint[self.group_field] = group; }
+ _(self.columns).each(function (column) {
+ var val = record[column.name],
+ aggregate = datapoint[column.name];
+ switch(column.operator) {
+ case '+':
+ datapoint[column.name] = (aggregate || 0) + val;
+ return;
+ case '*':
+ datapoint[column.name] = (aggregate || 1) * val;
+ return;
+ case 'min':
+ datapoint[column.name] = (aggregate || Infinity) > val
+ ? val
+ : aggregate;
+ return;
+ case 'max':
+ datapoint[column.name] = (aggregate || -Infinity) < val
+ ? val
+ : aggregate;
}
- } else {
- column_descriptor = graph_data[column_key];
- column_descriptor[this.operator_field] += result[this.operator_field];
- if (this.operator.length > 1) {
- column_descriptor[this.operator_field_one] += result[this.operator_field_one];
- }
- }
- graph_data[column_key] = column_descriptor;
- }, this);
+ });
+
+ if (!r) { graph_data.push(datapoint); }
+ });
+ graph_data = _(graph_data).sortBy(function (point) {
+ return point[self.abscissa] + '[[--]]' + point[self.group_field];
+ });
if (this.chart == 'bar') {
- return this.schedule_bar(_.values(graph_data));
+ return this.schedule_bar(graph_data);
} else if (this.chart == "pie") {
- return this.schedule_pie(_.values(graph_data));
+ return this.schedule_pie(graph_data);
}
},
schedule_bar: function(results) {
var self = this;
- var view_chart = '';
- var group_list = [];
- var legend_list = [];
- var newkey = '', newkey_one;
- var string_legend = '';
-
- if((self.group_field.length) && (this.operator.length <= 1)){
- view_chart = self.orientation == 'horizontal'? 'stackedBarH' : 'stackedBar';
- }else{
- view_chart = self.orientation == 'horizontal'? 'barH' : 'bar';
- }
-
- _.each(results, function (result) {
- if ((self.group_field.length) && (this.operator.length <= 1)) {
- var legend_key = '';
- _.each(self.group_field, function (res) {
- result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
- legend_key += result[res];
- });
- newkey = legend_key.replace(/\s+/g,'_').replace(/[^a-zA-Z 0-9]+/g,'_');
- string_legend = legend_key;
- } else {
- newkey = string_legend = "val";
- }
- if (_.contains(group_list, newkey) && _.contains(legend_list, string_legend)) {
- return;
- }
- group_list.push(newkey);
- legend_list.push(string_legend);
-
- if (this.operator.length > 1) {
- newkey_one = "val1";
- group_list.push(newkey_one);
- legend_list.push(newkey_one);
+ var group_list, view_chart;
+ if (!this.group_field) {
+ view_chart = (this.orientation === 'horizontal') ? 'barH' : 'bar';
+ group_list = _(this.columns).map(function (column, index) {
+ return {
+ group: column.name,
+ text: self.fields[column.name].string,
+ color: COLOR_PALETTE[index % (COLOR_PALETTE.length)]
+ }
+ });
+ } else {
+ // dhtmlx handles clustered bar charts (> 1 column per abscissa
+ // value) and stacked bar charts (basically the same but with the
+ // columns on top of one another instead of side by side), but it
+ // does not handle clustered stacked bar charts
+ if (this.columns.length > 1) {
+ this.$element.text(
+ 'OpenERP Web does not support combining grouping and '
+ + 'multiple columns in graph at this time.');
+ throw new Error(
+ 'dhtmlx can not handle columns counts of that magnitude');
}
- }, this);
+ // transform series for clustered charts into series for stacked
+ // charts
+ view_chart = (this.orientation === 'horizontal')
+ ? 'stackedBarH' : 'stackedBar';
+ group_list = _(results).chain()
+ .pluck(this.group_field)
+ .uniq()
+ .map(function (value, index) {
+ return {
+ group: self.ordinate + '_' +
+ value.toLowerCase().replace(/\s/g, '_'),
+ text: value,
+ color: COLOR_PALETTE[index % COLOR_PALETTE.length]
+ };
+ }).value();
- if (group_list.length <=1){
- group_list = [];
- legend_list = [];
- newkey = string_legend = "val";
- group_list.push(newkey);
- legend_list.push(string_legend);
+ results = _(results).chain()
+ .groupBy(function (record) { return record[self.abscissa]; })
+ .map(function (records) {
+ var r = {};
+ // second argument is coerced to a str, no good for boolean
+ r[self.abscissa] = records[0][self.abscissa];
+ _(records).each(function (record) {
+ var key = _.sprintf('%s_%s',
+ self.ordinate,
+ record[self.group_field].toLowerCase().replace(/\s/g, '_'));
+ r[key] = record[self.ordinate];
+ });
+ return r;
+ })
+ .value();
}
- var abscissa_data = {};
- _.each(results, function (result) {
- var label = result[self.chart_info_fields],
- section = {};
- if ((self.group_field.length) && (group_list.length > 1) && (self.operator.length <= 1)){
- var legend_key_two = '';
- _.each(self.group_field, function (res) {
- result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res];
- legend_key_two += result[res];
- });
- newkey = legend_key_two.replace(/\s+/g,'_').replace(/[^a-zA-Z 0-9]+/g,'_');
- }else{
- newkey = "val";
- }
- if (abscissa_data[label] == undefined){
- section[self.chart_info_fields] = label;
- _.each(group_list, function (group) {
- section[group] = 0;
- });
- } else {
- section = abscissa_data[label];
- }
- section[newkey] = result[self.operator_field];
- if (self.operator.length > 1){
- section[newkey_one] = result[self.operator_field_one];
- }
- abscissa_data[label] = section;
- });
-
- //for legend color
- var grp_color = _.map(legend_list, function (group_legend, index) {
- var legend = {color: COLOR_PALETTE[index]};
- if (group_legend == "val"){
- legend['text'] = self.fields[self.operator_field]['string']
- }else if(group_legend == "val1"){
- legend['text'] = self.fields[self.operator_field_one]['string']
- }else{
- legend['text'] = group_legend;
- }
- return legend;
- });
-
- //for axis's value and title
- var max,min,step;
- var maximum,minimum;
- if(_.isEmpty(abscissa_data)){
- max = 9;
- min = 0;
- step=1;
- }else{
- var max_min = [];
- _.each(abscissa_data, function (abscissa_datas) {
- _.each(group_list, function(res){
- max_min.push(abscissa_datas[res]);
- });
- });
- maximum = Math.max.apply(Math,max_min);
- minimum = Math.min.apply(Math,max_min);
- if (maximum == minimum){
- if (maximum == 0){
- max = 9;
- min = 0;
- step=1;
- }else if(maximum > 0){
- max = maximum + (10 - maximum % 10);
- min = 0;
- step = Math.round(max/10);
- }else{
- max = 0;
- min = minimum - (10 + minimum % 10);
- step = Math.round(Math.abs(min)/10);
- }
- }
- }
var abscissa_description = {
- template: self.chart_info_fields,
- title: "<b>"+self.x_title+"</b>"
+ title: "<b>" + this.fields[this.abscissa].string + "</b>",
+ template: function (obj) {
+ return obj[self.abscissa] || 'Undefined';
+ }
};
var ordinate_description = {
lines: true,
- title: "<b>"+self.y_title+"</b>",
- start: min,
- step: step,
- end: max
+ title: "<b>" + this.fields[this.ordinate].string + "</b>"
};
- var x_axis, y_axis, tooltip;
- if (self.orientation == 'horizontal'){
- x_axis = ordinate_description;
- y_axis = abscissa_description;
- }else{
- x_axis = abscissa_description;
- y_axis = ordinate_description;
+ var x_axis, y_axis;
+ if (self.orientation == 'horizontal') {
+ x_axis = ordinate_description;
+ y_axis = abscissa_description;
+ } else {
+ x_axis = abscissa_description;
+ y_axis = ordinate_description;
}
- tooltip = self.chart_info_fields;
-
+
var bar_chart = new dhtmlXChart({
view: view_chart,
- container: self.elem_id+"-barchart",
- value:"#"+group_list[0]+"#",
+ container: this.element_id+"-barchart",
+ value:"#"+group_list[0].group+"#",
gradient: "3d",
border: false,
width: 1024,
tooltip:{
- template:"#"+tooltip+"#"+","+grp_color[0]['text']+"="+"#"+group_list[0]+"#"
+ template: _.sprintf("#%s#, %s=#%s#",
+ this.abscissa, group_list[0].text, group_list[0].group)
},
radius: 0,
- color:grp_color[0]['color'],
+ color:group_list[0].color,
origin:0,
- xAxis:{
- template:function(obj){
- if(x_axis['template']){
- var val = obj[x_axis['template']];
- val = (typeof val == 'object')?val[1]:(!val?'Undefined':val);
- if(val.length > 12){
- val = val.substring(0,12);
- }
- return val;
- }else{
- return obj;
- }
- },
- title:x_axis['title'],
- lines:x_axis['lines']
- },
- yAxis:{
- template:function(obj){
- if(y_axis['template']){
- var vals = obj[y_axis['template']];
- vals = (typeof vals == 'object')?vals[1]:(!vals?'Undefined':vals);
- if(vals.length > 12){
- vals = vals.substring(0,12);
- }
- return vals;
- }else{
- return obj;
- }
- },
- title:y_axis['title'],
- lines: y_axis['lines'],
- start:y_axis['start'],
- step:y_axis['step'],
- end:y_axis['end']
- },
+ xAxis: x_axis,
+ yAxis: y_axis,
padding: {
left: 75
},
legend: {
- values: grp_color,
+ values: group_list,
align:"left",
valign:"top",
layout: "x",
- marker:{
+ marker: {
type:"round",
width:12
}
}
});
for (var m = 1; m<group_list.length;m++){
+ var column = group_list[m];
+ if (column.group === this.group_field) { continue; }
bar_chart.addSeries({
- value: "#"+group_list[m]+"#",
+ value: "#"+column.group+"#",
tooltip:{
- template:"#"+tooltip+"#"+","+grp_color[m]['text']+"="+"#"+group_list[m]+"#"
+ template: _.sprintf("#%s#, %s=#%s#",
+ this.abscissa, column.text, column.group)
},
- color: grp_color[m]['color']
+ color: column.color
});
}
- bar_chart.parse(_.values(abscissa_data), "json");
- jQuery("#"+self.elem_id+"-barchart").height(jQuery("#"+self.elem_id+"-barchart").height()+50);
+ bar_chart.parse(results, "json");
+ jQuery("#"+this.element_id+"-barchart").height(jQuery("#"+this.element_id+"-barchart").height()+50);
bar_chart.attachEvent("onItemClick", function(id) {
self.open_list_view(bar_chart.get(id));
});
@@ -383,15 +303,15 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
var self = this;
var chart = new dhtmlXChart({
view:"pie3D",
- container:self.elem_id+"-piechart",
- value:"#"+self.operator_field+"#",
+ container:self.element_id+"-piechart",
+ value:"#"+self.ordinate+"#",
pieInnerText:function(obj) {
- var sum = chart.sum("#"+self.operator_field+"#");
- var val = obj[self.operator_field] / sum * 100 ;
+ var sum = chart.sum("#"+self.ordinate+"#");
+ var val = obj[self.ordinate] / sum * 100 ;
return val.toFixed(1) + "%";
},
tooltip:{
- template:"#"+self.chart_info_fields+"#"+"="+"#"+self.operator_field+"#"
+ template:"#"+self.abscissa+"#"+"="+"#"+self.ordinate+"#"
},
gradient:"3d",
height: 20,
@@ -406,9 +326,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
width:12
},
template:function(obj){
- var val = obj[self.chart_info_fields];
- val = (typeof val == 'object')?val[1]:val;
- return val;
+ return obj[self.abscissa] || 'Undefined';
}
}
});
@@ -422,20 +340,13 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
if($(".dhx_tooltip").is(":visible")) {
$(".dhx_tooltip").remove('div');
}
- id = id[this.chart_info_fields];
+ id = id[this.abscissa];
if (typeof id == 'object'){
id = id[0];
}
- var record_id = "";
- this.dataset.model = this.model;
- if (typeof this.chart_info_fields == 'object'){
- record_id = this.chart_info_fields[0];
- }else{
- record_id = this.chart_info_fields;
- }
- this.dataset.domain = [[record_id, '=', id],['id','in',this.dataset.ids]];
- var modes = !!modes ? modes.split(",") : ["list", "form", "graph"];
+ var record_id = this.abscissa;
+ var modes = ["list", "form", "graph"];
var views = [];
_.each(modes, function(mode) {
var view = [false, mode];
@@ -446,7 +357,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
});
this.do_action({
"res_model" : this.dataset.model,
- "domain" : this.dataset.domain,
+ "domain" : [[record_id, '=', id], ['id','in',this.dataset.ids]],
"views" : views,
"type" : "ir.actions.act_window",
"auto_search" : true,
@@ -462,18 +373,18 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
contexts: contexts,
group_by_seq: groupbys
}, function (results) {
- // TODO: handle non-empty results.group_by with read_group
- if(results.group_by && results.group_by != ''){
- self.chart_info_fields = results.group_by[0];
- }else{
- self.chart_info_fields = self.chart_info;
+ // TODO: handle non-empty results.group_by with read_group?
+ if(!_(results.group_by).isEmpty()){
+ self.abscissa = results.group_by[0];
+ } else {
+ self.abscissa = self.first_field;
}
- self.dataset.context = results.context;
- self.dataset.domain = results.domain;
- self.dataset.read_slice([],{}, $.proxy(self, 'load_chart'));
+ self.dataset.read_slice(self.list_fields(), {
+ context: results.context,
+ domain: results.domain
+ }, $.proxy(self, 'schedule_chart'));
});
}
});
-// here you may tweak globals object, if any, and play with on_* or do_* callbacks on them
};
// vim:et fdc=0 fdl=0:
diff --git a/addons/web_graph/static/src/xml/web_graph.xml b/addons/web_graph/static/src/xml/web_graph.xml
index 4320b9f156e..90349623946 100644
--- a/addons/web_graph/static/src/xml/web_graph.xml
+++ b/addons/web_graph/static/src/xml/web_graph.xml
@@ -1,4 +1,4 @@
<template>
- <div t-name="GraphView" t-att-id="elem_id+'-'+chart+'chart'"
+ <div t-name="GraphView" t-att-id="element_id+'-'+chart+'chart'"
style="height:300px;position:relative;"/>
</template> \ No newline at end of file
diff --git a/addons/web_hello/__openerp__.py b/addons/web_hello/__openerp__.py
index 7a80645f3b8..498f5129961 100644
--- a/addons/web_hello/__openerp__.py
+++ b/addons/web_hello/__openerp__.py
@@ -5,4 +5,5 @@
"js": ["static/*/*.js", "static/*/js/*.js"],
"css": [],
'active': False,
+ 'web_preload': False,
}
diff --git a/addons/web_hello/static/openerp/base_hello.js b/addons/web_hello/static/openerp/base_hello.js
index 99c97006eac..91caf263196 100644
--- a/addons/web_hello/static/openerp/base_hello.js
+++ b/addons/web_hello/static/openerp/base_hello.js
@@ -13,6 +13,13 @@ openerp.web.SearchView = openerp.web.SearchView.extend({
// here you may tweak globals object, if any, and play with on_* or do_* callbacks on them
+openerp.web.Login = openerp.web.Login.extend({
+ start: function() {
+ console.log('Hello there');
+ this._super.apply(this,arguments);
+ }
+});
+
};
// vim:et fdc=0 fdl=0:
diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css
index afac6cdef47..f92203589b2 100644
--- a/addons/web_kanban/static/src/css/kanban.css
+++ b/addons/web_kanban/static/src/css/kanban.css
@@ -1,8 +1,21 @@
.openerp .oe_kanban_view .oe_column {
- float: left;
width: 100%;
}
+.openerp .oe_vertical_text {
+ writing-mode:tb-rl;
+ -webkit-transform:rotate(90deg);
+ -moz-transform:rotate(90deg);
+ -o-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+ display:block;
+ width:30px;
+ height:20px;
+ align:center;
+ font-size:24px;
+}
+
.openerp .oe_kanban_view .ui-sortable-placeholder {
border: 1px dotted black;
visibility: visible !important;
@@ -14,6 +27,10 @@
font-size: 1.5em;
font-weight: bold;
}
+.openerp .oe_kanban_view .fold-columns-icon {
+ float: left;
+ padding: 2px 2px 0 0;
+}
.openerp .oe_kanban_action_button {
height: 22px;
margin: 0;
@@ -200,4 +217,3 @@
.openerp .oe_kanban_color_alert .oe_kanban_color_border {
border-color: #c00 !important;
}
-
diff --git a/addons/web_kanban/static/src/img/minus-icon.png b/addons/web_kanban/static/src/img/minus-icon.png
new file mode 100644
index 00000000000..cb655d1e503
--- /dev/null
+++ b/addons/web_kanban/static/src/img/minus-icon.png
Binary files differ
diff --git a/addons/web_kanban/static/src/img/plus-icon.png b/addons/web_kanban/static/src/img/plus-icon.png
new file mode 100644
index 00000000000..7bb9b1be856
--- /dev/null
+++ b/addons/web_kanban/static/src/img/plus-icon.png
Binary files differ
diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js
index 8d6727facaa..46173341361 100644
--- a/addons/web_kanban/static/src/js/kanban.js
+++ b/addons/web_kanban/static/src/js/kanban.js
@@ -18,6 +18,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
this.all_display_data = false;
this.groups = [];
this.qweb = new QWeb2.Engine();
+ this.aggregates = {};
+ this.NO_OF_COLUMNS = 3;
if (this.options.action_views_ids.form) {
this.form_dialog = new openerp.web.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start();
this.form_dialog.on_form_dialog_saved.add_last(this.on_record_saved);
@@ -36,8 +38,17 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
}
},
add_qweb_template: function() {
+ var group_operator = ["avg", "max", "min", "sum", "count"]
for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
var child = this.fields_view.arch.children[i];
+ if (child.tag === "field") {
+ for(j=0, jj=group_operator.length; j < jj; j++) {
+ if (child.attrs[group_operator[j]]) {
+ this.aggregates[child.attrs.name] = child.attrs[group_operator[j]];
+ break;
+ }
+ }
+ }
if (child.tag === "templates") {
this.transform_qweb_template(child);
this.qweb.add_template(openerp.web.json_node_to_xml(child));
@@ -118,14 +129,33 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
}
}
},
- on_show_data: function(data) {
+ sort_group: function (first, second) {
+ if (first.header && second.header)
+ {
+ first = first.header.toLowerCase();
+ second = second.header.toLowerCase();
+ if (first > second) return 1;
+ else if (first < second) return -1;
+ else return 0;
+ }
+ else return 0;
+ },
+ on_show_data: function() {
var self = this;
- this.$element.html(QWeb.render("KanbanView", {"data": data}));
+ if (!this.group_by.length) {
+ this.do_record_group();
+ }
+ self.all_display_data.sort(this.sort_group);
+ this.$element.html(QWeb.render("KanbanView", {"data": self.all_display_data}));
this.on_reload_kanban();
+ this.$element.find(".oe_vertical_text").hide();
var drag_handel = false;
if (this.$element.find(".oe_kanban_draghandle").length > 0) {
drag_handel = ".oe_kanban_draghandle";
}
+ if (!this.group_by.length) {
+ drag_handel = true;
+ }
this.$element.find(".oe_column").sortable({
connectWith: ".oe_column",
handle : drag_handel,
@@ -134,36 +164,44 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
self.source_index['column'] = ui.item.parent().attr('id');
},
stop: self.on_receive_record,
+ scroll: false
});
this.$element.find(".oe_column").disableSelection()
this.$element.find('button.oe_kanban_button_new').click(this.do_add_record);
+ this.$element.find(".fold-columns-icon").click(function(event) {
+ self.do_fold_unfold_columns(event, this.id);
+ });
},
- on_button_click: function (button_attrs, record_id) {
- var self = this;
- if (this.groups.length) {
- _.each(this.groups, function (group) {
- group.list([],
- function (groups) {},
- function (dataset) {
- dataset.read_slice([], {}, function(records) {
- var index = parseInt(_.indexOf(dataset.ids, record_id));
- if(index >= 0) {
- self.on_confirm_click(dataset, button_attrs, index, record_id);
- }
- });
- }
- );
- });
- } else {
- var index = parseInt(_.indexOf(self.dataset.ids, record_id));
- if (index >= 0) {
- _.extend(self.dataset, {domain: self.domain, context: self.context});
- self.on_confirm_click(self.dataset, button_attrs, index, record_id);
+ do_fold_unfold_columns: function(event, element_id) {
+ var column_id = "column_" + element_id;
+ var column_element = this.$element.find("#" + column_id + " .oe_fold_column");
+ if (column_element.is(":hidden")) {
+ this.$element.find("#" + column_id).find("img.fold-columns-icon").attr('src', '/web_kanban/static/src/img/minus-icon.png');
+ column_element.show();
+ this.$element.find("#" + column_id + ".oe_table_column").css("width",Math.round(99 / this.all_display_data.length) + "%");
+ this.$element.find("#" + column_id + ".oe_vertical_text").hide();
+ }
+ else{
+ this.$element.find("#" + column_id).find("img.fold-columns-icon").attr('src', '/web_kanban/static/src/img/plus-icon.png');
+ column_element.hide();
+ this.$element.find("#" + column_id + ".oe_table_column").css("width","0.5%");
+ this.$element.find("#" + column_id + ".oe_vertical_text").show();
+ }
+
+ },
+ do_record_group: function() {
+ if (this.NO_OF_COLUMNS && this.all_display_data.length > 0) {
+ var records = this.all_display_data[0].records;
+ var record_per_group = Math.round((records).length / this.NO_OF_COLUMNS);
+ this.all_display_data = [];
+ for (var i=0, ii=this.NO_OF_COLUMNS; i < ii; i++) {
+ this.all_display_data.push({'records': records.slice(0,record_per_group), 'value':i, 'header' : false, 'ids':[]});
+ records.splice(0,record_per_group);
}
}
},
- on_confirm_click: function (dataset, button_attrs, index, record_id) {
- this.on_execute_button_click(dataset, button_attrs, record_id);
+ on_button_click: function (button_attrs, record_id) {
+ this.on_execute_button_click(this.dataset, button_attrs, record_id);
},
do_add_record: function() {
this.dataset.index = null;
@@ -179,8 +217,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
},
on_record_saved: function(r) {
var id = this.form_dialog.form.datarecord.id;
- // TODO fme: reload record instead of all. need refactoring
- this.do_actual_search();
+ this.on_reload_record(id);
},
do_change_color: function(record_id, $e) {
var self = this,
@@ -198,12 +235,41 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
var data = {};
data[$e.data('name')] = $(this).data('color');
self.dataset.write(id, data, {}, function() {
- // TODO fme: reload record instead of all. need refactoring
- self.do_actual_search();
+ self.on_reload_record(id);
});
$cpicker.remove();
});
},
+ /**
+ Reload one record in view.
+ record_id : reload record id.
+ */
+ on_reload_record: function (record_id){
+ var self = this;
+ this.dataset.read_ids([record_id], [], function (records) {
+ if (records.length > 0) {
+ for (var i=0, ii=self.all_display_data.length; i < ii; i++) {
+ for(j=0, jj=self.all_display_data[i].records.length; j < jj; j++) {
+ if (self.all_display_data[i].records[j].id == record_id) {
+ _.extend(self.all_display_data[i].records[j], records[0]);
+ self.$element.find("#main_" + record_id).children().remove();
+ self.$element.find("#main_" + record_id).append(self.qweb.render('kanban-box', {
+ record: self.do_transform_record(self.all_display_data[i].records[j]),
+ kanban_color: self.kanban_color,
+ kanban_gravatar: self.kanban_gravatar
+ }));
+ break;
+ }
+ }
+ }
+ self.$element.find("#main_" + record_id + " .oe_kanban_action").click(self.on_action_clicked);
+ self.$element.find("#main_" + record_id + " .oe_kanban_box_show_onclick_trigger").click(function() {
+ $(this).parent("#main_" + record_id + " .oe_kanban_box").find(".oe_kanban_box_show_onclick").toggle();
+ });
+ }
+ });
+
+ },
do_delete: function (id) {
var self = this;
return $.when(this.dataset.unlink([id])).then(function () {
@@ -228,16 +294,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
this.do_execute_action(
button_attrs, dataset,
record_id, function () {
- var count = 1;
- _.each(self.all_display_data, function(data, index) {
- self.dataset.read_ids( data.ids, [], function(records){
- self.all_display_data[index].records = records;
- if(self.all_display_data.length == count) {
- self.do_actual_search();
- }
- count++;
- });
- });
+ self.do_actual_search();
}
);
},
@@ -249,9 +306,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
if (!ui.item.attr("id")) {
return false;
}
- // TODO fme: check what was this sequence
- if (self.fields_view.fields.sequence && (self.source_index.index >= 0 && self.source_index.index != from) ||
- (self.source_index.column && self.source_index.column != ui.item.parent().attr('id'))) {
+ if (self.fields_view.fields.sequence != undefined && ((self.source_index.index >= 0 && self.source_index.index != from) ||
+ (self.source_index.column && self.source_index.column != ui.item.parent().attr('id')))) {
var child_record = ui.item.parent().children();
var data, sequence = 1, index = to;
child_record.splice(0, to);
@@ -338,9 +394,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
kanban_gravatar: self.kanban_gravatar
}));
});
- } else {
- self.$element.find("#column_" + data.value).remove();
- self.all_display_data.splice(index, 1);
}
});
this.$element.find('.oe_kanban_action').click(this.on_action_clicked);
@@ -377,8 +430,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
do_search: function (domains, contexts, group_by) {
var self = this;
this.rpc('/web/session/eval_domain_and_context', {
- domains: domains,
- contexts: contexts,
+ domains: [this.dataset.get_domain()].concat(domains),
+ contexts: [this.dataset.get_context()].concat(contexts),
group_by_seq: group_by
}, function (results) {
self.domain = results.domain;
@@ -392,23 +445,27 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
group_by = self.group_by;
if (!group_by.length && this.fields_view.arch.attrs.default_group_by) {
group_by = [this.fields_view.arch.attrs.default_group_by];
+ self.group_by = group_by;
}
self.datagroup = new openerp.web.DataGroup(self, self.model, self.domain, self.context, group_by);
- self.dataset.context = self.context;
- self.dataset.domain = self.domain;
- self.datagroup.list([],
+ self.datagroup.list(_.keys(self.fields_view.fields),
function (groups) {
self.groups = groups;
- self.do_render_group(groups);
+ if (groups.length) {
+ self.do_render_group(groups);
+ }
+ else {
+ self.all_display_data = [];
+ self.on_show_data();
+ }
},
function (dataset) {
- self.domain = dataset.domain;
- self.context = dataset.context;
self.groups = [];
- self.dataset.read_slice([], {}, function(records) {
- self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}];
+ self.dataset.read_slice([], {'domain': self.domain, 'context': self.context}, function(records) {
+ if (records.length) self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}];
+ else self.all_display_data = [];
self.$element.find(".oe_kanban_view").remove();
- self.on_show_data(self.all_display_data);
+ self.on_show_data();
});
}
);
@@ -417,8 +474,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
this.all_display_data = [];
var self = this;
_.each(datagroups, function (group) {
- self.dataset.context = group.context;
- self.dataset.domain = group.domain;
var group_name = group.value;
var group_value = group.value;
if (!group.value) {
@@ -428,11 +483,15 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
group_name = group.value[1];
group_value = group.value[0];
}
- self.dataset.read_slice([], {}, function(records) {
- self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids});
+ var group_aggregates = {};
+ _.each(self.aggregates, function(value, key) {
+ group_aggregates[value] = group.aggregates[key];
+ });
+ self.dataset.read_slice([], {'domain': group.domain, 'context': group.context}, function(records) {
+ self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids, 'aggregates': group_aggregates});
if (datagroups.length == self.all_display_data.length) {
self.$element.find(".oe_kanban_view").remove();
- self.on_show_data(self.all_display_data);
+ self.on_show_data();
}
});
});
diff --git a/addons/web_kanban/static/src/xml/web_kanban.xml b/addons/web_kanban/static/src/xml/web_kanban.xml
index aa7bc73190d..8f3aaf457e9 100644
--- a/addons/web_kanban/static/src/xml/web_kanban.xml
+++ b/addons/web_kanban/static/src/xml/web_kanban.xml
@@ -9,15 +9,22 @@
</td>
</tr>
<tr>
- <td t-foreach="data" t-as="columns" class="oe_table_column oe_column_heading" t-att-id="'column_' + columns.value">
- <t t-if="columns.value" t-esc="columns.header"/>
+ <td t-foreach="data" t-as="columns" class="oe_table_column" t-att-id="'column_' + columns.value">
+ <img t-att-id="columns.value" class="fold-columns-icon" src="/web_kanban/static/src/img/minus-icon.png"/>
+ <div class="oe_fold_column" t-att-id="'column_' + columns.value">
+ <div class="oe_column_heading" t-if="columns.value and columns.header">
+ <t t-esc="columns.header"/>
+ </div>
+ <div t-foreach="columns.aggregates or {}" t-as="aggregate">
+ <i><u><t t-esc="aggregate"/>:</u> <t t-esc="aggregate_value"/></i>
+ </div>
+ </div>
</td>
</tr>
<tr>
- <td t-foreach="data" t-as="columns" class="oe_table_column" t-att-id="'column_' + columns.value" t-attf-style="width: #{Math.round(99 / data.length)}%">
- <div class="oe_column" t-att-id="'column_' + columns.value">
- <div t-foreach="columns.records" t-as="record" class="oe_kanban_record" t-att-id="'main_' + record.id"/>
- </div>
+ <td t-foreach="data" t-as="columns" class="oe_column oe_table_column" t-att-id="'column_' + columns.value" t-attf-style="width: #{Math.round(99 / data.length)}%">
+ <p t-if="columns.header" class="oe_vertical_text" t-att-id="'column_' + columns.value"><t t-esc="columns.header"/></p>
+ <div t-foreach="columns.records" t-as="record" class="oe_fold_column oe_kanban_record" t-att-id="'main_' + record.id"/>
</td>
</tr>
</table>
diff --git a/addons/web_tests/__init__.py b/addons/web_tests/__init__.py
new file mode 100644
index 00000000000..40a96afc6ff
--- /dev/null
+++ b/addons/web_tests/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/addons/web_tests/__openerp__.py b/addons/web_tests/__openerp__.py
new file mode 100644
index 00000000000..d6221900f00
--- /dev/null
+++ b/addons/web_tests/__openerp__.py
@@ -0,0 +1,8 @@
+{
+ "name": "Tests",
+ "version": "2.0",
+ "depends": [],
+ "js": ["static/src/js/*.js"],
+ "css": ['static/src/css/*.css'],
+ 'active': True,
+}
diff --git a/addons/web_tests/static/src/css/web_tests.css b/addons/web_tests/static/src/css/web_tests.css
new file mode 100644
index 00000000000..b14d6541f43
--- /dev/null
+++ b/addons/web_tests/static/src/css/web_tests.css
@@ -0,0 +1,3 @@
+.oe-bunchaforms > div {
+ float: left;
+}
diff --git a/addons/web_tests/static/src/js/web_tests.js b/addons/web_tests/static/src/js/web_tests.js
new file mode 100644
index 00000000000..b21275d1eb7
--- /dev/null
+++ b/addons/web_tests/static/src/js/web_tests.js
@@ -0,0 +1,37 @@
+openerp.web_tests = function (db) {
+ db.web.client_actions.add(
+ 'buncha-forms', 'instance.web_tests.BunchaForms');
+ db.web_tests = {};
+ db.web_tests.BunchaForms = db.web.Widget.extend({
+ init: function (parent) {
+ this._super(parent);
+ this.dataset = new db.web.DataSetSearch(this, 'test.listview.relations');
+ this.form = new db.web.FormView(this, this.dataset, false, {
+ action_buttons: false,
+ pager: false
+ });
+ this.form.registry = db.web.form.readonly;
+ },
+ render: function () {
+ return '<div class="oe-bunchaforms"></div>';
+ },
+ start: function () {
+ $.when(
+ this.dataset.read_slice(),
+ this.form.appendTo(this.$element)).then(this.on_everything_loaded);
+ },
+ on_everything_loaded: function (slice) {
+ var records = slice[0].records;
+ if (!records.length) {
+ this.form.on_record_loaded({});
+ return;
+ }
+ this.form.on_record_loaded(records[0]);
+ _(records.slice(1)).each(function (record, index) {
+ this.dataset.index = index+1;
+ this.form.reposition($('<div>').appendTo(this.$element));
+ this.form.on_record_loaded(record);
+ }, this);
+ }
+ });
+};