[MERGE] Merge from openerp-web.

bzr revid: jra@tinyerp.com-20110921070239-jpcjedusne6m7sq1
This commit is contained in:
Jiten (OpenERP)
2011-09-21 12:32:39 +05:30
36 changed files with 1557 additions and 561 deletions
+1
View File
@@ -9,3 +9,4 @@ RE:^include/
RE:^share/
RE:^man/
RE:^lib/
logging.cfg
+2 -6
View File
@@ -5,7 +5,7 @@ import logging
_logger = logging.getLogger(__name__)
try:
def wsgi_postload():
import openerp.wsgi
import os
import tempfile
@@ -17,16 +17,12 @@ try:
o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions")
o.addons_path = os.path.dirname(os.path.dirname(__file__))
o.serve_static = True
o.server_host = '127.0.0.1'
o.server_port = 8069
o.backend = 'local'
app = common.dispatch.Root(o)
#import openerp.wsgi
openerp.wsgi.register_wsgi_handler(app)
except ImportError:
_logger.info("standalone mode")
# TODO
# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point
+4 -4
View File
@@ -32,10 +32,10 @@
"static/src/js/views.js",
"static/src/js/data.js",
"static/src/js/data_export.js",
"static/src/js/form.js",
"static/src/js/list.js",
"static/src/js/list-editable.js",
"static/src/js/search.js",
"static/src/js/view_form.js",
"static/src/js/view_list.js",
"static/src/js/view_list_editable.js",
"static/src/js/view_tree.js",
],
'css' : [
@@ -45,5 +45,5 @@
"static/src/css/base.css",
"static/src/css/data_export.css",
],
'wsgi' : 'app',
'post_load' : 'wsgi_postload',
}
-2
View File
@@ -1,4 +1,2 @@
#!/usr/bin/python
# TODO if from openerpserver use backendlocal
# from backendlocal import *
from dispatch import *
-23
View File
@@ -1,23 +0,0 @@
#----------------------------------------------------------
# OpenERPSession local openerp backend access
#----------------------------------------------------------
class OpenERPUnboundException(Exception):
pass
class OpenERPConnector(object):
pass
class OpenERPAuth(object):
pass
class OpenERPModel(object):
def __init__(self, session, model):
self._session = session
self._model = model
def __getattr__(self, name):
return lambda *l:self._session.execute(self._model, name, *l)
class OpenERPSession(object):
def __init__(self, model_factory=OpenERPModel):
pass
+3 -2
View File
@@ -22,7 +22,8 @@ import ast
import nonliterals
import http
# import backendlocal as backend
import backendrpc as backend
import session as backend
import openerplib
__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
'WebRequest', 'JsonRequest', 'HttpRequest']
@@ -166,7 +167,7 @@ class JsonRequest(WebRequest):
_logger.debug("--> %s.%s\n%s", controller.__class__.__name__, method.__name__, pprint.pformat(self.jsonrequest))
response['id'] = self.jsonrequest.get('id')
response["result"] = method(controller, self, **self.params)
except backend.OpenERPUnboundException:
except openerplib.AuthenticationError:
error = {
'code': 100,
'message': "OpenERP Session Invalid",
+4 -3
View File
@@ -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)
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) Stephane Wirtel
# Copyright (C) 2011 Nicolas Vanhoren
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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.
#
##############################################################################
from main import *
+97
View File
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) Stephane Wirtel
# Copyright (C) 2011 Nicolas Vanhoren
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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.
#
##############################################################################
import datetime
DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
DEFAULT_SERVER_DATE_FORMAT,
DEFAULT_SERVER_TIME_FORMAT)
def str_to_datetime(str):
"""
Converts a string to a datetime object using OpenERP's
datetime string format (exemple: '2011-12-01 15:12:35').
No timezone information is added, the datetime is a naive instance, but
according to OpenERP 6.1 specification the timezone is always UTC.
"""
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_DATETIME_FORMAT)
def str_to_date(str):
"""
Converts a string to a date object using OpenERP's
date string format (exemple: '2011-12-01').
"""
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_DATE_FORMAT).date()
def str_to_time(str):
"""
Converts a string to a time object using OpenERP's
time string format (exemple: '15:12:35').
"""
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_TIME_FORMAT).time()
def datetime_to_str(obj):
"""
Converts a datetime object to a string using OpenERP's
datetime string format (exemple: '2011-12-01 15:12:35').
The datetime instance should not have an attached timezone and be in UTC.
"""
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
def date_to_str(obj):
"""
Converts a date object to a string using OpenERP's
date string format (exemple: '2011-12-01').
"""
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_DATE_FORMAT)
def time_to_str(obj):
"""
Converts a time object to a string using OpenERP's
time string format (exemple: '15:12:35').
"""
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_TIME_FORMAT)
+399
View File
@@ -0,0 +1,399 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) Stephane Wirtel
# Copyright (C) 2011 Nicolas Vanhoren
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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.
#
##############################################################################
"""
OpenERP Client Library
Home page: http://pypi.python.org/pypi/openerp-client-lib
Code repository: https://code.launchpad.net/~niv-openerp/openerp-client-lib/trunk
"""
import xmlrpclib
import logging
import socket
try:
import cPickle as pickle
except ImportError:
import pickle
try:
import cStringIO as StringIO
except ImportError:
import StringIO
_logger = logging.getLogger(__name__)
def _getChildLogger(logger, subname):
return logging.getLogger(logger.name + "." + subname)
class Connector(object):
"""
The base abstract class representing a connection to an OpenERP Server.
"""
__logger = _getChildLogger(_logger, 'connector')
def __init__(self, hostname, port):
"""
Initilize by specifying an hostname and a port.
:param hostname: Host name of the server.
:param port: Port for the connection to the server.
"""
self.hostname = hostname
self.port = port
class XmlRPCConnector(Connector):
"""
A type of connector that uses the XMLRPC protocol.
"""
PROTOCOL = 'xmlrpc'
__logger = _getChildLogger(_logger, 'connector.xmlrpc')
def __init__(self, hostname, port=8069):
"""
Initialize by specifying the hostname and the port.
:param hostname: The hostname of the computer holding the instance of OpenERP.
:param port: The port used by the OpenERP instance for XMLRPC (default to 8069).
"""
Connector.__init__(self, hostname, port)
self.url = 'http://%s:%d/xmlrpc' % (self.hostname, self.port)
def send(self, service_name, method, *args):
url = '%s/%s' % (self.url, service_name)
service = xmlrpclib.ServerProxy(url)
return getattr(service, method)(*args)
class NetRPC_Exception(Exception):
"""
Exception for NetRPC errors.
"""
def __init__(self, faultCode, faultString):
self.faultCode = faultCode
self.faultString = faultString
self.args = (faultCode, faultString)
class NetRPC(object):
"""
Low level class for NetRPC protocol.
"""
def __init__(self, sock=None):
if sock is None:
self.sock = socket.socket(
socket.AF_INET, socket.SOCK_STREAM)
else:
self.sock = sock
self.sock.settimeout(120)
def connect(self, host, port=False):
if not port:
buf = host.split('//')[1]
host, port = buf.split(':')
self.sock.connect((host, int(port)))
def disconnect(self):
self.sock.shutdown(socket.SHUT_RDWR)
self.sock.close()
def mysend(self, msg, exception=False, traceback=None):
msg = pickle.dumps([msg,traceback])
size = len(msg)
self.sock.send('%8d' % size)
self.sock.send(exception and "1" or "0")
totalsent = 0
while totalsent < size:
sent = self.sock.send(msg[totalsent:])
if sent == 0:
raise RuntimeError, "socket connection broken"
totalsent = totalsent + sent
def myreceive(self):
buf=''
while len(buf) < 8:
chunk = self.sock.recv(8 - len(buf))
if chunk == '':
raise RuntimeError, "socket connection broken"
buf += chunk
size = int(buf)
buf = self.sock.recv(1)
if buf != "0":
exception = buf
else:
exception = False
msg = ''
while len(msg) < size:
chunk = self.sock.recv(size-len(msg))
if chunk == '':
raise RuntimeError, "socket connection broken"
msg = msg + chunk
msgio = StringIO.StringIO(msg)
unpickler = pickle.Unpickler(msgio)
unpickler.find_global = None
res = unpickler.load()
if isinstance(res[0],Exception):
if exception:
raise NetRPC_Exception(str(res[0]), str(res[1]))
raise res[0]
else:
return res[0]
class NetRPCConnector(Connector):
"""
A type of connector that uses the NetRPC protocol.
"""
PROTOCOL = 'netrpc'
__logger = _getChildLogger(_logger, 'connector.netrpc')
def __init__(self, hostname, port=8070):
"""
Initialize by specifying the hostname and the port.
:param hostname: The hostname of the computer holding the instance of OpenERP.
:param port: The port used by the OpenERP instance for NetRPC (default to 8070).
"""
Connector.__init__(self, hostname, port)
def send(self, service_name, method, *args):
socket = NetRPC()
socket.connect(self.hostname, self.port)
socket.mysend((service_name, method, )+args)
result = socket.myreceive()
socket.disconnect()
return result
class Service(object):
"""
A class to execute RPC calls on a specific service of the remote server.
"""
def __init__(self, connector, service_name):
"""
:param connector: A valid Connector instance.
:param service_name: The name of the service on the remote server.
"""
self.connector = connector
self.service_name = service_name
self.__logger = _getChildLogger(_getChildLogger(_logger, 'service'),service_name)
def __getattr__(self, method):
"""
:param method: The name of the method to execute on the service.
"""
self.__logger.debug('method: %r', method)
def proxy(*args):
"""
:param args: A list of values for the method
"""
self.__logger.debug('args: %r', args)
result = self.connector.send(self.service_name, method, *args)
self.__logger.debug('result: %r', result)
return result
return proxy
class Connection(object):
"""
A class to represent a connection with authentication to an OpenERP Server.
It also provides utility methods to interact with the server more easily.
"""
__logger = _getChildLogger(_logger, 'connection')
def __init__(self, connector,
database=None,
login=None,
password=None,
user_id=None):
"""
Initialize with login information. The login information is facultative to allow specifying
it after the initialization of this object.
:param connector: A valid Connector instance to send messages to the remote server.
:param database: The name of the database to work on.
:param login: The login of the user.
:param password: The password of the user.
:param user_id: The user id is a number identifying the user. This is only useful if you
already know it, in most cases you don't need to specify it.
"""
self.connector = connector
self.set_login_info(database, login, password, user_id)
def set_login_info(self, database, login, password, user_id=None):
"""
Set login information after the initialisation of this object.
:param connector: A valid Connector instance to send messages to the remote server.
:param database: The name of the database to work on.
:param login: The login of the user.
:param password: The password of the user.
:param user_id: The user id is a number identifying the user. This is only useful if you
already know it, in most cases you don't need to specify it.
"""
self.database, self.login, self.password = database, login, password
self.user_id = user_id
def check_login(self, force=True):
"""
Checks that the login information is valid. Throws an AuthenticationError if the
authentication fails.
:param force: Force to re-check even if this Connection was already validated previously.
Default to True.
"""
if self.user_id and not force:
return
if not self.database or not self.login or self.password is None:
raise AuthenticationError("Creditentials not provided")
self.user_id = self.get_service("common").login(self.database, self.login, self.password)
if not self.user_id:
raise AuthenticationError("Authentication failure")
self.__logger.debug("Authenticated with user id %s", self.user_id)
def get_model(self, model_name):
"""
Returns a Model instance to allow easy remote manipulation of an OpenERP model.
:param model_name: The name of the model.
"""
return Model(self, model_name)
def get_service(self, service_name):
"""
Returns a Service instance to allow easy manipulation of one of the services offered by the remote server.
Please note this Connection instance does not need to have valid authentication information since authentication
is only necessary for the "object" service that handles models.
:param service_name: The name of the service.
"""
return Service(self.connector, service_name)
class AuthenticationError(Exception):
"""
An error thrown when an authentication to an OpenERP server failed.
"""
pass
class Model(object):
"""
Useful class to dialog with one of the models provided by an OpenERP server.
An instance of this class depends on a Connection instance with valid authentication information.
"""
def __init__(self, connection, model_name):
"""
:param connection: A valid Connection instance with correct authentication information.
:param model_name: The name of the model.
"""
self.connection = connection
self.model_name = model_name
self.__logger = _getChildLogger(_getChildLogger(_logger, 'object'), model_name)
def __getattr__(self, method):
"""
Provides proxy methods that will forward calls to the model on the remote OpenERP server.
:param method: The method for the linked model (search, read, write, unlink, create, ...)
"""
def proxy(*args):
"""
:param args: A list of values for the method
"""
self.connection.check_login(False)
self.__logger.debug(args)
result = self.connection.get_service('object').execute(
self.connection.database,
self.connection.user_id,
self.connection.password,
self.model_name,
method,
*args)
if method == "read":
if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
index = {}
for r in result:
index[r['id']] = r
result = [index[x] for x in args[0]]
self.__logger.debug('result: %r', result)
return result
return proxy
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, context=None):
"""
A shortcut method to combine a search() and a read().
:param domain: The domain for the search.
:param fields: The fields to extract (can be None or [] to extract all fields).
:param offset: The offset for the rows to read.
:param limit: The maximum number of rows to read.
:param order: The order to class the rows.
:param context: The context.
:return: A list of dictionaries containing all the specified fields.
"""
record_ids = self.search(domain or [], offset, limit or False, order or False, context or {})
records = self.read(record_ids, fields or [], context or {})
return records
def get_connector(hostname, protocol="xmlrpc", port="auto"):
"""
A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC.
:param hostname: The hostname to the remote server.
:param protocol: The name of the protocol, must be "xmlrpc" or "netrpc".
:param port: The number of the port. Defaults to auto.
"""
if port == 'auto':
port = 8069 if protocol=="xmlrpc" else 8070
if protocol == "xmlrpc":
return XmlRPCConnector(hostname, port)
elif protocol == "netrpc":
return NetRPCConnector(hostname, port)
else:
raise ValueError("You must choose xmlrpc or netrpc")
def get_connection(hostname, 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.
:param hostname: The hostname to the remote server.
:param protocol: The name of the protocol, must be "xmlrpc" or "netrpc".
:param port: The number of the port. Defaults to auto.
:param connector: A valid Connector instance to send messages to the remote server.
:param database: The name of the database to work on.
:param login: The login of the user.
:param password: The password of the user.
:param user_id: The user id is a number identifying the user. This is only useful if you
already know it, in most cases you don't need to specify it.
"""
return Connection(get_connector(hostname, protocol, port), database, login, password, user_id)
@@ -2,28 +2,12 @@
import datetime
import dateutil.relativedelta
import time
import xmlrpclib
import openerplib
import nonliterals
#----------------------------------------------------------
# OpenERPSession RPC openerp backend access
#----------------------------------------------------------
class OpenERPUnboundException(Exception):
pass
class OpenERPConnector(object):
pass
class OpenERPAuth(object):
pass
class OpenERPModel(object):
def __init__(self, session, model):
self._session = session
self._model = model
def __getattr__(self, name):
return lambda *l:self._session.execute(self._model, name, *l)
class OpenERPSession(object):
"""
@@ -42,14 +26,13 @@ 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, model_factory=OpenERPModel):
def __init__(self, server='127.0.0.1', port=8069):
self._server = server
self._port = port
self._db = False
self._uid = False
self._login = False
self._password = False
self.model_factory = model_factory
self._locale = 'en_US'
self.context = {}
self.contexts_store = {}
@@ -57,34 +40,38 @@ class OpenERPSession(object):
self._lang = {}
self.remote_timezone = 'utc'
self.client_timezone = False
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)
def proxy(self, service):
s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service))
return s
return self.build_connection().get_service(service)
def bind(self, db, uid, password):
def bind(self, db, uid, login, password):
self._db = db
self._uid = uid
self._login = login
self._password = password
def login(self, db, login, password):
uid = self.proxy('common').login(db, login, password)
self.bind(db, uid, password)
self._login = login
self.bind(db, uid, login, password)
if uid: self.get_context()
return uid
def assert_valid(self):
def assert_valid(self, force=False):
"""
Ensures this session is valid (logged into the openerp server)
"""
if not (self._db and self._uid and self._password):
raise OpenERPUnboundException()
self.build_connection().check_login(force)
def execute(self, model, func, *l, **d):
self.assert_valid()
r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
model = self.build_connection().get_model(model)
r = getattr(model, func)(*l, **d)
return r
def exec_workflow(self, model, id, signal):
@@ -97,9 +84,9 @@ class OpenERPSession(object):
:param model: an OpenERP model name
:type model: str
:rtype: :class:`openerpweb.openerpweb.OpenERPModel`
:rtype: a model object
"""
return self.model_factory(self, model)
return self.build_connection().get_model(model)
def get_context(self):
""" Re-initializes the current user's session context (based on
+10 -1
View File
@@ -296,7 +296,16 @@ class Session(openerpweb.Controller):
return {
"session_id": req.session_id,
"uid": req.session._uid,
"context": ctx
"context": ctx,
"db": req.session._db
}
@openerpweb.jsonrequest
def get_session_info(self, req):
req.session.assert_valid(force=True)
return {
"uid": req.session._uid,
"context": req.session.get_context() if req.session._uid else False,
"db": req.session._db
}
@openerpweb.jsonrequest
def change_password (self,req,fields):
+131 -65
View File
@@ -221,17 +221,11 @@ label.error {
height: 100%;
background: #f0eeee;
}
.openerp .oe-application-container {
height: 100%;
}
/* Menu */
.openerp .sf-menu {
margin-bottom: 0;
}
/*
.sf-menu a {
padding: 5px 5px;
}
*/
.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 h3 {
padding: 0 0 2px;
.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 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 */
@@ -821,6 +833,7 @@ label.error {
}
.openerp td.oe_form_frame_cell {
padding: 2px;
position: relative;
}
.openerp td.oe_form_frame_cell.oe_form_group {
padding: 0;
@@ -841,8 +854,8 @@ label.error {
.openerp label.oe_label_help {
cursor: help;
}
.openerp label.oe_label,
.openerp label.oe_label_help {
.openerp .oe_form_field label.oe_label, .openerp .oe_form_field label.oe_label_help {
text-align: right;
margin: 3px 0 0 10px;
}
@@ -887,12 +900,19 @@ label.error {
position: relative;
vertical-align: top;
}
.openerp img.ui-datepicker-trigger {
margin-left: -20px;
vertical-align: middle;
.openerp .oe_input_icon {
position: absolute;
cursor: pointer;
position: relative;
top: -1px;
right: 5px;
top: 5px;
}
.openerp .oe_input_icon_disabled {
position: absolute;
cursor: default;
opacity: 0.5;
filter:alpha(opacity=50);
right: 5px;
top: 5px;
}
.openerp img.oe_field_translate {
margin-left: -21px;
@@ -987,7 +1007,53 @@ label.error {
.openerp .view-manager-main-content {
width: 100%;
padding: 6px;
}
.openerp .oe-view-manager-header {
overflow: auto;
}
.openerp .oe-view-manager-header h2 {
float: left;
}
.openerp .oe-view-manager-header blockquote {
display: none;
font-size: 85%;
margin: 0;
background: #fff;
border-bottom: 1px solid #CECBCB;
padding: 1px 10px;
color: #4C4C4C;
}
.openerp .oe-view-manager-header blockquote p {
margin: 0;
padding: 6px 1px 4px;
}
.openerp .oe-view-manager-header blockquote div {
text-align: right;
}
.openerp .oe-view-manager-header blockquote div button {
border: none;
background: none;
padding: 0 4px;
margin: 0;
display: inline;
text-decoration: underline;
color: inherit;
}
.openerp .oe-view-manager-logs {
clear: both;
font-size: 85%;
margin: 0.25em 0;
background: #fff;
padding: 0 10px;
color: #4C4C4C;
list-style: none;
}
.openerp .oe-view-manager-logs li:before {
content: '→ ';
}
.openerp .oe-view-manager-logs a {
text-decoration: none;
color: inherit;
}
.openerp .view-manager-main-sidebar {
+146 -56
View File
@@ -581,24 +581,29 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
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() {
this._super();
},
do_update: function () {
var self = this;
this.$content.remove();
if (! this.session.uid)
return;
var func = new openerp.web.Model(self.session, "res.users").get_func("read");
func(self.session.uid, ["name", "company_id"]).then(function(res) {
self.$content = $(QWeb.render("Header-content", {widget: self, user: res}));
self.$content.appendTo(self.$element);
self.$element.find(".logout").click(self.on_logout);
self.$element.find("a.preferences").click(self.on_preferences);
self.$element.find(".about").click(self.on_about);
self.shortcut_load();
});
var fct = function() {
self.$content.remove();
if (!self.session.uid)
return;
var func = new openerp.web.Model(self.session, "res.users").get_func("read");
return func(self.session.uid, ["name", "company_id"]).pipe(function(res) {
self.$content = $(QWeb.render("Header-content", {widget: self, user: res}));
self.$content.appendTo(self.$element);
self.$element.find(".logout").click(self.on_logout);
self.$element.find("a.preferences").click(self.on_preferences);
self.$element.find(".about").click(self.on_about);
return self.shortcut_load();
});
};
this.update_promise = this.update_promise.pipe(fct, fct);
},
on_about: function() {
var self = this;
@@ -771,70 +776,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');
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 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;
@@ -873,7 +940,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
// Do you autorize this ? will be replaced by notify() in controller
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.header.on_logout.add(this.login.on_logout);
@@ -886,6 +952,11 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
this.menu = new openerp.web.Menu(this, "oe_menu", "oe_secondary_menu");
this.menu.on_action.add(this.on_menu_action);
this.url_internal_hashchange = false;
this.url_external_hashchange = false;
jQuery(window).bind('hashchange', this.on_url_hashchange);
},
start: function() {
this.header.appendTo($("#oe_header"));
@@ -899,6 +970,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
this.action_manager.stop();
this.action_manager = new openerp.web.ActionManager(this);
this.action_manager.appendTo($("#oe_app"));
this.action_manager.do_url_set_hash.add_last(this.do_url_set_hash);
// if using saved actions, load the action and give it to action manager
var parameters = jQuery.deparam(jQuery.param.querystring());
@@ -925,7 +997,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
load_url_state: function () {
var self = this;
// TODO: add actual loading if there is url state to unpack, test on window.location.hash
// not logged in
if (!this.session.uid) { return; }
var ds = new openerp.web.DataSetSearch(this, 'res.users');
@@ -961,6 +1032,25 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
self.action_manager.do_action(action);
});
},
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);
}
},
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;
}
},
on_menu_action: function(action) {
this.action_manager.do_action(action);
},
+79 -64
View File
@@ -2,6 +2,10 @@
* OpenERP Web core
*--------------------------------------------------------*/
if (!console.debug) {
console.debug = console.log;
}
openerp.web.core = function(openerp) {
openerp.web.qweb = new QWeb2.Engine();
openerp.web.qweb.debug = (window.location.search.indexOf('?debug') !== -1);
@@ -349,12 +353,10 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
this.port = (port == undefined) ? location.port : port;
this.rpc_mode = (server == location.hostname) ? "ajax" : "jsonp";
this.debug = (window.location.search.indexOf('?debug') !== -1);
this.db = "";
this.login = "";
this.password = "";
this.user_context= {};
this.uid = false;
this.session_id = false;
this.uid = false;
this.user_context= {};
this.db = false;
this.module_list = [];
this.module_loaded = {"web": true};
this.context = {};
@@ -474,58 +476,47 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
},
session_login: function(db, login, password, success_callback) {
var self = this;
this.db = db;
this.login = login;
this.password = password;
var params = { db: this.db, login: this.login, password: this.password };
var params = { db: db, login: login, password: password };
this.rpc("/web/session/login", params, function(result) {
self.session_id = result.session_id;
self.uid = result.uid;
self.user_context = result.context;
self.db = result.db;
self.session_save();
self.on_session_valid();
return true;
}).then(success_callback);
},
session_logout: function() {
this.uid = false;
},
/**
* Reloads uid and session_id from local storage, if they exist
*/
session_restore: function () {
this.uid = this.get_cookie('uid');
var self = this;
this.session_id = this.get_cookie('session_id');
this.db = this.get_cookie('db');
this.login = this.get_cookie('login');
this.user_context = this.get_cookie("user_context");
// we should do an rpc to confirm that this session_id is valid and if it is retrieve the information about db and login
// then call on_session_valid
if (this.uid)
this.on_session_valid();
else
this.on_session_invalid();
return this.rpc("/web/session/get_session_info", {}).then(function(result) {
self.uid = result.uid;
self.user_context = result.context;
self.db = result.db;
if (self.uid)
self.on_session_valid();
else
self.on_session_invalid();
}, function() {
self.on_session_invalid();
});
},
/**
* Saves the session id and uid locally
*/
session_save: function () {
this.set_cookie('uid', this.uid);
this.set_cookie('session_id', this.session_id);
this.set_cookie('db', this.db);
this.set_cookie('login', this.login);
this.set_cookie('user_context', this.user_context);
},
logout: function() {
delete this.uid;
delete this.session_id;
delete this.db;
delete this.login;
this.set_cookie('uid', '');
this.set_cookie('session_id', '');
this.set_cookie('db', '');
this.set_cookie('login', '');
this.on_session_invalid(function() {});
this.reload_client();
},
reload_client: function() {
window.location.reload();
},
/**
* Fetches a cookie stored by an openerp session
@@ -749,39 +740,62 @@ openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp
*/
rpc: function(url, data, success, error) {
return this.session.rpc(url, data, success, error);
},
log: function() {
var args = Array.prototype.slice.call(arguments);
var caller = arguments.callee.caller;
// TODO add support for line number using
// https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js
// args.unshift("" + caller.debug_name);
this.on_log.apply(this,args);
},
on_log: function() {
if(this.session.debug) {
var notify = false;
var body = false;
if(window.console) {
console.log(arguments);
} else {
body = true;
}
var a = Array.prototype.slice.call(arguments, 0);
for(var i = 0; i < a.length; i++) {
var v = a[i]==null ? "null" : a[i].toString();
if(i==0) {
notify = v.match(/^not/);
body = v.match(/^bod/);
}
if(body) {
$('<pre></pre>').text(v).appendTo($('body'));
}
}
}
}
});
/**
* Base class for all visual components. Provides a lot of functionalities helpful
* for the management of a part of the DOM.
*
* Widget handles:
* - Rendering with QWeb.
* - Life-cycle management and parenting (when a parent is destroyed, all its children are
* destroyed too).
* - Insertion in DOM.
*
* Widget also extends SessionAware for ease of use.
*
* Guide to create implementations of the Widget class:
* ==============================================
*
* Here is a sample child class:
*
* MyWidget = openerp.base.Widget.extend({
* // the name of the QWeb template to use for rendering
* template: "MyQWebTemplate",
* // identifier prefix, it is useful to put an obvious one for debugging
* identifier_prefix: 'my-id-prefix-',
*
* init: function(parent) {
* this._super(parent);
* // 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 * /);
*
* // if you have some asynchronous operations, it's a good idea to return
* // a promise in start()
* var promise = this.rpc(...);
* return promise;
* }
* });
*
* Now this class can simply be used with the following syntax:
*
* var my_widget = new MyWidget(this);
* my_widget.appendTo($(".some-div"));
*
* With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
* DOM inside the ".some-div" div and its events were binded.
*
* And of course, when you don't need that widget anymore, just do:
*
* my_widget.stop();
*
* That will kill the widget in a clean way and erase its content from the dom.
*/
openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widget# */{
/**
* The name of the QWeb template that will be used for rendering. Must be
@@ -929,6 +943,7 @@ 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);
}
+1 -1
View File
@@ -356,7 +356,7 @@ openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet
* @returns {$.Deferred}
*/
write: function (id, data, options, callback, error_callback) {
var options = options || {};
options = options || {};
return this.rpc('/web/dataset/save', {
model: this.model,
id: id,
+6 -1
View File
@@ -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;
+72 -43
View File
@@ -2,6 +2,7 @@ openerp.web.search = function(openerp) {
var QWeb = openerp.web.qweb;
openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
template: "EmptyComponent",
/**
* @constructs openerp.web.SearchView
* @extends openerp.web.Widget
@@ -12,8 +13,8 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
* @param view_id
* @param defaults
*/
init: function(parent, element_id, dataset, view_id, defaults) {
this._super(parent, element_id);
init: function(parent, dataset, view_id, defaults) {
this._super(parent);
this.dataset = dataset;
this.model = dataset.model;
this.view_id = view_id;
@@ -28,7 +29,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
this.ready = $.Deferred();
},
start: function() {
//this.log('Starting SearchView '+this.model+this.view_id)
this._super();
this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
return this.ready.promise();
},
@@ -326,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');
@@ -464,30 +466,6 @@ openerp.web.search.Widget = openerp.web.Widget.extend( /** @lends openerp.web.se
}));
}
});
openerp.web.search.FilterGroup = openerp.web.search.Widget.extend(/** @lends openerp.web.search.FilterGroup# */{
template: 'SearchView.filters',
/**
* Inclusive group of filters, creates a continuous "button" with clickable
* sections (the normal display for filters is to be a self-contained button)
*
* @constructs openerp.web.search.FilterGroup
* @extends openerp.web.search.Widget
*
* @param {Array<openerp.web.search.Filter>} filters elements of the group
* @param {openerp.web.SearchView} view view in which the filters are contained
*/
init: function (filters, view) {
this._super(view);
this.filters = filters;
this.length = filters.length;
},
start: function () {
this._super();
_.each(this.filters, function (filter) {
filter.start();
});
}
});
openerp.web.search.add_expand_listener = function($root) {
$root.find('a.searchview_group_string').click(function (e) {
$root.toggleClass('folded expanded');
@@ -534,6 +512,50 @@ openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp.
"get_domain not implemented for widget " + this.attrs.type);
}
});
openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends openerp.web.search.FilterGroup# */{
template: 'SearchView.filters',
/**
* Inclusive group of filters, creates a continuous "button" with clickable
* sections (the normal display for filters is to be a self-contained button)
*
* @constructs openerp.web.search.FilterGroup
* @extends openerp.web.search.Input
*
* @param {Array<openerp.web.search.Filter>} filters elements of the group
* @param {openerp.web.SearchView} view view in which the filters are contained
*/
init: function (filters, view) {
this._super(view);
this.filters = filters;
this.length = filters.length;
},
start: function () {
this._super();
_.each(this.filters, function (filter) {
filter.start();
});
},
get_context: function () { },
/**
* Handles domains-fetching for all the filters within it: groups them.
*/
get_domain: function () {
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; }
if (domains.length === 1) { return domains[0]; }
for (var i=domains.length; --i;) {
domains.unshift(['|']);
}
return _.extend(new openerp.web.CompoundDomain(), {
__domains: domains
});
}
});
openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{
template: 'SearchView.filter',
/**
@@ -587,12 +609,11 @@ openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.w
}
return this.attrs.context;
},
get_domain: function () {
if (!this.is_enabled()) {
return;
}
return this.attrs.domain;
}
/**
* Does not return anything: filter domain is handled at the FilterGroup
* level
*/
get_domain: function () { }
});
openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ {
template: 'SearchView.field',
@@ -701,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;
}
}
});
/**
@@ -711,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;
}
}
});
/**
@@ -1049,7 +1078,7 @@ openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/**
throw e;
}
type = "char";
this.log('Unknow field type ' + e.key);
console.log('Unknow field type ' + e.key);
}
this.value = new (openerp.web.search.custom_filters.get_object(type))
(this);
@@ -1149,11 +1178,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({
@@ -1168,11 +1197,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({
@@ -10,24 +10,23 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
* view should be displayed (if there is one active).
*/
searchable: false,
template: "FormView",
form_template: "FormView",
/**
* @constructs openerp.web.FormView
* @extends openerp.web.View
*
* @param {openerp.web.Session} session the current openerp session
* @param {String} element_id this view's root element id
* @param {openerp.web.DataSet} dataset the dataset this view will work with
* @param {String} view_id the identifier of the OpenERP view object
*
* @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance
*/
init: function(parent, element_id, dataset, view_id, options) {
this._super(parent, element_id);
init: function(parent, dataset, view_id, options) {
this._super(parent);
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;
@@ -35,7 +34,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.datarecord = {};
this.ready = false;
this.show_invalid = true;
this.dirty = false;
this.dirty_for_user = false;
this.default_focus_field = null;
this.default_focus_button = null;
this.registry = openerp.web.form.widgets;
@@ -45,7 +44,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
_.defaults(this.options, {"always_show_new_button": true});
},
start: function() {
//this.log('Starting FormView '+this.model+this.view_id)
this._super();
return this.init_view();
},
init_view: function() {
if (this.embedded_view) {
var def = $.Deferred().then(this.on_loaded);
var self = this;
@@ -76,7 +78,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.fields_view = data;
var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this }));
this.$element.html(QWeb.render(this.form_template, { 'frame': frame, 'view': this }));
_.each(this.widgets, function(w) {
w.start();
});
@@ -90,12 +92,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.$form_header.find('button.oe_form_button_save_edit').click(this.do_save_edit);
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);
if (this.session.debug) {
this.$form_header.find('button.oe_get_xml_view').click(function() {
$('<xmp>' + openerp.web.json_node_to_xml(self.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600});
});
}
this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate);
if (this.options.sidebar && this.options.sidebar_id) {
this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
@@ -142,7 +139,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
this.$form_header.find('.oe_form_on_update').show();
this.$form_header.find('button.oe_form_button_new').show();
}
this.dirty = false;
this.dirty_for_user = false;
this.datarecord = record;
for (var f in this.fields) {
var field = this.fields[f];
@@ -152,7 +149,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
}
if (!record.id) {
// New record: Second pass in order to trigger the onchanges
this.dirty = true;
this.show_invalid = false;
for (var f in record) {
var field = this.fields[f];
@@ -181,21 +177,23 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
}
},
on_pager_action: function(action) {
switch (action) {
case 'first':
this.dataset.index = 0;
break;
case 'previous':
this.dataset.previous();
break;
case 'next':
this.dataset.next();
break;
case 'last':
this.dataset.index = this.dataset.ids.length - 1;
break;
if (this.can_be_discarded()) {
switch (action) {
case 'first':
this.dataset.index = 0;
break;
case 'previous':
this.dataset.previous();
break;
case 'next':
this.dataset.next();
break;
case 'last':
this.dataset.index = this.dataset.ids.length - 1;
break;
}
this.reload();
}
this.reload();
},
do_update_pager: function(hide_index) {
var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager');
@@ -262,7 +260,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
self.on_processed_onchange(response, processed);
});
} else {
this.log("Wrong on_change format", on_change);
console.log("Wrong on_change format", on_change);
}
}
},
@@ -277,7 +275,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
processed.push(field.name);
if (field.get_value() != value) {
field.set_value(value);
field.dirty = true;
field.dirty = this.dirty_for_user = true;
if (_.indexOf(processed, field.name) < 0) {
this.do_onchange(field, processed);
}
@@ -305,13 +303,37 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
var self = this;
var def = $.Deferred();
$.when(this.has_been_loaded).then(function() {
self.dataset.default_get(
_.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() {
def.resolve();
if (self.can_be_discarded()) {
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();
},
on_button_duplicate: function() {
var self = this;
var def = $.Deferred();
$.when(this.has_been_loaded).then(function() {
if (self.can_be_discarded()) {
self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
return self.on_created({ result : new_id });
}).then(function() {
def.resolve();
});
}
});
return def.promise();
},
can_be_discarded: function() {
return !this.dirty_for_user || confirm(_t("Warning, the record has been modified, your changes will be discarded."));
},
/**
* Triggers saving the form's record. Chooses between creating a new
* record or saving an existing one depending on whether the record
@@ -346,8 +368,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
first_invalid_field.focus();
this.on_invalid();
return false;
} else if (form_dirty) {
this.log("About to save", values);
} 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);
@@ -357,11 +379,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
self.on_saved(r, success);
});
}
} else {
setTimeout(function() {
self.on_saved({ result: true }, success);
});
return true;
}
},
do_save_edit: function() {
@@ -386,7 +403,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
if (!r.result) {
// should not happen in the server, but may happen for internal purpose
} else {
console.debug(_.sprintf("The record #%s has been saved.", this.datarecord.id));
if (success) {
success(r);
}
@@ -467,11 +483,11 @@ openerp.web.FormDialog = openerp.web.Dialog.extend({
},
start: function() {
this._super();
this.form = new openerp.web.FormView(this, this.element_id, this.dataset, this.view_id, {
this.form = new openerp.web.FormView(this, this.dataset, this.view_id, {
sidebar: false,
pager: false
});
this.form.start();
this.form.appendTo(this.$element);
this.form.on_created.add_last(this.on_form_dialog_saved);
this.form.on_saved.add_last(this.on_form_dialog_saved);
return this;
@@ -595,7 +611,7 @@ openerp.web.form.compute_domain = function(expr, fields) {
stack.push(!_(val).contains(field_value));
break;
default:
this.log("Unsupported operator in modifiers :", op);
console.log("Unsupported operator in modifiers :", op);
}
}
return _.all(stack, _.identity);
@@ -816,7 +832,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
},
on_click: function(saved) {
var self = this;
if (!this.node.attrs.special && this.view.dirty && saved !== true) {
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);
});
@@ -843,7 +859,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
on_confirmed: function() {
var self = this;
this.view.execute_action(
this.view.do_execute_action(
this.node.attrs, this.view.dataset, this.view.datarecord.id, function () {
self.view.reload();
});
@@ -880,7 +896,7 @@ openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
var self = this;
this.$element.find("label").dblclick(function() {
var widget = self['for'] || self;
self.log(widget.element_id , widget);
console.log(widget.element_id , widget);
window.w = widget;
});
}
@@ -945,7 +961,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();
@@ -964,7 +980,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f
}
},
on_ui_change: function() {
this.dirty = this.view.dirty = true;
this.dirty = this.view.dirty_for_user = true;
this.validate();
if (this.is_valid()) {
this.set_value_from_ui();
@@ -1112,37 +1128,51 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
this.jqueryui_object = 'datetimepicker';
},
start: function() {
var self = this;
this._super.apply(this, arguments);
this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({
dateFormat: 'yy-mm-dd',
timeFormat: 'hh:mm:ss',
showOn: 'button',
buttonImage: '/web/static/src/img/ui/field_calendar.png',
buttonImageOnly: true,
constrainInput: false
this.$element.find('input').change(this.on_ui_change);
this.picker({
onSelect: this.on_picker_select,
changeMonth: true,
changeYear: true,
showWeek: true,
showButtonPanel: false
});
this.$element.find('img.oe_datepicker_trigger').click(function() {
if (!self.readonly) {
self.picker('setDate', self.value || new Date());
self.$element.find('.oe_datepicker').toggle();
}
});
this.$element.find('.ui-datepicker-inline').removeClass('ui-widget-content ui-corner-all');
this.$element.find('button.oe_datepicker_close').click(function() {
self.$element.find('.oe_datepicker').hide();
});
},
picker: function() {
return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments);
},
on_picker_select: function(text, instance) {
var date = this.picker('getDate');
this.$element.find('input').val(date ? this.format_client(date) : '').change();
},
set_value: function(value) {
this._super.apply(this, arguments);
if (!value) {
this.$element.find('input').val('');
} else {
this.$element.find('input').unbind('change');
// jQuery UI date picker wrongly call on_change event herebelow
this.$element.find('input')[this.jqueryui_object]('setDate', this.parse(value));
this.$element.find('input').change(this.on_ui_change);
}
value = this.parse(value);
this._super(value);
this.$element.find('input').val(value ? this.format_client(value) : '');
},
get_value: function() {
return this.format(this.value);
},
set_value_from_ui: function() {
this.value = this.$element.find('input')[this.jqueryui_object]('getDate') || false;
if (this.value) {
this.value = this.format(this.value);
}
var value = this.$element.find('input').val() || false;
this.value = this.parse_client(value);
this._super();
},
update_dom: function() {
this._super.apply(this, arguments);
this.$element.find('input').datepicker(this.readonly ? 'disable' : 'enable');
this.$element.find('input').attr('disabled', this.readonly);
this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', this.readonly);
},
validate: function() {
this.invalid = false;
@@ -1150,15 +1180,26 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
if (value === "") {
this.invalid = this.required;
} else {
this.invalid = !this.$element.find('input')[this.jqueryui_object]('getDate');
try {
this.parse_client(value);
this.invalid = false;
} catch(e) {
this.invalid = true;
}
}
},
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);
},
format_client: function(v) {
return openerp.web.format_value(v, this.field);
}
});
@@ -1166,6 +1207,10 @@ openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
init: function(view, node) {
this._super(view, node);
this.jqueryui_object = 'datepicker';
},
on_picker_select: function(text, instance) {
this._super(text, instance);
this.$element.find('.oe_datepicker').hide();
}
});
@@ -1980,7 +2025,7 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
self.on_ui_change();
});
this.list_view = new openerp.web.form.Many2ManyListView(this, this.list_id, this.dataset, false, {
this.list_view = new openerp.web.form.Many2ManyListView(this, this.dataset, false, {
'addable': 'Add',
'selectable': self.multi_selection
});
@@ -1989,7 +2034,7 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
self.is_started.resolve();
});
setTimeout(function () {
self.list_view.start();
self.list_view.appendTo($("#" + self.list_id));
}, 0);
},
set_value: function(value) {
@@ -2099,7 +2144,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
this.searchview.stop();
}
this.searchview = new openerp.web.SearchView(this,
this.element_id + "_search", this.dataset, false, {
this.dataset, false, {
"selectable": !this.options.disable_multiple_selection,
"deletable": false
});
@@ -2128,15 +2173,17 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
self.stop();
});
self.view_list = new openerp.web.form.SelectCreateListView(self,
self.element_id + "_view_list", self.dataset, false,
self.dataset, false,
{'deletable': false});
self.view_list.popup = self;
self.view_list.do_show();
self.view_list.start().then(function() {
self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() {
self.view_list.do_show();
}).pipe(function() {
self.searchview.do_search();
});
});
this.searchview.start();
this.searchview.appendTo($("#" + this.element_id + "_search"));
},
create_row: function(data) {
var self = this;
@@ -2163,11 +2210,11 @@ 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.element_id + "_view_form", this.dataset, false);
this.view_form = new openerp.web.FormView(this, this.dataset, false);
if (this.options.alternative_form_view) {
this.view_form.set_embedded_view(this.options.alternative_form_view);
}
this.view_form.start();
this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
this.view_form.on_loaded.add_last(function() {
var $buttons = self.view_form.$element.find(".oe_form_buttons");
$buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self}));
@@ -2272,11 +2319,11 @@ 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.element_id + "_view_form", this.dataset, false);
this.view_form = new openerp.web.FormView(this, this.dataset, false);
if (this.options.alternative_form_view) {
this.view_form.set_embedded_view(this.options.alternative_form_view);
}
this.view_form.start();
this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
this.view_form.on_loaded.add_last(function() {
var $buttons = self.view_form.$element.find(".oe_form_buttons");
$buttons.html(QWeb.render("FormOpenPopup.form.buttons"));
@@ -2507,7 +2554,7 @@ openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
});
openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
template: "FieldStatus",
template: "EmptyComponent",
start: function() {
this._super();
this.selected_value = null;
@@ -2524,6 +2571,7 @@ openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
var self = this;
var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","),
function(x) { return x.trim(); });
shown = _.select(shown, function(x) { return x.length > 0; });
if (shown.length == 0) {
this.to_show = this.field.selection;
@@ -31,7 +31,6 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
* @extends openerp.web.View
*
* @param parent parent object
* @param element_id the id of the DOM elements this view should link itself to
* @param {openerp.web.DataSet} dataset the dataset the view should work with
* @param {String} view_id the listview's identifier, if any
* @param {Object} options A set of options used to configure the view
@@ -41,12 +40,10 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
* @param {void|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button.
* @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
* @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
*
* @borrows openerp.web.ActionExecutor#execute_action as #execute_action
*/
init: function(parent, element_id, dataset, view_id, options) {
init: function(parent, dataset, view_id, options) {
var self = this;
this._super(parent, element_id);
this._super(parent);
this.set_default_options(_.extend({}, this.defaults, options || {}));
this.dataset = dataset;
this.model = dataset.model;
@@ -130,6 +127,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
* @returns {$.Deferred} loading promise
*/
start: function() {
this._super();
this.$element.addClass('oe-listview');
return this.reload_view(null, null, true);
},
@@ -159,7 +157,6 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
on_loaded: function(data, grouped) {
var self = this;
this.fields_view = data;
//this.log(this.fields_view);
this.name = "" + this.fields_view.arch.attrs.string;
this.setup_columns(this.fields_view.fields, grouped);
@@ -491,7 +488,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
return field.name === name;
});
if (!action) { return; }
this.execute_action(action, this.dataset, id, callback);
this.do_execute_action(action, this.dataset, id, callback);
},
/**
* Handles the activation of a record (clicking on it)
@@ -778,6 +775,31 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis
this.$current.empty().append(
QWeb.render('ListView.rows', _.extend({
render_cell: openerp.web.format_cell}, this)));
this.pad_table_to(5);
},
pad_table_to: function (count) {
if (this.records.length >= count ||
_(this.columns).any(function(column) { return column.meta; })) {
return;
}
var cells = [];
if (this.options.selectable) {
cells.push('<td title="selection"></td>');
}
_(this.columns).each(function(column) {
if (column.invisible !== '1') {
cells.push('<td title="' + column.string + '">&nbsp;</td>');
}
});
if (this.options.deletable) {
cells.push('<td><button type="button" style="visibility: hidden"> </button></td>');
}
cells.unshift('<tr>');
cells.push('</tr>');
var row = cells.join('');
this.$current.append(new Array(count - this.records.length + 1).join(row));
this.refresh_zebra(this.records.length);
},
/**
* Gets the ids of all currently selected records, if any
@@ -1140,12 +1162,14 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L
}
// ondrop, move relevant record & fix sequences
list.$current.sortable({
axis: 'y',
items: '> tr[data-id]',
stop: function (event, ui) {
var to_move = list.records.get(ui.item.data('id')),
target_id = ui.item.prev().data('id');
list.records.remove(to_move);
var to = target_id ? list.records.indexOf(list.records.get(target_id)) : 0;
var to = target_id ? list.records.indexOf(list.records.get(target_id)) + 1 : 0;
list.records.add(to_move, { at: to });
// resequencing time!
@@ -180,11 +180,13 @@ openerp.web.list_editable = function (openerp) {
}
self.edition = true;
self.edition_id = record_id;
self.edition_form = _.extend(new openerp.web.FormView(
self, $new_row.attr('id'), self.dataset, false), {
template: 'ListView.row.form',
registry: openerp.web.list.form.widgets
self.edition_form = _.extend(new openerp.web.ListEditableFormView(self, self.dataset, false), {
form_template: 'ListView.row.form',
registry: openerp.web.list.form.widgets,
$element: $new_row
});
// 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')
@@ -333,4 +335,11 @@ openerp.web.list_editable = function (openerp) {
});
list_form_widgets.add(key, new_path);
});
openerp.web.ListEditableFormView = openerp.web.FormView.extend({
init_view: function() {},
_render_and_insert: function () {
return this.start();
}
});
};
+2 -3
View File
@@ -19,13 +19,12 @@ openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView#
* @extends openerp.web.View
*
* @param parent
* @param element_id
* @param dataset
* @param view_id
* @param options
*/
init: function(parent, element_id, dataset, view_id, options) {
this._super(parent, element_id);
init: function(parent, dataset, view_id, options) {
this._super(parent);
this.dataset = dataset;
this.model = dataset.model;
this.view_id = view_id;
+163 -72
View File
@@ -2,17 +2,21 @@
* OpenERP web library
*---------------------------------------------------------*/
openerp.web.views = function(openerp) {
openerp.web.views = function(db) {
var _t = openerp.web._t;
var QWeb = openerp.web.qweb;
var _t = db.web._t;
/**
* Registry for all the client actions key: tag value: widget
*/
openerp.web.client_actions = new openerp.web.Registry();
db.web.client_actions = new db.web.Registry();
openerp.web.ActionManager = openerp.web.Widget.extend({
/**
* Registry for all the main views
*/
db.web.views = new db.web.Registry();
db.web.ActionManager = db.web.Widget.extend({
identifier_prefix: "actionmanager",
init: function(parent) {
this._super(parent);
@@ -20,7 +24,6 @@ openerp.web.ActionManager = openerp.web.Widget.extend({
this.dialog = null;
this.dialog_viewmanager = null;
this.client_widget = null;
this.url = {}
},
render: function() {
return "<div id='"+this.element_id+"'></div>";
@@ -44,8 +47,11 @@ openerp.web.ActionManager = openerp.web.Widget.extend({
}
},
url_update: function(action) {
var url = {};
if(action.id)
url.action_id = action.id;
// this.url = {
// "model": action.model,
// "model": action.res_model,
// "domain": action.domain,
// };
// action.res_model
@@ -56,14 +62,15 @@ openerp.web.ActionManager = openerp.web.Widget.extend({
// action.res_id
// mode
// menu
this.do_url_set_hash(url);
},
url_stringify: function(action) {
do_url_set_hash: function(url) {
},
url_parse: function(action) {
},
on_url_update: function(url) {
},
do_url_action: function(url) {
on_url_hashchange: function(url) {
var self = this;
self.rpc("/web/action/load", { action_id: url.action_id }, function(result) {
self.do_action(result.result);
});
},
do_action: function(action, on_close) {
var type = action.type.replace(/\./g,'_');
@@ -76,7 +83,7 @@ openerp.web.ActionManager = openerp.web.Widget.extend({
pager : !popup
}, action.flags || {});
if (!(type in this)) {
this.log("Action manager can't handle action of type " + action.type, action);
console.log("Action manager can't handle action of type " + action.type, action);
return;
}
this[type](action, on_close);
@@ -84,20 +91,20 @@ openerp.web.ActionManager = openerp.web.Widget.extend({
ir_actions_act_window: function (action, on_close) {
if (action.target === 'new') {
if (this.dialog == null) {
this.dialog = new openerp.web.Dialog(this, { title: action.name, width: '80%' });
this.dialog = new db.web.Dialog(this, { title: action.name, width: '80%' });
if(on_close)
this.dialog.on_close.add(on_close);
this.dialog.start();
} else {
this.dialog_viewmanager.stop();
}
this.dialog_viewmanager = new openerp.web.ViewManagerAction(this, action);
this.dialog_viewmanager = new db.web.ViewManagerAction(this, action);
this.dialog_viewmanager.appendTo(this.dialog.$element);
this.dialog.open();
} else {
this.dialog_stop();
this.content_stop();
this.inner_viewmanager = new openerp.web.ViewManagerAction(this, action);
this.inner_viewmanager = new db.web.ViewManagerAction(this, action);
this.inner_viewmanager.appendTo(this.$element);
this.url_update(action);
}
@@ -122,25 +129,33 @@ openerp.web.ActionManager = openerp.web.Widget.extend({
},
ir_actions_client: function (action) {
this.content_stop();
var ClientWidget = openerp.web.client_actions.get_object(action.tag);
var ClientWidget = db.web.client_actions.get_object(action.tag);
(this.client_widget = new ClientWidget(this, action.params)).appendTo(this);
},
ir_actions_report_xml: function(action) {
var self = this;
$.blockUI();
this.session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
complete: $.unblockUI
self.rpc("/web/session/eval_domain_and_context", {
contexts: [action.context],
domains: []
}).then(function(res) {
action = _.clone(action);
action.context = res.context;
self.session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
complete: $.unblockUI
});
});
}
});
openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.ViewManager# */{
db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{
identifier_prefix: "viewmanager",
template: "ViewManager",
/**
* @constructs openerp.web.ViewManager
* @extends openerp.web.Widget
* @constructs db.web.ViewManager
* @extends db.web.Widget
*
* @param parent
* @param dataset
@@ -155,10 +170,13 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View
this.views_src = _.map(views, function(x) {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;});
this.views = {};
this.flags = this.flags || {};
this.registry = openerp.web.views;
this.registry = db.web.views;
},
render: function() {
return QWeb.render(this.template, {"prefix": this.element_id, views: this.views_src})
return db.web.qweb.render(this.template, {
self: this,
prefix: this.element_id,
views: this.views_src});
},
/**
* @returns {jQuery.Deferred} initial view loading promise
@@ -202,8 +220,7 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View
if (!view.controller) {
// Lazy loading of views
var controllerclass = this.registry.get_object(view_type);
var controller = new controllerclass(this, this.element_id + '_view_' + view_type,
this.dataset, view.view_id, view.options);
var controller = new controllerclass(this, this.dataset, view.view_id, view.options);
if (view.embedded_view) {
controller.set_embedded_view(view.embedded_view);
}
@@ -222,7 +239,8 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View
unique: true
});
}
view_promise = controller.start();
var container = $("#" + this.element_id + '_view_' + view_type);
view_promise = controller.appendTo(container);
$.when(view_promise).then(function() {
self.on_controller_inited(view_type, controller);
});
@@ -274,15 +292,15 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View
if (this.searchview) {
this.searchview.stop();
}
this.searchview = new openerp.web.SearchView(
this, this.element_id + "_search", this.dataset,
this.searchview = new db.web.SearchView(
this, this.dataset,
view_id, search_defaults);
this.searchview.on_search.add(function(domains, contexts, groupbys) {
var controller = self.views[self.active_view].controller;
controller.do_search.call(controller, domains, contexts, groupbys);
});
return this.searchview.start();
return this.searchview.appendTo($("#" + this.element_id + "_search"));
},
/**
* Called when one of the view want to execute an action
@@ -294,16 +312,20 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View
on_remove: function() {
},
on_edit: function() {
}
},
/**
* Called by children view after executing an action
*/
on_action_executed: function () {}
});
openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{
db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{
template:"ViewManagerAction",
/**
* @constructs openerp.web.ViewManagerAction
* @extends openerp.web.ViewManager
* @constructs db.web.ViewManagerAction
* @extends db.web.ViewManager
*
* @param {openerp.web.ActionManager} parent parent object/widget
* @param {db.web.ActionManager} parent parent object/widget
* @param {Object} action descriptor for the action this viewmanager needs to manage its views.
*/
init: function(parent, action) {
@@ -312,7 +334,7 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner
// ``_super()``) rpc requests will blow up.
this.session = parent.session;
this.action = action;
var dataset = new openerp.web.DataSetSearch(this, action.res_model, action.context, action.domain);
var dataset = new db.web.DataSetSearch(this, action.res_model, action.context, action.domain);
if (action.res_id) {
dataset.ids.push(action.res_id);
dataset.index = 0;
@@ -324,6 +346,12 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner
// buttons, sidebar, ...) displaying
this.flags.search_view = this.flags.pager = this.flags.sidebar = this.flags.action_buttons = false;
}
// setup storage for session-wise menu hiding
if (this.session.hidden_menutips) {
return;
}
this.session.hidden_menutips = {}
},
/**
* Initializes the ViewManagerAction: sets up the searchview (if the
@@ -332,6 +360,8 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner
* launches an initial search after both views are done rendering.
*/
start: function() {
var self = this;
var searchview_loaded;
if (this.flags.search_view !== false) {
var search_defaults = {};
@@ -355,19 +385,55 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner
// schedule auto_search
manager_ready.then(this.searchview.do_search);
}
this.$element.find('.oe_get_xml_view').click(function () {
// TODO: add search view?
$('<pre>').text(db.web.json_node_to_xml(
self.views[self.active_view].controller.fields_view.arch, true))
.dialog({ width: '95%'});
});
if (this.action.help && !this.flags.low_profile) {
var Users = new db.web.DataSet(self, 'res.users'),
header = this.$element.find('.oe-view-manager-header');
header.delegate('blockquote button', 'click', function() {
var $this = $(this);
//noinspection FallthroughInSwitchStatementJS
switch ($this.attr('name')) {
case 'disable':
Users.write(self.session.uid, {menu_tips:false});
case 'hide':
$this.closest('blockquote').hide();
self.session.hidden_menutips[self.action.id] = true;
}
});
if (!(self.action.id in self.session.hidden_menutips)) {
Users.read_ids([this.session.uid], ['menu_tips'], function(users) {
var user = users[0];
if (!(user && user.id === self.session.uid)) {
return;
}
header.find('blockquote').toggle(user.menu_tips);
});
}
}
return manager_ready;
},
on_mode_switch: function (view_type) {
var self = this;
return $.when(
this._super(view_type),
this.shortcut_check(this.views[view_type]));
this.shortcut_check(this.views[view_type])).then(function () {
var view_id = self.views[self.active_view].controller.fields_view.view_id;
self.$element.find('.oe_get_xml_view span').text(view_id);
});
},
shortcut_check : function(view) {
var self = this;
var grandparent = this.widget_parent && this.widget_parent.widget_parent;
// display shortcuts if on the first view for the action
var $shortcut_toggle = this.$element.find('.oe-shortcut-toggle');
if (!(grandparent instanceof openerp.web.WebClient) ||
if (!(grandparent instanceof db.web.WebClient) ||
!(view.view_type === this.views_src[0].view_type
&& view.view_id === this.views_src[0].view_id)) {
$shortcut_toggle.hide();
@@ -399,10 +465,39 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner
$shortcut_toggle.addClass("oe-shortcut-remove");
}
});
},
/**
* Intercept do_action resolution from children views
*/
on_action_executed: function () {
new db.web.DataSet(this, 'res.log')
.call('get', [], this.do_display_log);
},
/**
* @param {Array<Object>} log_records
*/
do_display_log: function (log_records) {
var self = this,
$logs = this.$element.find('ul.oe-view-manager-logs:first').empty();
_(log_records).each(function (record) {
$(_.sprintf('<li><a href="#">%s</a></li>', record.name))
.appendTo($logs)
.delegate('a', 'click', function (e) {
self.do_action({
type: 'ir.actions.act_window',
res_model: record.res_model,
res_id: record.res_id,
// TODO: need to have an evaluated context here somehow
//context: record.context,
views: [[false, 'form']]
});
return false;
});
});
}
});
openerp.web.Sidebar = openerp.web.Widget.extend({
db.web.Sidebar = db.web.Widget.extend({
init: function(parent, element_id) {
this._super(parent, element_id);
this.items = {};
@@ -411,7 +506,7 @@ openerp.web.Sidebar = openerp.web.Widget.extend({
start: function() {
this._super(this);
var self = this;
this.$element.html(QWeb.render('Sidebar'));
this.$element.html(db.web.qweb.render('Sidebar'));
this.$element.find(".toggle-sidebar").click(function(e) {
self.do_toggle();
});
@@ -453,7 +548,7 @@ openerp.web.Sidebar = openerp.web.Widget.extend({
this.items[items[i].element_id] = items[i];
}
}
var $section = $(QWeb.render("Sidebar.section", {
var $section = $(db.web.qweb.render("Sidebar.section", {
section_id: section_id,
name: name,
classname: 'oe_sidebar_' + code,
@@ -509,7 +604,7 @@ openerp.web.Sidebar = openerp.web.Widget.extend({
}
});
openerp.web.TranslateDialog = openerp.web.Dialog.extend({
db.web.TranslateDialog = db.web.Dialog.extend({
dialog_title: _t("Translations"),
init: function(view) {
// TODO fme: should add the language to fields_view_get because between the fields view get
@@ -529,14 +624,14 @@ openerp.web.TranslateDialog = openerp.web.Dialog.extend({
this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name });
this.languages = null;
this.languages_loaded = $.Deferred();
(new openerp.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(),
(new db.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(),
[['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }, this.on_languages_loaded);
},
start: function() {
var self = this;
this._super();
$.when(this.languages_loaded).then(function() {
self.$element.html(QWeb.render('TranslateDialog', { widget: self }));
self.$element.html(db.web.qweb.render('TranslateDialog', { widget: self }));
self.$element.tabs();
if (!(self.view.translatable_fields && self.view.translatable_fields.length)) {
self.hide_tabs('fields');
@@ -641,11 +736,8 @@ openerp.web.TranslateDialog = openerp.web.Dialog.extend({
}
});
/**
* @class
* @extends openerp.web.Widget
*/
openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{
template: "EmptyComponent",
set_default_options: function(options) {
this.options = options || {};
_.defaults(this.options, {
@@ -658,7 +750,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
},
open_translate_dialog: function(field) {
if (!this.translate_dialog) {
this.translate_dialog = new openerp.web.TranslateDialog(this).start();
this.translate_dialog = new db.web.TranslateDialog(this).start();
}
this.translate_dialog.open(field);
},
@@ -670,12 +762,16 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
* @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
* @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
* @param {Object} [action_data.context=null] additional action context, to add to the current context
* @param {openerp.web.DataSet} dataset a dataset object used to communicate with the server
* @param {db.web.DataSet} dataset a dataset object used to communicate with the server
* @param {Object} [record_id] the identifier of the object on which the action is to be applied
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
*/
execute_action: function (action_data, dataset, record_id, on_closed) {
do_execute_action: function (action_data, dataset, record_id, on_closed) {
var self = this;
var result_handler = function () {
if (on_closed) { on_closed.apply(null, arguments); }
self.widget_parent.on_action_executed.apply(null, arguments);
};
var handler = function (r) {
var action = r.result;
if (action && action.constructor == Object) {
@@ -685,14 +781,14 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
active_ids: [record_id || false],
active_model: dataset.model
});
action.context = new openerp.web.CompoundContext(dataset.get_context(), action.context);
self.do_action(action, on_closed);
} else if (on_closed) {
on_closed(action);
action.context = new db.web.CompoundContext(dataset.get_context(), action.context);
self.do_action(action, result_handler);
} else {
result_handler();
}
};
var context = new openerp.web.CompoundContext(dataset.get_context(), action_data.context || {});
var context = new db.web.CompoundContext(dataset.get_context(), action_data.context || {});
if (action_data.special) {
handler({result: {"type":"ir.actions.act_window_close"}});
@@ -707,7 +803,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
/**
* Directly set a view to use instead of calling fields_view_get. This method must
* be called before start(). When an embedded view is set, underlying implementations
* of openerp.web.View must use the provided view instead of any other one.
* of db.web.View must use the provided view instead of any other one.
*
* @param embedded_view A view.
*/
@@ -754,21 +850,21 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
},
on_sidebar_manage_view: function() {
if (this.fields_view && this.fields_view.arch) {
$('<xmp>' + openerp.web.json_node_to_xml(this.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600});
$('<xmp>' + db.web.json_node_to_xml(this.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600});
} else {
this.notification.warn("Manage Views", "Could not find current view declaration");
}
},
on_sidebar_edit_workflow: function() {
this.log('Todo');
console.log('Todo');
},
on_sidebar_customize_object: function() {
this.log('Todo');
console.log('Todo');
},
on_sidebar_import: function() {
},
on_sidebar_export: function() {
var export_view = new openerp.web.DataExport(this, this.dataset);
var export_view = new db.web.DataExport(this, this.dataset);
export_view.start();
},
on_sidebar_translate: function() {
@@ -778,12 +874,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
}
});
/**
* Registry for all the main views
*/
openerp.web.views = new openerp.web.Registry();
openerp.web.json_node_to_xml = function(node, single_quote, indent) {
db.web.json_node_to_xml = function(node, single_quote, indent) {
// For debugging purpose, this function will convert a json node back to xml
// Maybe usefull for xml view editor
@@ -812,7 +903,7 @@ openerp.web.json_node_to_xml = function(node, single_quote, indent) {
r += '>\n';
var childs = [];
for (var i = 0, ii = node.children.length; i < ii; i++) {
childs.push(openerp.web.json_node_to_xml(node.children[i], single_quote, indent + 1));
childs.push(db.web.json_node_to_xml(node.children[i], single_quote, indent + 1));
}
r += childs.join('\n');
r += '\n' + sindent + '</' + node.tag + '>';
+74 -65
View File
@@ -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>
@@ -342,22 +348,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>
<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="#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="About" class="about"><img src="/web/static/src/img/header-about.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>
</li>
-->
</ul>
<div class="block">
<a href="#logout" class="logout">LOGOUT</a>
@@ -374,45 +370,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>
@@ -420,12 +410,14 @@
<table class="view-manager-main-table">
<tr>
<td class="view-manager-main-content">
<div class="oe_vm_switch">
<t t-if="views.length != 1" t-foreach="views" t-as="view">
<button type="button" t-att-data-view-type="view.view_type">
<t t-esc="view.view_type"/>
</button>
</t>
<div class="oe-view-manager-header">
<div class="oe_vm_switch">
<t t-if="views.length != 1" t-foreach="views" t-as="view">
<button type="button" t-att-data-view-type="view.view_type">
<t t-esc="view.view_type"/>
</button>
</t>
</div>
</div>
<div t-attf-id="#{prefix}_search" t-opentag="true"/>
<t t-foreach="views" t-as="view">
@@ -442,9 +434,26 @@
</t>
<t t-extend="ViewManager" t-name="ViewManagerAction">
<t t-jquery=".view-manager-main-content" t-operation="prepend">
<t t-jquery=".oe-view-manager-header" t-operation="prepend">
<blockquote t-if="self.action.help and !self.flags.low_profile
and !(self.action.id in self.session.hidden_menutips)">
<p><t t-esc="self.action.help"/></p>
<div>
<button type="button" name="hide">Hide this tip</button>
<button type="button" name="disable">Disable all tips</button>
</div>
</blockquote>
<a class="oe-shortcut-toggle" title="Add / Remove Shortcut..."
href="javascript: void(0)"> </a>
href="javascript: void(0)"> </a>
<h2 class="oe_view_title">
<t t-esc="self.action.name"/>
<button t-if="self.session.debug" class="oe_get_xml_view">
View#<span></span>
</button>
</h2>
</t>
<t t-jquery=".oe-view-manager-header" t-operation="after">
<ul class="oe-view-manager-logs"></ul>
</t>
</t>
@@ -502,7 +511,6 @@
</div>
</t>
<t t-name="TreeView">
<h2 class="oe_view_title"><t t-esc="title"/></h2>
<select t-if="toolbar" style="width: 30%">
</select>
<table class="oe-treeview-table">
@@ -632,7 +640,6 @@
</t>
<t t-name="FormView">
<div class="oe_form_header" t-att-id="view.element_id + '_header'">
<h2 class="oe_view_title"><t t-esc="view.fields_view.arch.attrs.string"/> <button t-if="view.session.debug" class="oe_get_xml_view">View#<t t-esc="view.fields_view.view_id"/></button></h2>
<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>
@@ -644,6 +651,7 @@
</button>
<!--<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>
</div>
<div class="oe_form_pager" t-if="view.options.pager !== false">
<button type="button" data-pager-action="first">First</button>
@@ -793,11 +801,13 @@
<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">
<input type="text" size="1" style="width: 100%"
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
t-att-class="'field_' + widget.type"
/>
<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>
</div>
</t>
<t t-name="FieldSelection">
<select
@@ -974,7 +984,6 @@
</button>
</t>
<t t-name="SearchView">
<h2 class="oe_view_title"><t t-esc="view.attrs['string']"/></h2>
<form class="oe_forms">
<t t-call="SearchView.render_lines"/>
<div class="oe_search-view-buttons" style="text-align: right;">
@@ -1393,9 +1402,6 @@
</p>
</div>
</t>
<t t-name="FieldStatus">
<div t-att-id="widget.element_id"></div>
</t>
<t t-name="FieldStatus.content">
<ul class="oe-arrow-list">
<t t-set="size" t-value="widget.to_show.length"/>
@@ -1406,4 +1412,7 @@
</t>
</ul>
</t>
<t t-name="EmptyComponent">
<div></div>
</t>
</templates>
@@ -8,8 +8,8 @@ QWeb.add_template('/web_calendar/static/src/xml/web_calendar.xml');
openerp.web.views.add('calendar', 'openerp.web_calendar.CalendarView');
openerp.web_calendar.CalendarView = openerp.web.View.extend({
// Dhtmlx scheduler ?
init: function(parent, element_id, dataset, view_id, options) {
this._super(parent, element_id);
init: function(parent, dataset, view_id, options) {
this._super(parent);
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
@@ -30,6 +30,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
this.color_map = {};
},
start: function() {
this._super();
this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type":"calendar", 'toolbar': true}, this.on_loaded);
},
stop: function() {
@@ -344,11 +345,11 @@ openerp.web_calendar.CalendarFormDialog = openerp.web.Dialog.extend({
},
start: function() {
this._super();
this.form = new openerp.web.FormView(this, this.element_id, this.dataset, this.view_id, {
this.form = new openerp.web.FormView(this, this.dataset, this.view_id, {
sidebar: false,
pager: false
});
this.form.start();
this.form.appendTo(this.$element);
this.form.on_created.add_last(this.on_form_dialog_saved);
this.form.on_saved.add_last(this.on_form_dialog_saved);
},
@@ -223,7 +223,8 @@ openerp.web.form.DashBoard = openerp.web.form.Widget.extend({
sidebar : false,
views_switcher : false,
action_buttons : false,
pager: false
pager: false,
low_profile: true
};
var am = new openerp.web.ActionManager(this);
this.action_managers.push(am);
@@ -287,13 +288,14 @@ if (!openerp.web_dashboard) {
}
openerp.web_dashboard.ConfigOverview = openerp.web.View.extend({
template: 'ConfigOverview',
init: function (parent, element_id) {
this._super(parent, element_id);
init: function (parent) {
this._super(parent);
this.dataset = new openerp.web.DataSetSearch(
this, 'ir.actions.todo');
this.dataset.domain = [['type', '=', 'manual']];
},
start: function () {
this._super();
$.when(this.dataset.read_slice(['state', 'action_id', 'category_id']),
this.dataset.call('progress'))
.then(this.on_records_loaded);
@@ -333,7 +335,7 @@ openerp.web_dashboard.ConfigOverview = openerp.web.View.extend({
});
})
.delegate('li:not(.oe-done)', 'click', function () {
self.widget_parent.widget_parent.widget_parent.execute_action({
self.widget_parent.widget_parent.widget_parent.do_execute_action({
type: 'object',
name: 'action_launch'
}, self.dataset,
@@ -351,6 +353,7 @@ openerp.web.client_actions.add(
openerp.web_dashboard.ApplicationTiles = openerp.web.View.extend({
template: 'ApplicationTiles',
start: function () {
this._super();
var self = this;
return new openerp.web.DataSetSearch(
this, 'ir.ui.menu', null, [['parent_id', '=', false]])
@@ -392,6 +395,7 @@ openerp.web_dashboard.Widget = openerp.web.View.extend(/** @lends openerp.web_da
this.widget_id = options.widget_id;
},
start: function () {
this._super();
return new openerp.web.DataSet(this, 'res.widget').read_ids(
[this.widget_id], ['title'], this.on_widget_loaded);
},
@@ -31,8 +31,6 @@
<t t-name="DashBoard.action">
<div t-att-data-id="action.attrs.name" class="oe-dashboard-action">
<h2 class="oe-dashboard-action-header oe_view_title">
<t t-esc="action.attrs.string"/>
<!--<a href="#" class="oe-dashboard-action-rename"><img src="/web/static/src/img/icons/gtk-edit.png" width="16" height="16"/></a>-->
<input class="oe-dashboard-action-input" type="text" name="title" value="" style="display: none"/>
<span class='ui-icon ui-icon-closethick'></span>
<span class='ui-icon ui-icon-minusthick oe-dashboard-fold' t-if="!action.attrs.fold"></span>
+3 -2
View File
@@ -8,8 +8,8 @@ QWeb.add_template('/web_diagram/static/src/xml/base_diagram.xml');
openerp.web.views.add('diagram', 'openerp.web.DiagramView');
openerp.web.DiagramView = openerp.web.View.extend({
searchable: false,
init: function(parent, element_id, dataset, view_id, options) {
this._super(parent, element_id);
init: function(parent, dataset, view_id, options) {
this._super(parent);
this.set_default_options(options);
this.view_manager = parent;
this.dataset = dataset;
@@ -20,6 +20,7 @@ openerp.web.DiagramView = openerp.web.View.extend({
this.ids = this.dataset.ids;
},
start: function() {
this._super();
return this.rpc("/web_diagram/diagram/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
},
@@ -1,7 +1,6 @@
<template>
<t t-name="DiagramView">
<div class="oe_diagram_header" t-att-id="element_id + '_header'">
<h2 class="oe_view_title"><t t-esc="fields_view.arch.attrs.string"/></h2>
<div class="oe_diagram_buttons">
<button type="button" id="new_node" class="oe_diagram_button_new">New Node</button>
<button type="button" id="new_edge" class="oe_diagram_button_new">New Edge</button>
@@ -19,4 +18,4 @@
</div>
<div id="dia-canvas" class="diagram show_grid" style="overflow: auto;"></div>
</t>
</template>
</template>
+3 -2
View File
@@ -7,8 +7,8 @@ QWeb.add_template('/web_gantt/static/src/xml/web_gantt.xml');
openerp.web.views.add('gantt', 'openerp.web_gantt.GanttView');
openerp.web_gantt.GanttView = openerp.web.View.extend({
init: function(parent, element_id, dataset, view_id) {
this._super(parent, element_id);
init: function(parent, dataset, view_id) {
this._super(parent);
this.view_manager = parent || new openerp.web.NullViewManager();
this.dataset = dataset;
this.model = dataset.model;
@@ -32,6 +32,7 @@ init: function(parent, element_id, dataset, view_id) {
},
start: function() {
this._super();
this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type": "gantt"}, this.on_loaded);
},
+3 -2
View File
@@ -17,8 +17,8 @@ QWeb.add_template('/web_graph/static/src/xml/web_graph.xml');
openerp.web.views.add('graph', 'openerp.web_graph.GraphView');
openerp.web_graph.GraphView = openerp.web.View.extend({
init: function(parent, element_id, dataset, view_id) {
this._super(parent, element_id);
init: function(parent, dataset, view_id) {
this._super(parent);
this.view_manager = parent;
this.dataset = dataset;
this.dataset_index = 0;
@@ -33,6 +33,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
this.$element.hide();
},
start: function() {
this._super();
return this.rpc("/web_graph/graphview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
},
on_loaded: function(data) {
+4 -3
View File
@@ -4,8 +4,8 @@ var QWeb = openerp.web.qweb;
QWeb.add_template('/web_kanban/static/src/xml/web_kanban.xml');
openerp.web.views.add('kanban', 'openerp.web_kanban.KanbanView');
openerp.web_kanban.KanbanView = openerp.web.View.extend({
init: function (parent, element_id, dataset, view_id, options) {
this._super(parent, element_id);
init: function (parent, dataset, view_id, options) {
this._super(parent);
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
@@ -24,6 +24,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
}
},
start: function() {
this._super();
return this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type": "kanban"}, this.on_loaded);
},
on_loaded: function(data) {
@@ -224,7 +225,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
},
on_execute_button_click: function (dataset, button_attrs, record_id) {
var self = this;
this.execute_action(
this.do_execute_action(
button_attrs, dataset,
record_id, function () {
var count = 1;
@@ -138,7 +138,6 @@ openerp.web_mobile.Shortcuts = openerp.web.Widget.extend({
$shortcut = $(ev.currentTarget);
id = $shortcut.data('menu');
res_id = $shortcut.data('res');
if(!$('#oe_list').html().length){
this.listview = new openerp.web_mobile.ListView(this, "oe_list", res_id);
this.listview.start();
+12
View File
@@ -282,6 +282,18 @@ view managers can correctly communicate with them:
defining e.g. the ``start`` method) or at the instance level (in the
class's ``init``), though you should generally set it on the class.
Frequent development tasks
--------------------------
There are a number of tasks which OpenERP Web developers do or will need to
perform quite regularly. To make these easier, we have written a few guides
to help you get started:
.. toctree::
:maxdepth: 1
guides/client-action.rst
Utility behaviors
-----------------
+92
View File
@@ -0,0 +1,92 @@
Creating a new client action
============================
Client actions are the client-side of OpenERP's "Server Actions": instead of
allowing for semi-arbitrary code to be executed in the server, they allow
for execution of client-customized code.
On the server side, a client action is an action of type ``ir.actions.client``,
which has (at most) two properties: a mandatory ``tag``, which is an arbitrary
string by which the client will identify the action, and an optional ``params``
which is simply a map of keys and values sent to the client as-is (this way,
client actions can be made generic and reused in multiple contexts).
General Structure
-----------------
In the OpenERP Web code, a client action only requires two pieces of
information:
* Mapping the action's ``tag`` to an OpenERP Web object
* The OpenERP Web object itself, which must inherit from
:js:class:`openerp.web.Widget`
Our example will be the actual code for the widgets client action (a client
action displaying a ``res.widget`` object, used in the homepage dashboard of
the web client):
.. code-block:: javascript
// Registers the object 'openerp.web_dashboard.Widget' to the client
// action tag 'board.home.widgets'
openerp.web.client_actions.add(
'board.home.widgets', 'openerp.web_dashboard.Widget');
// This object inherits from View, but only Widget is required
openerp.web_dashboard.Widget = openerp.web.View.extend({
template: 'HomeWidget'
});
At this point, the generic ``Widget`` lifecycle takes over, the template is
rendered, inserted in the client DOM, bound on the object's ``$element``
property and the object is started.
If the client action takes parameters, these parameters are passed in as a
second positional parameter to the constructor:
.. code-block:: javascript
init: function (parent, params) {
// execute the Widget's init
this._super(parent);
// board.home.widgets only takes a single param, the identifier of the
// res.widget object it should display. Store it for later
this.widget_id = params.widget_id;
}
More complex initialization (DOM manipulations, RPC requests, ...) should be
performed in the ``start()`` method.
.. note::
As required by ``Widget``'s contract, if ``start`` executes any
asynchronous code it should return a ``$.Deferred`` so callers know when
it's ready for interaction.
Although generally speaking client actions are not really interacted with.
.. code-block:: javascript
start: function () {
return $.when(
this._super(),
// Simply read the res.widget object this action should display
new openerp.web.DataSet(this, 'res.widget').read_ids(
[this.widget_id], ['title'], this.on_widget_loaded));
}
The client action can then behave exactly as it wishes to within its root
(``this.$element``). In this case, it performs further renderings once its
widget's content is retrieved:
.. code-block:: javascript
on_widget_loaded: function (widgets) {
var widget = widgets[0];
var url = _.sprintf(
'/web_dashboard/widgets/content?session_id=%s&widget_id=%d',
this.session.session_id, widget.id);
this.$element.html(QWeb.render('HomeWidget.content', {
widget: widget,
url: url
}));
}
+5 -1
View File
@@ -37,11 +37,15 @@ optparser.add_option("--log-level", dest="log_level",
default='debug', help="Log level", metavar="LOG_LEVEL")
optparser.add_option("--log-config", dest="log_config",
default='', help="Log config file", metavar="LOG_CONFIG")
optparser.add_option('--multi-threaded', dest='threaded',
default=False, action='store_true',
help="Use multiple threads to handle requests")
import web.common.dispatch
if __name__ == "__main__":
(options, args) = optparser.parse_args(sys.argv[1:])
options.backend = 'rpc'
os.environ["TZ"] = "UTC"
@@ -54,5 +58,5 @@ if __name__ == "__main__":
werkzeug.serving.run_simple(
'0.0.0.0', options.socket_port, app,
use_reloader=options.reloader, threaded=True)
use_reloader=options.reloader, threaded=options.threaded)