[MERGE] from trunk
bzr revid: xmo@openerp.com-20121107112516-64hqps4jjgmrs3a4
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
.PHONY: all doc release clean
|
||||
|
||||
HOST = 127.0.0.1
|
||||
PORT = 8080
|
||||
|
||||
all: run
|
||||
|
||||
run:
|
||||
python openerp-web.py -a ${HOST} -p ${PORT}
|
||||
|
||||
release:
|
||||
python setup.py sdist
|
||||
|
||||
install:
|
||||
python setup.py install
|
||||
|
||||
clean:
|
||||
@find . -name '*.pyc' -exec rm -f {} +
|
||||
@find . -name '*.pyo' -exec rm -f {} +
|
||||
@find . -name '*.swp' -exec rm -f {} +
|
||||
@find . -name '*~' -exec rm -f {} +
|
||||
@rm -rf build
|
||||
@rm -rf dist
|
||||
@rm -rf *.egg-info
|
||||
|
||||
doc:
|
||||
make -C doc html
|
||||
|
||||
cloc:
|
||||
cloc addons/*/common/*.py addons/*/controllers/*.py addons/*/static/src/*.js addons/*/static/src/js/*.js addons/*/static/src/css/*.css addons/*/static/src/xml/*.xml
|
||||
|
||||
blamestat:
|
||||
echo addons/*/common/*.py addons/*/controllers/*.py addons/*/static/src/js/*.js addons/*/static/src/css/*.css addons/*/static/src/xml/*.xml | xargs -t -n 1 bzr blame -v --long --all | awk '{print $2}' | sort | uniq -c | sort -n
|
||||
|
||||
+5
-3
@@ -1,9 +1,11 @@
|
||||
OpenERP Web
|
||||
-----------
|
||||
|
||||
To build the documentation use:
|
||||
The OpenERP Web Client supports the following web browsers:
|
||||
|
||||
$ make doc
|
||||
* Internet Explorer 9+
|
||||
* Google Chrome 22+
|
||||
* Firefox 13+
|
||||
* Any browser using the latest version of Chrome Frame
|
||||
|
||||
then look at doc/build/html/index.html
|
||||
|
||||
|
||||
+3
-30
@@ -1,31 +1,4 @@
|
||||
import logging
|
||||
|
||||
from . import common
|
||||
from . import controllers
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Options(object):
|
||||
pass
|
||||
|
||||
def wsgi_postload():
|
||||
import openerp
|
||||
import os
|
||||
import tempfile
|
||||
import getpass
|
||||
_logger.info("embedded mode")
|
||||
o = Options()
|
||||
o.dbfilter = openerp.tools.config['dbfilter']
|
||||
o.server_wide_modules = openerp.conf.server_wide_modules or ['web']
|
||||
try:
|
||||
username = getpass.getuser()
|
||||
except Exception:
|
||||
username = "unknown"
|
||||
o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
|
||||
o.addons_path = openerp.modules.module.ad_paths
|
||||
o.serve_static = True
|
||||
o.backend = 'local'
|
||||
|
||||
app = common.http.Root(o)
|
||||
openerp.wsgi.register_wsgi_handler(app)
|
||||
import http
|
||||
import controllers
|
||||
|
||||
wsgi_postload = http.wsgi_postload
|
||||
|
||||
+17
-19
@@ -1,28 +1,30 @@
|
||||
{
|
||||
"name" : "web",
|
||||
"category": "Hidden",
|
||||
"description":
|
||||
'name': 'Web',
|
||||
'category': 'Hidden',
|
||||
'description':
|
||||
"""
|
||||
OpenERP Web core module.
|
||||
This module provides the core of the OpenERP web client.
|
||||
OpenERP Web core module.
|
||||
========================
|
||||
|
||||
This module provides the core of the OpenERP Web Client.
|
||||
""",
|
||||
"depends" : [],
|
||||
'depends': [],
|
||||
'auto_install': True,
|
||||
'post_load' : 'wsgi_postload',
|
||||
'post_load': 'wsgi_postload',
|
||||
'js' : [
|
||||
"static/lib/datejs/globalization/en-US.js",
|
||||
"static/lib/datejs/core.js",
|
||||
"static/lib/datejs/parser.js",
|
||||
"static/lib/datejs/sugarpak.js",
|
||||
"static/lib/datejs/extras.js",
|
||||
"static/lib/jquery/jquery-1.7.2b1.js",
|
||||
"static/lib/jquery/jquery-1.7.2.js",
|
||||
"static/lib/jquery.MD5/jquery.md5.js",
|
||||
"static/lib/jquery.form/jquery.form.js",
|
||||
"static/lib/jquery.validate/jquery.validate.js",
|
||||
"static/lib/jquery.ba-bbq/jquery.ba-bbq.js",
|
||||
"static/lib/spinjs/spin.js",
|
||||
"static/lib/jquery.autosize/jquery.autosize.js",
|
||||
"static/lib/jquery.blockUI/jquery.blockUI.js",
|
||||
"static/lib/jquery.superfish/js/hoverIntent.js",
|
||||
"static/lib/jquery.superfish/js/superfish.js",
|
||||
"static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js",
|
||||
"static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js",
|
||||
"static/lib/jquery.ui.notify/js/jquery.notify.js",
|
||||
@@ -30,14 +32,13 @@
|
||||
"static/lib/jquery.scrollTo/jquery.scrollTo-min.js",
|
||||
"static/lib/jquery.tipsy/jquery.tipsy.js",
|
||||
"static/lib/jquery.textext/jquery.textext.js",
|
||||
"static/lib/json/json2.js",
|
||||
"static/lib/jquery.timeago/jquery.timeago.js",
|
||||
"static/lib/qweb/qweb2.js",
|
||||
"static/lib/underscore/underscore.js",
|
||||
"static/lib/underscore/underscore.string.js",
|
||||
"static/lib/backbone/backbone.js",
|
||||
|
||||
"static/lib/labjs/LAB.src.js",
|
||||
"static/lib/py.js/lib/py.js",
|
||||
"static/lib/cleditor/jquery.cleditor.js",
|
||||
"static/lib/py.js/lib/py.js",
|
||||
"static/src/js/boot.js",
|
||||
"static/src/js/corelib.js",
|
||||
"static/src/js/coresetup.js",
|
||||
@@ -47,24 +48,21 @@
|
||||
"static/src/js/views.js",
|
||||
"static/src/js/data.js",
|
||||
"static/src/js/data_export.js",
|
||||
"static/src/js/data_import.js",
|
||||
"static/src/js/search.js",
|
||||
"static/src/js/view_form.js",
|
||||
"static/src/js/view_list.js",
|
||||
"static/src/js/view_list_editable.js",
|
||||
"static/src/js/view_tree.js",
|
||||
"static/src/js/view_editor.js"
|
||||
],
|
||||
'css' : [
|
||||
"static/lib/jquery.superfish/css/superfish.css",
|
||||
"static/lib/jquery.ui.bootstrap/css/custom-theme/jquery-ui-1.8.16.custom.css",
|
||||
"static/lib/jquery.ui.timepicker/css/jquery-ui-timepicker-addon.css",
|
||||
"static/lib/jquery.ui.notify/css/ui.notify.css",
|
||||
"static/lib/jquery.tipsy/tipsy.css",
|
||||
# "static/src/css/base_old.css",
|
||||
"static/lib/jquery.textext/jquery.textext.css",
|
||||
"static/src/css/base.css",
|
||||
"static/src/css/data_export.css",
|
||||
"static/src/css/data_import.css",
|
||||
"static/lib/cleditor/jquery.cleditor.css",
|
||||
],
|
||||
'qweb' : [
|
||||
"static/src/xml/*.xml",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
from . import http
|
||||
from . import nonliterals
|
||||
from . import release
|
||||
from . import session
|
||||
from . import xml2json
|
||||
@@ -1,32 +0,0 @@
|
||||
# -*- 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 *
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# -*- 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)
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
_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 get_service(self, service_name):
|
||||
"""
|
||||
Returns a Service instance to allow easy manipulation of one of the services offered by the remote server.
|
||||
|
||||
:param service_name: The name of the service.
|
||||
"""
|
||||
return Service(self, service_name)
|
||||
|
||||
class XmlRPCConnector(Connector):
|
||||
"""
|
||||
A type of connector that uses the XMLRPC protocol.
|
||||
"""
|
||||
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).
|
||||
"""
|
||||
self.url = 'http://%s:%d/xmlrpc' % (hostname, 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 XmlRPCSConnector(XmlRPCConnector):
|
||||
"""
|
||||
A type of connector that uses the secured XMLRPC protocol.
|
||||
"""
|
||||
PROTOCOL = 'xmlrpcs'
|
||||
|
||||
__logger = _getChildLogger(_logger, 'connector.xmlrpcs')
|
||||
|
||||
def __init__(self, hostname, port=8069):
|
||||
super(XmlRPCSConnector, self).__init__(hostname, port)
|
||||
self.url = 'https://%s:%d/xmlrpc' % (hostname, port)
|
||||
|
||||
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 or "")
|
||||
|
||||
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)
|
||||
self.user_context = None
|
||||
|
||||
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("Credentials not provided")
|
||||
|
||||
# TODO use authenticate instead of login
|
||||
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_user_context(self):
|
||||
"""
|
||||
Query the default context of the user.
|
||||
"""
|
||||
if not self.user_context:
|
||||
self.user_context = self.get_model('res.users').context_get()
|
||||
return self.user_context
|
||||
|
||||
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 self.connector.get_service(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 or "")
|
||||
|
||||
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, **kw):
|
||||
"""
|
||||
: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_kw(
|
||||
self.connection.database,
|
||||
self.connection.user_id,
|
||||
self.connection.password,
|
||||
self.model_name,
|
||||
method,
|
||||
args, kw)
|
||||
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] if x in index]
|
||||
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 {})
|
||||
if not record_ids: return []
|
||||
records = self.read(record_ids, fields or [], context or {})
|
||||
return records
|
||||
|
||||
def get_connector(hostname=None, protocol="xmlrpc", port="auto"):
|
||||
"""
|
||||
A shortcut method to easily create a connector to a remote server using XMLRPC.
|
||||
|
||||
:param hostname: The hostname to the remote server.
|
||||
:param protocol: The name of the protocol, must be "xmlrpc" or "xmlrpcs".
|
||||
:param port: The number of the port. Defaults to auto.
|
||||
"""
|
||||
if port == 'auto':
|
||||
port = 8069
|
||||
if protocol == "xmlrpc":
|
||||
return XmlRPCConnector(hostname, port)
|
||||
elif protocol == "xmlrpcs":
|
||||
return XmlRPCSConnector(hostname, port)
|
||||
else:
|
||||
raise ValueError("You must choose xmlrpc or xmlrpcs")
|
||||
|
||||
def get_connection(hostname=None, protocol="xmlrpc", port='auto', database=None,
|
||||
login=None, password=None, user_id=None):
|
||||
"""
|
||||
A shortcut method to easily create a connection to a remote OpenERP server.
|
||||
|
||||
:param hostname: The hostname to the remote server.
|
||||
:param protocol: The name of the protocol, must be "xmlrpc" or "xmlrpcs".
|
||||
: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)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
name = 'openerp-web'
|
||||
version = '6.1rc1'
|
||||
description = "Web Client of OpenERP, the Enterprise Management Software"
|
||||
long_description = "OpenERP Web is the web client of the OpenERP, a free enterprise management software"
|
||||
author = "OpenERP SA"
|
||||
author_email = "info@openerp.com"
|
||||
support_email = 'support@openerp.com'
|
||||
url = "http://www.openerp.com/"
|
||||
download_url = ''
|
||||
license = "AGPL"
|
||||
@@ -1,26 +0,0 @@
|
||||
# xml2json-direct
|
||||
# Simple and straightforward XML-to-JSON converter in Python
|
||||
# New BSD Licensed
|
||||
#
|
||||
# URL: http://code.google.com/p/xml2json-direct/
|
||||
|
||||
def from_elementtree(el, preserve_whitespaces=False):
|
||||
res = {}
|
||||
if el.tag[0] == "{":
|
||||
ns, name = el.tag.rsplit("}", 1)
|
||||
res["tag"] = name
|
||||
res["namespace"] = ns[1:]
|
||||
else:
|
||||
res["tag"] = el.tag
|
||||
res["attrs"] = {}
|
||||
for k, v in el.items():
|
||||
res["attrs"][k] = v
|
||||
kids = []
|
||||
if el.text and (preserve_whitespaces or el.text.strip() != ''):
|
||||
kids.append(el.text)
|
||||
for kid in el:
|
||||
kids.append(from_elementtree(kid, preserve_whitespaces))
|
||||
if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
|
||||
kids.append(kid.tail)
|
||||
res["children"] = kids
|
||||
return res
|
||||
+879
-675
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Vendored
@@ -1,5 +1,5 @@
|
||||
Don't stop the world now: asynchronous development and Javascript
|
||||
=================================================================
|
||||
Asynchronous Operations
|
||||
=======================
|
||||
|
||||
As a language (and runtime), javascript is fundamentally
|
||||
single-threaded. This means any blocking request or computation will
|
||||
@@ -1,6 +1,16 @@
|
||||
API changes from OpenERP Web 6.1 to 6.2
|
||||
API changes from OpenERP Web 6.1 to 7.0
|
||||
=======================================
|
||||
|
||||
Supported browsers
|
||||
------------------
|
||||
|
||||
The OpenERP Web Client supports the following web browsers:
|
||||
|
||||
* Internet Explorer 9+
|
||||
* Google Chrome 22+
|
||||
* Firefox 13+
|
||||
* Any browser using the latest version of Chrome Frame
|
||||
|
||||
DataSet -> Model
|
||||
----------------
|
||||
|
||||
@@ -85,8 +95,8 @@ DataGroup -> also Model
|
||||
-----------------------
|
||||
|
||||
Alongside the deprecation of ``DataSet`` for
|
||||
:js:class:`~openerp.web.Model`, OpenERP Web 6.2 also deprecates
|
||||
``DataGroup`` and its subtypes in favor of a single method on
|
||||
:js:class:`~openerp.web.Model`, OpenERP Web 7.0 removes
|
||||
``DataGroup`` and its subtypes as public objects in favor of a single method on
|
||||
:js:class:`~openerp.web.Query`:
|
||||
:js:func:`~openerp.web.Query.group_by`.
|
||||
|
||||
@@ -106,3 +116,8 @@ Because it is heavily related to ``DataSet`` (as it *yields*
|
||||
``DataGroup`` (if we want to stay consistent), which is a good time to
|
||||
make the API more imperative and look more like what most developers
|
||||
are used to.
|
||||
|
||||
But as ``DataGroup`` users in 6.1 were rare (and there really was little reason
|
||||
to use it), it has been removed as a public API.
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ copyright = u'2012, OpenERP s.a.'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '6.1'
|
||||
version = '7.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '6.1'
|
||||
release = '7.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -0,0 +1,55 @@
|
||||
Notes on the usage of the Form View as a sub-widget
|
||||
===================================================
|
||||
|
||||
Undocumented stuff
|
||||
------------------
|
||||
|
||||
* ``initial_mode`` *option* defines the starting mode of the form
|
||||
view, one of ``view`` and ``edit`` (?). Default value is ``view``
|
||||
(non-editable form).
|
||||
|
||||
* ``embedded_view`` *attribute* has to be set separately when
|
||||
providing a view directly, no option available for that usage.
|
||||
|
||||
* View arch **must** contain node with
|
||||
``@class="oe_form_container"``, otherwise everything will break
|
||||
without any info
|
||||
|
||||
* Root element of view arch not being ``form`` may or may not work
|
||||
correctly, no idea.
|
||||
|
||||
* Freeform views => ``@version="7.0"``
|
||||
|
||||
* Form is not entirely loaded (some widgets may not appear) unless
|
||||
``on_record_loaded`` is called (or ``do_show``, which itself calls
|
||||
``on_record_loaded``).
|
||||
|
||||
* "Empty" form => ``on_button_new`` (...), or manually call
|
||||
``default_get`` + ``on_record_loaded``
|
||||
|
||||
* Form fields default to width: 100%, padding, !important margin, can
|
||||
be reached via ``.oe_form_field``
|
||||
|
||||
* Form *will* render buttons and a pager, offers options to locate
|
||||
both outside of form itself (``$buttons`` and ``$pager``), providing
|
||||
empty jquery objects (``$()``) seems to stop displaying both but not
|
||||
sure if there are deleterious side-effects.
|
||||
|
||||
Other options:
|
||||
|
||||
* Pass in ``$(document.createDocumentFragment)`` to ensure it's a
|
||||
DOM-compatible tree completely outside of the actual DOM.
|
||||
|
||||
* ???
|
||||
|
||||
* readonly fields probably don't have a background, beware if need of
|
||||
overlay
|
||||
|
||||
* What is the difference between ``readonly`` and
|
||||
``effective_readonly``?
|
||||
|
||||
* No facilities for DOM events handling/delegations e.g. handling
|
||||
keyup/keydown/keypress from a form fields into the form's user.
|
||||
|
||||
* Also no way to reverse from a DOM node (e.g. DOMEvent#target) back to a
|
||||
form view field easily
|
||||
@@ -0,0 +1,112 @@
|
||||
.. highlight:: javascript
|
||||
|
||||
Creating a new client action
|
||||
============================
|
||||
|
||||
Client actions are the client-side version 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 object
|
||||
|
||||
* Providing said object. Two different types of objects can be mapped
|
||||
to a client action:
|
||||
|
||||
* An OpenERP Web widget, which must inherit from
|
||||
:js:class:`openerp.web.Widget`
|
||||
|
||||
* A regular javascript function
|
||||
|
||||
The major difference is in the lifecycle of these:
|
||||
|
||||
* if the client action maps to a function, the function will simply be
|
||||
called when executing the action. The function can have no further
|
||||
interaction with the Web Client itself, although it can return an
|
||||
action which will be executed after it.
|
||||
|
||||
* if, on the other hand, the client action maps to a
|
||||
:js:class:`~openerp.web.Widget`, that
|
||||
:js:class:`~openerp.web.Widget` will be instantiated and added to
|
||||
the web client's canvas, with the usual
|
||||
:js:class:`~openerp.web.Widget` lifecycle (essentially, it will
|
||||
either take over the content area of the client or it will be
|
||||
integrated within a dialog).
|
||||
|
||||
For example, to create a client action displaying a ``res.widget``
|
||||
object::
|
||||
|
||||
// Registers the object 'openerp.web_dashboard.Widget' to the client
|
||||
// action tag 'board.home.widgets'
|
||||
instance.web.client_actions.add(
|
||||
'board.home.widgets', 'openerp.web_dashboard.Widget');
|
||||
instance.web_dashboard.Widget = instance.web.Widget.extend({
|
||||
template: 'HomeWidget'
|
||||
});
|
||||
|
||||
At this point, the generic :js:class:`~openerp.web.Widget` lifecycle
|
||||
takes over, the template is rendered, inserted in the client DOM,
|
||||
bound on the object's ``$el`` property and the object is started.
|
||||
|
||||
If the client action takes parameters, these parameters are passed in as a
|
||||
second positional parameter to the constructor::
|
||||
|
||||
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 :js:func:`~openerp.web.Widget.start()`
|
||||
method.
|
||||
|
||||
.. note::
|
||||
|
||||
As required by :js:class:`~openerp.web.Widget`'s contract, if
|
||||
:js:func:`~openerp.web.Widget.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 instance.web.Model('res.widget').call(
|
||||
'read', [[this.widget_id], ['title']])
|
||||
.then(this.proxy('on_widget_loaded'));
|
||||
}
|
||||
|
||||
The client action can then behave exactly as it wishes to within its
|
||||
root (``this.$el``). In this case, it performs further renderings once
|
||||
its widget's content is retrieved::
|
||||
|
||||
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.$el.html(QWeb.render('HomeWidget.content', {
|
||||
widget: widget,
|
||||
url: url
|
||||
}));
|
||||
}
|
||||
@@ -11,26 +11,18 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog-6.2
|
||||
changelog-7.0
|
||||
|
||||
async
|
||||
rpc
|
||||
|
||||
widget
|
||||
search-view
|
||||
|
||||
Older stuff
|
||||
-----------
|
||||
list-view
|
||||
form-notes
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
getting-started
|
||||
production
|
||||
widgets
|
||||
addons
|
||||
development
|
||||
project
|
||||
old-version
|
||||
guides/client-action
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
@@ -0,0 +1,531 @@
|
||||
List View
|
||||
=========
|
||||
|
||||
Style Hooks
|
||||
-----------
|
||||
|
||||
The list view provides a few style hook classes for re-styling of list views in
|
||||
various situations:
|
||||
|
||||
``.oe_list``
|
||||
|
||||
The root element of the list view, styling rules should be rooted
|
||||
on that class.
|
||||
|
||||
``table.oe_list_content``
|
||||
|
||||
The root table for the listview, accessory components may be
|
||||
generated or added outside this section, this is the list view
|
||||
"proper".
|
||||
|
||||
``.oe_list_buttons``
|
||||
|
||||
The action buttons array for the list view, with its sub-elements
|
||||
|
||||
``.oe_list_add``
|
||||
|
||||
The default "Create"/"Add" button of the list view
|
||||
|
||||
``.oe_alternative``
|
||||
|
||||
The "alternative choice" for the list view, by default text
|
||||
along the lines of "or import" with a link.
|
||||
|
||||
``.oe_list_field_cell``
|
||||
|
||||
The cell (``td``) for a given field of the list view, cells which
|
||||
are *not* fields (e.g. name of a group, or number of items in a
|
||||
group) will not have this class. The field cell can be further
|
||||
specified:
|
||||
|
||||
``.oe_number``
|
||||
|
||||
Numeric cell types (integer and float)
|
||||
|
||||
``.oe_button``
|
||||
|
||||
Action button (``button`` tag in the view) inside the cell
|
||||
|
||||
``.oe_readonly``
|
||||
|
||||
Readonly field cell
|
||||
|
||||
``.oe_list_field_$type``
|
||||
|
||||
Additional class for the precise type of the cell, ``$type``
|
||||
is the field's @widget if there is one, otherwise it's the
|
||||
field's type.
|
||||
|
||||
``.oe_list_record_selector``
|
||||
|
||||
Selector cells
|
||||
|
||||
Editable list view
|
||||
++++++++++++++++++
|
||||
|
||||
The editable list view module adds a few supplementary style hook
|
||||
classes, for edition situations:
|
||||
|
||||
``.oe_list_editable``
|
||||
|
||||
Added to the ``.oe_list`` when the list is editable (however that
|
||||
was done). The class may be removed on-the-fly if the list becomes
|
||||
non-editable.
|
||||
|
||||
``.oe_editing``
|
||||
|
||||
Added to both ``.oe_list`` and ``.oe_list_button`` (as the
|
||||
buttons may be outside of the list view) when a row of the list is
|
||||
currently being edited.
|
||||
|
||||
``tr.oe_edition``
|
||||
|
||||
Class set on the row being edited itself. Note that the edition
|
||||
form is *not* contained within the row, this allows for styling or
|
||||
modifying the row while it's being edited separately. Mostly for
|
||||
fields which can not be edited (e.g. read-only fields).
|
||||
|
||||
Columns display customization
|
||||
-----------------------------
|
||||
|
||||
The list view provides a registry to
|
||||
:js:class:`openerp.web.list.Column` objects allowing for the
|
||||
customization of a column's display (e.g. so that a binary field is
|
||||
rendered as a link to the binary file directly in the list view).
|
||||
|
||||
The registry is ``instance.web.list.columns``, the keys are of the
|
||||
form ``tag.type`` where ``tag`` can be ``field`` or ``button``, and
|
||||
``type`` can be either the field's type or the field's ``@widget`` (in
|
||||
the view).
|
||||
|
||||
Most of the time, you'll want to define a ``tag.widget`` key
|
||||
(e.g. ``field.progressbar``).
|
||||
|
||||
.. js:class:: openerp.web.list.Column(id, tag, attrs)
|
||||
|
||||
.. js:function:: openerp.web.list.Column.format(record_data, options)
|
||||
|
||||
Top-level formatting method, returns an empty string if the
|
||||
column is invisible (unless the ``process_modifiers=false``
|
||||
option is provided); returns ``options.value_if_empty`` or an
|
||||
empty string if there is no value in the record for the
|
||||
column.
|
||||
|
||||
Otherwise calls :js:func:`~openerp.web.list.Column._format`
|
||||
and returns its result.
|
||||
|
||||
This method only needs to be overridden if the column has no
|
||||
concept of values (and needs to bypass that check), for a
|
||||
button for instance.
|
||||
|
||||
Otherwise, custom columns should generally override
|
||||
:js:func:`~openerp.web.list.Column._format` instead.
|
||||
|
||||
:returns: String
|
||||
|
||||
.. js:function:: openerp.web.list.Column._format(record_data, options)
|
||||
|
||||
Never called directly, called if the column is visible and has
|
||||
a value.
|
||||
|
||||
The default implementation calls
|
||||
:js:func:`~openerp.web.format_value` and htmlescapes the
|
||||
result (via ``_.escape``).
|
||||
|
||||
Note that the implementation of
|
||||
:js:func:`~openerp.web.list.Column._format` *must* escape the
|
||||
data provided to it, its output will *not* be escaped by
|
||||
:js:func:`~openerp.web.list.Column.format`.
|
||||
|
||||
:returns: String
|
||||
|
||||
Editable list view
|
||||
------------------
|
||||
|
||||
List view edition is an extension to the base listview providing the
|
||||
capability of inline record edition by delegating to an embedded form
|
||||
view.
|
||||
|
||||
Editability status
|
||||
++++++++++++++++++
|
||||
|
||||
The editability status of a list view can be queried through the
|
||||
:js:func:`~openerp.web.ListView.editable` method, will return a falsy
|
||||
value if the listview is not currently editable.
|
||||
|
||||
The editability status is based on three flags:
|
||||
|
||||
``tree/@editable``
|
||||
|
||||
If present, can be either ``"top"`` or ``"bottom"``. Either will
|
||||
make the list view editable, with new records being respectively
|
||||
created at the top or at the bottom of the view.
|
||||
|
||||
``context.set_editable``
|
||||
|
||||
Boolean flag extracted from a search context (during the
|
||||
:js:func:`~openerp.web.ListView.do_search`` handler), ``true``
|
||||
will make the view editable (from the top), ``false`` or the
|
||||
absence of the flag is a noop.
|
||||
|
||||
``defaults.editable``
|
||||
|
||||
Like ``tree/@editable``, one of absent (``null``)), ``"top"`` or
|
||||
``"bottom"``, fallback for the list view if none of the previous
|
||||
two flags are set.
|
||||
|
||||
These three flags can only *make* a listview editable, they can *not*
|
||||
override a previously set flag. To do that, a listview user should
|
||||
instead cancel :ref:`the edit:before event <listview-edit-before>`.
|
||||
|
||||
The editable list view module adds a number of methods to the list
|
||||
view, on top of implementing the :js:class:`EditorDelegate` protocol:
|
||||
|
||||
Interaction Methods
|
||||
+++++++++++++++++++
|
||||
|
||||
.. js:function:: openerp.web.ListView.ensure_saved
|
||||
|
||||
Attempts to resolve the pending edition, if any, by saving the
|
||||
edited row's current state.
|
||||
|
||||
:returns: delegate resolving to all editions having been saved, or
|
||||
rejected if a pending edition could not be saved
|
||||
(e.g. validation failure)
|
||||
|
||||
.. js:function:: openerp.web.ListView.start_edition([record][, options])
|
||||
|
||||
Starts editing the provided record inline, through an overlay form
|
||||
view of editable fields in the record.
|
||||
|
||||
If no record is provided, creates a new one according to the
|
||||
editability configuration of the list view.
|
||||
|
||||
This method resolves any pending edition when invoked, before
|
||||
starting a new edition.
|
||||
|
||||
:param record: record to edit, or null to create a new record
|
||||
:type record: :js:class:`~openerp.web.list.Record`
|
||||
:param EditOptions options:
|
||||
:returns: delegate to the form used for the edition
|
||||
|
||||
.. js:function:: openerp.web.ListView.save_edition
|
||||
|
||||
Resolves the pending edition.
|
||||
|
||||
:returns: delegate to the save being completed, resolves to an
|
||||
object with two attributes ``created`` (flag indicating
|
||||
whether the saved record was just created or was
|
||||
updated) and ``record`` the reloaded record having been
|
||||
edited.
|
||||
|
||||
.. js:function:: openerp.web.ListView.cancel_edition([force=false])
|
||||
|
||||
Cancels pending edition, cleans up the list view in case of
|
||||
creation (removes the empty record being created).
|
||||
|
||||
:param Boolean force: doesn't check if the user has added any
|
||||
data, discards the edition unconditionally
|
||||
|
||||
Utility Methods
|
||||
+++++++++++++++
|
||||
|
||||
.. js:function:: openerp.web.ListView.get_cells_for(row)
|
||||
|
||||
Extracts the cells from a listview row, and puts them in a
|
||||
{fieldname: cell} mapping for analysis and manipulation.
|
||||
|
||||
:param jQuery row:
|
||||
:rtype: Object
|
||||
|
||||
.. js:function:: openerp.web.ListView.with_event(event_name, event, action[, args][, trigger_params])
|
||||
|
||||
Executes ``action`` in the context of the view's editor,
|
||||
bracketing it with cancellable event signals.
|
||||
|
||||
:param String event_name: base name for the bracketing event, will
|
||||
be postfixed by ``:before`` and
|
||||
``:after`` before being called
|
||||
(respectively before and after
|
||||
``action`` is executed)
|
||||
:param Object event: object passed to the ``:before`` event
|
||||
handlers.
|
||||
:param Function action: function called with the view's editor as
|
||||
its ``this``. May return a deferred.
|
||||
:param Array args: arguments passed to ``action``
|
||||
:param Array trigger_params: arguments passed to the ``:after``
|
||||
event handler alongside the results
|
||||
of ``action``
|
||||
|
||||
Behavioral Customizations
|
||||
+++++++++++++++++++++++++
|
||||
|
||||
.. js:function:: openerp.web.ListView.handle_onwrite(record)
|
||||
|
||||
Implements the handling of the ``onwrite`` listview attribute:
|
||||
calls the RPC methods specified by ``@onwrite``, and if that
|
||||
method returns an array of ids loads or reloads the records
|
||||
corresponding to those ids.
|
||||
|
||||
:param record: record being written having triggered the
|
||||
``onwrite`` callback
|
||||
:type record: openerp.web.list.Record
|
||||
:returns: deferred to all reloadings being done
|
||||
|
||||
Events
|
||||
++++++
|
||||
|
||||
For simpler interactions by/with external users of the listview, the
|
||||
view provides a number of dedicated events to its lifecycle.
|
||||
|
||||
.. note:: if an event is defined as *cancellable*, it means its first
|
||||
parameter is an object on which the ``cancel`` attribute can
|
||||
be set. If the ``cancel`` attribute is set, the view will
|
||||
abort its current behavior as soon as possible, and rollback
|
||||
any state modification.
|
||||
|
||||
Generally speaking, an event should only be cancelled (by
|
||||
setting the ``cancel`` flag to ``true``), uncancelling an
|
||||
event is undefined as event handlers are executed on a
|
||||
first-come-first-serve basis and later handlers may
|
||||
re-cancel an uncancelled event.
|
||||
|
||||
.. _listview-edit-before:
|
||||
|
||||
``edit:before`` *cancellable*
|
||||
|
||||
Invoked before the list view starts editing a record.
|
||||
|
||||
Provided with an event object with a single property ``record``,
|
||||
holding the attributes of the record being edited (``record`` is
|
||||
empty *but not null* for a new record)
|
||||
|
||||
``edit:after``
|
||||
|
||||
Invoked after the list view has gone into an edition state,
|
||||
provided with the attributes of the record being edited (see
|
||||
``edit:before``) as first parameter and the form used for the
|
||||
edition as second parameter.
|
||||
|
||||
``save:before`` *cancellable*
|
||||
|
||||
Invoked right before saving a pending edition, provided with an
|
||||
event object holding the listview's editor (``editor``) and the
|
||||
edition form (``form``)
|
||||
|
||||
``save:after``
|
||||
|
||||
Invoked after a save has been completed
|
||||
|
||||
``cancel:before`` *cancellable*
|
||||
|
||||
Invoked before cancelling a pending edition, provided with the
|
||||
same information as ``save:before``.
|
||||
|
||||
``cancel:after``
|
||||
|
||||
Invoked after a pending edition has been cancelled.
|
||||
|
||||
DOM events
|
||||
++++++++++
|
||||
|
||||
The list view has grown hooks for the ``keyup`` event on its edition
|
||||
form (during edition): any such event bubbling out of the edition form
|
||||
will be forwarded to a method ``keyup_EVENTNAME``, where ``EVENTNAME``
|
||||
is the name of the key in ``$.ui.keyCode``.
|
||||
|
||||
The method will also get the event object (originally passed to the
|
||||
``keyup`` handler) as its sole parameter.
|
||||
|
||||
The base editable list view has handlers for the ``ENTER`` and
|
||||
``ESCAPE`` keys.
|
||||
|
||||
Editor
|
||||
------
|
||||
|
||||
The list-edition modules does not generally interact with the embedded
|
||||
formview, delegating instead to its
|
||||
:js:class:`~openerp.web.list.Editor`.
|
||||
|
||||
.. js:class:: openerp.web.list.Editor(parent[, options])
|
||||
|
||||
The editor object provides a more convenient interface to form
|
||||
views, and simplifies the usage of form views for semi-arbitrary
|
||||
edition of stuff.
|
||||
|
||||
However, the editor does *not* task itself with being internally
|
||||
consistent at this point: calling
|
||||
e.g. :js:func:`~openerp.web.list.Editor.edit` multiple times in a
|
||||
row without saving or cancelling each edit is undefined.
|
||||
|
||||
:param parent:
|
||||
:type parent: :js:class:`~openerp.web.Widget`
|
||||
:param EditorOptions options:
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.is_editing([record_state])
|
||||
|
||||
Indicates whether the editor is currently in the process of
|
||||
providing edition for a record.
|
||||
|
||||
Can be filtered by the state of the record being edited
|
||||
(whether it's a record being *created* or a record being
|
||||
*altered*), in which case it asserts both that an edition is
|
||||
underway and that the record being edited respectively does
|
||||
not yet exist in the database or already exists there.
|
||||
|
||||
:param record_state: state of the record being edited.
|
||||
Either ``"new"`` or ``"edit"``.
|
||||
:type record_state: String
|
||||
:rtype: Boolean
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.edit(record, configureField[, options])
|
||||
|
||||
Loads the provided record into the internal form view and
|
||||
displays the form view.
|
||||
|
||||
Will also attempt to focus the first visible field of the form
|
||||
view.
|
||||
|
||||
:param Object record: record to load into the form view
|
||||
(key:value mapping similar to the result
|
||||
of a ``read``)
|
||||
:param configureField: function called with each field of the
|
||||
form view right after the form is
|
||||
displayed, lets whoever called this
|
||||
method do some last-minute
|
||||
configuration of form fields.
|
||||
:type configureField: Function<String, openerp.web.form.Field>
|
||||
:param EditOptions options:
|
||||
:returns: jQuery delegate to the form object
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.save
|
||||
|
||||
Attempts to save the internal form, then hide it
|
||||
|
||||
:returns: delegate to the record under edition (with ``id``
|
||||
added for a creation). The record is not updated
|
||||
from when it was passed in, aside from the ``id``
|
||||
attribute.
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.cancel([force=false])
|
||||
|
||||
Attemps to cancel the edition of the internal form, then hide
|
||||
the form
|
||||
|
||||
:param Boolean force: unconditionally cancels the edition of
|
||||
the internal form, even if the user has
|
||||
already entered data in it.
|
||||
:returns: delegate to the record under edition
|
||||
|
||||
.. js:class:: EditorOptions
|
||||
|
||||
.. js:attribute:: EditorOptions.formView
|
||||
|
||||
Form view (sub)-class to instantiate and delegate edition to.
|
||||
|
||||
By default, :js:class:`~openerp.web.FormView`
|
||||
|
||||
.. js:attribute:: EditorOptions.delegate
|
||||
|
||||
Object used to get various bits of information about how to
|
||||
display stuff.
|
||||
|
||||
By default, uses the editor's parent widget. See
|
||||
:js:class:`EditorDelegate` for the methods and attributes to
|
||||
provide.
|
||||
|
||||
.. js:class:: EditorDelegate
|
||||
|
||||
Informal protocol defining the methods and attributes expected of
|
||||
the :js:class:`~openerp.web.list.Editor`'s delegate.
|
||||
|
||||
.. js:attribute:: EditorDelegate.dataset
|
||||
|
||||
The dataset passed to the form view to synchronize the form
|
||||
view and the outer widget.
|
||||
|
||||
.. js:function:: EditorDelegate.edition_view(editor)
|
||||
|
||||
Called by the :js:class:`~openerp.web.list.Editor` object to
|
||||
get a form view (JSON) to pass along to the form view it
|
||||
created.
|
||||
|
||||
The result should be a valid form view, see :doc:`Form Notes
|
||||
<form-notes>` for various peculiarities of the form view
|
||||
format.
|
||||
|
||||
:param editor: editor object asking for the view
|
||||
:type editor: :js:class:`~openerp.web.list.Editor`
|
||||
:returns: form view
|
||||
:rtype: Object
|
||||
|
||||
.. js:function:: EditorDelegate.prepends_on_create
|
||||
|
||||
By default, the :js:class:`~openerp.web.list.Editor` will
|
||||
append the ids of newly created records to the
|
||||
:js:attr:`EditorDelegate.dataset`. If this method returns
|
||||
``true``, it will prepend these ids instead.
|
||||
|
||||
:returns: whether new records should be prepended to the
|
||||
dataset (instead of appended)
|
||||
:rtype: Boolean
|
||||
|
||||
|
||||
.. js:class:: EditOptions
|
||||
|
||||
Options object optionally passed into a method starting an edition
|
||||
to configure its setup and behavior
|
||||
|
||||
.. js:attribute:: focus_field
|
||||
|
||||
Name of the field to set focus on after setting up the edition
|
||||
of the record.
|
||||
|
||||
If this option is not provided, or the requested field can not
|
||||
be focused (invisible, readonly or not in the view), the first
|
||||
visible non-readonly field is focused.
|
||||
|
||||
Changes from 6.1
|
||||
----------------
|
||||
|
||||
* The editable listview behavior has been rewritten pretty much from
|
||||
scratch, any code touching on editability will have to be modified
|
||||
|
||||
* The overloading of :js:class:`~openerp.web.ListView.Groups` and
|
||||
:js:class:`~openerp.web.ListView.List` for editability has been
|
||||
drastically simplified, and most of the behavior has been moved to
|
||||
the list view itself. Only
|
||||
:js:func:`~openerp.web.ListView.List.row_clicked` is still
|
||||
overridden.
|
||||
|
||||
* A new method ``get_row_for(record) -> jQuery(tr) | null`` has been
|
||||
added to both ListView.List and ListView.Group, it can be called
|
||||
from the list view to get the table row matching a record (if such
|
||||
a row exists).
|
||||
|
||||
* :js:func:`~openerp.web.ListView.do_button_action`'s core behavior
|
||||
has been split away to
|
||||
:js:func:`~openerp.web.ListView.handle_button`. This allows bypassing
|
||||
overrides of :js:func:`~openerp.web.ListView.do_button_action` in a
|
||||
parent class.
|
||||
|
||||
Ideally, :js:func:`~openerp.web.ListView.handle_button` should not be
|
||||
overridden.
|
||||
|
||||
* Modifiers handling has been improved (all modifiers information
|
||||
should now be available through :js:func:`~Column.modifiers_for`,
|
||||
not just ``invisible``)
|
||||
|
||||
* Changed some handling of the list view's record: a record may now
|
||||
have no id, and the listview will handle that correctly (for new
|
||||
records being created) as well as correctly handle the ``id`` being
|
||||
set.
|
||||
|
||||
* Extended the internal collections structure of the list view with
|
||||
`#find`_, `#succ`_ and `#pred`_.
|
||||
|
||||
.. _#find: http://underscorejs.org/#find
|
||||
|
||||
.. _#succ: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:succ
|
||||
|
||||
.. _#pred: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:pred
|
||||
@@ -1,5 +1,5 @@
|
||||
Outside the box: network interactions
|
||||
=====================================
|
||||
RPC Calls
|
||||
=========
|
||||
|
||||
Building static displays is all nice and good and allows for neat
|
||||
effects (and sometimes you're given data to display from third parties
|
||||
@@ -137,7 +137,7 @@ around and use them differently/add new specifications on them.
|
||||
asked of the server. Grouping
|
||||
can actually be an array or
|
||||
varargs.
|
||||
:rtype: Deferred<Array<openerp.web.Group>> | null
|
||||
:rtype: Deferred<Array<openerp.web.QueryGroup>> | null
|
||||
|
||||
The second set of methods is the "mutator" methods, they create a
|
||||
**new** :js:class:`~openerp.web.Query` object with the relevant
|
||||
@@ -189,7 +189,7 @@ in that they're recursive, and level n+1 relies on data provided
|
||||
directly by the grouping at level n. As a result, while ``read_group``
|
||||
works it's not a very intuitive API.
|
||||
|
||||
OpenERP Web 6.2 eschews direct calls to ``read_group`` in favor of
|
||||
OpenERP Web 7.0 eschews direct calls to ``read_group`` in favor of
|
||||
calling a method of :js:class:`~openerp.web.Query`, `much in the way
|
||||
it is one in SQLAlchemy
|
||||
<http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.group_by>`_ [#]_:
|
||||
@@ -238,76 +238,7 @@ regular query for records):
|
||||
});
|
||||
|
||||
The result of a (successful) :js:func:`~openerp.web.Query.group_by` is
|
||||
an array of :js:class:`~openerp.web.data.Group`.
|
||||
|
||||
Synchronizing views (provisional)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. note:: this API may not be final, and may not even remain
|
||||
|
||||
While the high-level RPC API is mostly stateless, some objects in
|
||||
OpenERP Web need to share state information. One of those is OpenERP
|
||||
views, especially between "collection-based" views (lists, graphs) and
|
||||
"record-based" views (forms, diagrams), which gets its very own API
|
||||
for traversing collections of records, the aptly-named
|
||||
:js:class:`~openerp.web.Traverser`.
|
||||
|
||||
A :js:class:`~openerp.web.Traverser` is linked to a
|
||||
:js:class:`~openerp.web.Model` and is used to iterate over it
|
||||
asynchronously (and using indexes).
|
||||
|
||||
.. js:class:: openerp.web.Traverser(model)
|
||||
|
||||
.. js:function:: openerp.web.Traverser.model()
|
||||
|
||||
:returns: the :js:class:`~openerp.web.Model` this traverser
|
||||
instance is bound to
|
||||
|
||||
.. js:function:: openerp.web.Traverser.index([idx])
|
||||
|
||||
If provided with an index parameter, sets that as the new
|
||||
index for the traverser.
|
||||
|
||||
:param Number idx: the new index for the traverser
|
||||
:returns: the current index for the traverser
|
||||
|
||||
.. js:function:: openerp.web.Traverser.current([fields])
|
||||
|
||||
Fetches the traverser's "current" record (that is, the record
|
||||
at the current index of the traverser)
|
||||
|
||||
:param Array<String> fields: fields to return in the record
|
||||
:rtype: Deferred<>
|
||||
|
||||
.. js:function:: openerp.web.Traverser.next([fields])
|
||||
|
||||
Increases the traverser's internal index by one, the fetches
|
||||
the corresponding record. Roughly equivalent to:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var idx = traverser.index();
|
||||
traverser.index(idx+1);
|
||||
traverser.current();
|
||||
|
||||
:param Array<String> fields: fields to return in the record
|
||||
:rtype: Deferred<>
|
||||
|
||||
.. js:function:: openerp.web.Traverser.previous([fields])
|
||||
|
||||
Similar to :js:func:`~openerp.web.Traverser.next` but iterates
|
||||
the traverser backwards rather than forward.
|
||||
|
||||
:param Array<String> fields: fields to return in the record
|
||||
:rtype: Deferred<>
|
||||
|
||||
.. js:function:: openerp.web.Traverser.size()
|
||||
|
||||
Shortcut to checking the size of the backing model, calling
|
||||
``traverser.size()`` is equivalent to calling
|
||||
``traverser.model().query([]).count()``
|
||||
|
||||
:rtype: Deferred<Number>
|
||||
an array of :js:class:`~openerp.web.QueryGroup`.
|
||||
|
||||
Low-level API: RPC calls to Python side
|
||||
---------------------------------------
|
||||
@@ -1,7 +1,7 @@
|
||||
Search View
|
||||
===========
|
||||
|
||||
OpenERP Web 6.2 implements a unified facets-based search view instead
|
||||
OpenERP Web 7.0 implements a unified facets-based search view instead
|
||||
of the previous form-like search view (composed of buttons and
|
||||
multiple fields). The goal for this change is twofold:
|
||||
|
||||
@@ -145,6 +145,40 @@ started only once (per view).
|
||||
dynamically collects, lays out and renders filters? =>
|
||||
exercises drawer thingies
|
||||
|
||||
Converting from facet objects
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
Ultimately, the point of the search view is to allow searching. In
|
||||
OpenERP this is done via :ref:`domains <openerpserver:domains>`. On
|
||||
the other hand, the OpenERP Web 7 search view's state is modelled
|
||||
after a collection of :js:class:`~openerp.web.search.Facet`, and each
|
||||
field of a search view may have special requirements when it comes to
|
||||
the domains it produces [#special]_.
|
||||
|
||||
So there needs to be some way of mapping
|
||||
:js:class:`~openerp.web.search.Facet` objects to OpenERP search data.
|
||||
|
||||
This is done via an input's
|
||||
:js:func:`~openerp.web.search.Input.get_domain` and
|
||||
:js:func:`~openerp.web.search.Input.get_context`. Each takes a
|
||||
:js:class:`~openerp.web.search.Facet` and returns whatever it's
|
||||
supposed to generate (a domain or a context, respectively). Either can
|
||||
return ``null`` if the current value does not map to a domain or
|
||||
context, and can throw an :js:class:`~openerp.web.search.Invalid`
|
||||
exception if the value is not valid at all for the field.
|
||||
|
||||
.. note::
|
||||
|
||||
The :js:class:`~openerp.web.search.Facet` object can have any
|
||||
number of values (from 1 upwards)
|
||||
|
||||
.. note::
|
||||
|
||||
There is a third conversion method,
|
||||
:js:func:`~openerp.web.search.Input.get_groupby`, which returns an
|
||||
``Array`` of groupby domains rather than a single context. At this
|
||||
point, it is only implemented on (and used by) filters.
|
||||
|
||||
Programmatic interactions: internal model
|
||||
-----------------------------------------
|
||||
|
||||
@@ -256,42 +290,8 @@ with directly by external objects or search view controls
|
||||
|
||||
Can be of any type.
|
||||
|
||||
Converting from facet objects
|
||||
-----------------------------
|
||||
|
||||
Ultimately, the point of the search view is to allow searching. In
|
||||
OpenERP this is done via :ref:`domains <openerpserver:domains>`. On
|
||||
the other hand, the OpenERP Web 7 search view's state is modelled
|
||||
after a collection of :js:class:`~openerp.web.search.Facet`, and each
|
||||
field of a search view may have special requirements when it comes to
|
||||
the domains it produces [#special]_.
|
||||
|
||||
So there needs to be some way of mapping
|
||||
:js:class:`~openerp.web.search.Facet` objects to OpenERP search data.
|
||||
|
||||
This is done via an input's
|
||||
:js:func:`~openerp.web.search.Input.get_domain` and
|
||||
:js:func:`~openerp.web.search.Input.get_context`. Each takes a
|
||||
:js:class:`~openerp.web.search.Facet` and returns whatever it's
|
||||
supposed to generate (a domain or a context, respectively). Either can
|
||||
return ``null`` if the current value does not map to a domain or
|
||||
context, and can throw an :js:class:`~openerp.web.search.Invalid`
|
||||
exception if the value is not valid at all for the field.
|
||||
|
||||
.. note::
|
||||
|
||||
The :js:class:`~openerp.web.search.Facet` object can have any
|
||||
number of values (from 1 upwards)
|
||||
|
||||
.. note::
|
||||
|
||||
There is a third conversion method,
|
||||
:js:func:`~openerp.web.search.Input.get_groupby`, which returns an
|
||||
``Array`` of groupby domains rather than a single context. At this
|
||||
point, it is only implemented on (and used by) filters.
|
||||
|
||||
Field services
|
||||
++++++++++++++
|
||||
--------------
|
||||
|
||||
:js:class:`~openerp.web.search.Field` provides a default
|
||||
implementation of :js:func:`~openerp.web.search.Input.get_domain` and
|
||||
@@ -377,7 +377,7 @@ necessarily having to reimplement all of
|
||||
:js:class:`~openerp.web.search.Field`
|
||||
|
||||
Arbitrary data storage
|
||||
++++++++++++++++++++++
|
||||
----------------------
|
||||
|
||||
:js:class:`~openerp.web.search.Facet` and
|
||||
:js:class:`~openerp.web.search.FacetValue` objects (and structures)
|
||||
@@ -0,0 +1,274 @@
|
||||
User Interaction: Widget
|
||||
========================
|
||||
|
||||
This is the base class for all visual components. It corresponds to an MVC
|
||||
view. It provides a number of services to handle a section of a page:
|
||||
|
||||
* Rendering with QWeb
|
||||
|
||||
* Parenting-child relations
|
||||
|
||||
* Life-cycle management (including facilitating children destruction when a
|
||||
parent object is removed)
|
||||
|
||||
* DOM insertion, via jQuery-powered insertion methods. Insertion targets can
|
||||
be anything the corresponding jQuery method accepts (generally selectors,
|
||||
DOM nodes and jQuery objects):
|
||||
|
||||
:js:func:`~openerp.base.Widget.appendTo`
|
||||
Renders the widget and inserts it as the last child of the target, uses
|
||||
`.appendTo()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.prependTo`
|
||||
Renders the widget and inserts it as the first child of the target, uses
|
||||
`.prependTo()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.insertAfter`
|
||||
Renders the widget and inserts it as the preceding sibling of the target,
|
||||
uses `.insertAfter()`_
|
||||
|
||||
:js:func:`~openerp.base.Widget.insertBefore`
|
||||
Renders the widget and inserts it as the following sibling of the target,
|
||||
uses `.insertBefore()`_
|
||||
|
||||
* Backbone-compatible shortcuts
|
||||
|
||||
DOM Root
|
||||
--------
|
||||
|
||||
A :js:class:`~openerp.web.Widget` is responsible for a section of the
|
||||
page materialized by the DOM root of the widget. The DOM root is
|
||||
available via the :js:attr:`~openerp.web.Widget.el` and
|
||||
:js:attr:`~openerp.web.Widget.$element` attributes, which are
|
||||
respectively the raw DOM Element and the jQuery wrapper around the DOM
|
||||
element.
|
||||
|
||||
There are two main ways to define and generate this DOM root:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.template
|
||||
|
||||
Should be set to the name of a QWeb template (a
|
||||
:js:class:`String`). If set, the template will be rendered after
|
||||
the widget has been initialized but before it has been
|
||||
started. The root element generated by the template will be set as
|
||||
the DOM root of the widget.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.tagName
|
||||
|
||||
Used if the widget has no template defined. Defaults to ``div``,
|
||||
will be used as the tag name to create the DOM element to set as
|
||||
the widget's DOM root. It is possible to further customize this
|
||||
generated DOM root with the following attributes:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.id
|
||||
|
||||
Used to generate an ``id`` attribute on the generated DOM
|
||||
root.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.className
|
||||
|
||||
Used to generate a ``class`` attribute on the generated DOM root.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.attributes
|
||||
|
||||
Mapping (object literal) of attribute names to attribute
|
||||
values. Each of these k:v pairs will be set as a DOM attribute
|
||||
on the generated DOM root.
|
||||
|
||||
None of these is used in case a template is specified on the widget.
|
||||
|
||||
The DOM root can also be defined programmatically by overridding
|
||||
|
||||
.. js:function:: openerp.web.Widget.renderElement
|
||||
|
||||
Renders the widget's DOM root and sets it. The default
|
||||
implementation will render a set template or generate an element
|
||||
as described above, and will call
|
||||
:js:func:`~openerp.web.Widget.setElement` on the result.
|
||||
|
||||
Any override to :js:func:`~openerp.web.Widget.renderElement` which
|
||||
does not call its ``_super`` **must** call
|
||||
:js:func:`~openerp.web.Widget.setElement` with whatever it
|
||||
generated or the widget's behavior is undefined.r
|
||||
|
||||
.. note::
|
||||
|
||||
The default :js:func:`~openerp.web.Widget.renderElement` can
|
||||
be called repeatedly, it will *replace* the previous DOM root
|
||||
(using ``replaceWith``). However, this requires that the
|
||||
widget correctly sets and unsets its events (and children
|
||||
widgets). Generally,
|
||||
:js:func:`~openerp.web.Widget.renderElement` should not be
|
||||
called repeatedly unless the widget advertizes this feature.
|
||||
|
||||
Accessing DOM content
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Because a widget is only responsible for the content below its DOM
|
||||
root, there is a shortcut for selecting sub-sections of a widget's
|
||||
DOM:
|
||||
|
||||
.. js:function:: openerp.web.Widget.$(selector)
|
||||
|
||||
Applies the CSS selector specified as parameter to the widget's
|
||||
DOM root.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.$(selector);
|
||||
|
||||
is functionally identical to:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.$element.find(selector);
|
||||
|
||||
:param String selector: CSS selector
|
||||
:returns: jQuery object
|
||||
|
||||
.. note:: this helper method is compatible with
|
||||
``Backbone.View.$``
|
||||
|
||||
Resetting the DOM root
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. js:function:: openerp.web.Widget.setElement(element)
|
||||
|
||||
Re-sets the widget's DOM root to the provided element, also
|
||||
handles re-setting the various aliases of the DOM root as well as
|
||||
unsetting and re-setting delegated events.
|
||||
|
||||
:param Element element: a DOM element or jQuery object to set as
|
||||
the widget's DOM root
|
||||
|
||||
.. note:: should be mostly compatible with `Backbone's
|
||||
setElement`_
|
||||
|
||||
DOM events handling
|
||||
-------------------
|
||||
|
||||
A widget will generally need to respond to user action within its
|
||||
section of the page. This entails binding events to DOM elements.
|
||||
|
||||
To this end, :js:class:`~openerp.web.Widget` provides an shortcut:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.events
|
||||
|
||||
Events are a mapping of ``event selector`` (an event name and a
|
||||
CSS selector separated by a space) to a callback. The callback can
|
||||
be either a method name in the widget or a function. In either
|
||||
case, the ``this`` will be set to the widget.
|
||||
|
||||
The selector is used for jQuery's `event delegation`_, the
|
||||
callback will only be triggered for descendants of the DOM root
|
||||
matching the selector [0]_. If the selector is left out (only an
|
||||
event name is specified), the event will be set directly on the
|
||||
widget's DOM root.
|
||||
|
||||
.. js:function:: openerp.web.Widget.delegateEvents
|
||||
|
||||
This method is in charge of binding
|
||||
:js:attr:`~openerp.web.Widget.events` to the DOM. It is
|
||||
automatically called after setting the widget's DOM root.
|
||||
|
||||
It can be overridden to set up more complex events than the
|
||||
:js:attr:`~openerp.web.Widget.events` map allows, but the parent
|
||||
should always be called (or :js:attr:`~openerp.web.Widget.events`
|
||||
won't be handled correctly).
|
||||
|
||||
.. js:function:: openerp.web.Widget.undelegateEvents
|
||||
|
||||
This method is in charge of unbinding
|
||||
:js:attr:`~openerp.web.Widget.events` from the DOM root when the
|
||||
widget is destroyed or the DOM root is reset, in order to avoid
|
||||
leaving "phantom" events.
|
||||
|
||||
It should be overridden to un-set any event set in an override of
|
||||
:js:func:`~openerp.web.Widget.delegateEvents`.
|
||||
|
||||
.. note:: this behavior should be compatible with `Backbone's
|
||||
delegateEvents`_, apart from not accepting any argument.
|
||||
|
||||
Subclassing Widget
|
||||
------------------
|
||||
|
||||
:js:class:`~openerp.base.Widget` is subclassed in the standard manner (via the
|
||||
:js:func:`~openerp.base.Class.extend` method), and provides a number of
|
||||
abstract properties and concrete methods (which you may or may not want to
|
||||
override). Creating a subclass looks like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var MyWidget = openerp.base.Widget.extend({
|
||||
// QWeb template to use when rendering the object
|
||||
template: "MyQWebTemplate",
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
// insert code to execute before rendering, for object
|
||||
// initialization
|
||||
},
|
||||
start: function() {
|
||||
this._super();
|
||||
// post-rendering initialization code, at this point
|
||||
// ``this.$element`` has been initialized
|
||||
this.$element.find(".my_button").click(/* an example of event binding * /);
|
||||
|
||||
// if ``start`` is asynchronous, return a promise object so callers
|
||||
// know when the object is done initializing
|
||||
return this.rpc(/* … */)
|
||||
}
|
||||
});
|
||||
|
||||
The new class can then be used in the following manner:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// Create the instance
|
||||
var my_widget = new MyWidget(this);
|
||||
// Render and insert into DOM
|
||||
my_widget.appendTo(".some-div");
|
||||
|
||||
After these two lines have executed (and any promise returned by ``appendTo``
|
||||
has been resolved if needed), the widget is ready to be used.
|
||||
|
||||
.. note:: the insertion methods will start the widget themselves, and will
|
||||
return the result of :js:func:`~openerp.base.Widget.start()`.
|
||||
|
||||
If for some reason you do not want to call these methods, you will
|
||||
have to first call :js:func:`~openerp.base.Widget.render()` on the
|
||||
widget, then insert it into your DOM and start it.
|
||||
|
||||
If the widget is not needed anymore (because it's transient), simply terminate
|
||||
it:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
my_widget.destroy();
|
||||
|
||||
will unbind all DOM events, remove the widget's content from the DOM and
|
||||
destroy all widget data.
|
||||
|
||||
.. [0] not all DOM events are compatible with events delegation
|
||||
|
||||
.. _.appendTo():
|
||||
http://api.jquery.com/appendTo/
|
||||
|
||||
.. _.prependTo():
|
||||
http://api.jquery.com/prependTo/
|
||||
|
||||
.. _.insertAfter():
|
||||
http://api.jquery.com/insertAfter/
|
||||
|
||||
.. _.insertBefore():
|
||||
http://api.jquery.com/insertBefore/
|
||||
|
||||
.. _event delegation:
|
||||
http://api.jquery.com/delegate/
|
||||
|
||||
.. _Backbone's setElement:
|
||||
http://backbonejs.org/#View-setElement
|
||||
|
||||
.. _Backbone's delegateEvents:
|
||||
http://backbonejs.org/#View-delegateEvents
|
||||
|
||||
@@ -6,14 +6,18 @@ import ast
|
||||
import cgi
|
||||
import contextlib
|
||||
import functools
|
||||
import getpass
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import urllib
|
||||
import urlparse
|
||||
import uuid
|
||||
import xmlrpclib
|
||||
|
||||
@@ -25,17 +29,15 @@ import werkzeug.utils
|
||||
import werkzeug.wrappers
|
||||
import werkzeug.wsgi
|
||||
|
||||
from . import nonliterals
|
||||
from . import session
|
||||
from . import openerplib
|
||||
import openerp
|
||||
|
||||
__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
|
||||
'WebRequest', 'JsonRequest', 'HttpRequest']
|
||||
import nonliterals
|
||||
import session
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# OpenERP Web RequestHandler
|
||||
# RequestHandler
|
||||
#----------------------------------------------------------
|
||||
class WebRequest(object):
|
||||
""" Parent class for all OpenERP Web request types, mostly deals with
|
||||
@@ -44,7 +46,6 @@ class WebRequest(object):
|
||||
|
||||
:param request: a wrapped werkzeug Request object
|
||||
:type request: :class:`werkzeug.wrappers.BaseRequest`
|
||||
:param config: configuration object
|
||||
|
||||
.. attribute:: httprequest
|
||||
|
||||
@@ -56,10 +57,6 @@ class WebRequest(object):
|
||||
a :class:`~collections.Mapping` holding the HTTP session data for the
|
||||
current http session
|
||||
|
||||
.. attribute:: config
|
||||
|
||||
config parameter provided to the request object
|
||||
|
||||
.. attribute:: params
|
||||
|
||||
:class:`~collections.Mapping` of request parameters, not generally
|
||||
@@ -83,11 +80,10 @@ class WebRequest(object):
|
||||
|
||||
``bool``, indicates whether the debug mode is active on the client
|
||||
"""
|
||||
def __init__(self, request, config):
|
||||
def __init__(self, request):
|
||||
self.httprequest = request
|
||||
self.httpresponse = None
|
||||
self.httpsession = request.session
|
||||
self.config = config
|
||||
|
||||
def init(self, params):
|
||||
self.params = dict(params)
|
||||
@@ -96,7 +92,6 @@ class WebRequest(object):
|
||||
self.session = self.httpsession.get(self.session_id)
|
||||
if not self.session:
|
||||
self.httpsession[self.session_id] = self.session = session.OpenERPSession()
|
||||
self.session.config = self.config
|
||||
self.context = self.params.pop('context', None)
|
||||
self.debug = self.params.pop('debug', False) != False
|
||||
|
||||
@@ -133,10 +128,9 @@ class JsonRequest(WebRequest):
|
||||
"id": null}
|
||||
|
||||
"""
|
||||
def dispatch(self, controller, method):
|
||||
def dispatch(self, method):
|
||||
""" Calls the method asked for by the JSON-RPC2 or JSONP request
|
||||
|
||||
:param controller: the instance of the controller which received the request
|
||||
:param method: the method which received the request
|
||||
|
||||
:returns: an utf8 encoded JSON-RPC2 or JSONP reply
|
||||
@@ -175,10 +169,10 @@ class JsonRequest(WebRequest):
|
||||
self.jsonrequest = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
|
||||
self.init(self.jsonrequest.get("params", {}))
|
||||
if _logger.isEnabledFor(logging.DEBUG):
|
||||
_logger.debug("--> %s.%s\n%s", controller.__class__.__name__, method.__name__, pprint.pformat(self.jsonrequest))
|
||||
_logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest))
|
||||
response['id'] = self.jsonrequest.get('id')
|
||||
response["result"] = method(controller, self, **self.params)
|
||||
except openerplib.AuthenticationError:
|
||||
response["result"] = method(self, **self.params)
|
||||
except session.AuthenticationError:
|
||||
error = {
|
||||
'code': 100,
|
||||
'message': "OpenERP Session Invalid",
|
||||
@@ -235,16 +229,13 @@ def jsonrequest(f):
|
||||
the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
|
||||
beforehand)
|
||||
"""
|
||||
@functools.wraps(f)
|
||||
def json_handler(controller, request, config):
|
||||
return JsonRequest(request, config).dispatch(controller, f)
|
||||
json_handler.exposed = True
|
||||
return json_handler
|
||||
f.exposed = 'json'
|
||||
return f
|
||||
|
||||
class HttpRequest(WebRequest):
|
||||
""" Regular GET/POST request
|
||||
"""
|
||||
def dispatch(self, controller, method):
|
||||
def dispatch(self, method):
|
||||
params = dict(self.httprequest.args)
|
||||
params.update(self.httprequest.form)
|
||||
params.update(self.httprequest.files)
|
||||
@@ -255,9 +246,9 @@ class HttpRequest(WebRequest):
|
||||
akw[key] = value
|
||||
else:
|
||||
akw[key] = type(value)
|
||||
_logger.debug("%s --> %s.%s %r", self.httprequest.method, controller.__class__.__name__, method.__name__, akw)
|
||||
_logger.debug("%s --> %s.%s %r", self.httprequest.method, method.im_class.__name__, method.__name__, akw)
|
||||
try:
|
||||
r = method(controller, self, **self.params)
|
||||
r = method(self, **self.params)
|
||||
except xmlrpclib.Fault, e:
|
||||
r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps({
|
||||
'code': 200,
|
||||
@@ -322,23 +313,36 @@ def httprequest(f):
|
||||
merged in the same dictionary), apart from the ``session_id``, ``context``
|
||||
and ``debug`` keys (which are stripped out beforehand)
|
||||
"""
|
||||
@functools.wraps(f)
|
||||
def http_handler(controller, request, config):
|
||||
return HttpRequest(request, config).dispatch(controller, f)
|
||||
http_handler.exposed = True
|
||||
return http_handler
|
||||
f.exposed = 'http'
|
||||
return f
|
||||
|
||||
#----------------------------------------------------------
|
||||
# OpenERP Web werkzeug Session Managment wraped using with
|
||||
# Controller registration with a metaclass
|
||||
#----------------------------------------------------------
|
||||
addons_module = {}
|
||||
addons_manifest = {}
|
||||
controllers_class = []
|
||||
controllers_object = {}
|
||||
controllers_path = {}
|
||||
|
||||
class ControllerType(type):
|
||||
def __init__(cls, name, bases, attrs):
|
||||
super(ControllerType, cls).__init__(name, bases, attrs)
|
||||
controllers_class.append(("%s.%s" % (cls.__module__, cls.__name__), cls))
|
||||
|
||||
class Controller(object):
|
||||
__metaclass__ = ControllerType
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Session context manager
|
||||
#----------------------------------------------------------
|
||||
STORES = {}
|
||||
|
||||
@contextlib.contextmanager
|
||||
def session_context(request, storage_path, session_cookie='sessionid'):
|
||||
def session_context(request, storage_path, session_cookie='httpsessionid'):
|
||||
session_store, session_lock = STORES.get(storage_path, (None, None))
|
||||
if not session_store:
|
||||
session_store = werkzeug.contrib.sessions.FilesystemSessionStore(
|
||||
storage_path)
|
||||
session_store = werkzeug.contrib.sessions.FilesystemSessionStore( storage_path)
|
||||
session_lock = threading.Lock()
|
||||
STORES[storage_path] = session_store, session_lock
|
||||
|
||||
@@ -381,7 +385,7 @@ def session_context(request, storage_path, session_cookie='sessionid'):
|
||||
# note that domains_store and contexts_store are append-only (we
|
||||
# only ever add items to them), so we can just update one with the
|
||||
# other to get the right result, if we want to merge the
|
||||
# ``context`` dict we'll need something smarter
|
||||
# ``context`` dict we'll need something smarter
|
||||
in_store = session_store.get(sid)
|
||||
for k, v in request.session.iteritems():
|
||||
stored = in_store.get(k)
|
||||
@@ -401,64 +405,54 @@ def session_context(request, storage_path, session_cookie='sessionid'):
|
||||
session_store.save(request.session)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# OpenERP Web Module/Controller Loading and URL Routing
|
||||
# WSGI Application
|
||||
#----------------------------------------------------------
|
||||
addons_module = {}
|
||||
addons_manifest = {}
|
||||
controllers_class = []
|
||||
controllers_object = {}
|
||||
controllers_path = {}
|
||||
# Add potentially missing (older ubuntu) font mime types
|
||||
mimetypes.add_type('application/font-woff', '.woff')
|
||||
mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
|
||||
mimetypes.add_type('application/x-font-ttf', '.ttf')
|
||||
|
||||
class ControllerType(type):
|
||||
def __init__(cls, name, bases, attrs):
|
||||
super(ControllerType, cls).__init__(name, bases, attrs)
|
||||
controllers_class.append(("%s.%s" % (cls.__module__, cls.__name__), cls))
|
||||
class DisableCacheMiddleware(object):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
def __call__(self, environ, start_response):
|
||||
def start_wrapped(status, headers):
|
||||
referer = environ.get('HTTP_REFERER', '')
|
||||
parsed = urlparse.urlparse(referer)
|
||||
debug = parsed.query.count('debug') >= 1
|
||||
|
||||
class Controller(object):
|
||||
__metaclass__ = ControllerType
|
||||
new_headers = []
|
||||
unwanted_keys = ['Last-Modified']
|
||||
if debug:
|
||||
new_headers = [('Cache-Control', 'no-cache')]
|
||||
unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
|
||||
|
||||
for k, v in headers:
|
||||
if k not in unwanted_keys:
|
||||
new_headers.append((k, v))
|
||||
|
||||
start_response(status, new_headers)
|
||||
return self.app(environ, start_wrapped)
|
||||
|
||||
class Root(object):
|
||||
"""Root WSGI application for the OpenERP Web Client.
|
||||
|
||||
:param options: mandatory initialization options object, must provide
|
||||
the following attributes:
|
||||
|
||||
``server_host`` (``str``)
|
||||
hostname of the OpenERP server to dispatch RPC to
|
||||
``server_port`` (``int``)
|
||||
RPC port of the OpenERP server
|
||||
``serve_static`` (``bool | None``)
|
||||
whether this application should serve the various
|
||||
addons's static files
|
||||
``storage_path`` (``str``)
|
||||
filesystem path where HTTP session data will be stored
|
||||
``dbfilter`` (``str``)
|
||||
only used in case the list of databases is requested
|
||||
by the server, will be filtered by this pattern
|
||||
"""
|
||||
def __init__(self, options, openerp_addons_namespace=True):
|
||||
self.root = '/web/webclient/home'
|
||||
self.config = options
|
||||
|
||||
if not hasattr(self.config, 'connector'):
|
||||
if self.config.backend == 'local':
|
||||
self.config.connector = LocalConnector()
|
||||
else:
|
||||
self.config.connector = openerplib.get_connector(
|
||||
hostname=self.config.server_host, port=self.config.server_port)
|
||||
|
||||
self.session_cookie = 'sessionid'
|
||||
def __init__(self):
|
||||
self.httpsession_cookie = 'httpsessionid'
|
||||
self.addons = {}
|
||||
|
||||
static_dirs = self._load_addons(openerp_addons_namespace)
|
||||
if options.serve_static:
|
||||
self.dispatch = werkzeug.wsgi.SharedDataMiddleware(
|
||||
self.dispatch, static_dirs)
|
||||
static_dirs = self._load_addons()
|
||||
app = werkzeug.wsgi.SharedDataMiddleware( self.dispatch, static_dirs)
|
||||
self.dispatch = DisableCacheMiddleware(app)
|
||||
|
||||
if options.session_storage:
|
||||
if not os.path.exists(options.session_storage):
|
||||
os.mkdir(options.session_storage, 0700)
|
||||
self.session_storage = options.session_storage
|
||||
try:
|
||||
username = getpass.getuser()
|
||||
except Exception:
|
||||
username = "unknown"
|
||||
self.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
|
||||
|
||||
if not os.path.exists(self.session_storage):
|
||||
os.mkdir(self.session_storage, 0700)
|
||||
_logger.debug('HTTP sessions stored in: %s', self.session_storage)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
@@ -477,21 +471,13 @@ class Root(object):
|
||||
request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
|
||||
request.app = self
|
||||
|
||||
if request.path == '/':
|
||||
params = urllib.urlencode(request.args)
|
||||
return werkzeug.utils.redirect(self.root + '?' + params, 301)(
|
||||
environ, start_response)
|
||||
elif request.path == '/mobile':
|
||||
return werkzeug.utils.redirect(
|
||||
'/web_mobile/static/src/web_mobile.html', 301)(environ, start_response)
|
||||
|
||||
handler = self.find_handler(*(request.path.split('/')[1:]))
|
||||
|
||||
if not handler:
|
||||
response = werkzeug.exceptions.NotFound()
|
||||
else:
|
||||
with session_context(request, self.session_storage, self.session_cookie) as session:
|
||||
result = handler( request, self.config)
|
||||
with session_context(request, self.session_storage, self.httpsession_cookie) as session:
|
||||
result = handler( request)
|
||||
|
||||
if isinstance(result, basestring):
|
||||
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
|
||||
@@ -500,17 +486,17 @@ class Root(object):
|
||||
response = result
|
||||
|
||||
if hasattr(response, 'set_cookie'):
|
||||
response.set_cookie(self.session_cookie, session.sid)
|
||||
response.set_cookie(self.httpsession_cookie, session.sid)
|
||||
|
||||
return response(environ, start_response)
|
||||
|
||||
def _load_addons(self, openerp_addons_namespace=True):
|
||||
def _load_addons(self):
|
||||
"""
|
||||
Loads all addons at the specified addons path, returns a mapping of
|
||||
static URLs to the corresponding directories
|
||||
"""
|
||||
statics = {}
|
||||
for addons_path in self.config.addons_path:
|
||||
for addons_path in openerp.modules.module.ad_paths:
|
||||
for module in os.listdir(addons_path):
|
||||
if module not in addons_module:
|
||||
manifest_path = os.path.join(addons_path, module, '__openerp__.py')
|
||||
@@ -519,7 +505,7 @@ class Root(object):
|
||||
manifest = ast.literal_eval(open(manifest_path).read())
|
||||
manifest['addons_path'] = addons_path
|
||||
_logger.debug("Loading %s", module)
|
||||
if openerp_addons_namespace:
|
||||
if 'openerp.addons' in sys.modules:
|
||||
m = __import__('openerp.addons.' + module)
|
||||
else:
|
||||
m = __import__(module)
|
||||
@@ -545,69 +531,25 @@ class Root(object):
|
||||
"""
|
||||
if l:
|
||||
ps = '/' + '/'.join(l)
|
||||
meth = 'index'
|
||||
method_name = 'index'
|
||||
while ps:
|
||||
c = controllers_path.get(ps)
|
||||
if c:
|
||||
m = getattr(c, meth)
|
||||
if getattr(m, 'exposed', False):
|
||||
_logger.debug("Dispatching to %s %s %s", ps, c, meth)
|
||||
return m
|
||||
ps, _slash, meth = ps.rpartition('/')
|
||||
method = getattr(c, method_name, None)
|
||||
if method:
|
||||
exposed = getattr(method, 'exposed', False)
|
||||
if exposed == 'json':
|
||||
_logger.debug("Dispatch json to %s %s %s", ps, c, method_name)
|
||||
return lambda request: JsonRequest(request).dispatch(method)
|
||||
elif exposed == 'http':
|
||||
_logger.debug("Dispatch http to %s %s %s", ps, c, method_name)
|
||||
return lambda request: HttpRequest(request).dispatch(method)
|
||||
ps, _slash, method_name = ps.rpartition('/')
|
||||
if not ps and method_name:
|
||||
ps = '/'
|
||||
return None
|
||||
|
||||
class LibException(Exception):
|
||||
""" Base of all client lib exceptions """
|
||||
def __init__(self,code=None,message=None):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
class ApplicationError(LibException):
|
||||
""" maps to code: 1, server side: Exception or openerp.exceptions.DeferredException"""
|
||||
|
||||
class Warning(LibException):
|
||||
""" maps to code: 2, server side: openerp.exceptions.Warning"""
|
||||
|
||||
class AccessError(LibException):
|
||||
""" maps to code: 3, server side: openerp.exceptions.AccessError"""
|
||||
|
||||
class AccessDenied(LibException):
|
||||
""" maps to code: 4, server side: openerp.exceptions.AccessDenied"""
|
||||
|
||||
|
||||
class LocalConnector(openerplib.Connector):
|
||||
"""
|
||||
A type of connector that uses the XMLRPC protocol.
|
||||
"""
|
||||
PROTOCOL = 'local'
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def send(self, service_name, method, *args):
|
||||
import openerp
|
||||
import traceback
|
||||
import xmlrpclib
|
||||
try:
|
||||
result = openerp.netsvc.dispatch_rpc(service_name, method, args)
|
||||
except Exception,e:
|
||||
# TODO change the except to raise LibException instead of their emulated xmlrpc fault
|
||||
if isinstance(e, openerp.osv.osv.except_osv):
|
||||
fault = xmlrpclib.Fault('warning -- ' + e.name + '\n\n' + e.value, '')
|
||||
elif isinstance(e, openerp.exceptions.Warning):
|
||||
fault = xmlrpclib.Fault('warning -- Warning\n\n' + str(e), '')
|
||||
elif isinstance(e, openerp.exceptions.AccessError):
|
||||
fault = xmlrpclib.Fault('warning -- AccessError\n\n' + str(e), '')
|
||||
elif isinstance(e, openerp.exceptions.AccessDenied):
|
||||
fault = xmlrpclib.Fault('AccessDenied', str(e))
|
||||
elif isinstance(e, openerp.exceptions.DeferredException):
|
||||
info = e.traceback
|
||||
formatted_info = "".join(traceback.format_exception(*info))
|
||||
fault = xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info)
|
||||
else:
|
||||
info = sys.exc_info()
|
||||
formatted_info = "".join(traceback.format_exception(*info))
|
||||
fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info)
|
||||
raise fault
|
||||
return result
|
||||
def wsgi_postload():
|
||||
openerp.wsgi.register_wsgi_handler(Root())
|
||||
|
||||
# vim:et:ts=4:sw=4:
|
||||
+915
-780
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+927
-795
File diff suppressed because it is too large
Load Diff
+822
-778
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+927
-795
File diff suppressed because it is too large
Load Diff
+881
-792
File diff suppressed because it is too large
Load Diff
+933
-799
File diff suppressed because it is too large
Load Diff
+923
-794
File diff suppressed because it is too large
Load Diff
+929
-795
File diff suppressed because it is too large
Load Diff
+927
-795
File diff suppressed because it is too large
Load Diff
+819
-778
File diff suppressed because it is too large
Load Diff
+927
-795
File diff suppressed because it is too large
Load Diff
+928
-795
File diff suppressed because it is too large
Load Diff
+820
-783
File diff suppressed because it is too large
Load Diff
+803
-784
File diff suppressed because it is too large
Load Diff
+924
-795
File diff suppressed because it is too large
Load Diff
+927
-795
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+927
-795
File diff suppressed because it is too large
Load Diff
+843
-782
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+924
-795
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+803
-784
File diff suppressed because it is too large
Load Diff
+929
-797
File diff suppressed because it is too large
Load Diff
+857
-725
File diff suppressed because it is too large
Load Diff
+952
-809
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+927
-801
File diff suppressed because it is too large
Load Diff
+1059
-914
File diff suppressed because it is too large
Load Diff
+890
-792
File diff suppressed because it is too large
Load Diff
+931
-797
File diff suppressed because it is too large
Load Diff
+856
-798
File diff suppressed because it is too large
Load Diff
+917
-794
File diff suppressed because it is too large
Load Diff
+1104
-959
File diff suppressed because it is too large
Load Diff
+917
-782
File diff suppressed because it is too large
Load Diff
+1007
-873
File diff suppressed because it is too large
Load Diff
+975
-842
File diff suppressed because it is too large
Load Diff
+800
-781
File diff suppressed because it is too large
Load Diff
+916
-809
File diff suppressed because it is too large
Load Diff
+800
-781
File diff suppressed because it is too large
Load Diff
+924
-795
File diff suppressed because it is too large
Load Diff
+930
-796
File diff suppressed because it is too large
Load Diff
+970
-836
File diff suppressed because it is too large
Load Diff
+800
-781
File diff suppressed because it is too large
Load Diff
+901
-808
File diff suppressed because it is too large
Load Diff
+933
-801
File diff suppressed because it is too large
Load Diff
+832
-783
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,58 @@ import babel
|
||||
import dateutil.relativedelta
|
||||
import logging
|
||||
import time
|
||||
import openerplib
|
||||
import traceback
|
||||
import sys
|
||||
import xmlrpclib
|
||||
|
||||
from . import nonliterals
|
||||
import openerp
|
||||
|
||||
import nonliterals
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# OpenERPSession RPC openerp backend access
|
||||
#----------------------------------------------------------
|
||||
class AuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, session, service_name):
|
||||
self.session = session
|
||||
self.service_name = service_name
|
||||
|
||||
def __getattr__(self, method):
|
||||
def proxy_method(*args):
|
||||
result = self.session.send(self.service_name, method, *args)
|
||||
return result
|
||||
return proxy_method
|
||||
|
||||
class Model(object):
|
||||
def __init__(self, session, model):
|
||||
self.session = session
|
||||
self.model = model
|
||||
self.proxy = self.session.proxy('object')
|
||||
|
||||
def __getattr__(self, method):
|
||||
def proxy(*args, **kw):
|
||||
result = self.proxy.execute_kw(self.session._db, self.session._uid, self.session._password, self.model, method, args, kw)
|
||||
# reorder read
|
||||
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] if x in index]
|
||||
return result
|
||||
return proxy
|
||||
|
||||
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, context=None):
|
||||
record_ids = self.search(domain or [], offset, limit or False, order or False, context or {})
|
||||
if not record_ids: return []
|
||||
records = self.read(record_ids, fields or [], context or {})
|
||||
return records
|
||||
|
||||
class OpenERPSession(object):
|
||||
"""
|
||||
An OpenERP RPC session, a given user can own multiple such sessions
|
||||
@@ -31,7 +75,6 @@ class OpenERPSession(object):
|
||||
"""
|
||||
def __init__(self):
|
||||
self._creation_time = time.time()
|
||||
self.config = None
|
||||
self._db = False
|
||||
self._uid = False
|
||||
self._login = False
|
||||
@@ -42,25 +85,27 @@ class OpenERPSession(object):
|
||||
self.domains_store = {}
|
||||
self.jsonp_requests = {} # FIXME use a LRU
|
||||
|
||||
def __getstate__(self):
|
||||
state = dict(self.__dict__)
|
||||
if "config" in state:
|
||||
del state['config']
|
||||
return state
|
||||
|
||||
def openerp_entreprise(self):
|
||||
if not self._uid:
|
||||
return False
|
||||
else:
|
||||
return self.model('publisher_warranty.contract').status()['status'] == 'full'
|
||||
|
||||
def build_connection(self):
|
||||
conn = openerplib.Connection(self.config.connector, database=self._db, login=self._login,
|
||||
user_id=self._uid, password=self._password)
|
||||
return conn
|
||||
def send(self, service_name, method, *args):
|
||||
code_string = "warning -- %s\n\n%s"
|
||||
try:
|
||||
return openerp.netsvc.dispatch_rpc(service_name, method, args)
|
||||
except openerp.osv.osv.except_osv, e:
|
||||
raise xmlrpclib.Fault(code_string % (e.name, e.value), '')
|
||||
except openerp.exceptions.Warning, e:
|
||||
raise xmlrpclib.Fault(code_string % ("Warning", e), '')
|
||||
except openerp.exceptions.AccessError, e:
|
||||
raise xmlrpclib.Fault(code_string % ("AccessError", e), '')
|
||||
except openerp.exceptions.AccessDenied, e:
|
||||
raise xmlrpclib.Fault('AccessDenied', str(e))
|
||||
except openerp.exceptions.DeferredException, e:
|
||||
formatted_info = "".join(traceback.format_exception(*e.traceback))
|
||||
raise xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info)
|
||||
except Exception, e:
|
||||
formatted_info = "".join(traceback.format_exception(*(sys.exc_info())))
|
||||
raise xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info)
|
||||
|
||||
def proxy(self, service):
|
||||
return self.build_connection().get_service(service)
|
||||
return Service(self, service)
|
||||
|
||||
def bind(self, db, uid, login, password):
|
||||
self._db = db
|
||||
@@ -68,8 +113,7 @@ class OpenERPSession(object):
|
||||
self._login = login
|
||||
self._password = password
|
||||
|
||||
def authenticate(self, db, login, password, env):
|
||||
# TODO use the openerplib API once it exposes authenticate()
|
||||
def authenticate(self, db, login, password, env=None):
|
||||
uid = self.proxy('common').authenticate(db, login, password, env)
|
||||
self.bind(db, uid, login, password)
|
||||
|
||||
@@ -80,7 +124,12 @@ class OpenERPSession(object):
|
||||
"""
|
||||
Ensures this session is valid (logged into the openerp server)
|
||||
"""
|
||||
self.build_connection().check_login(force)
|
||||
if self._uid and not force:
|
||||
return
|
||||
# TODO use authenticate instead of login
|
||||
uid = self.proxy("common").login(self._db, self._login, self._password)
|
||||
if not uid:
|
||||
raise AuthenticationError("Authentication failure")
|
||||
|
||||
def ensure_valid(self):
|
||||
if self._uid:
|
||||
@@ -91,7 +140,7 @@ class OpenERPSession(object):
|
||||
|
||||
def execute(self, model, func, *l, **d):
|
||||
self.assert_valid()
|
||||
model = self.build_connection().get_model(model)
|
||||
model = self.model(model)
|
||||
r = getattr(model, func)(*l, **d)
|
||||
return r
|
||||
|
||||
@@ -107,7 +156,8 @@ class OpenERPSession(object):
|
||||
:type model: str
|
||||
:rtype: a model object
|
||||
"""
|
||||
return self.build_connection().get_model(model)
|
||||
|
||||
return Model(self, model)
|
||||
|
||||
def get_context(self):
|
||||
""" Re-initializes the current user's session context (based on
|
||||
@@ -117,7 +167,7 @@ class OpenERPSession(object):
|
||||
:returns: the new context
|
||||
"""
|
||||
assert self._uid, "The user needs to be logged-in to initialize his context"
|
||||
self.context = self.build_connection().get_user_context() or {}
|
||||
self.context = self.model('res.users').context_get() or {}
|
||||
self.context['uid'] = self._uid
|
||||
self._fix_lang(self.context)
|
||||
return self.context
|
||||
@@ -216,3 +266,5 @@ class OpenERPSession(object):
|
||||
cdomain = nonliterals.CompoundDomain(domain)
|
||||
cdomain.session = self
|
||||
return cdomain.evaluate(context or {})
|
||||
|
||||
# vim:et:ts=4:sw=4:
|
||||
@@ -1,4 +1,4 @@
|
||||
// Backbone.js 0.9.1
|
||||
// Backbone.js 0.9.2
|
||||
|
||||
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Backbone may be freely distributed under the MIT license.
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
// Current version of the library. Keep in sync with `package.json`.
|
||||
Backbone.VERSION = '0.9.1';
|
||||
Backbone.VERSION = '0.9.2';
|
||||
|
||||
// Require Underscore, if we're on the server, and it's not already present.
|
||||
var _ = root._;
|
||||
@@ -71,6 +71,9 @@
|
||||
// Backbone.Events
|
||||
// -----------------
|
||||
|
||||
// Regular expression used to split event strings
|
||||
var eventSplitter = /\s+/;
|
||||
|
||||
// A module that can be mixed in to *any object* in order to provide it with
|
||||
// custom events. You may bind with `on` or remove with `off` callback functions
|
||||
// to an event; trigger`-ing an event fires all callbacks in succession.
|
||||
@@ -80,89 +83,110 @@
|
||||
// object.on('expand', function(){ alert('expanded'); });
|
||||
// object.trigger('expand');
|
||||
//
|
||||
Backbone.Events = {
|
||||
var Events = Backbone.Events = {
|
||||
|
||||
// Bind an event, specified by a string name, `ev`, to a `callback`
|
||||
// Bind one or more space separated events, `events`, to a `callback`
|
||||
// function. Passing `"all"` will bind the callback to all events fired.
|
||||
on: function(events, callback, context) {
|
||||
var ev;
|
||||
events = events.split(/\s+/);
|
||||
var calls = this._callbacks || (this._callbacks = {});
|
||||
while (ev = events.shift()) {
|
||||
// Create an immutable callback list, allowing traversal during
|
||||
// modification. The tail is an empty object that will always be used
|
||||
// as the next node.
|
||||
var list = calls[ev] || (calls[ev] = {});
|
||||
var tail = list.tail || (list.tail = list.next = {});
|
||||
tail.callback = callback;
|
||||
tail.context = context;
|
||||
list.tail = tail.next = {};
|
||||
|
||||
var calls, event, node, tail, list;
|
||||
if (!callback) return this;
|
||||
events = events.split(eventSplitter);
|
||||
calls = this._callbacks || (this._callbacks = {});
|
||||
|
||||
// Create an immutable callback list, allowing traversal during
|
||||
// modification. The tail is an empty object that will always be used
|
||||
// as the next node.
|
||||
while (event = events.shift()) {
|
||||
list = calls[event];
|
||||
node = list ? list.tail : {};
|
||||
node.next = tail = {};
|
||||
node.context = context;
|
||||
node.callback = callback;
|
||||
calls[event] = {tail: tail, next: list ? list.next : node};
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// Remove one or many callbacks. If `context` is null, removes all callbacks
|
||||
// with that function. If `callback` is null, removes all callbacks for the
|
||||
// event. If `ev` is null, removes all bound callbacks for all events.
|
||||
// event. If `events` is null, removes all bound callbacks for all events.
|
||||
off: function(events, callback, context) {
|
||||
var ev, calls, node;
|
||||
if (!events) {
|
||||
var event, calls, node, tail, cb, ctx;
|
||||
|
||||
// No events, or removing *all* events.
|
||||
if (!(calls = this._callbacks)) return;
|
||||
if (!(events || callback || context)) {
|
||||
delete this._callbacks;
|
||||
} else if (calls = this._callbacks) {
|
||||
events = events.split(/\s+/);
|
||||
while (ev = events.shift()) {
|
||||
node = calls[ev];
|
||||
delete calls[ev];
|
||||
if (!callback || !node) continue;
|
||||
// Create a new list, omitting the indicated event/context pairs.
|
||||
while ((node = node.next) && node.next) {
|
||||
if (node.callback === callback &&
|
||||
(!context || node.context === context)) continue;
|
||||
this.on(ev, node.callback, node.context);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Loop through the listed events and contexts, splicing them out of the
|
||||
// linked list of callbacks if appropriate.
|
||||
events = events ? events.split(eventSplitter) : _.keys(calls);
|
||||
while (event = events.shift()) {
|
||||
node = calls[event];
|
||||
delete calls[event];
|
||||
if (!node || !(callback || context)) continue;
|
||||
// Create a new list, omitting the indicated callbacks.
|
||||
tail = node.tail;
|
||||
while ((node = node.next) !== tail) {
|
||||
cb = node.callback;
|
||||
ctx = node.context;
|
||||
if ((callback && cb !== callback) || (context && ctx !== context)) {
|
||||
this.on(event, cb, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// Trigger an event, firing all bound callbacks. Callbacks are passed the
|
||||
// same arguments as `trigger` is, apart from the event name.
|
||||
// Listening for `"all"` passes the true event name as the first argument.
|
||||
// Trigger one or many events, firing all bound callbacks. Callbacks are
|
||||
// passed the same arguments as `trigger` is, apart from the event name
|
||||
// (unless you're listening on `"all"`, which will cause your callback to
|
||||
// receive the true name of the event as the first argument).
|
||||
trigger: function(events) {
|
||||
var event, node, calls, tail, args, all, rest;
|
||||
if (!(calls = this._callbacks)) return this;
|
||||
all = calls['all'];
|
||||
(events = events.split(/\s+/)).push(null);
|
||||
// Save references to the current heads & tails.
|
||||
while (event = events.shift()) {
|
||||
if (all) events.push({next: all.next, tail: all.tail, event: event});
|
||||
if (!(node = calls[event])) continue;
|
||||
events.push({next: node.next, tail: node.tail});
|
||||
}
|
||||
// Traverse each list, stopping when the saved tail is reached.
|
||||
all = calls.all;
|
||||
events = events.split(eventSplitter);
|
||||
rest = slice.call(arguments, 1);
|
||||
while (node = events.pop()) {
|
||||
tail = node.tail;
|
||||
args = node.event ? [node.event].concat(rest) : rest;
|
||||
while ((node = node.next) !== tail) {
|
||||
node.callback.apply(node.context || this, args);
|
||||
|
||||
// For each event, walk through the linked list of callbacks twice,
|
||||
// first to trigger the event, then to trigger any `"all"` callbacks.
|
||||
while (event = events.shift()) {
|
||||
if (node = calls[event]) {
|
||||
tail = node.tail;
|
||||
while ((node = node.next) !== tail) {
|
||||
node.callback.apply(node.context || this, rest);
|
||||
}
|
||||
}
|
||||
if (node = all) {
|
||||
tail = node.tail;
|
||||
args = [event].concat(rest);
|
||||
while ((node = node.next) !== tail) {
|
||||
node.callback.apply(node.context || this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Aliases for backwards compatibility.
|
||||
Backbone.Events.bind = Backbone.Events.on;
|
||||
Backbone.Events.unbind = Backbone.Events.off;
|
||||
Events.bind = Events.on;
|
||||
Events.unbind = Events.off;
|
||||
|
||||
// Backbone.Model
|
||||
// --------------
|
||||
|
||||
// Create a new model, with defined attributes. A client id (`cid`)
|
||||
// is automatically generated and assigned for you.
|
||||
Backbone.Model = function(attributes, options) {
|
||||
var Model = Backbone.Model = function(attributes, options) {
|
||||
var defaults;
|
||||
attributes || (attributes = {});
|
||||
if (options && options.parse) attributes = this.parse(attributes);
|
||||
@@ -173,16 +197,31 @@
|
||||
this.attributes = {};
|
||||
this._escapedAttributes = {};
|
||||
this.cid = _.uniqueId('c');
|
||||
if (!this.set(attributes, {silent: true})) {
|
||||
throw new Error("Can't create an invalid model");
|
||||
}
|
||||
delete this._changed;
|
||||
this.changed = {};
|
||||
this._silent = {};
|
||||
this._pending = {};
|
||||
this.set(attributes, {silent: true});
|
||||
// Reset change tracking.
|
||||
this.changed = {};
|
||||
this._silent = {};
|
||||
this._pending = {};
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
this.initialize.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Attach all inheritable methods to the Model prototype.
|
||||
_.extend(Backbone.Model.prototype, Backbone.Events, {
|
||||
_.extend(Model.prototype, Events, {
|
||||
|
||||
// A hash of attributes whose current and previous value differ.
|
||||
changed: null,
|
||||
|
||||
// A hash of attributes that have silently changed since the last time
|
||||
// `change` was called. Will become pending attributes on the next call.
|
||||
_silent: null,
|
||||
|
||||
// A hash of attributes that have changed since the last `'change'` event
|
||||
// began.
|
||||
_pending: null,
|
||||
|
||||
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
||||
// CouchDB users may want to set this to `"_id"`.
|
||||
@@ -193,7 +232,7 @@
|
||||
initialize: function(){},
|
||||
|
||||
// Return a copy of the model's `attributes` object.
|
||||
toJSON: function() {
|
||||
toJSON: function(options) {
|
||||
return _.clone(this.attributes);
|
||||
},
|
||||
|
||||
@@ -206,20 +245,22 @@
|
||||
escape: function(attr) {
|
||||
var html;
|
||||
if (html = this._escapedAttributes[attr]) return html;
|
||||
var val = this.attributes[attr];
|
||||
var val = this.get(attr);
|
||||
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
|
||||
},
|
||||
|
||||
// Returns `true` if the attribute contains a value that is not null
|
||||
// or undefined.
|
||||
has: function(attr) {
|
||||
return this.attributes[attr] != null;
|
||||
return this.get(attr) != null;
|
||||
},
|
||||
|
||||
// Set a hash of model attributes on the object, firing `"change"` unless
|
||||
// you choose to silence it.
|
||||
set: function(key, value, options) {
|
||||
var attrs, attr, val;
|
||||
|
||||
// Handle both `"key", value` and `{key: value}` -style arguments.
|
||||
if (_.isObject(key) || key == null) {
|
||||
attrs = key;
|
||||
options = value;
|
||||
@@ -231,7 +272,7 @@
|
||||
// Extract attributes and options.
|
||||
options || (options = {});
|
||||
if (!attrs) return this;
|
||||
if (attrs instanceof Backbone.Model) attrs = attrs.attributes;
|
||||
if (attrs instanceof Model) attrs = attrs.attributes;
|
||||
if (options.unset) for (attr in attrs) attrs[attr] = void 0;
|
||||
|
||||
// Run validation.
|
||||
@@ -240,33 +281,37 @@
|
||||
// Check for changes of `id`.
|
||||
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
||||
|
||||
var changes = options.changes = {};
|
||||
var now = this.attributes;
|
||||
var escaped = this._escapedAttributes;
|
||||
var prev = this._previousAttributes || {};
|
||||
var alreadySetting = this._setting;
|
||||
this._changed || (this._changed = {});
|
||||
this._setting = true;
|
||||
|
||||
// Update attributes.
|
||||
// For each `set` attribute...
|
||||
for (attr in attrs) {
|
||||
val = attrs[attr];
|
||||
if (!_.isEqual(now[attr], val)) delete escaped[attr];
|
||||
options.unset ? delete now[attr] : now[attr] = val;
|
||||
if (this._changing && !_.isEqual(this._changed[attr], val)) {
|
||||
this.trigger('change:' + attr, this, val, options);
|
||||
this._moreChanges = true;
|
||||
|
||||
// If the new and current value differ, record the change.
|
||||
if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
|
||||
delete escaped[attr];
|
||||
(options.silent ? this._silent : changes)[attr] = true;
|
||||
}
|
||||
delete this._changed[attr];
|
||||
|
||||
// Update or delete the current value.
|
||||
options.unset ? delete now[attr] : now[attr] = val;
|
||||
|
||||
// If the new and previous value differ, record the change. If not,
|
||||
// then remove changes for this attribute.
|
||||
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
|
||||
this._changed[attr] = val;
|
||||
this.changed[attr] = val;
|
||||
if (!options.silent) this._pending[attr] = true;
|
||||
} else {
|
||||
delete this.changed[attr];
|
||||
delete this._pending[attr];
|
||||
}
|
||||
}
|
||||
|
||||
// Fire the `"change"` events, if the model has been changed.
|
||||
if (!alreadySetting) {
|
||||
if (!options.silent && this.hasChanged()) this.change(options);
|
||||
this._setting = false;
|
||||
}
|
||||
// Fire the `"change"` events.
|
||||
if (!options.silent) this.change(options);
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -304,6 +349,8 @@
|
||||
// state will be `set` again.
|
||||
save: function(key, value, options) {
|
||||
var attrs, current;
|
||||
|
||||
// Handle both `("key", value)` and `({key: value})` -style calls.
|
||||
if (_.isObject(key) || key == null) {
|
||||
attrs = key;
|
||||
options = value;
|
||||
@@ -311,18 +358,30 @@
|
||||
attrs = {};
|
||||
attrs[key] = value;
|
||||
}
|
||||
|
||||
options = options ? _.clone(options) : {};
|
||||
if (options.wait) current = _.clone(this.attributes);
|
||||
|
||||
// If we're "wait"-ing to set changed attributes, validate early.
|
||||
if (options.wait) {
|
||||
if (!this._validate(attrs, options)) return false;
|
||||
current = _.clone(this.attributes);
|
||||
}
|
||||
|
||||
// Regular saves `set` attributes before persisting to the server.
|
||||
var silentOptions = _.extend({}, options, {silent: true});
|
||||
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// After a successful server-side save, the client is (optionally)
|
||||
// updated with the server-side state.
|
||||
var model = this;
|
||||
var success = options.success;
|
||||
options.success = function(resp, status, xhr) {
|
||||
var serverAttrs = model.parse(resp, xhr);
|
||||
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
|
||||
if (options.wait) {
|
||||
delete options.wait;
|
||||
serverAttrs = _.extend(attrs || {}, serverAttrs);
|
||||
}
|
||||
if (!model.set(serverAttrs, options)) return false;
|
||||
if (success) {
|
||||
success(model, resp);
|
||||
@@ -330,6 +389,8 @@
|
||||
model.trigger('sync', model, resp, options);
|
||||
}
|
||||
};
|
||||
|
||||
// Finish configuring and sending the Ajax request.
|
||||
options.error = Backbone.wrapError(options.error, model, options);
|
||||
var method = this.isNew() ? 'create' : 'update';
|
||||
var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
|
||||
@@ -349,7 +410,11 @@
|
||||
model.trigger('destroy', model, model.collection, options);
|
||||
};
|
||||
|
||||
if (this.isNew()) return triggerDestroy();
|
||||
if (this.isNew()) {
|
||||
triggerDestroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
options.success = function(resp) {
|
||||
if (options.wait) triggerDestroy();
|
||||
if (success) {
|
||||
@@ -358,6 +423,7 @@
|
||||
model.trigger('sync', model, resp, options);
|
||||
}
|
||||
};
|
||||
|
||||
options.error = Backbone.wrapError(options.error, model, options);
|
||||
var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
|
||||
if (!options.wait) triggerDestroy();
|
||||
@@ -368,7 +434,7 @@
|
||||
// using Backbone's restful methods, override this to change the endpoint
|
||||
// that will be called.
|
||||
url: function() {
|
||||
var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();
|
||||
var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
|
||||
if (this.isNew()) return base;
|
||||
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
|
||||
},
|
||||
@@ -393,18 +459,33 @@
|
||||
// a `"change:attribute"` event for each changed attribute.
|
||||
// Calling this will cause all objects observing the model to update.
|
||||
change: function(options) {
|
||||
if (this._changing || !this.hasChanged()) return this;
|
||||
options || (options = {});
|
||||
var changing = this._changing;
|
||||
this._changing = true;
|
||||
this._moreChanges = true;
|
||||
for (var attr in this._changed) {
|
||||
this.trigger('change:' + attr, this, this._changed[attr], options);
|
||||
|
||||
// Silent changes become pending changes.
|
||||
for (var attr in this._silent) this._pending[attr] = true;
|
||||
|
||||
// Silent changes are triggered.
|
||||
var changes = _.extend({}, options.changes, this._silent);
|
||||
this._silent = {};
|
||||
for (var attr in changes) {
|
||||
this.trigger('change:' + attr, this, this.get(attr), options);
|
||||
}
|
||||
while (this._moreChanges) {
|
||||
this._moreChanges = false;
|
||||
if (changing) return this;
|
||||
|
||||
// Continue firing `"change"` events while there are pending changes.
|
||||
while (!_.isEmpty(this._pending)) {
|
||||
this._pending = {};
|
||||
this.trigger('change', this, options);
|
||||
// Pending and silent changes still remain.
|
||||
for (var attr in this.changed) {
|
||||
if (this._pending[attr] || this._silent[attr]) continue;
|
||||
delete this.changed[attr];
|
||||
}
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
}
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
delete this._changed;
|
||||
|
||||
this._changing = false;
|
||||
return this;
|
||||
},
|
||||
@@ -412,8 +493,8 @@
|
||||
// Determine if the model has changed since the last `"change"` event.
|
||||
// If you specify an attribute name, determine if that attribute has changed.
|
||||
hasChanged: function(attr) {
|
||||
if (!arguments.length) return !_.isEmpty(this._changed);
|
||||
return this._changed && _.has(this._changed, attr);
|
||||
if (!arguments.length) return !_.isEmpty(this.changed);
|
||||
return _.has(this.changed, attr);
|
||||
},
|
||||
|
||||
// Return an object containing all the attributes that have changed, or
|
||||
@@ -423,7 +504,7 @@
|
||||
// You can also pass an attributes object to diff against the model,
|
||||
// determining if there *would be* a change.
|
||||
changedAttributes: function(diff) {
|
||||
if (!diff) return this.hasChanged() ? _.clone(this._changed) : false;
|
||||
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
||||
var val, changed = false, old = this._previousAttributes;
|
||||
for (var attr in diff) {
|
||||
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
||||
@@ -451,9 +532,9 @@
|
||||
return !this.validate(this.attributes);
|
||||
},
|
||||
|
||||
// Run validation against a set of incoming attributes, returning `true`
|
||||
// if all is well. If a specific `error` callback has been passed,
|
||||
// call that instead of firing the general `"error"` event.
|
||||
// Run validation against the next complete set of model attributes,
|
||||
// returning `true` if all is well. If a specific `error` callback has
|
||||
// been passed, call that instead of firing the general `"error"` event.
|
||||
_validate: function(attrs, options) {
|
||||
if (options.silent || !this.validate) return true;
|
||||
attrs = _.extend({}, this.attributes, attrs);
|
||||
@@ -475,8 +556,9 @@
|
||||
// Provides a standard collection class for our sets of models, ordered
|
||||
// or unordered. If a `comparator` is specified, the Collection will maintain
|
||||
// its models in sort order, as they're added and removed.
|
||||
Backbone.Collection = function(models, options) {
|
||||
var Collection = Backbone.Collection = function(models, options) {
|
||||
options || (options = {});
|
||||
if (options.model) this.model = options.model;
|
||||
if (options.comparator) this.comparator = options.comparator;
|
||||
this._reset();
|
||||
this.initialize.apply(this, arguments);
|
||||
@@ -484,11 +566,11 @@
|
||||
};
|
||||
|
||||
// Define the Collection's inheritable methods.
|
||||
_.extend(Backbone.Collection.prototype, Backbone.Events, {
|
||||
_.extend(Collection.prototype, Events, {
|
||||
|
||||
// The default model for a collection is just a **Backbone.Model**.
|
||||
// This should be overridden in most cases.
|
||||
model: Backbone.Model,
|
||||
model: Model,
|
||||
|
||||
// Initialize is an empty function by default. Override it with your own
|
||||
// initialization logic.
|
||||
@@ -496,14 +578,14 @@
|
||||
|
||||
// The JSON representation of a Collection is an array of the
|
||||
// models' attributes.
|
||||
toJSON: function() {
|
||||
return this.map(function(model){ return model.toJSON(); });
|
||||
toJSON: function(options) {
|
||||
return this.map(function(model){ return model.toJSON(options); });
|
||||
},
|
||||
|
||||
// Add a model, or list of models to the set. Pass **silent** to avoid
|
||||
// firing the `add` event for every new model.
|
||||
add: function(models, options) {
|
||||
var i, index, length, model, cid, id, cids = {}, ids = {};
|
||||
var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
|
||||
options || (options = {});
|
||||
models = _.isArray(models) ? models.slice() : [models];
|
||||
|
||||
@@ -513,16 +595,24 @@
|
||||
if (!(model = models[i] = this._prepareModel(models[i], options))) {
|
||||
throw new Error("Can't add an invalid model to a collection");
|
||||
}
|
||||
if (cids[cid = model.cid] || this._byCid[cid] ||
|
||||
(((id = model.id) != null) && (ids[id] || this._byId[id]))) {
|
||||
throw new Error("Can't add the same model to a collection twice");
|
||||
cid = model.cid;
|
||||
id = model.id;
|
||||
if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
|
||||
dups.push(i);
|
||||
continue;
|
||||
}
|
||||
cids[cid] = ids[id] = model;
|
||||
}
|
||||
|
||||
// Remove duplicates.
|
||||
i = dups.length;
|
||||
while (i--) {
|
||||
models.splice(dups[i], 1);
|
||||
}
|
||||
|
||||
// Listen to added models' events, and index models for lookup by
|
||||
// `id` and by `cid`.
|
||||
for (i = 0; i < length; i++) {
|
||||
for (i = 0, length = models.length; i < length; i++) {
|
||||
(model = models[i]).on('all', this._onModelEvent, this);
|
||||
this._byCid[model.cid] = model;
|
||||
if (model.id != null) this._byId[model.id] = model;
|
||||
@@ -566,9 +656,37 @@
|
||||
return this;
|
||||
},
|
||||
|
||||
// Add a model to the end of the collection.
|
||||
push: function(model, options) {
|
||||
model = this._prepareModel(model, options);
|
||||
this.add(model, options);
|
||||
return model;
|
||||
},
|
||||
|
||||
// Remove a model from the end of the collection.
|
||||
pop: function(options) {
|
||||
var model = this.at(this.length - 1);
|
||||
this.remove(model, options);
|
||||
return model;
|
||||
},
|
||||
|
||||
// Add a model to the beginning of the collection.
|
||||
unshift: function(model, options) {
|
||||
model = this._prepareModel(model, options);
|
||||
this.add(model, _.extend({at: 0}, options));
|
||||
return model;
|
||||
},
|
||||
|
||||
// Remove a model from the beginning of the collection.
|
||||
shift: function(options) {
|
||||
var model = this.at(0);
|
||||
this.remove(model, options);
|
||||
return model;
|
||||
},
|
||||
|
||||
// Get a model from the set by id.
|
||||
get: function(id) {
|
||||
if (id == null) return null;
|
||||
if (id == null) return void 0;
|
||||
return this._byId[id.id != null ? id.id : id];
|
||||
},
|
||||
|
||||
@@ -582,6 +700,17 @@
|
||||
return this.models[index];
|
||||
},
|
||||
|
||||
// Return models with matching attributes. Useful for simple cases of `filter`.
|
||||
where: function(attrs) {
|
||||
if (_.isEmpty(attrs)) return [];
|
||||
return this.filter(function(model) {
|
||||
for (var key in attrs) {
|
||||
if (attrs[key] !== model.get(key)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// Force the collection to re-sort itself. You don't need to call this under
|
||||
// normal circumstances, as the set will maintain sort order as each item
|
||||
// is added.
|
||||
@@ -613,7 +742,7 @@
|
||||
this._removeReference(this.models[i]);
|
||||
}
|
||||
this._reset();
|
||||
this.add(models, {silent: true, parse: options.parse});
|
||||
this.add(models, _.extend({silent: true}, options));
|
||||
if (!options.silent) this.trigger('reset', this, options);
|
||||
return this;
|
||||
},
|
||||
@@ -679,7 +808,8 @@
|
||||
|
||||
// Prepare a model or hash of attributes to be added to this collection.
|
||||
_prepareModel: function(model, options) {
|
||||
if (!(model instanceof Backbone.Model)) {
|
||||
options || (options = {});
|
||||
if (!(model instanceof Model)) {
|
||||
var attrs = model;
|
||||
options.collection = this;
|
||||
model = new this.model(attrs, options);
|
||||
@@ -702,12 +832,12 @@
|
||||
// Sets need to update their indexes when models change ids. All other
|
||||
// events simply proxy through. "add" and "remove" events that originate
|
||||
// in other collections are ignored.
|
||||
_onModelEvent: function(ev, model, collection, options) {
|
||||
if ((ev == 'add' || ev == 'remove') && collection != this) return;
|
||||
if (ev == 'destroy') {
|
||||
_onModelEvent: function(event, model, collection, options) {
|
||||
if ((event == 'add' || event == 'remove') && collection != this) return;
|
||||
if (event == 'destroy') {
|
||||
this.remove(model, options);
|
||||
}
|
||||
if (model && ev === 'change:' + model.idAttribute) {
|
||||
if (model && event === 'change:' + model.idAttribute) {
|
||||
delete this._byId[model.previous(model.idAttribute)];
|
||||
this._byId[model.id] = model;
|
||||
}
|
||||
@@ -725,7 +855,7 @@
|
||||
|
||||
// Mix in each Underscore method as a proxy to `Collection#models`.
|
||||
_.each(methods, function(method) {
|
||||
Backbone.Collection.prototype[method] = function() {
|
||||
Collection.prototype[method] = function() {
|
||||
return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
|
||||
};
|
||||
});
|
||||
@@ -735,7 +865,7 @@
|
||||
|
||||
// Routers map faux-URLs to actions, and fire events when routes are
|
||||
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
||||
Backbone.Router = function(options) {
|
||||
var Router = Backbone.Router = function(options) {
|
||||
options || (options = {});
|
||||
if (options.routes) this.routes = options.routes;
|
||||
this._bindRoutes();
|
||||
@@ -749,7 +879,7 @@
|
||||
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
|
||||
|
||||
// Set up all inheritable **Backbone.Router** properties and methods.
|
||||
_.extend(Backbone.Router.prototype, Backbone.Events, {
|
||||
_.extend(Router.prototype, Events, {
|
||||
|
||||
// Initialize is an empty function by default. Override it with your own
|
||||
// initialization logic.
|
||||
@@ -762,7 +892,7 @@
|
||||
// });
|
||||
//
|
||||
route: function(route, name, callback) {
|
||||
Backbone.history || (Backbone.history = new Backbone.History);
|
||||
Backbone.history || (Backbone.history = new History);
|
||||
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
||||
if (!callback) callback = this[name];
|
||||
Backbone.history.route(route, _.bind(function(fragment) {
|
||||
@@ -815,7 +945,7 @@
|
||||
|
||||
// Handles cross-browser history management, based on URL fragments. If the
|
||||
// browser does not support `onhashchange`, falls back to polling.
|
||||
Backbone.History = function() {
|
||||
var History = Backbone.History = function() {
|
||||
this.handlers = [];
|
||||
_.bindAll(this, 'checkUrl');
|
||||
};
|
||||
@@ -827,15 +957,23 @@
|
||||
var isExplorer = /msie [\w.]+/;
|
||||
|
||||
// Has the history handling already been started?
|
||||
var historyStarted = false;
|
||||
History.started = false;
|
||||
|
||||
// Set up all inheritable **Backbone.History** properties and methods.
|
||||
_.extend(Backbone.History.prototype, Backbone.Events, {
|
||||
_.extend(History.prototype, Events, {
|
||||
|
||||
// The default interval to poll for hash changes, if necessary, is
|
||||
// twenty times a second.
|
||||
interval: 50,
|
||||
|
||||
// Gets the true hash value. Cannot use location.hash directly due to bug
|
||||
// in Firefox where location.hash will always be decoded.
|
||||
getHash: function(windowOverride) {
|
||||
var loc = windowOverride ? windowOverride.location : window.location;
|
||||
var match = loc.href.match(/#(.*)$/);
|
||||
return match ? match[1] : '';
|
||||
},
|
||||
|
||||
// Get the cross-browser normalized URL fragment, either from the URL,
|
||||
// the hash, or the override.
|
||||
getFragment: function(fragment, forcePushState) {
|
||||
@@ -845,10 +983,9 @@
|
||||
var search = window.location.search;
|
||||
if (search) fragment += search;
|
||||
} else {
|
||||
fragment = window.location.hash;
|
||||
fragment = this.getHash();
|
||||
}
|
||||
}
|
||||
fragment = decodeURIComponent(fragment);
|
||||
if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
|
||||
return fragment.replace(routeStripper, '');
|
||||
},
|
||||
@@ -856,10 +993,11 @@
|
||||
// Start the hash change handling, returning `true` if the current URL matches
|
||||
// an existing route, and `false` otherwise.
|
||||
start: function(options) {
|
||||
if (History.started) throw new Error("Backbone.history has already been started");
|
||||
History.started = true;
|
||||
|
||||
// Figure out the initial configuration. Do we need an iframe?
|
||||
// Is pushState desired ... is it available?
|
||||
if (historyStarted) throw new Error("Backbone.history has already been started");
|
||||
this.options = _.extend({}, {root: '/'}, this.options, options);
|
||||
this._wantsHashChange = this.options.hashChange !== false;
|
||||
this._wantsPushState = !!this.options.pushState;
|
||||
@@ -867,6 +1005,7 @@
|
||||
var fragment = this.getFragment();
|
||||
var docMode = document.documentMode;
|
||||
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
||||
|
||||
if (oldIE) {
|
||||
this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
|
||||
this.navigate(fragment);
|
||||
@@ -885,7 +1024,6 @@
|
||||
// Determine if we need to change the base url, for a pushState link
|
||||
// opened by a non-pushState browser.
|
||||
this.fragment = fragment;
|
||||
historyStarted = true;
|
||||
var loc = window.location;
|
||||
var atRoot = loc.pathname == this.options.root;
|
||||
|
||||
@@ -900,7 +1038,7 @@
|
||||
// Or if we've started out with a hash-based route, but we're currently
|
||||
// in a browser where it could be `pushState`-based instead...
|
||||
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
|
||||
this.fragment = loc.hash.replace(routeStripper, '');
|
||||
this.fragment = this.getHash().replace(routeStripper, '');
|
||||
window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
|
||||
}
|
||||
|
||||
@@ -914,7 +1052,7 @@
|
||||
stop: function() {
|
||||
$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
|
||||
clearInterval(this._checkUrlInterval);
|
||||
historyStarted = false;
|
||||
History.started = false;
|
||||
},
|
||||
|
||||
// Add a route to be tested when the fragment changes. Routes added later
|
||||
@@ -927,10 +1065,10 @@
|
||||
// calls `loadUrl`, normalizing across the hidden iframe.
|
||||
checkUrl: function(e) {
|
||||
var current = this.getFragment();
|
||||
if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
|
||||
if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
|
||||
if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
|
||||
if (current == this.fragment) return false;
|
||||
if (this.iframe) this.navigate(current);
|
||||
this.loadUrl() || this.loadUrl(window.location.hash);
|
||||
this.loadUrl() || this.loadUrl(this.getHash());
|
||||
},
|
||||
|
||||
// Attempt to load the current URL fragment. If a route succeeds with a
|
||||
@@ -953,12 +1091,12 @@
|
||||
//
|
||||
// The options object can contain `trigger: true` if you wish to have the
|
||||
// route callback be fired (not usually desirable), or `replace: true`, if
|
||||
// you which to modify the current URL without adding an entry to the history.
|
||||
// you wish to modify the current URL without adding an entry to the history.
|
||||
navigate: function(fragment, options) {
|
||||
if (!historyStarted) return false;
|
||||
if (!History.started) return false;
|
||||
if (!options || options === true) options = {trigger: options};
|
||||
var frag = (fragment || '').replace(routeStripper, '');
|
||||
if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
|
||||
if (this.fragment == frag) return;
|
||||
|
||||
// If pushState is available, we use it to set the fragment as a real URL.
|
||||
if (this._hasPushState) {
|
||||
@@ -971,7 +1109,7 @@
|
||||
} else if (this._wantsHashChange) {
|
||||
this.fragment = frag;
|
||||
this._updateHash(window.location, frag, options.replace);
|
||||
if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
|
||||
if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
|
||||
// Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
|
||||
// When replace is true, we don't want this.
|
||||
if(!options.replace) this.iframe.document.open().close();
|
||||
@@ -1002,7 +1140,7 @@
|
||||
|
||||
// Creating a Backbone.View creates its initial element outside of the DOM,
|
||||
// if an existing element is not provided...
|
||||
Backbone.View = function(options) {
|
||||
var View = Backbone.View = function(options) {
|
||||
this.cid = _.uniqueId('view');
|
||||
this._configure(options || {});
|
||||
this._ensureElement();
|
||||
@@ -1011,13 +1149,13 @@
|
||||
};
|
||||
|
||||
// Cached regex to split keys for `delegate`.
|
||||
var eventSplitter = /^(\S+)\s*(.*)$/;
|
||||
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
||||
|
||||
// List of view options to be merged as properties.
|
||||
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
||||
|
||||
// Set up all inheritable **Backbone.View** properties and methods.
|
||||
_.extend(Backbone.View.prototype, Backbone.Events, {
|
||||
_.extend(View.prototype, Events, {
|
||||
|
||||
// The default `tagName` of a View's element is `"div"`.
|
||||
tagName: 'div',
|
||||
@@ -1061,7 +1199,8 @@
|
||||
// Change the view's element (`this.el` property), including event
|
||||
// re-delegation.
|
||||
setElement: function(element, delegate) {
|
||||
this.$el = $(element);
|
||||
if (this.$el) this.undelegateEvents();
|
||||
this.$el = (element instanceof $) ? element : $(element);
|
||||
this.el = this.$el[0];
|
||||
if (delegate !== false) this.delegateEvents();
|
||||
return this;
|
||||
@@ -1088,8 +1227,8 @@
|
||||
for (var key in events) {
|
||||
var method = events[key];
|
||||
if (!_.isFunction(method)) method = this[events[key]];
|
||||
if (!method) throw new Error('Event "' + events[key] + '" does not exist');
|
||||
var match = key.match(eventSplitter);
|
||||
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
|
||||
var match = key.match(delegateEventSplitter);
|
||||
var eventName = match[1], selector = match[2];
|
||||
method = _.bind(method, this);
|
||||
eventName += '.delegateEvents' + this.cid;
|
||||
@@ -1145,8 +1284,7 @@
|
||||
};
|
||||
|
||||
// Set up inheritance for the model, collection, and view.
|
||||
Backbone.Model.extend = Backbone.Collection.extend =
|
||||
Backbone.Router.extend = Backbone.View.extend = extend;
|
||||
Model.extend = Collection.extend = Router.extend = View.extend = extend;
|
||||
|
||||
// Backbone.sync
|
||||
// -------------
|
||||
@@ -1177,6 +1315,9 @@
|
||||
Backbone.sync = function(method, model, options) {
|
||||
var type = methodMap[method];
|
||||
|
||||
// Default options, unless specified.
|
||||
options || (options = {});
|
||||
|
||||
// Default JSON-request options.
|
||||
var params = {type: type, dataType: 'json'};
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user