[Merge] Merge from openerp-web.

bzr revid: jra@tinyerp.com-20111007051113-w33l3jffcxlgz811
This commit is contained in:
Jiten (OpenERP) 2011-10-07 10:41:13 +05:30
commit 2adffb46e5
40 changed files with 777 additions and 881 deletions

View File

@ -1,6 +1,5 @@
import common
import controllers
import common.dispatch
import logging
import optparse
@ -22,6 +21,6 @@ def wsgi_postload():
o.serve_static = True
o.backend = 'local'
app = common.dispatch.Root(o)
app = common.http.Root(o)
openerp.wsgi.register_wsgi_handler(app)

View File

@ -1,5 +1,6 @@
{
"name" : "web",
"category" : "Hidden",
"depends" : [],
'active': True,
'post_load' : 'wsgi_postload',

View File

@ -1,2 +1,6 @@
#!/usr/bin/python
from dispatch import *
import http
import nonliterals
import release
import session
import xml2json

View File

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
""" Backport of Python 2.6's ast.py for Python 2.5
"""
__all__ = ['literal_eval']
try:
from ast import literal_eval
except ImportError:
from _ast import *
from _ast import __version__
def parse(expr, filename='<unknown>', mode='exec'):
"""
Parse an expression into an AST node.
Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
"""
return compile(expr, filename, mode, PyCF_ONLY_AST)
def literal_eval(node_or_string):
"""
Safely evaluate an expression node or a string containing a Python
expression. The string or node provided may only consist of the
following Python literal structures: strings, numbers, tuples, lists,
dicts, booleans, and None.
"""
_safe_names = {'None': None, 'True': True, 'False': False}
if isinstance(node_or_string, basestring):
node_or_string = parse(node_or_string, mode='eval')
if isinstance(node_or_string, Expression):
node_or_string = node_or_string.body
def _convert(node):
if isinstance(node, Str):
return node.s
elif isinstance(node, Num):
return node.n
elif isinstance(node, Tuple):
return tuple(map(_convert, node.elts))
elif isinstance(node, List):
return list(map(_convert, node.elts))
elif isinstance(node, Dict):
return dict((_convert(k), _convert(v)) for k, v
in zip(node.keys, node.values))
elif isinstance(node, Name):
if node.id in _safe_names:
return _safe_names[node.id]
raise ValueError('malformed string')
return _convert(node_or_string)

View File

@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
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)

View File

@ -1,425 +0,0 @@
#!/usr/bin/python
from __future__ import with_statement
import functools
import logging
import os
import pprint
import sys
import traceback
import uuid
import xmlrpclib
import simplejson
import werkzeug.datastructures
import werkzeug.exceptions
import werkzeug.utils
import werkzeug.wrappers
import werkzeug.wsgi
import ast
import nonliterals
import http
import session
import openerplib
__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
'WebRequest', 'JsonRequest', 'HttpRequest']
_logger = logging.getLogger(__name__)
#-----------------------------------------------------------
# Globals (wont move into a pool)
#-----------------------------------------------------------
addons_module = {}
addons_manifest = {}
controllers_class = {}
controllers_object = {}
controllers_path = {}
#----------------------------------------------------------
# OpenERP Web RequestHandler
#----------------------------------------------------------
class WebRequest(object):
""" Parent class for all OpenERP Web request types, mostly deals with
initialization and setup of the request object (the dispatching itself has
to be handled by the subclasses)
:param request: a wrapped werkzeug Request object
:type request: :class:`werkzeug.wrappers.BaseRequest`
:param config: configuration object
.. attribute:: httprequest
the original :class:`werkzeug.wrappers.Request` object provided to the
request
.. attribute:: httpsession
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
useful as they're provided directly to the handler method as keyword
arguments
.. attribute:: session_id
opaque identifier for the :class:`session.OpenERPSession` instance of
the current request
.. attribute:: session
:class:`~session.OpenERPSession` instance for the current request
.. attribute:: context
:class:`~collections.Mapping` of context values for the current request
.. attribute:: debug
``bool``, indicates whether the debug mode is active on the client
"""
def __init__(self, request, config):
self.httprequest = request
self.httpresponse = None
self.httpsession = request.session
self.config = config
def init(self, params):
self.params = dict(params)
# OpenERP session setup
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
self.session = self.httpsession.setdefault(self.session_id, session.OpenERPSession())
self.session.config = self.config
self.context = self.params.pop('context', None)
self.debug = self.params.pop('debug', False) != False
class JsonRequest(WebRequest):
""" JSON-RPC2 over HTTP.
Sucessful request::
--> {"jsonrpc": "2.0",
"method": "call",
"params": {"session_id": "SID",
"context": {},
"arg1": "val1" },
"id": null}
<-- {"jsonrpc": "2.0",
"result": { "res1": "val1" },
"id": null}
Request producing a error::
--> {"jsonrpc": "2.0",
"method": "call",
"params": {"session_id": "SID",
"context": {},
"arg1": "val1" },
"id": null}
<-- {"jsonrpc": "2.0",
"error": {"code": 1,
"message": "End user error message.",
"data": {"code": "codestring",
"debug": "traceback" } },
"id": null}
"""
def dispatch(self, controller, method, requestf=None, request=None):
""" Calls the method asked for by the JSON-RPC2 request
:param controller: the instance of the controller which received the request
:param method: the method which received the request
:param requestf: a file-like object containing an encoded JSON-RPC2 request
:param request: a JSON-RPC2 request
:returns: an utf8 encoded JSON-RPC2 reply
"""
response = {"jsonrpc": "2.0" }
error = None
try:
# Read POST content or POST Form Data named "request"
if requestf:
self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
else:
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))
response['id'] = self.jsonrequest.get('id')
response["result"] = method(controller, self, **self.params)
except openerplib.AuthenticationError:
error = {
'code': 100,
'message': "OpenERP Session Invalid",
'data': {
'type': 'session_invalid',
'debug': traceback.format_exc()
}
}
except xmlrpclib.Fault, e:
error = {
'code': 200,
'message': "OpenERP Server Error",
'data': {
'type': 'server_exception',
'fault_code': e.faultCode,
'debug': "Client %s\nServer %s" % (
"".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
}
}
except Exception:
logging.getLogger(__name__ + '.JSONRequest.dispatch').exception\
("An error occured while handling a json request")
error = {
'code': 300,
'message': "OpenERP WebClient Error",
'data': {
'type': 'client_exception',
'debug': "Client %s" % traceback.format_exc()
}
}
if error:
response["error"] = error
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug("<--\n%s", pprint.pformat(response))
content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
return werkzeug.wrappers.Response(
content, headers=[('Content-Type', 'application/json'),
('Content-Length', len(content))])
def jsonrequest(f):
""" Decorator marking the decorated method as being a handler for a
JSON-RPC request (the exact request path is specified via the
``$(Controller._cp_path)/$methodname`` combination.
If the method is called, it will be provided with a :class:`JsonRequest`
instance and all ``params`` sent during the JSON-RPC request, apart from
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, requestf=request.stream)
json_handler.exposed = True
return json_handler
class HttpRequest(WebRequest):
""" Regular GET/POST request
"""
def dispatch(self, controller, method):
params = dict(self.httprequest.args)
params.update(self.httprequest.form)
params.update(self.httprequest.files)
self.init(params)
akw = {}
for key, value in self.httprequest.args.iteritems():
if isinstance(value, basestring) and len(value) < 1024:
akw[key] = value
else:
akw[key] = type(value)
_logger.debug("%s --> %s.%s %r", self.httprequest.method, controller.__class__.__name__, method.__name__, akw)
r = method(controller, self, **self.params)
if self.debug or 1:
if isinstance(r, werkzeug.wrappers.BaseResponse):
_logger.debug('<-- %s', r)
else:
_logger.debug("<-- size: %s", len(r))
return r
def make_response(self, data, headers=None, cookies=None):
""" Helper for non-HTML responses, or HTML responses with custom
response headers or cookies.
While handlers can just return the HTML markup of a page they want to
send as a string if non-HTML data is returned they need to create a
complete response object, or the returned data will not be correctly
interpreted by the clients.
:param basestring data: response body
:param headers: HTTP headers to set on the response
:type headers: ``[(name, value)]``
:param collections.Mapping cookies: cookies to set on the client
"""
response = werkzeug.wrappers.Response(data, headers=headers)
if cookies:
for k, v in cookies.iteritems():
response.set_cookie(k, v)
return response
def not_found(self, description=None):
""" Helper for 404 response, return its result from the method
"""
return werkzeug.exceptions.NotFound(description)
def httprequest(f):
""" Decorator marking the decorated method as being a handler for a
normal HTTP request (the exact request path is specified via the
``$(Controller._cp_path)/$methodname`` combination.
If the method is called, it will be provided with a :class:`HttpRequest`
instance and all ``params`` sent during the request (``GET`` and ``POST``
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
class ControllerType(type):
def __init__(cls, name, bases, attrs):
super(ControllerType, cls).__init__(name, bases, attrs)
controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
class Controller(object):
__metaclass__ = ControllerType
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):
self.root = '/web/webclient/home?debug=1'
self.config = options
if self.config.backend == 'local':
conn = openerplib.get_connector(protocol='local')
else:
conn = openerplib.get_connector(hostname=self.config.server_host,
port=self.config.server_port)
self.config.connector = conn
self.session_cookie = 'sessionid'
self.addons = {}
static_dirs = self._load_addons()
if options.serve_static:
self.dispatch = werkzeug.wsgi.SharedDataMiddleware(
self.dispatch, static_dirs)
if options.session_storage:
if not os.path.exists(options.session_storage):
os.mkdir(options.session_storage, 0700)
self.session_storage = options.session_storage
def __call__(self, environ, start_response):
""" Handle a WSGI request
"""
return self.dispatch(environ, start_response)
def dispatch(self, environ, start_response):
"""
Performs the actual WSGI dispatching for the application, may be
wrapped during the initialization of the object.
Call the object directly.
"""
request = werkzeug.wrappers.Request(environ)
request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
if request.path == '/':
return werkzeug.utils.redirect(self.root, 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 http.session(request, self.session_storage, self.session_cookie) as session:
result = handler(
request, self.config)
if isinstance(result, basestring):
response = werkzeug.wrappers.Response(
result, headers=[('Content-Type', 'text/html; charset=utf-8'),
('Content-Length', len(result))])
else:
response = result
response.set_cookie(self.session_cookie, session.sid)
return response(environ, start_response)
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:
if addons_path not in sys.path:
sys.path.insert(0, addons_path)
for module in os.listdir(addons_path):
if module not in addons_module:
manifest_path = os.path.join(addons_path, module, '__openerp__.py')
path_static = os.path.join(addons_path, module, 'static')
if os.path.isfile(manifest_path) and os.path.isdir(path_static):
manifest = ast.literal_eval(open(manifest_path).read())
manifest['addons_path'] = addons_path
_logger.info("Loading %s", module)
m = __import__(module)
addons_module[module] = m
addons_manifest[module] = manifest
statics['/%s/static' % module] = path_static
for k, v in controllers_class.items():
if k not in controllers_object:
o = v()
controllers_object[k] = o
if hasattr(o, '_cp_path'):
controllers_path[o._cp_path] = o
return statics
def find_handler(self, *l):
"""
Tries to discover the controller handling the request for the path
specified by the provided parameters
:param l: path sections to a controller or controller method
:returns: a callable matching the path sections, or ``None``
:rtype: ``Controller | None``
"""
if len(l) > 1:
for i in range(len(l), 1, -1):
ps = "/" + "/".join(l[0:i])
if ps in controllers_path:
c = controllers_path[ps]
rest = l[i:] or ['index']
meth = rest[0]
m = getattr(c, meth)
if getattr(m, 'exposed', False):
_logger.debug("Dispatching to %s %s %s", ps, c, meth)
return m
return None

View File

@ -1,13 +1,286 @@
# -*- coding: utf-8 -*-
#----------------------------------------------------------
# OpenERP Web HTTP layer
#----------------------------------------------------------
import ast
import contextlib
import functools
import logging
import urllib
import os
import pprint
import sys
import traceback
import uuid
import xmlrpclib
import simplejson
import werkzeug.contrib.sessions
import werkzeug.datastructures
import werkzeug.exceptions
import werkzeug.utils
import werkzeug.wrappers
import werkzeug.wsgi
import nonliterals
import session
import openerplib
__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
'WebRequest', 'JsonRequest', 'HttpRequest']
_logger = logging.getLogger(__name__)
#----------------------------------------------------------
# OpenERP Web RequestHandler
#----------------------------------------------------------
class WebRequest(object):
""" Parent class for all OpenERP Web request types, mostly deals with
initialization and setup of the request object (the dispatching itself has
to be handled by the subclasses)
:param request: a wrapped werkzeug Request object
:type request: :class:`werkzeug.wrappers.BaseRequest`
:param config: configuration object
.. attribute:: httprequest
the original :class:`werkzeug.wrappers.Request` object provided to the
request
.. attribute:: httpsession
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
useful as they're provided directly to the handler method as keyword
arguments
.. attribute:: session_id
opaque identifier for the :class:`session.OpenERPSession` instance of
the current request
.. attribute:: session
:class:`~session.OpenERPSession` instance for the current request
.. attribute:: context
:class:`~collections.Mapping` of context values for the current request
.. attribute:: debug
``bool``, indicates whether the debug mode is active on the client
"""
def __init__(self, request, config):
self.httprequest = request
self.httpresponse = None
self.httpsession = request.session
self.config = config
def init(self, params):
self.params = dict(params)
# OpenERP session setup
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
self.session = self.httpsession.setdefault(self.session_id, session.OpenERPSession())
self.session.config = self.config
self.context = self.params.pop('context', None)
self.debug = self.params.pop('debug', False) != False
class JsonRequest(WebRequest):
""" JSON-RPC2 over HTTP.
Sucessful request::
--> {"jsonrpc": "2.0",
"method": "call",
"params": {"session_id": "SID",
"context": {},
"arg1": "val1" },
"id": null}
<-- {"jsonrpc": "2.0",
"result": { "res1": "val1" },
"id": null}
Request producing a error::
--> {"jsonrpc": "2.0",
"method": "call",
"params": {"session_id": "SID",
"context": {},
"arg1": "val1" },
"id": null}
<-- {"jsonrpc": "2.0",
"error": {"code": 1,
"message": "End user error message.",
"data": {"code": "codestring",
"debug": "traceback" } },
"id": null}
"""
def dispatch(self, controller, method, requestf=None, request=None):
""" Calls the method asked for by the JSON-RPC2 request
:param controller: the instance of the controller which received the request
:param method: the method which received the request
:param requestf: a file-like object containing an encoded JSON-RPC2 request
:param request: a JSON-RPC2 request
:returns: an utf8 encoded JSON-RPC2 reply
"""
response = {"jsonrpc": "2.0" }
error = None
try:
# Read POST content or POST Form Data named "request"
if requestf:
self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
else:
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))
response['id'] = self.jsonrequest.get('id')
response["result"] = method(controller, self, **self.params)
except openerplib.AuthenticationError:
error = {
'code': 100,
'message': "OpenERP Session Invalid",
'data': {
'type': 'session_invalid',
'debug': traceback.format_exc()
}
}
except xmlrpclib.Fault, e:
error = {
'code': 200,
'message': "OpenERP Server Error",
'data': {
'type': 'server_exception',
'fault_code': e.faultCode,
'debug': "Client %s\nServer %s" % (
"".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
}
}
except Exception:
logging.getLogger(__name__ + '.JSONRequest.dispatch').exception\
("An error occured while handling a json request")
error = {
'code': 300,
'message': "OpenERP WebClient Error",
'data': {
'type': 'client_exception',
'debug': "Client %s" % traceback.format_exc()
}
}
if error:
response["error"] = error
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug("<--\n%s", pprint.pformat(response))
content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
return werkzeug.wrappers.Response(
content, headers=[('Content-Type', 'application/json'),
('Content-Length', len(content))])
def jsonrequest(f):
""" Decorator marking the decorated method as being a handler for a
JSON-RPC request (the exact request path is specified via the
``$(Controller._cp_path)/$methodname`` combination.
If the method is called, it will be provided with a :class:`JsonRequest`
instance and all ``params`` sent during the JSON-RPC request, apart from
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, requestf=request.stream)
json_handler.exposed = True
return json_handler
class HttpRequest(WebRequest):
""" Regular GET/POST request
"""
def dispatch(self, controller, method):
params = dict(self.httprequest.args)
params.update(self.httprequest.form)
params.update(self.httprequest.files)
self.init(params)
akw = {}
for key, value in self.httprequest.args.iteritems():
if isinstance(value, basestring) and len(value) < 1024:
akw[key] = value
else:
akw[key] = type(value)
_logger.debug("%s --> %s.%s %r", self.httprequest.method, controller.__class__.__name__, method.__name__, akw)
r = method(controller, self, **self.params)
if self.debug or 1:
if isinstance(r, werkzeug.wrappers.BaseResponse):
_logger.debug('<-- %s', r)
else:
_logger.debug("<-- size: %s", len(r))
return r
def make_response(self, data, headers=None, cookies=None):
""" Helper for non-HTML responses, or HTML responses with custom
response headers or cookies.
While handlers can just return the HTML markup of a page they want to
send as a string if non-HTML data is returned they need to create a
complete response object, or the returned data will not be correctly
interpreted by the clients.
:param basestring data: response body
:param headers: HTTP headers to set on the response
:type headers: ``[(name, value)]``
:param collections.Mapping cookies: cookies to set on the client
"""
response = werkzeug.wrappers.Response(data, headers=headers)
if cookies:
for k, v in cookies.iteritems():
response.set_cookie(k, v)
return response
def not_found(self, description=None):
""" Helper for 404 response, return its result from the method
"""
return werkzeug.exceptions.NotFound(description)
def httprequest(f):
""" Decorator marking the decorated method as being a handler for a
normal HTTP request (the exact request path is specified via the
``$(Controller._cp_path)/$methodname`` combination.
If the method is called, it will be provided with a :class:`HttpRequest`
instance and all ``params`` sent during the request (``GET`` and ``POST``
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
#----------------------------------------------------------
# OpenERP Web werkzeug Session Managment wraped using with
#----------------------------------------------------------
STORES = {}
@contextlib.contextmanager
def session(request, storage_path, session_cookie='sessionid'):
def session_context(request, storage_path, session_cookie='sessionid'):
session_store = STORES.get(storage_path)
if not session_store:
session_store = werkzeug.contrib.sessions.FilesystemSessionStore(
@ -24,3 +297,157 @@ def session(request, storage_path, session_cookie='sessionid'):
yield request.session
finally:
session_store.save(request.session)
#----------------------------------------------------------
# OpenERP Web Module/Controller Loading and URL Routing
#----------------------------------------------------------
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["%s.%s" % (cls.__module__, cls.__name__)] = cls
class Controller(object):
__metaclass__ = ControllerType
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):
self.root = '/web/webclient/home'
self.config = options
if self.config.backend == 'local':
conn = openerplib.get_connector(protocol='local')
else:
conn = openerplib.get_connector(hostname=self.config.server_host,
port=self.config.server_port)
self.config.connector = conn
self.session_cookie = 'sessionid'
self.addons = {}
static_dirs = self._load_addons()
if options.serve_static:
self.dispatch = werkzeug.wsgi.SharedDataMiddleware(
self.dispatch, static_dirs)
if options.session_storage:
if not os.path.exists(options.session_storage):
os.mkdir(options.session_storage, 0700)
self.session_storage = options.session_storage
def __call__(self, environ, start_response):
""" Handle a WSGI request
"""
return self.dispatch(environ, start_response)
def dispatch(self, environ, start_response):
"""
Performs the actual WSGI dispatching for the application, may be
wrapped during the initialization of the object.
Call the object directly.
"""
request = werkzeug.wrappers.Request(environ)
request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
if request.path == '/':
params = urllib.urlencode(dict(request.args, debug=''))
return werkzeug.utils.redirect(self.root + '?' + params, 301)(
environ, start_response)
elif request.path == '/mobile':
return werkzeug.utils.redirect(
'/web_mobile/static/src/web_mobile.html', 301)(environ, start_response)
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)
if isinstance(result, basestring):
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
response = werkzeug.wrappers.Response(result, headers=headers)
else:
response = result
response.set_cookie(self.session_cookie, session.sid)
return response(environ, start_response)
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:
if addons_path not in sys.path:
sys.path.insert(0, addons_path)
for module in os.listdir(addons_path):
if module not in addons_module:
manifest_path = os.path.join(addons_path, module, '__openerp__.py')
path_static = os.path.join(addons_path, module, 'static')
if os.path.isfile(manifest_path) and os.path.isdir(path_static):
manifest = ast.literal_eval(open(manifest_path).read())
manifest['addons_path'] = addons_path
_logger.info("Loading %s", module)
m = __import__(module)
addons_module[module] = m
addons_manifest[module] = manifest
statics['/%s/static' % module] = path_static
for k, v in controllers_class.items():
if k not in controllers_object:
o = v()
controllers_object[k] = o
if hasattr(o, '_cp_path'):
controllers_path[o._cp_path] = o
return statics
def find_handler(self, *l):
"""
Tries to discover the controller handling the request for the path
specified by the provided parameters
:param l: path sections to a controller or controller method
:returns: a callable matching the path sections, or ``None``
:rtype: ``Controller | None``
"""
if len(l) > 1:
for i in range(len(l), 1, -1):
ps = "/" + "/".join(l[0:i])
if ps in controllers_path:
c = controllers_path[ps]
rest = l[i:] or ['index']
meth = rest[0]
m = getattr(c, meth)
if getattr(m, 'exposed', False):
_logger.debug("Dispatching to %s %s %s", ps, c, meth)
return m
return None
#

View File

@ -1,17 +1,16 @@
#!/usr/bin/python
import datetime
import dateutil.relativedelta
import logging
import time
import openerplib
import nonliterals
import logging
_logger = logging.getLogger(__name__)
#----------------------------------------------------------
# OpenERPSession RPC openerp backend access
#----------------------------------------------------------
class OpenERPSession(object):
"""
An OpenERP RPC session, a given user can own multiple such sessions

View File

@ -0,0 +1,38 @@
# xml2json-direct
# Simple and straightforward XML-to-JSON converter in Python
# New BSD Licensed
#
# URL: http://code.google.com/p/xml2json-direct/
class Xml2Json(object):
@staticmethod
def convert_to_json(s):
return simplejson.dumps(
Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
@staticmethod
def convert_to_structure(s):
root = ElementTree.fromstring(s)
return Xml2Json.convert_element(root)
@staticmethod
def convert_element(el, skip_whitespaces=True):
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 (not skip_whitespaces or el.text.strip() != ''):
kids.append(el.text)
for kid in el:
kids.append(Xml2Json.convert_element(kid))
if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
kids.append(kid.tail)
res["children"] = kids
return res

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import ast
import base64
import csv
import glob
@ -9,60 +10,16 @@ import os
import re
import simplejson
import textwrap
import xmlrpclib
import time
import xmlrpclib
import zlib
from xml.etree import ElementTree
from cStringIO import StringIO
from babel.messages.pofile import read_po
import babel.messages.pofile
import web.common.dispatch as openerpweb
import web.common.ast
import web.common.nonliterals
import web.common.release
openerpweb.ast = web.common.ast
openerpweb.nonliterals = web.common.nonliterals
# Should move to openerpweb.Xml2Json
class Xml2Json:
# xml2json-direct
# Simple and straightforward XML-to-JSON converter in Python
# New BSD Licensed
#
# URL: http://code.google.com/p/xml2json-direct/
@staticmethod
def convert_to_json(s):
return simplejson.dumps(
Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
@staticmethod
def convert_to_structure(s):
root = ElementTree.fromstring(s)
return Xml2Json.convert_element(root)
@staticmethod
def convert_element(el, skip_whitespaces=True):
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 (not skip_whitespaces or el.text.strip() != ''):
kids.append(el.text)
for kid in el:
kids.append(Xml2Json.convert_element(kid))
if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
kids.append(kid.tail)
res["children"] = kids
return res
import web.common
openerpweb = web.common.http
#----------------------------------------------------------
# OpenERP Web web Controllers
@ -200,7 +157,7 @@ class WebClient(openerpweb.Controller):
continue
try:
with open(f_name) as t_file:
po = read_po(t_file)
po = babel.messages.pofile.read_po(t_file)
except:
continue
for x in po:
@ -358,13 +315,22 @@ class Session(openerpweb.Controller):
@openerpweb.jsonrequest
def modules(self, req):
# TODO query server for installed web modules
mods = []
for name, manifest in openerpweb.addons_manifest.items():
# TODO replace by ir.module.module installed web
if name not in req.config.server_wide_modules and manifest.get('active', True):
mods.append(name)
return mods
# Compute available candidates module
loadable = openerpweb.addons_manifest.iterkeys()
loaded = req.config.server_wide_modules
candidates = [mod for mod in loadable if mod not in loaded]
# Compute active true modules that might be on the web side only
active = set(name for name in candidates
if openerpweb.addons_manifest[name].get('active'))
# Retrieve database installed modules
Modules = req.session.model('ir.module.module')
installed = set(module['name'] for module in Modules.search_read(
[('state','=','installed'), ('name','in', candidates)], ['name']))
# Merge both
return list(active | installed)
@openerpweb.jsonrequest
def eval_domain_and_context(self, req, contexts, domains,
@ -399,8 +365,8 @@ class Session(openerpweb.Controller):
no group by should be performed)
"""
context, domain = eval_context_and_domain(req.session,
openerpweb.nonliterals.CompoundContext(*(contexts or [])),
openerpweb.nonliterals.CompoundDomain(*(domains or [])))
web.common.nonliterals.CompoundContext(*(contexts or [])),
web.common.nonliterals.CompoundDomain(*(domains or [])))
group_by_sequence = []
for candidate in (group_by_seq or []):
@ -817,7 +783,7 @@ class View(openerpweb.Controller):
xml = self.transform_view(arch, session, evaluation_context)
else:
xml = ElementTree.fromstring(arch)
fvg['arch'] = Xml2Json.convert_element(xml)
fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
for field in fvg['fields'].itervalues():
if field.get('views'):
@ -882,7 +848,7 @@ class View(openerpweb.Controller):
def parse_domain(self, domain, session):
""" Parses an arbitrary string containing a domain, transforms it
to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
to either a literal domain or a :class:`web.common.nonliterals.Domain`
:param domain: the domain to parse, if the domain is not a string it
is assumed to be a literal domain and is returned as-is
@ -892,14 +858,14 @@ class View(openerpweb.Controller):
if not isinstance(domain, (str, unicode)):
return domain
try:
return openerpweb.ast.literal_eval(domain)
return ast.literal_eval(domain)
except ValueError:
# not a literal
return openerpweb.nonliterals.Domain(session, domain)
return web.common.nonliterals.Domain(session, domain)
def parse_context(self, context, session):
""" Parses an arbitrary string containing a context, transforms it
to either a literal context or a :class:`openerpweb.nonliterals.Context`
to either a literal context or a :class:`web.common.nonliterals.Context`
:param context: the context to parse, if the context is not a string it
is assumed to be a literal domain and is returned as-is
@ -909,9 +875,9 @@ class View(openerpweb.Controller):
if not isinstance(context, (str, unicode)):
return context
try:
return openerpweb.ast.literal_eval(context)
return ast.literal_eval(context)
except ValueError:
return openerpweb.nonliterals.Context(session, context)
return web.common.nonliterals.Context(session, context)
def parse_domains_and_contexts(self, elem, session):
""" Converts domains and contexts from the view into Python objects,
@ -999,10 +965,10 @@ class SearchView(View):
@openerpweb.jsonrequest
def save_filter(self, req, model, name, context_to_save, domain):
Model = req.session.model("ir.filters")
ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
ctx = web.common.nonliterals.CompoundContext(context_to_save)
ctx.session = req.session
ctx = ctx.evaluate()
domain = openerpweb.nonliterals.CompoundDomain(domain)
domain = web.common.nonliterals.CompoundDomain(domain)
domain.session = req.session
domain = domain.evaluate()
uid = req.session._uid
@ -1025,16 +991,17 @@ class Binary(openerpweb.Controller):
try:
if not id:
res = Model.default_get([field], context).get(field, '')
res = Model.default_get([field], context).get(field)
else:
res = Model.read([int(id)], [field], context)[0].get(field, '')
res = Model.read([int(id)], [field], context)[0].get(field)
image_data = base64.b64decode(res)
except (TypeError, xmlrpclib.Fault):
image_data = self.placeholder(req)
return req.make_response(image_data, [
('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
def placeholder(self, req):
return open(os.path.join(req.addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
addons_path = openerpweb.addons_manifest['web']['addons_path']
return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
@openerpweb.httprequest
def saveas(self, req, model, id, field, fieldname, **kw):
@ -1393,7 +1360,7 @@ class Reports(View):
report_srv = req.session.proxy("report")
context = req.session.eval_context(
openerpweb.nonliterals.CompoundContext(
web.common.nonliterals.CompoundContext(
req.context or {}, action[ "context"]))
report_data = {}
@ -1432,7 +1399,6 @@ class Reports(View):
('Content-Length', len(report))],
cookies={'fileToken': int(token)})
class Import(View):
_cp_path = "/web/import"

View File

@ -855,7 +855,7 @@ label.error {
cursor: help;
}
.openerp .oe_form_field label.oe_label, .openerp .oe_form_field label.oe_label_help {
.openerp .oe_forms label.oe_label, .openerp .oe_forms label.oe_label_help {
text-align: right;
margin: 3px 0 0 10px;
}
@ -943,7 +943,7 @@ label.error {
position: relative;
}
.openerp input.oe-binary-file {
z-index: 2;
z-index: 0;
line-height: 0;
font-size: 50px;
position: absolute;

View File

@ -480,14 +480,12 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
session_login: function(db, login, password, success_callback) {
var self = this;
var params = { db: db, login: login, password: password };
this.rpc("/web/session/login", params, function(result) {
return this.rpc("/web/session/login", params, function(result) {
self.session_id = result.session_id;
self.uid = result.uid;
self.user_context = result.context;
self.db = result.db;
self.session_save();
if (self.uid)
self.on_session_valid();
return true;
}).then(success_callback);
},

View File

@ -179,9 +179,13 @@ openerp.web.auto_date_to_str = function(value, type) {
* @param {String} [column.string] button label
* @param {String} [column.icon] button icon
* @param {String} [value_if_empty=''] what to display if the field's value is ``false``
* @param {Boolean} [process_modifiers=true] should the modifiers be computed ?
*/
openerp.web.format_cell = function (row_data, column, value_if_empty) {
var attrs = column.modifiers_for(row_data);
openerp.web.format_cell = function (row_data, column, value_if_empty, process_modifiers) {
var attrs = {};
if (process_modifiers !== false) {
attrs = column.modifiers_for(row_data);
}
if (attrs.invisible) { return ''; }
if (column.tag === 'button') {
return [

View File

@ -13,24 +13,35 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
* @param view_id
* @param defaults
*/
init: function(parent, dataset, view_id, defaults) {
init: function(parent, dataset, view_id, defaults, hidden) {
this._super(parent);
this.dataset = dataset;
this.model = dataset.model;
this.view_id = view_id;
this.defaults = defaults || {};
this.has_defaults = !_.isEmpty(this.defaults);
this.inputs = [];
this.enabled_filters = [];
this.has_focus = false;
this.hidden = !!hidden;
this.headless = this.hidden && !this.has_defaults;
this.ready = $.Deferred();
},
start: function() {
this._super();
this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
if (this.hidden) {
this.$element.hide();
}
if (this.headless) {
this.ready.resolve();
} else {
this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
}
return this.ready.promise();
},
show: function () {
@ -105,8 +116,9 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
*/
make_field: function (item, field) {
try {
return new (openerp.web.search.fields.get_object(field.type))
(item, field, this);
return new (openerp.web.search.fields.get_any(
[item.attrs.widget, field.type]))
(item, field, this);
} catch (e) {
if (! e instanceof openerp.web.KeyNotFound) {
throw e;
@ -137,10 +149,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
'defaults': this.defaults
});
// We don't understand why the following commented line does not work in Chrome but
// the non-commented line does. As far as we investigated, only God knows.
//this.$element.html(render);
jQuery(render).appendTo(this.$element);
this.$element.html(render);
this.$element.find(".oe_search-view-custom-filter-btn").click(ext.on_activate);
var f = this.$element.find('form');
@ -246,10 +255,13 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
* @param e jQuery event object coming from the "Search" button
*/
do_search: function (e) {
if (this.headless && !this.has_defaults) {
return this.on_search([], [], []);
}
// reset filters management
var select = this.$element.find(".oe_search-view-filters-management");
select.val("_filters");
if (e && e.preventDefault) { e.preventDefault(); }
var data = this.build_search_data();
@ -327,7 +339,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
this.notification.notify("Invalid Search", "triggered from search view");
},
do_clear: function () {
this.$element.find('.filter_label').removeClass('enabled');
this.$element.find('.filter_label, .filter_icon').removeClass('enabled');
this.enabled_filters.splice(0);
var string = $('a.searchview_group_string');
_.each(string, function(str){
@ -748,9 +760,30 @@ openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends
* @extends openerp.web.search.Field
*/
openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{
// This implementation is a basic <select> field, but it may have to be
// altered to be more in line with the GTK client, which uses a combo box
// (~ jquery.autocomplete):
// * If an option was selected in the list, behave as currently
// * If something which is not in the list was entered (via the text input),
// the default domain should become (`ilike` string_value) but **any
// ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
// is specified. So at least get_domain needs to be quite a bit
// overridden (if there's no @value and there is no filter_domain and
// there is no @operator, return [[name, 'ilike', str_val]]
template: 'SearchView.field.selection',
init: function () {
this._super.apply(this, arguments);
// prepend empty option if there is no empty option in the selection list
this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
return !item[1];
});
},
get_value: function () {
return this.$element.val();
var index = parseInt(this.$element.val(), 10);
if (isNaN(index)) { return null; }
var value = this.attrs.selection[index][0];
if (value === false) { return null; }
return value;
}
});
openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{

View File

@ -463,9 +463,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
return $.Deferred().then(success).resolve(_.extend(r, {created: true}));
}
},
do_search: function (domains, contexts, groupbys) {
console.debug("Searching form");
},
on_action: function (action) {
console.debug('Executing action', action);
},
@ -688,7 +685,8 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.
this.width = this.node.attrs.width;
},
start: function() {
this.$element = this.view.$element.find('.' + this.element_class);
this.$element = this.view.$element.find(
'.' + this.element_class.replace(/[^\r\n\f0-9A-Za-z_-]/g, "\\$&"));
},
process_modifiers: function() {
var compute_domain = openerp.web.form.compute_domain;
@ -792,7 +790,7 @@ openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
for (var i = 0; i < node.children.length; i++) {
var n = node.children[i];
if (n.tag == "page") {
var page = new openerp.web.form.WidgetNotebookPage(
var page = new (this.view.registry.get_object('notebookpage'))(
this.view, n, this, this.pages.length);
this.pages.push(page);
}
@ -898,11 +896,11 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
self.on_confirmed().then(function() {
def.resolve();
});
$(self).dialog("close");
$(this).dialog("close");
},
Cancel: function() {
def.resolve();
$(self).dialog("close");
$(this).dialog("close");
}
}
});
@ -911,7 +909,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
return self.on_confirmed();
}
};
if ((!this.node.attrs.special && this.view.dirty_for_user) || !this.view.datarecord.id) {
if (!this.node.attrs.special && (this.view.dirty_for_user || !this.view.datarecord.id)) {
return this.view.recursive_save().pipe(exec_action);
} else {
return exec_action();
@ -1787,6 +1785,9 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
result.result.context = _.extend(result.result.context || {}, additional_context);
self.do_action(result.result);
});
},
focus: function () {
this.$input.focus();
}
});
@ -2243,11 +2244,11 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
});
this.searchview.on_search.add(function(domains, contexts, groupbys) {
if (self.initial_ids) {
self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]),
self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
contexts, groupbys);
self.initial_ids = undefined;
} else {
self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys);
self.do_search(domains.concat([self.domain]), contexts, groupbys);
}
});
this.searchview.on_loaded.add_last(function () {
@ -2274,10 +2275,19 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
}).pipe(function() {
self.searchview.do_search();
});
});
this.searchview.appendTo($("#" + this.element_id + "_search"));
},
do_search: function(domains, contexts, groupbys) {
var self = this;
this.rpc('/web/session/eval_domain_and_context', {
domains: domains || [],
contexts: contexts || [],
group_by_seq: groupbys || []
}, function (results) {
self.view_list.do_search(results.domain, results.context, results.group_by);
});
},
create_row: function(data) {
var self = this;
var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
@ -2781,6 +2791,7 @@ openerp.web.form.widgets = new openerp.web.Registry({
'frame' : 'openerp.web.form.WidgetFrame',
'group' : 'openerp.web.form.WidgetFrame',
'notebook' : 'openerp.web.form.WidgetNotebook',
'notebookpage' : 'openerp.web.form.WidgetNotebookPage',
'separator' : 'openerp.web.form.WidgetSeparator',
'label' : 'openerp.web.form.WidgetLabel',
'button' : 'openerp.web.form.WidgetButton',

View File

@ -391,9 +391,6 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
if (this.sidebar) {
this.sidebar.$element.show();
}
if (!_(this.dataset.ids).isEmpty()) {
this.reload_content();
}
},
do_hide: function () {
this.$element.hide();
@ -442,41 +439,22 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
self.compute_aggregates();
}));
},
/**
* Event handler for a search, asks for the computation/folding of domains
* and contexts (and group-by), then reloads the view's content.
*
* @param {Array} domains a sequence of literal and non-literal domains
* @param {Array} contexts a sequence of literal and non-literal contexts
* @param {Array} groupbys a sequence of literal and non-literal group-by contexts
* @returns {$.Deferred} fold request evaluation promise
*/
do_search: function (domains, contexts, groupbys) {
return this.rpc('/web/session/eval_domain_and_context', {
domains: _([this.dataset.get_domain()].concat(domains)).compact(),
contexts: _([this.dataset.get_context()].concat(contexts)).compact(),
group_by_seq: groupbys
}, $.proxy(this, 'do_actual_search'));
},
/**
* Handler for the result of eval_domain_and_context, actually perform the
* searching
*
* @param {Object} results results of evaluating domain and process for a search
*/
do_actual_search: function (results) {
do_search: function (domain, context, group_by) {
this.groups.datagroup = new openerp.web.DataGroup(
this, this.model,
results.domain,
results.context,
results.group_by);
this, this.model, domain, context, group_by);
this.groups.datagroup.sort = this.dataset._sort;
if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
results.group_by = null;
if (_.isEmpty(group_by) && !context['group_by_no_leaf']) {
group_by = null;
}
this.reload_view(!!results.group_by, results.context).then(
this.reload_view(!!group_by, context).then(
$.proxy(this, 'reload_content'));
},
/**
@ -505,7 +483,13 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
do_select: function (ids, records) {
this.$element.find('.oe-list-delete')
.attr('disabled', !ids.length);
if (this.sidebar) {
if (ids.length) {
this.sidebar.do_unfold();
} else {
this.sidebar.do_fold();
}
}
if (!records.length) {
this.compute_aggregates();
return;
@ -536,14 +520,8 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
* @param {openerp.web.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
*/
do_activate_record: function (index, id, dataset) {
var self = this;
// TODO is it needed ?
this.dataset.read_slice([],{
context: dataset.get_context(),
domain: dataset.get_domain()
}, function () {
self.select_record(index);
});
this.dataset.ids = dataset.ids;
this.select_record(index);
},
/**
* Handles signal for the addition of a new record (can be a creation,
@ -642,7 +620,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
}
$footer_cells.filter(_.sprintf('[data-field=%s]', column.id))
.html(openerp.web.format_cell(aggregation, column));
.html(openerp.web.format_cell(aggregation, column, undefined, false));
});
},
get_selected_ids: function() {
@ -837,7 +815,9 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis
cells.push('</tr>');
var row = cells.join('');
this.$current.append(new Array(count - this.records.length + 1).join(row));
this.$current
.children('tr:not([data-id])').remove().end()
.append(new Array(count - this.records.length + 1).join(row));
this.refresh_zebra(this.records.length);
},
/**
@ -918,6 +898,7 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis
options: this.options,
record: record,
row_parity: (index % 2 === 0) ? 'even' : 'odd',
view: this.view,
render_cell: openerp.web.format_cell
});
},

View File

@ -56,11 +56,11 @@ openerp.web.list_editable = function (openerp) {
|| this.defaults.editable);
},
/**
* Replace do_actual_search to handle editability process
* Replace do_search to handle editability process
*/
do_actual_search: function (results) {
this.set_editable(results.context['set_editable']);
this._super(results);
do_search: function(domain, context, group_by) {
this.set_editable(context['set_editable']);
this._super.apply(this, arguments);
},
/**
* Replace do_add_record to handle editability (and adding new record
@ -120,6 +120,7 @@ openerp.web.list_editable = function (openerp) {
delete self.edition_id;
delete self.edition;
});
this.pad_table_to(5);
return cancelled.promise();
},
/**
@ -147,7 +148,7 @@ openerp.web.list_editable = function (openerp) {
var $new_row = $('<tr>', {
id: _.uniqueId('oe-editable-row-'),
'data-id': record_id,
'class': $(row).attr('class') + ' oe_forms',
'class': row ? $(row).attr('class') : '' + ' oe_forms',
click: function (e) {e.stopPropagation();}
})
.delegate('button.oe-edit-row-save', 'click', function () {
@ -173,10 +174,22 @@ openerp.web.list_editable = function (openerp) {
});
if (row) {
$new_row.replaceAll(row);
} else if (self.options.editable === 'top') {
self.$current.prepend($new_row);
} else if (self.options.editable) {
self.$current.append($new_row);
var $last_child = self.$current.children('tr:last');
if (self.records.length) {
if (self.options.editable === 'top') {
$new_row.insertBefore(
self.$current.children('[data-id]:first'));
} else {
$new_row.insertAfter(
self.$current.children('[data-id]:last'));
}
} else {
$new_row.prependTo(self.$current);
}
if ($last_child.is(':not([data-id])')) {
$last_child.remove();
}
}
self.edition = true;
self.edition_id = record_id;
@ -325,11 +338,13 @@ openerp.web.list_editable = function (openerp) {
this.$element.children().css('visibility', '');
if (this.modifiers.tree_invisible) {
var old_invisible = this.invisible;
this.invisible = !!this.modifiers.tree_invisible;
this.invisible = true;
this._super();
this.invisible = old_invisible;
} else if (this.invisible) {
this.$element.children().css('visibility', 'hidden');
} else {
this._super();
}
}
});

View File

@ -116,6 +116,9 @@ db.web.ActionManager = db.web.Widget.extend({
*/
},
ir_actions_act_window_close: function (action, on_closed) {
if (!this.dialog && on_closed) {
on_closed();
}
this.dialog_stop();
},
ir_actions_server: function (action, on_closed) {
@ -166,6 +169,7 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{
this.model = dataset ? dataset.model : undefined;
this.dataset = dataset;
this.searchview = null;
this.last_search = false;
this.active_view = null;
this.views_src = _.map(views, function(x) {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;});
this.views = {};
@ -224,35 +228,21 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{
controller.set_embedded_view(view.embedded_view);
}
controller.do_switch_view.add_last(this.on_mode_switch);
if (view_type === 'list' && this.flags.search_view === false && this.action && this.action['auto_search']) {
// In case the search view is not instantiated: manually call ListView#search
var domains = !_(self.action.domain).isEmpty()
? [self.action.domain] : [],
contexts = !_(self.action.context).isEmpty()
? [self.action.context] : [];
controller.on_loaded.add({
callback: function () {
controller.do_search(domains, contexts, []);
},
position: 'last',
unique: true
});
}
var container = $("#" + this.element_id + '_view_' + view_type);
view_promise = controller.appendTo(container);
this.views[view_type].controller = controller;
$.when(view_promise).then(function() {
self.on_controller_inited(view_type, controller);
if (self.searchview && view.controller.searchable !== false) {
self.do_searchview_search();
}
});
this.views[view_type].controller = controller;
} else if (this.searchview && view.controller.searchable !== false) {
self.do_searchview_search();
}
if (this.searchview) {
if (view.controller.searchable === false) {
this.searchview.hide();
} else {
this.searchview.show();
}
this.searchview[(view.controller.searchable === false || this.searchview.hidden) ? 'hide' : 'show']();
}
this.$element
@ -272,14 +262,6 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{
}
return view_promise;
},
/**
* Event launched when a controller has been inited.
*
* @param {String} view_type type of view
* @param {String} view the inited controller
*/
on_controller_inited: function(view_type, view) {
},
/**
* Sets up the current viewmanager's search view.
*
@ -293,14 +275,37 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{
}
this.searchview = new db.web.SearchView(
this, this.dataset,
view_id, search_defaults);
view_id, search_defaults, this.flags.search_view === false);
this.searchview.on_search.add(function(domains, contexts, groupbys) {
var controller = self.views[self.active_view].controller;
controller.do_search.call(controller, domains, contexts, groupbys);
});
this.searchview.on_search.add(this.do_searchview_search);
return this.searchview.appendTo($("#" + this.element_id + "_search"));
},
do_searchview_search: function(domains, contexts, groupbys) {
var self = this,
controller = this.views[this.active_view].controller;
if (domains || contexts) {
this.rpc('/web/session/eval_domain_and_context', {
domains: [this.action.domain || []].concat(domains || []),
contexts: [this.action.context || {}].concat(contexts || []),
group_by_seq: groupbys || []
}, function (results) {
self.dataset.context = results.context;
self.dataset.domain = results.domain;
self.last_search = [results.domain, results.context, results.group_by];
controller.do_search(results.domain, results.context, results.group_by);
});
} else if (this.last_search) {
controller.do_search.apply(controller, this.last_search);
}
},
/**
* Event launched when a controller has been inited.
*
* @param {String} view_type type of view
* @param {String} view the inited controller
*/
on_controller_inited: function(view_type, view) {
},
/**
* Called when one of the view want to execute an action
*/
@ -360,28 +365,24 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View
* launches an initial search after both views are done rendering.
*/
start: function() {
var self = this;
var self = this,
searchview_loaded,
search_defaults = {};
_.each(this.action.context, function (value, key) {
var match = /^search_default_(.*)$/.exec(key);
if (match) {
search_defaults[match[1]] = value;
}
});
// init search view
var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
var searchview_loaded;
if (this.flags.search_view !== false) {
var search_defaults = {};
_.each(this.action.context, function (value, key) {
var match = /^search_default_(.*)$/.exec(key);
if (match) {
search_defaults[match[1]] = value;
}
});
// init search view
var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
searchview_loaded = this.setup_search_view(
searchview_id || false, search_defaults);
}
searchview_loaded = this.setup_search_view(searchview_id || false, search_defaults);
var main_view_loaded = this._super();
var manager_ready = $.when(searchview_loaded, main_view_loaded);
if (searchview_loaded && this.action['auto_search']) {
if (searchview_loaded && this.action['auto_search'] !== false) {
// schedule auto_search
manager_ready.then(this.searchview.do_search);
}
@ -816,6 +817,8 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{
},
do_switch_view: function(view) {
},
do_search: function(view) {
},
set_common_sidebar_sections: function(sidebar) {
sidebar.add_section('customize', "Customize", [
{

View File

@ -767,7 +767,7 @@
<input type="text" size="1"
t-att-name="widget.name"
t-att-id="widget.element_id"
t-attf-class="field_#{widget.type} #{widget.element_class}"
t-attf-class="field_#{widget.type}"
t-attf-style="width: #{widget.field.translate ? '99' : '100'}%"
/>
<img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>
@ -775,7 +775,7 @@
<t t-name="FieldChar.readonly">
<div
t-att-id="widget.element_id"
t-attf-class="field_#{widget.type} #{widget.element_class}"
t-attf-class="field_#{widget.type}"
t-attf-style="width: #{widget.field.translate ? '99' : '100'}%">
</div>
</t>
@ -814,7 +814,7 @@
<textarea rows="6"
t-att-name="widget.name"
t-att-id="widget.element_id"
t-attf-class="field_#{widget.type} #{widget.element_class}"
t-attf-class="field_#{widget.type}"
t-attf-style="width: #{widget.field.translate ? '99' : '100'}%"
></textarea>
<img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>
@ -833,8 +833,8 @@
<t t-name="FieldSelection">
<select
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
t-attf-class="field_#{widget.type} #{widget.element_class}"
t-att-id="widget.element_id"
t-attf-class="field_#{widget.type}"
style="width: 100%">
<t t-foreach="widget.values" t-as="option">
<option><t t-esc="option[1]"/></option>
@ -842,8 +842,9 @@
</select>
</t>
<t t-name="FieldMany2One">
<div t-att-class="widget.element_class" class="oe-m2o">
<input type="text" size="1" style="width: 100%;"/>
<div class="oe-m2o">
<input type="text" size="1" style="width: 100%;"
t-att-id="widget.element_id"/>
<span class="oe-m2o-drop-down-button">
<img src="/web/static/src/img/down-arrow.png" /></span>
<span class="oe-m2o-cm-button" t-att-id="widget.name + '_open'">
@ -886,8 +887,8 @@
<t t-name="FieldBoolean">
<input type="checkbox"
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
t-attf-class="field_#{widget.type} #{widget.element_class}"/>
t-att-id="widget.element_id"
t-attf-class="field_#{widget.type}"/>
</t>
<t t-name="FieldProgressBar">
<div t-opentag="true" class="oe-progressbar">
@ -902,7 +903,7 @@
t-att-border="widget.readonly ? 0 : 1"
t-att-id="widget.element_id + '_field'"
t-att-name="widget.name"
t-attf-class="field_#{widget.type} #{widget.element_class}"
t-attf-class="field_#{widget.type}"
t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"
/>
@ -950,7 +951,7 @@
<input type="text" size="1"
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
t-attf-class="field_#{widget.type} #{widget.element_class}" style="width: 100%"
t-attf-class="field_#{widget.type}" style="width: 100%"
/>
</td>
<td class="oe-binary" nowrap="true">
@ -995,7 +996,6 @@
</t>
<t t-name="WidgetButton">
<button type="button"
t-attf-class="#{widget.element_class}"
t-att-title="widget.help"
style="width: 100%" class="button">
<img t-if="widget.node.attrs.icon" t-att-src="'/web/static/src/img/icons/' + widget.node.attrs.icon + '.png'" width="16" height="16"/>
@ -1092,14 +1092,14 @@
<div style="white-space: nowrap;">
<select t-att-name="attrs.name" t-att-id="element_id"
t-att-autofocus="attrs.default_focus === '1' || undefined">
<option/>
<option t-if="prepend_empty"/>
<t t-foreach="attrs.selection" t-as="option">
<t t-set="selected" t-value="defaults[attrs.name] === option[0]"/>
<option t-if="selected"
t-att-value="option[0]" selected="selected">
t-att-value="option_index" selected="selected">
<t t-esc="option[1]"/>
</option>
<option t-if="!selected" t-att-value="option[0]">
<option t-if="!selected" t-att-value="option_index">
<t t-esc="option[1]"/>
</option>
</t>

View File

@ -1,5 +1,6 @@
{
"name": "web calendar",
"category" : "Hidden",
"version": "2.0",
"depends": ['web'],
"js": [

View File

@ -13,9 +13,8 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
this.fields_view = {};
this.view_id = view_id;
this.domain = this.dataset.domain || [];
this.context = this.dataset.context || {};
this.has_been_loaded = $.Deferred();
this.creating_event_id = null;
this.dataset_events = [];
@ -31,7 +30,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
},
start: function() {
this._super();
this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type":"calendar", 'toolbar': true}, this.on_loaded);
return this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type":"calendar", 'toolbar': true}, this.on_loaded);
},
stop: function() {
scheduler.clearAll();
@ -47,6 +46,10 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
this.view_id = this.fields_view.view_id;
// mode, one of month, week or day
this.mode = this.fields_view.arch.attrs.mode;
// date_start is mandatory, date_delay and date_stop are optional
this.date_start = this.fields_view.arch.attrs.date_start;
this.date_delay = this.fields_view.arch.attrs.date_delay;
this.date_stop = this.fields_view.arch.attrs.date_stop;
@ -55,6 +58,10 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
this.day_length = this.fields_view.arch.attrs.day_length || 8;
this.color_field = this.fields_view.arch.attrs.color;
this.fields = this.fields_view.fields;
if (!this.date_start) {
throw new Error("Calendar view has not defined 'date_start' attribute.");
}
//* Calendar Fields *
this.calendar_fields.date_start = {'name': this.date_start, 'kind': this.fields[this.date_start].type};
@ -68,9 +75,6 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
if (this.date_stop) {
this.calendar_fields.date_stop = {'name': this.date_stop, 'kind': this.fields[this.date_stop].type};
}
if (!this.date_delay && !this.date_stop) {
throw new Error("Calendar view has none of the following attributes : 'date_stop', 'date_delay'");
}
for (var fld = 0; fld < this.fields_view.arch.children.length; fld++) {
this.info_fields.push(this.fields_view.arch.children[fld].attrs.name);
@ -92,9 +96,6 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
this.init_scheduler();
this.has_been_loaded.resolve();
if (this.dataset.ids.length) {
this.dataset.read_ids(this.dataset.ids, _.keys(this.fields), this.on_events_loaded);
}
},
init_scheduler: function() {
var self = this;
@ -111,9 +112,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
scheduler.config.drag_resize = true;
scheduler.config.drag_create = true;
// Initialize Sceduler
this.mode = this.mode || 'month';
scheduler.init('openerp_scheduler', null, this.mode);
scheduler.init('openerp_scheduler', null, this.mode || 'month');
scheduler.detachAllEvents();
scheduler.attachEvent('onEventAdded', this.do_create_event);
@ -200,7 +199,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
convert_event: function(evt) {
var date_start = openerp.web.str_to_datetime(evt[this.date_start]),
date_stop = this.date_stop ? openerp.web.str_to_datetime(evt[this.date_stop]) : null,
date_delay = evt[this.date_delay] || null,
date_delay = evt[this.date_delay] || 1.0,
res_text = '',
res_description = [];
@ -293,26 +292,17 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
}
return data;
},
do_search: function(domains, contexts, groupbys) {
do_search: function(domain, context, group_by) {
var self = this;
scheduler.clearAll();
$.when(this.has_been_loaded).then(function() {
self.rpc('/web/session/eval_domain_and_context', {
domains: domains,
contexts: contexts,
group_by_seq: groupbys
}, function (results) {
// TODO: handle non-empty results.group_by with read_group
self.dataset.context = self.context = results.context;
self.dataset.domain = self.domain = results.domain;
self.dataset.read_slice(_.keys(self.fields), {
offset:0,
limit: self.limit
}, function(events) {
self.dataset_events = events;
self.on_events_loaded(events);
}
);
// TODO: handle non-empty results.group_by with read_group
self.dataset.read_slice(_.keys(self.fields), {
offset: 0,
limit: self.limit
}, function(events) {
self.dataset_events = events;
self.on_events_loaded(events);
});
});
},

View File

@ -1,5 +1,6 @@
{
"name": "Web Chat",
"category" : "Hidden",
"version": "2.0",
"depends": ['web'],
"js": [
@ -10,6 +11,6 @@
'static/src/js/web_chat.js'
],
"css": [],
# 'active': True,
'active': False,
'installable': False,
}

View File

@ -2,7 +2,7 @@
import time
import simplejson
import web.common as openerpweb
import web.common.http as openerpweb
import logging
_logger = logging.getLogger(__name__)

View File

@ -1,5 +1,6 @@
{
"name": "web Dashboard",
"category" : "Hidden",
"version": "2.0",
"depends": ['web'],
"js": [

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import web.common as openerpweb
import web.common.http as openerpweb
WIDGET_CONTENT_PATTERN = """<!DOCTYPE html>
<html>

View File

@ -1,5 +1,6 @@
{
"name" : "OpenERP Web installer home",
"category" : "Hidden",
"version" : "2.0",
"depends" : ['web'],
'active': True,

View File

@ -94,24 +94,34 @@ openerp.web_default_home = function (openerp) {
})
},
install_module: function (module_name) {
var self = this;
var Modules = new openerp.web.DataSetSearch(
this, 'ir.module.module', null, [['name', '=', module_name], ['state', '=', 'uninstalled']]);
this, 'ir.module.module', null,
[['name', '=', module_name], ['state', '=', 'uninstalled']]);
var Upgrade = new openerp.web.DataSet(this, 'base.module.upgrade');
$.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
Modules.read_slice(['id'], {}, function (records) {
if (!(records.length === 1)) { return; }
if (!(records.length === 1)) { $.unblockUI(); return; }
Modules.call('state_update',
[_.pluck(records, 'id'), 'to install', ['uninstalled']],
function () {
Upgrade.call('upgrade_module', [[]], function () {
$.unblockUI();
// TODO: less brutal reloading
window.location.reload(true);
self.run_configuration_wizards();
});
}
)
});
},
run_configuration_wizards: function () {
var self = this;
new openerp.web.DataSet(this, 'res.config').call('start', [[]], function (action) {
$.unblockUI();
self.do_action(action, function () {
// TODO: less brutal reloading
window.location.reload(true);
});
});
}
});
};

View File

@ -1,5 +1,6 @@
{
"name" : "OpenERP Web Diagram",
"category" : "Hidden",
"version" : "2.0",
"depends" : ["base"],
"js": [

View File

@ -1,4 +1,4 @@
import web.common as openerpweb
import web.common.http as openerpweb
from web.controllers.main import View
class DiagramView(View):

View File

@ -1,5 +1,6 @@
{
"name": "web Gantt",
"category" : "Hidden",
"version": "2.0",
"depends": ['web'],
"js": [

View File

@ -509,15 +509,7 @@ init: function(parent, dataset, view_id) {
do_search: function (domains, contexts, groupbys) {
var self = this;
this.grp = groupbys;
return this.rpc('/web/session/eval_domain_and_context', {
domains: domains,
contexts: contexts,
group_by_seq: groupbys
}, function (results) {
self.dataset.context = results.context;
self.dataset.domain = results.domain;
self.reload_gantt();
});
self.reload_gantt();
}
});

View File

@ -1,5 +1,6 @@
{
"name": "web Graph",
"category" : "Hidden",
"version": "2.0",
"depends": ['web'],
"js": [
@ -7,4 +8,4 @@
"static/src/js/graph.js"],
"css": ["static/lib/dhtmlxGraph/codebase/dhtmlxchart.css"],
"active": True
}
}

View File

@ -29,7 +29,6 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
this.group_field = null;
},
do_show: function () {
// TODO: re-trigger search
this.$element.show();
},
do_hide: function () {
@ -79,9 +78,6 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
}
}, this);
this.ordinate = this.columns[0].name;
this.dataset.read_slice(
this.list_fields(), {}, $.proxy(this, 'schedule_chart'));
},
schedule_chart: function(results) {
var self = this;
@ -366,24 +362,14 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
});
},
do_search: function(domains, contexts, groupbys) {
var self = this;
this.rpc('/web/session/eval_domain_and_context', {
domains: domains,
contexts: contexts,
group_by_seq: groupbys
}, function (results) {
// TODO: handle non-empty results.group_by with read_group?
if(!_(results.group_by).isEmpty()){
self.abscissa = results.group_by[0];
} else {
self.abscissa = self.first_field;
}
self.dataset.read_slice(self.list_fields(), {
context: results.context,
domain: results.domain
}, $.proxy(self, 'schedule_chart'));
});
do_search: function(domain, context, group_by) {
// TODO: handle non-empty group_by with read_group?
if (!_(group_by).isEmpty()) {
this.abscissa = group_by[0];
} else {
this.abscissa = this.first_field;
}
this.dataset.read_slice(this.list_fields(), {}, $.proxy(this, 'schedule_chart'));
}
});
};

View File

@ -1,5 +1,6 @@
{
"name": "Hello",
"category" : "Hidden",
"version": "2.0",
"depends": [],
"js": ["static/*/*.js", "static/*/js/*.js"],

View File

@ -1,5 +1,6 @@
{
"name" : "Base Kanban",
"category" : "Hidden",
"version" : "2.0",
"depends" : ["web"],
"js": [

View File

@ -9,8 +9,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
this.domain = dataset.domain;
this.context = dataset.context;
this.view_id = view_id;
this.fields_view = {};
this.group_by = [];
@ -33,9 +31,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
var self = this;
this.fields_view = data;
this.add_qweb_template();
if (this.qweb.has_template('kanban-box')) {
this.do_actual_search();
}
},
add_qweb_template: function() {
var group_operator = ["avg", "max", "min", "sum", "count"]
@ -294,7 +289,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
this.do_execute_action(
button_attrs, dataset,
record_id, function () {
self.do_actual_search();
self.on_reload_record(record_id);
}
);
},
@ -427,28 +422,18 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
});
return new_record;
},
do_search: function (domains, contexts, group_by) {
do_search: function (domain, context, group_by) {
var self = this;
this.rpc('/web/session/eval_domain_and_context', {
domains: [this.dataset.get_domain()].concat(domains),
contexts: [this.dataset.get_context()].concat(contexts),
group_by_seq: group_by
}, function (results) {
self.domain = results.domain;
self.context = results.context;
self.group_by = results.group_by;
self.do_actual_search();
});
},
do_actual_search : function () {
self.group_by = group_by;
var self = this,
group_by = self.group_by;
if (!group_by.length && this.fields_view.arch.attrs.default_group_by) {
group_by = [this.fields_view.arch.attrs.default_group_by];
self.group_by = group_by;
}
self.datagroup = new openerp.web.DataGroup(self, self.model, self.domain, self.context, group_by);
self.datagroup.list(_.keys(self.fields_view.fields),
self.datagroup = new openerp.web.DataGroup(self, self.model, domain, context, group_by);
self.datagroup.list(
_.keys(self.fields_view.fields),
function (groups) {
self.groups = groups;
if (groups.length) {
@ -461,9 +446,12 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
},
function (dataset) {
self.groups = [];
self.dataset.read_slice([], {'domain': self.domain, 'context': self.context}, function(records) {
if (records.length) self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}];
else self.all_display_data = [];
self.dataset.read_slice([], {}, function(records) {
if (records.length) {
self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}];
} else {
self.all_display_data = [];
}
self.$element.find(".oe_kanban_view").remove();
self.on_show_data();
});
@ -487,7 +475,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
_.each(self.aggregates, function(value, key) {
group_aggregates[value] = group.aggregates[key];
});
self.dataset.read_slice([], {'domain': group.domain, 'context': group.context}, function(records) {
self.dataset.read_slice([], {'domain': group.domain, 'conext': group.context}, function(records) {
self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids, 'aggregates': group_aggregates});
if (datagroups.length == self.all_display_data.length) {
self.$element.find(".oe_kanban_view").remove();

View File

@ -1,5 +1,6 @@
{
"name" : "OpenERP Web mobile",
"category" : "Hidden",
"version" : "2.0",
"depends" : [],
'active': True,

View File

@ -1,5 +1,6 @@
{
"name" : "OpenERP Web web",
"category" : "Hidden",
"version" : "2.0",
"depends" : [],
'active': False,

View File

@ -1,5 +1,6 @@
{
"name": "Tests",
"category" : "Hidden",
"version": "2.0",
"depends": [],
"js": ["static/src/js/*.js"],

View File

@ -40,7 +40,7 @@ server_options.add_option('--no-serve-static', dest='serve_static',
default=True, action='store_false',
help="Do not serve static files via this server")
server_options.add_option('--multi-threaded', dest='threaded',
default=True, action='store_true',
default=False, action='store_true',
help="Spawn one thread per HTTP request")
server_options.add_option('--proxy-mode', dest='proxy_mode',
default=False, action='store_true',
@ -55,7 +55,7 @@ logging_opts.add_option("--log-config", dest="log_config", default=os.path.join(
help="Logging configuration file", metavar="FILE")
optparser.add_option_group(logging_opts)
import web.common.dispatch
import web.common.http
if __name__ == "__main__":
(options, args) = optparser.parse_args(sys.argv[1:])
@ -71,7 +71,7 @@ if __name__ == "__main__":
else:
logging.basicConfig(level=getattr(logging, options.log_level.upper()))
app = web.common.dispatch.Root(options)
app = web.common.http.Root(options)
if options.proxy_mode:
app = werkzeug.contrib.fixers.ProxyFix(app)