merge upstream

bzr revid: chs@openerp.com-20110926112623-q68d3tebr80qt6l9
This commit is contained in:
Christophe Simonis 2011-09-26 13:26:23 +02:00
commit 4261f1ad11
24 changed files with 885 additions and 181 deletions

View File

@ -2,27 +2,26 @@ import common
import controllers import controllers
import common.dispatch import common.dispatch
import logging import logging
import optparse
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class Options(object):
pass
def wsgi_postload(): def wsgi_postload():
import openerp.wsgi import openerp.wsgi
import openerp.tools
import os import os
import tempfile import tempfile
_logger.info("embedded mode") _logger.info("embedded mode")
class Options(object):
pass
o = Options() o = Options()
o.dbfilter = '.*' o.dbfilter = openerp.tools.config['dbfilter']
o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions") o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions")
o.addons_path = os.path.dirname(os.path.dirname(__file__)) o.addons_path = os.path.dirname(os.path.dirname(__file__))
o.serve_static = True o.serve_static = True
o.backend = 'local' o.backend = 'local'
app = common.dispatch.Root(o) app = common.dispatch.Root(o)
#import openerp.wsgi
openerp.wsgi.register_wsgi_handler(app) openerp.wsgi.register_wsgi_handler(app)
# TODO
# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point

View File

@ -20,6 +20,7 @@
"static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js", "static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js",
"static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js", "static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js",
"static/lib/jquery.ui.notify/js/jquery.notify.js", "static/lib/jquery.ui.notify/js/jquery.notify.js",
"static/lib/jquery.deferred-queue/jquery.deferred-queue.js",
"static/lib/json/json2.js", "static/lib/json/json2.js",
"static/lib/qweb/qweb2.js", "static/lib/qweb/qweb2.js",
"static/lib/underscore/underscore.js", "static/lib/underscore/underscore.js",
@ -33,6 +34,7 @@
"static/src/js/views.js", "static/src/js/views.js",
"static/src/js/data.js", "static/src/js/data.js",
"static/src/js/data_export.js", "static/src/js/data_export.js",
"static/src/js/data_import.js",
"static/src/js/search.js", "static/src/js/search.js",
"static/src/js/view_form.js", "static/src/js/view_form.js",
"static/src/js/view_list.js", "static/src/js/view_list.js",
@ -45,6 +47,7 @@
"static/lib/jquery.ui.notify/css/ui.notify.css", "static/lib/jquery.ui.notify/css/ui.notify.css",
"static/src/css/base.css", "static/src/css/base.css",
"static/src/css/data_export.css", "static/src/css/data_export.css",
"static/src/css/data_import.css",
], ],
'post_load' : 'wsgi_postload', 'post_load' : 'wsgi_postload',
} }

View File

@ -21,8 +21,7 @@ import werkzeug.wsgi
import ast import ast
import nonliterals import nonliterals
import http import http
# import backendlocal as backend import session
import session as backend
import openerplib import openerplib
__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller', __all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
@ -79,12 +78,12 @@ class WebRequest(object):
.. attribute:: session_id .. attribute:: session_id
opaque identifier for the :class:`backend.OpenERPSession` instance of opaque identifier for the :class:`session.OpenERPSession` instance of
the current request the current request
.. attribute:: session .. attribute:: session
:class:`~backend.OpenERPSession` instance for the current request :class:`~session.OpenERPSession` instance for the current request
.. attribute:: context .. attribute:: context
@ -100,13 +99,12 @@ class WebRequest(object):
self.httpresponse = None self.httpresponse = None
self.httpsession = request.session self.httpsession = request.session
self.config = config self.config = config
def init(self, params): def init(self, params):
self.params = dict(params) self.params = dict(params)
# OpenERP session setup # OpenERP session setup
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
self.session = self.httpsession.setdefault( self.session = self.httpsession.setdefault(self.session_id, session.OpenERPSession(self.config))
self.session_id, backend.OpenERPSession(
self.config.server_host, self.config.server_port))
self.context = self.params.pop('context', None) self.context = self.params.pop('context', None)
self.debug = self.params.pop('debug', False) != False self.debug = self.params.pop('debug', False) != False
@ -389,15 +387,14 @@ class Root(object):
for module in os.listdir(addons_path): for module in os.listdir(addons_path):
if module not in addons_module: if module not in addons_module:
manifest_path = os.path.join(addons_path, module, '__openerp__.py') manifest_path = os.path.join(addons_path, module, '__openerp__.py')
if os.path.isfile(manifest_path): 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 = ast.literal_eval(open(manifest_path).read())
_logger.info("Loading %s", module) _logger.info("Loading %s", module)
m = __import__(module) m = __import__(module)
addons_module[module] = m addons_module[module] = m
addons_manifest[module] = manifest addons_manifest[module] = manifest
statics['/%s/static' % module] = path_static
statics['/%s/static' % module] = \
os.path.join(addons_path, module, 'static')
for k, v in controllers_class.items(): for k, v in controllers_class.items():
if k not in controllers_object: if k not in controllers_object:
o = v() o = v()

View File

@ -20,6 +20,7 @@ def session(request, storage_path, session_cookie='sessionid'):
else: else:
request.session = session_store.new() request.session = session_store.new()
yield request.session try:
yield request.session
session_store.save(request.session) finally:
session_store.save(request.session)

View File

@ -190,6 +190,34 @@ class NetRPCConnector(Connector):
socket.disconnect() socket.disconnect()
return result return result
class LocalConnector(Connector):
"""
A type of connector that uses the XMLRPC protocol.
"""
PROTOCOL = 'local'
__logger = _getChildLogger(_logger, 'connector.local')
def __init__(self):
pass
def send(self, service_name, method, *args):
import openerp
# TODO Exception handling
# This will be changed to be xmlrpc compatible
# OpenERPWarning code 1
# OpenERPException code 2
try:
result = openerp.netsvc.dispatch_rpc(service_name, method, args, None)
except openerp.netsvc.OpenERPDispatcherException, e:
fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback)
raise fault
except:
exc_type, exc_value, exc_tb = sys.exc_info()
fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value))
raise fault
return result
class Service(object): class Service(object):
""" """
A class to execute RPC calls on a specific service of the remote server. A class to execute RPC calls on a specific service of the remote server.
@ -363,7 +391,7 @@ class Model(object):
records = self.read(record_ids, fields or [], context or {}) records = self.read(record_ids, fields or [], context or {})
return records return records
def get_connector(hostname, protocol="xmlrpc", port="auto"): def get_connector(hostname=None, protocol="xmlrpc", port="auto"):
""" """
A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC. A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC.
@ -377,10 +405,12 @@ def get_connector(hostname, protocol="xmlrpc", port="auto"):
return XmlRPCConnector(hostname, port) return XmlRPCConnector(hostname, port)
elif protocol == "netrpc": elif protocol == "netrpc":
return NetRPCConnector(hostname, port) return NetRPCConnector(hostname, port)
elif protocol == "local":
return LocalConnector()
else: else:
raise ValueError("You must choose xmlrpc or netrpc") raise ValueError("You must choose xmlrpc or netrpc or local")
def get_connection(hostname, protocol="xmlrpc", port='auto', database=None, def get_connection(hostname=None, protocol="xmlrpc", port='auto', database=None,
login=None, password=None, user_id=None): login=None, password=None, user_id=None):
""" """
A shortcut method to easily create a connection to a remote OpenERP server. A shortcut method to easily create a connection to a remote OpenERP server.

View File

@ -26,9 +26,8 @@ class OpenERPSession(object):
Used to store references to non-literal domains which need to be Used to store references to non-literal domains which need to be
round-tripped to the client browser. round-tripped to the client browser.
""" """
def __init__(self, server='127.0.0.1', port=8069): def __init__(self, config):
self._server = server self.config = config
self._port = port
self._db = False self._db = False
self._uid = False self._uid = False
self._login = False self._login = False
@ -40,11 +39,16 @@ class OpenERPSession(object):
self._lang = {} self._lang = {}
self.remote_timezone = 'utc' self.remote_timezone = 'utc'
self.client_timezone = False self.client_timezone = False
def build_connection(self): def build_connection(self):
return openerplib.get_connection(hostname=self._server, port=self._port, if self.config.backend == 'local':
database=self._db, login=self._login, conn = openerplib.get_connection(protocol='local', database=self._db,
user_id=self._uid, password=self._password) login=self._login, user_id=self._uid, password=self._password)
else:
conn = openerplib.get_connection(hostname=self.config.server_host,
port=self.config.server_port, database=self._db, login=self._login,
user_id=self._uid, password=self._password)
return conn
def proxy(self, service): def proxy(self, service):
return self.build_connection().get_service(service) return self.build_connection().get_service(service)

View File

@ -172,14 +172,14 @@ class WebClient(openerpweb.Controller):
"grouping", "decimal_point", "thousands_sep"]) "grouping", "decimal_point", "thousands_sep"])
else: else:
lang_obj = None lang_obj = None
if lang.count("_") > 0: if lang.count("_") > 0:
separator = "_" separator = "_"
else: else:
separator = "@" separator = "@"
langs = lang.split(separator) langs = lang.split(separator)
langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)] langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
transs = {} transs = {}
for addon_name in mods: for addon_name in mods:
transl = {"messages":[]} transl = {"messages":[]}
@ -246,7 +246,7 @@ class Database(openerpweb.Controller):
password, db = operator.itemgetter( password, db = operator.itemgetter(
'drop_pwd', 'drop_db')( 'drop_pwd', 'drop_db')(
dict(map(operator.itemgetter('name', 'value'), fields))) dict(map(operator.itemgetter('name', 'value'), fields)))
try: try:
return req.session.proxy("db").drop(password, db) return req.session.proxy("db").drop(password, db)
except xmlrpclib.Fault, e: except xmlrpclib.Fault, e:
@ -297,7 +297,7 @@ class Session(openerpweb.Controller):
@openerpweb.jsonrequest @openerpweb.jsonrequest
def login(self, req, db, login, password): def login(self, req, db, login, password):
req.session.login(db, login, password) req.session.login(db, login, password)
ctx = req.session.get_context() ctx = req.session.get_context() if req.session._uid else {}
return { return {
"session_id": req.session_id, "session_id": req.session_id,
@ -342,7 +342,7 @@ class Session(openerpweb.Controller):
} }
except Exception, e: except Exception, e:
return {"error": e, "title": "Languages"} return {"error": e, "title": "Languages"}
@openerpweb.jsonrequest @openerpweb.jsonrequest
def modules(self, req): def modules(self, req):
# TODO query server for installed web modules # TODO query server for installed web modules
@ -721,7 +721,7 @@ class DataSet(openerpweb.Controller):
args[domain_id] = d args[domain_id] = d
if context_id and len(args) - 1 >= context_id: if context_id and len(args) - 1 >= context_id:
args[context_id] = c args[context_id] = c
for i in xrange(len(args)): for i in xrange(len(args)):
if isinstance(args[i], web.common.nonliterals.BaseContext): if isinstance(args[i], web.common.nonliterals.BaseContext):
args[i] = req.session.eval_context(args[i]) args[i] = req.session.eval_context(args[i])
@ -969,7 +969,7 @@ class SearchView(View):
if field.get('context'): if field.get('context'):
field["context"] = self.parse_domain(field["context"], req.session) field["context"] = self.parse_domain(field["context"], req.session)
return {'fields': fields} return {'fields': fields}
@openerpweb.jsonrequest @openerpweb.jsonrequest
def get_filters(self, req, model): def get_filters(self, req, model):
Model = req.session.model("ir.filters") Model = req.session.model("ir.filters")
@ -978,7 +978,7 @@ class SearchView(View):
filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session)) filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session)) filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
return filters return filters
@openerpweb.jsonrequest @openerpweb.jsonrequest
def save_filter(self, req, model, name, context_to_save, domain): def save_filter(self, req, model, name, context_to_save, domain):
Model = req.session.model("ir.filters") Model = req.session.model("ir.filters")
@ -1388,7 +1388,7 @@ class Reports(View):
report_data['form'] = action['datas']['form'] report_data['form'] = action['datas']['form']
if 'ids' in action['datas']: if 'ids' in action['datas']:
report_ids = action['datas']['ids'] report_ids = action['datas']['ids']
report_id = report_srv.report( report_id = report_srv.report(
req.session._db, req.session._uid, req.session._password, req.session._db, req.session._uid, req.session._password,
action["report_name"], report_ids, action["report_name"], report_ids,
@ -1400,6 +1400,7 @@ class Reports(View):
req.session._db, req.session._uid, req.session._password, report_id) req.session._db, req.session._uid, req.session._password, report_id)
if report_struct["state"]: if report_struct["state"]:
break break
time.sleep(self.POLLING_DELAY) time.sleep(self.POLLING_DELAY)
report = base64.b64decode(report_struct['result']) report = base64.b64decode(report_struct['result'])
@ -1413,3 +1414,108 @@ class Reports(View):
('Content-Type', report_mimetype), ('Content-Type', report_mimetype),
('Content-Length', len(report))], ('Content-Length', len(report))],
cookies={'fileToken': int(token)}) cookies={'fileToken': int(token)})
class Import(View):
_cp_path = "/web/import"
def fields_get(self, req, model):
Model = req.session.model(model)
fields = Model.fields_get(False, req.session.eval_context(req.context))
return fields
@openerpweb.httprequest
def detect_data(self, req, csvfile, csvsep, csvdel, csvcode, jsonp):
try:
data = list(csv.reader(
csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
except csv.Error, e:
csvfile.seek(0)
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {
'message': 'Error parsing CSV file: %s' % e,
# decodes each byte to a unicode character, which may or
# may not be printable, but decoding will succeed.
# Otherwise simplejson will try to decode the `str` using
# utf-8, which is very likely to blow up on characters out
# of the ascii range (in range [128, 256))
'preview': csvfile.read(200).decode('iso-8859-1')}}))
try:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps(
{'records': data[:10]}, encoding=csvcode))
except UnicodeDecodeError:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({
'message': u"Failed to decode CSV file using encoding %s, "
u"try switching to a different encoding" % csvcode
}))
@openerpweb.httprequest
def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
meta):
modle_obj = req.session.model(model)
skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
simplejson.loads(meta))
error = None
if not (csvdel and len(csvdel) == 1):
error = u"The CSV delimiter must be a single character"
if not indices and fields:
error = u"You must select at least one field to import"
if error:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message': error}}))
# skip ignored records
data_record = itertools.islice(
csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
skip, None)
# if only one index, itemgetter will return an atom rather than a tuple
if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
else: mapper = operator.itemgetter(*indices)
data = None
error = None
try:
# decode each data row
data = [
[record.decode(csvcode) for record in row]
for row in itertools.imap(mapper, data_record)
# don't insert completely empty rows (can happen due to fields
# filtering in case of e.g. o2m content rows)
if any(row)
]
except UnicodeDecodeError:
error = u"Failed to decode CSV file using encoding %s" % csvcode
except csv.Error, e:
error = u"Could not process CSV file: %s" % e
# If the file contains nothing,
if not data:
error = u"File to import is empty"
if error:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message': error}}))
try:
(code, record, message, _nope) = modle_obj.import_data(
fields, data, 'init', '', False,
req.session.eval_context(req.context))
except xmlrpclib.Fault, e:
error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error':error}))
if code != -1:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'success':True}))
msg = u"Error during import: %s\n\nTrying to import record %r" % (
message, record)
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message':msg}}))

View File

@ -0,0 +1,21 @@
Copyright 2011 Xavier Morel. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY XAVIER MOREL ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,59 @@
.. -*- restructuredtext -*-
In jQuery 1.5, jQuery has introduced a Deferred object in order to
better handle callbacks.
Along with Deferred, it introduced ``jQuery.when`` which allows, among
other things, for multiplexing deferreds (waiting on multiple
deferreds at the same time).
While this is very nice if all deferreds are available at the same
point, it doesn't really work if the resolution of a deferred can
generate more deferreds, or for collections of deferreds coming from
multiple sources (which may not be aware of one another).
Deferred.queue tries to be a solution to this. It is based on the
principle of FIFO multiple-producers multiple-consumers tasks queues,
such as Python's `queue.Queue`_. Any code with a reference to the
queue can add a promise to the queue, and the queue (which is a
promise itself) will only be resolved once all promises within are
resolved.
Quickstart
----------
Deferred.queue has a very simple life cycle: it is dormant when
created, it starts working as soon as promises get added to it (via
``.push``, or by providing them directly to its constructor), and as
soon as the last promise in the queue is resolved it resolves itself.
If any promise in the queue fails, the queue itself will be rejected
(without waiting for further promises).
Once a queue has been resolved or rejected, adding new promises to it
results in an error.
API
---
``jQuery.Deferred.queue([promises...])``
Creates a new deferred queue. Can be primed with a series of promise
objects.
``.push(promise...)``
Adds promises to the queue. Returns the queue itself (can be
chained).
If the queue has already been resolved or rejected, raises an error.
``.then([doneCallbacks][, failCallbacks])``
Promise/A ``then`` method.
``.done(doneCallbacks)``
jQuery ``done`` extension to promise objects
``.fail(failCallbacks)``
jQuery ``fail`` extension to promise objects
.. _queue.Queue:
http://docs.python.org/dev/library/queue.html

View File

@ -0,0 +1,34 @@
(function ($) {
"use strict";
$.extend($.Deferred, {
queue: function () {
var queueDeferred = $.Deferred();
var promises = 0;
function resolve() {
if (--promises > 0) {
return;
}
setTimeout($.proxy(queueDeferred, 'resolve'), 0);
}
var promise = $.extend(queueDeferred.promise(), {
push: function () {
if (this.isResolved() || this.isRejected()) {
throw new Error("Can not add promises to a resolved "
+ "or rejected promise queue");
}
promises += 1;
$.when.apply(null, arguments).then(
resolve, $.proxy(queueDeferred, 'reject'));
return this;
}
});
if (arguments.length) {
promise.push.apply(promise, arguments);
}
return promise;
}
});
})(jQuery)

View File

@ -892,6 +892,9 @@ label.error {
.openerp .oe_forms input.field_datetime { .openerp .oe_forms input.field_datetime {
min-width: 11em; min-width: 11em;
} }
.openerp .oe_forms.oe_frame .oe_datepicker_root {
width: 100%;
}
.openerp .oe_forms .button { .openerp .oe_forms .button {
color: #4c4c4c; color: #4c4c4c;
white-space: nowrap; white-space: nowrap;
@ -904,7 +907,14 @@ label.error {
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
right: 5px; right: 5px;
top: 5px; top: 3px;
}
.openerp .oe_datepicker_root {
position: relative;
display: inline-block;
}
.openerp .oe_datepicker_root input[type="text"] {
min-width: 160px;
} }
.openerp .oe_input_icon_disabled { .openerp .oe_input_icon_disabled {
position: absolute; position: absolute;

View File

@ -0,0 +1,30 @@
.openerp .oe_import_grid {
border: none;
border-collapse: collapse;
}
.openerp .oe_import_grid-header .oe_import_grid-cell {
background: url(../img/gradientlinebg.gif) repeat-x #CCCCCC;
border-bottom: 1px solid #E3E3E3;
font-weight: bold;
text-align: left;
}
.openerp .oe_import_grid-row .oe_import_grid-cell {
border-bottom: 1px solid #E3E3E3;
}
.openerp .separator.horizontal {
font-weight: bold;
border-bottom-width: 1px;
margin: 6px 4px 6px 1px;
height: 20px;
}
.openerp .duplicate_fld{
background-color:#FF6666;
}
.openerp .select_fld{
background: none repeat scroll 0 0 white;
}
.openerp .ui-autocomplete {
max-height: 300px;
overflow-y: auto;
padding-right: 20px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

View File

@ -58,7 +58,7 @@ openerp.web = function(instance) {
openerp.web.formats(instance); openerp.web.formats(instance);
openerp.web.chrome(instance); openerp.web.chrome(instance);
openerp.web.data(instance); openerp.web.data(instance);
var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export"]; var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"];
for(var i=0; i<files.length; i++) { for(var i=0; i<files.length; i++) {
if(openerp.web[files[i]]) { if(openerp.web[files[i]]) {
openerp.web[files[i]](instance); openerp.web[files[i]](instance);

View File

@ -218,9 +218,8 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
this.$element.closest(".openerp") this.$element.closest(".openerp")
.removeClass("login-mode") .removeClass("login-mode")
.addClass("database_block"); .addClass("database_block");
var self = this; var self = this;
var fetch_db = this.rpc("/web/database/get_list", {}, function(result) { var fetch_db = this.rpc("/web/database/get_list", {}, function(result) {
self.db_list = result.db_list; self.db_list = result.db_list;
}); });
@ -232,7 +231,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
self.lang_list = result.lang_list; self.lang_list = result.lang_list;
}); });
$.when(fetch_db, fetch_langs).then(function () {self.do_create();}); $.when(fetch_db, fetch_langs).then(function () {self.do_create();});
this.$element.find('#db-create').click(this.do_create); this.$element.find('#db-create').click(this.do_create);
this.$element.find('#db-drop').click(this.do_drop); this.$element.find('#db-drop').click(this.do_drop);
this.$element.find('#db-backup').click(this.do_backup); this.$element.find('#db-backup').click(this.do_backup);
@ -399,7 +398,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
do_restore: function() { do_restore: function() {
var self = this; var self = this;
self.$option_id.html(QWeb.render("RestoreDB", self)); self.$option_id.html(QWeb.render("RestoreDB", self));
self.$option_id.find("form[name=restore_db_form]").validate({ self.$option_id.find("form[name=restore_db_form]").validate({
submitHandler: function (form) { submitHandler: function (form) {
$.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'}); $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
@ -467,10 +466,11 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
/** /**
* @constructs openerp.web.Login * @constructs openerp.web.Login
* @extends openerp.web.Widget * @extends openerp.web.Widget
* *
* @param parent * @param parent
* @param element_id * @param element_id
*/ */
init: function(parent, element_id) { init: function(parent, element_id) {
this._super(parent, element_id); this._super(parent, element_id);
this.has_local_storage = typeof(localStorage) != 'undefined'; this.has_local_storage = typeof(localStorage) != 'undefined';
@ -496,7 +496,7 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
}, },
display: function() { display: function() {
var self = this; var self = this;
this.$element.html(QWeb.render("Login", this)); this.$element.html(QWeb.render("Login", this));
this.database = new openerp.web.Database( this.database = new openerp.web.Database(
this, "oe_database", "oe_db_options"); this, "oe_database", "oe_db_options");
@ -574,14 +574,13 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
/** /**
* @constructs openerp.web.Header * @constructs openerp.web.Header
* @extends openerp.web.Widget * @extends openerp.web.Widget
* *
* @param parent * @param parent
*/ */
init: function(parent) { init: function(parent) {
this._super(parent); this._super(parent);
this.qs = "?" + jQuery.param.querystring(); this.qs = "?" + jQuery.param.querystring();
this.$content = $(); this.$content = $();
console.debug("initializing header with id", this.element_id);
this.update_promise = $.Deferred().resolve(); this.update_promise = $.Deferred().resolve();
}, },
start: function() { start: function() {
@ -666,7 +665,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
}); });
}); });
}, },
on_action: function(action) { on_action: function(action) {
}, },
on_preferences: function(){ on_preferences: function(){
@ -702,10 +701,11 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
}, },
Save: function(){ Save: function(){
var inner_viewmanager = action_manager.inner_viewmanager; var inner_viewmanager = action_manager.inner_viewmanager;
inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save(function(){ inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save()
inner_viewmanager.start(); .then(function() {
self.dialog.stop();
window.location.reload();
}); });
$(this).dialog('destroy')
} }
} }
}); });
@ -713,7 +713,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
action_manager.appendTo(this.dialog); action_manager.appendTo(this.dialog);
action_manager.render(this.dialog); action_manager.render(this.dialog);
}, },
change_password :function() { change_password :function() {
var self = this; var self = this;
this.dialog = new openerp.web.Dialog(this,{ this.dialog = new openerp.web.Dialog(this,{
@ -728,21 +728,13 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
submitHandler: function (form) { submitHandler: function (form) {
self.rpc("/web/session/change_password",{ self.rpc("/web/session/change_password",{
'fields': $(form).serializeArray() 'fields': $(form).serializeArray()
}, function(result) { }, function(result) {
if (result.error) { if (result.error) {
self.display_error(result); self.display_error(result);
return; return;
} } else {
else { self.session.logout();
if (result.new_password) { }
self.session.password = result.new_password;
var session = new openerp.web.Session(self.session.server, self.session.port);
session.start();
session.session_login(self.session.db, self.session.login, self.session.password)
}
}
self.notification.notify("Changed Password", "Password has been changed successfully");
self.dialog.close();
}); });
} }
}); });
@ -766,7 +758,7 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{
/** /**
* @constructs openerp.web.Menu * @constructs openerp.web.Menu
* @extends openerp.web.Widget * @extends openerp.web.Widget
* *
* @param parent * @param parent
* @param element_id * @param element_id
* @param secondary_menu_id * @param secondary_menu_id
@ -918,7 +910,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
/** /**
* @constructs openerp.web.WebClient * @constructs openerp.web.WebClient
* @extends openerp.web.Widget * @extends openerp.web.Widget
* *
* @param element_id * @param element_id
*/ */
init: function(element_id) { init: function(element_id) {
@ -963,7 +955,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
this.session.start(); this.session.start();
this.login.start(); this.login.start();
this.menu.start(); this.menu.start();
console.debug("The openerp client has been initialized.");
}, },
on_logged: function() { on_logged: function() {
if(this.action_manager) if(this.action_manager)
@ -1009,7 +1000,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
self.execute_home_action(home_action[0], ds); self.execute_home_action(home_action[0], ds);
}) })
}, },
default_home: function () { default_home: function () {
}, },
/** /**
* Bundles the execution of the home action * Bundles the execution of the home action
@ -1034,7 +1025,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
}, },
do_url_set_hash: function(url) { do_url_set_hash: function(url) {
if(!this.url_external_hashchange) { if(!this.url_external_hashchange) {
console.log("url set #hash to",url);
this.url_internal_hashchange = true; this.url_internal_hashchange = true;
jQuery.bbq.pushState(url); jQuery.bbq.pushState(url);
} }
@ -1042,10 +1032,8 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
on_url_hashchange: function() { on_url_hashchange: function() {
if(this.url_internal_hashchange) { if(this.url_internal_hashchange) {
this.url_internal_hashchange = false; this.url_internal_hashchange = false;
console.log("url jump to FLAG OFF");
} else { } else {
var url = jQuery.deparam.fragment(); var url = jQuery.deparam.fragment();
console.log("url jump to",url);
this.url_external_hashchange = true; this.url_external_hashchange = true;
this.action_manager.on_url_hashchange(url); this.action_manager.on_url_hashchange(url);
this.url_external_hashchange = false; this.url_external_hashchange = false;

View File

@ -1,7 +1,10 @@
/*--------------------------------------------------------- /*---------------------------------------------------------
* OpenERP Web core * OpenERP Web core
*--------------------------------------------------------*/ *--------------------------------------------------------*/
var console;
if (!console) {
console = {log: function () {}};
}
if (!console.debug) { if (!console.debug) {
console.debug = console.log; console.debug = console.log;
} }
@ -483,7 +486,8 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
self.user_context = result.context; self.user_context = result.context;
self.db = result.db; self.db = result.db;
self.session_save(); self.session_save();
self.on_session_valid(); if (self.uid)
self.on_session_valid();
return true; return true;
}).then(success_callback); }).then(success_callback);
}, },
@ -771,7 +775,6 @@ openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp
* // stuff that you want to init before the rendering * // stuff that you want to init before the rendering
* }, * },
* start: function() { * start: function() {
* this._super();
* // stuff you want to make after the rendering, `this.$element` holds a correct value * // stuff you want to make after the rendering, `this.$element` holds a correct value
* this.$element.find(".my_button").click(/* an example of event binding * /); * this.$element.find(".my_button").click(/* an example of event binding * /);
* *
@ -915,6 +918,8 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg
* @returns {jQuery.Deferred} * @returns {jQuery.Deferred}
*/ */
start: function() { start: function() {
/* The default implementation is only useful for retro-compatibility, it is
not necessary to call it using _super() when using Widget for new components. */
if (!this.$element) { if (!this.$element) {
var tmp = document.getElementById(this.element_id); var tmp = document.getElementById(this.element_id);
this.$element = tmp ? $(tmp) : undefined; this.$element = tmp ? $(tmp) : undefined;
@ -922,7 +927,7 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg
return $.Deferred().done().promise(); return $.Deferred().done().promise();
}, },
/** /**
* Destroys the current widget, also destory all its children before destroying itself. * Destroys the current widget, also destroy all its children before destroying itself.
*/ */
stop: function() { stop: function() {
_.each(_.clone(this.widget_children), function(el) { _.each(_.clone(this.widget_children), function(el) {
@ -943,7 +948,6 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg
* If that's not the case this method will simply return `false`. * If that's not the case this method will simply return `false`.
*/ */
do_action: function(action, on_finished) { do_action: function(action, on_finished) {
console.log('Widget.do_action', action, on_finished);
if (this.widget_parent) { if (this.widget_parent) {
return this.widget_parent.do_action(action, on_finished); return this.widget_parent.do_action(action, on_finished);
} }

View File

@ -0,0 +1,289 @@
openerp.web.data_import = function(openerp) {
var QWeb = openerp.web.qweb;
/**
* Safari does not deal well at all with raw JSON data being returned. As a
* result, we're going to cheat by using a pseudo-jsonp: instead of getting
* JSON data in the iframe, we're getting a ``script`` tag which consists of a
* function call and the returned data (the json dump).
*
* The function is an auto-generated name bound to ``window``, which calls
* back into the callback provided here.
*
* @param {Object} form the form element (DOM or jQuery) to use in the call
* @param {Object} attributes jquery.form attributes object
* @param {Function} callback function to call with the returned data
*/
function jsonp(form, attributes, callback) {
attributes = attributes || {};
var options = {jsonp: _.uniqueId('import_callback_')};
window[options.jsonp] = function () {
delete window[options.jsonp];
callback.apply(null, arguments);
};
if ('data' in attributes) {
_.extend(attributes.data, options);
} else {
_.extend(attributes, {data: options});
}
$(form).ajaxSubmit(attributes);
}
openerp.web.DataImport = openerp.web.Dialog.extend({
template: 'ImportDataView',
dialog_title: "Import Data",
init: function(parent, dataset){
var self = this;
this._super(parent, {});
this.model = parent.model;
this.fields = [];
this.all_fields = [];
this.required_fields = null;
var convert_fields = function (root, prefix) {
prefix = prefix || '';
_(root.fields).each(function (f) {
self.all_fields.push(prefix + f.name);
if (f.fields) {
convert_fields(f, prefix + f.id + '/');
}
});
};
this.ready = $.Deferred.queue().then(function () {
self.required_fields = _(self.fields).chain()
.filter(function (field) { return field.required; })
.pluck('name')
.value();
convert_fields(self);
self.all_fields.sort();
});
},
start: function() {
var self = this;
this._super();
this.open({
modal: true,
width: '70%',
height: 'auto',
position: 'top',
buttons: [
{text: "Close", click: function() { self.stop(); }},
{text: "Import File", click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'}
],
close: function(event, ui) {
self.stop();
}
});
this.toggle_import_button(false);
this.$element.find('#csvfile').change(this.on_autodetect_data);
this.$element.find('fieldset').change(this.on_autodetect_data);
this.$element.find('fieldset legend').click(function() {
$(this).next().toggle();
});
this.ready.push(new openerp.web.DataSet(this, this.model).call(
'fields_get', [], function (fields) {
self.graft_fields(fields);
}));
},
graft_fields: function (fields, parent, level) {
parent = parent || this;
level = level || 0;
var self = this;
_(fields).each(function (field, field_name) {
var f = {
id: field_name,
name: field_name,
string: field.string,
required: field.required
};
switch (field.type) {
case 'many2many':
case 'many2one':
f.name += '/id';
break;
case 'one2many':
f.name += '/id';
f.fields = [];
// only fetch sub-fields to a depth of 2 levels
if (level < 2) {
self.ready.push(new openerp.web.DataSet(self, field.relation).call(
'fields_get', [], function (fields) {
self.graft_fields(fields, f, level+1);
}));
}
break;
}
parent.fields.push(f);
});
},
toggle_import_button: function (newstate) {
this.$dialog.dialog('widget')
.find('.oe-dialog-import-button')
.button('option', 'disabled', !newstate);
},
do_import: function() {
if(!this.$element.find('#csvfile').val()) { return; }
var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10);
var with_headers = this.$element.find('#file_has_headers').prop('checked');
if (!lines_to_skip && with_headers) {
lines_to_skip = 1;
}
var indices = [], fields = [];
this.$element.find(".sel_fields").each(function(index, element) {
var val = element.value;
if (!val) {
return;
}
indices.push(index);
fields.push(val);
});
jsonp(this.$element.find('#import_data'), {
url: '/web/import/import_data',
data: {
model: this.model,
meta: JSON.stringify({
skip: lines_to_skip,
indices: indices,
fields: fields
})
}
}, this.on_import_results);
},
on_autodetect_data: function() {
if(!this.$element.find('#csvfile').val()) { return; }
jsonp(this.$element.find('#import_data'), {
url: '/web/import/detect_data'
}, this.on_import_results);
},
on_import_results: function(results) {
this.$element.find('#result').empty();
var headers, result_node = this.$element.find("#result");
if (results['records']) {
var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10),
with_headers = this.$element.find('#file_has_headers').prop('checked');
headers = with_headers ? results.records[0] : null;
result_node.append(QWeb.render('ImportView.result', {
'headers': headers,
'records': lines_to_skip ? results.records.slice(lines_to_skip)
: with_headers ? results.records.slice(1)
: results.records
}));
} else if (results['error']) {
result_node.append(QWeb.render('ImportView.error', {
'error': results['error']}));
} else if (results['success']) {
if (this.widget_parent.widget_parent.active_view == "list") {
this.widget_parent.reload_content();
}
this.stop();
return;
}
var self = this;
this.ready.then(function () {
var $fields = self.$element.find('.sel_fields').bind('blur', function () {
if (this.value && !_(self.all_fields).contains(this.value)) {
this.value = '';
}
}).autocomplete({
minLength: 0,
source: self.all_fields,
change: self.on_check_field_values
}).focus(function () {
$(this).autocomplete('search');
});
// Column auto-detection
_(headers).each(function (header, index) {
var f =_(self.fields).detect(function (field) {
// TODO: levenshtein between header and field.string
return field.name === header || field.string.toLowerCase() === header;
});
if (f) {
$fields.eq(index).val(f.name);
}
});
self.on_check_field_values();
});
},
/**
* Looks through all the field selections, and tries to find if two
* (or more) columns were matched to the same model field.
*
* Returns a map of the multiply-mapped fields to an array of offending
* columns (not actually columns, but the inputs containing the same field
* names).
*
* Also has the side-effect of marking the discovered inputs with the class
* ``duplicate_fld``.
*
* @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
*/
find_duplicate_fields: function() {
// Maps values to DOM nodes, in order to discover duplicates
var values = {}, duplicates = {};
this.$element.find(".sel_fields").each(function(index, element) {
var value = element.value;
var $element = $(element).removeClass('duplicate_fld');
if (!value) { return; }
if (!(value in values)) {
values[value] = element;
} else {
var same_valued_field = values[value];
if (value in duplicates) {
duplicates[value].push(element);
} else {
duplicates[value] = [same_valued_field, element];
}
$element.add(same_valued_field).addClass('duplicate_fld');
}
});
return duplicates;
},
on_check_field_values: function () {
this.$element.find("#message, #msg").remove();
var required_valid = this.check_required();
var duplicates = this.find_duplicate_fields();
if (_.isEmpty(duplicates)) {
this.toggle_import_button(required_valid);
} else {
var $err = $('<div id="msg" style="color: red;">Destination fields should only be selected once, some fields are selected more than once:</div>').insertBefore(this.$element.find('#result'));
var $dupes = $('<dl>').appendTo($err);
_(duplicates).each(function(elements, value) {
$('<dt>').text(value).appendTo($dupes);
_(elements).each(function(element) {
var cell = $(element).closest('td');
$('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes);
});
});
this.toggle_import_button(false);
}
},
check_required: function() {
if (!this.required_fields.length) { return true; }
var selected_fields = _(this.$element.find('.sel_fields').get()).chain()
.pluck('value')
.compact()
.value();
var missing_fields = _.difference(this.required_fields, selected_fields);
if (missing_fields.length) {
this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>');
return false;
}
return true;
},
stop: function() {
$(this.$dialog).remove();
this._super();
}
});
};

View File

@ -118,26 +118,26 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) {
var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate,
Date.CultureInfo.formatPatterns.longTime)); Date.CultureInfo.formatPatterns.longTime));
if (tmp !== null) if (tmp !== null)
return tmp; return openerp.web.datetime_to_str(tmp);
tmp = Date.parse(value); tmp = Date.parse(value);
if (tmp !== null) if (tmp !== null)
return tmp; return openerp.web.datetime_to_str(tmp);
throw value + " is not a valid datetime"; throw value + " is not a valid datetime";
case 'date': case 'date':
var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate); var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate);
if (tmp !== null) if (tmp !== null)
return tmp; return openerp.web.date_to_str(tmp);
tmp = Date.parse(value); tmp = Date.parse(value);
if (tmp !== null) if (tmp !== null)
return tmp; return openerp.web.date_to_str(tmp);
throw value + " is not a valid date"; throw value + " is not a valid date";
case 'time': case 'time':
var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime); var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime);
if (tmp !== null) if (tmp !== null)
return tmp; return openerp.web.time_to_str(tmp);
tmp = Date.parse(value); tmp = Date.parse(value);
if (tmp !== null) if (tmp !== null)
return tmp; return openerp.web.time_to_str(tmp);
throw value + " is not a valid time"; throw value + " is not a valid time";
} }
return value; return value;

View File

@ -795,20 +795,18 @@ openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @
* @extends openerp.web.search.DateField * @extends openerp.web.search.DateField
*/ */
openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{ openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{
/** template: "SearchView.date",
* enables date picker on the HTML widgets
*/
start: function () { start: function () {
this._super(); this._super();
this.$element.addClass('field_date').datepicker({ this.datewidget = new openerp.web.DateWidget(this);
dateFormat: 'yy-mm-dd' this.datewidget.prependTo(this.$element);
}); this.datewidget.$element.find("input").attr("size", 15);
}, this.datewidget.$element.find("input").attr("autofocus",
stop: function () { this.attrs.default_focus === '1' ? 'autofocus' : undefined);
this.$element.datepicker('destroy'); this.datewidget.set_value(this.defaults[this.attrs.name] || false);
}, },
get_value: function () { get_value: function () {
return this.$element.val(); return this.datewidget.get_value() || null;
} }
}); });
/** /**
@ -1123,7 +1121,7 @@ openerp.web.search.ExtendedSearchProposition.Char = openerp.web.OldWidget.extend
} }
}); });
openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.extend({ openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.extend({
template: 'SearchView.extended_search.proposition.datetime', template: 'SearchView.extended_search.proposition.empty',
identifier_prefix: 'extended-search-proposition-datetime', identifier_prefix: 'extended-search-proposition-datetime',
operators: [ operators: [
{value: "=", text: "is equal to"}, {value: "=", text: "is equal to"},
@ -1134,18 +1132,16 @@ openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.ex
{value: "<=", text: "less or equal than"} {value: "<=", text: "less or equal than"}
], ],
get_value: function() { get_value: function() {
return this.$element.val(); return this.datewidget.get_value();
}, },
start: function() { start: function() {
this._super(); this._super();
this.$element.datetimepicker({ this.datewidget = new openerp.web.DateTimeWidget(this);
dateFormat: 'yy-mm-dd', this.datewidget.prependTo(this.$element);
timeFormat: 'hh:mm:ss'
});
} }
}); });
openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend({ openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend({
template: 'SearchView.extended_search.proposition.date', template: 'SearchView.extended_search.proposition.empty',
identifier_prefix: 'extended-search-proposition-date', identifier_prefix: 'extended-search-proposition-date',
operators: [ operators: [
{value: "=", text: "is equal to"}, {value: "=", text: "is equal to"},
@ -1156,14 +1152,12 @@ openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend
{value: "<=", text: "less or equal than"} {value: "<=", text: "less or equal than"}
], ],
get_value: function() { get_value: function() {
return this.$element.val(); return this.datewidget.get_value();
}, },
start: function() { start: function() {
this._super(); this._super();
this.$element.datepicker({ this.datewidget = new openerp.web.DateWidget(this);
dateFormat: 'yy-mm-dd', this.datewidget.prependTo(this.$element);
timeFormat: 'hh:mm:ss'
});
} }
}); });
openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.extend({ openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.extend({

View File

@ -1007,19 +1007,18 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f
* the fields'context with the action's context. * the fields'context with the action's context.
*/ */
build_context: function() { build_context: function() {
// I previously belevied contexts should be herrited, but now I doubt it
//var a_context = this.view.dataset.get_context() || {};
var f_context = this.field.context || null; var f_context = this.field.context || null;
// maybe the default_get should only be used when we do a default_get? // maybe the default_get should only be used when we do a default_get?
var v_context1 = this.node.attrs.default_get || {}; var v_contexts = _.compact([this.node.attrs.default_get || null,
var v_context2 = this.node.attrs.context || {}; this.node.attrs.context || null]);
var v_context = new openerp.web.CompoundContext(v_context1, v_context2); var v_context = new openerp.web.CompoundContext();
if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true _.each(v_contexts, function(x) {v_context.add(x);});
if (_.detect(v_contexts, function(x) {return !!x.__ref;})) {
var fields_values = this._build_view_fields_values(); var fields_values = this._build_view_fields_values();
v_context.set_eval_context(fields_values); v_context.set_eval_context(fields_values);
} }
// if there is a context on the node, overrides the model's context // if there is a context on the node, overrides the model's context
var ctx = f_context || v_context; var ctx = v_contexts.length > 0 ? v_context : f_context;
return ctx; return ctx;
}, },
build_domain: function() { build_domain: function() {
@ -1121,16 +1120,13 @@ openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
} }
}); });
openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ openerp.web.DateTimeWidget = openerp.web.Widget.extend({
init: function(view, node) { template: "web.datetimepicker",
this._super(view, node); jqueryui_object: 'datetimepicker',
this.template = "FieldDate"; type_of_date: "datetime",
this.jqueryui_object = 'datetimepicker';
},
start: function() { start: function() {
var self = this; var self = this;
this._super.apply(this, arguments); this.$element.find('input').change(this.on_change);
this.$element.find('input').change(this.on_ui_change);
this.picker({ this.picker({
onSelect: this.on_picker_select, onSelect: this.on_picker_select,
changeMonth: true, changeMonth: true,
@ -1148,6 +1144,8 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
this.$element.find('button.oe_datepicker_close').click(function() { this.$element.find('button.oe_datepicker_close').click(function() {
self.$element.find('.oe_datepicker').hide(); self.$element.find('.oe_datepicker').hide();
}); });
this.set_readonly(false);
this.value = false;
}, },
picker: function() { picker: function() {
return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments); return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments);
@ -1157,60 +1155,93 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
this.$element.find('input').val(date ? this.format_client(date) : '').change(); this.$element.find('input').val(date ? this.format_client(date) : '').change();
}, },
set_value: function(value) { set_value: function(value) {
value = this.parse(value); this.value = value;
this._super(value);
this.$element.find('input').val(value ? this.format_client(value) : ''); this.$element.find('input').val(value ? this.format_client(value) : '');
}, },
get_value: function() { get_value: function() {
return this.format(this.value); return this.value;
}, },
set_value_from_ui: function() { set_value_from_ui: function() {
var value = this.$element.find('input').val() || false; var value = this.$element.find('input').val() || false;
this.value = this.parse_client(value); this.value = this.parse_client(value);
this._super();
}, },
update_dom: function() { set_readonly: function(readonly) {
this._super.apply(this, arguments); this.readonly = readonly;
this.$element.find('input').attr('disabled', this.readonly); this.$element.find('input').attr('disabled', this.readonly);
this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', this.readonly); this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
}, },
validate: function() { is_valid: function(required) {
this.invalid = false;
var value = this.$element.find('input').val(); var value = this.$element.find('input').val();
if (value === "") { if (value === "") {
this.invalid = this.required; return !required;
} else { } else {
try { try {
this.parse_client(value); this.parse_client(value);
this.invalid = false; return true;
} catch(e) { } catch(e) {
this.invalid = true; return false;
} }
} }
}, },
focus: function() { focus: function() {
this.$element.find('input').focus(); this.$element.find('input').focus();
}, },
parse: openerp.web.auto_str_to_date,
parse_client: function(v) { parse_client: function(v) {
return openerp.web.parse_value(v, this.field); return openerp.web.parse_value(v, {"widget": this.type_of_date});
},
format: function(val) {
return openerp.web.auto_date_to_str(val, this.field.type);
}, },
format_client: function(v) { format_client: function(v) {
return openerp.web.format_value(v, this.field); return openerp.web.format_value(v, {"widget": this.type_of_date});
},
on_change: function() {
if (this.is_valid()) {
this.set_value_from_ui();
}
}
});
openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({
jqueryui_object: 'datepicker',
type_of_date: "date",
on_picker_select: function(text, instance) {
this._super(text, instance);
this.$element.find('.oe_datepicker').hide();
}
});
openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
template: "EmptyComponent",
build_widget: function() {
return new openerp.web.DateTimeWidget(this);
},
start: function() {
var self = this;
this._super.apply(this, arguments);
this.datewidget = this.build_widget();
this.datewidget.on_change.add(this.on_ui_change);
this.datewidget.appendTo(this.$element);
},
set_value: function(value) {
this._super(value);
this.datewidget.set_value(value);
},
get_value: function() {
return this.datewidget.get_value();
},
update_dom: function() {
this._super.apply(this, arguments);
this.datewidget.set_readonly(this.readonly);
},
validate: function() {
this.invalid = !this.datewidget.is_valid(this.required);
},
focus: function() {
this.datewidget.focus();
} }
}); });
openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
init: function(view, node) { build_widget: function() {
this._super(view, node); return new openerp.web.DateWidget(this);
this.jqueryui_object = 'datepicker';
},
on_picker_select: function(text, instance) {
this._super(text, instance);
this.$element.find('.oe_datepicker').hide();
} }
}); });
@ -1436,7 +1467,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
this.$input = this.$element.find("input"); this.$input = this.$element.find("input");
this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
this.$menu_btn = this.$element.find(".oe-m2o-cm-button"); this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
// context menu // context menu
var init_context_menu_def = $.Deferred().then(function(e) { var init_context_menu_def = $.Deferred().then(function(e) {
var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context()); var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
@ -1444,7 +1475,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
[[self.field.relation, false]], false, rdataset.get_context()], false, 0) [[self.field.relation, false]], false, rdataset.get_context()], false, 0)
.then(function(result) { .then(function(result) {
self.related_entries = result; self.related_entries = result;
var $cmenu = $("#" + self.cm_id); var $cmenu = $("#" + self.cm_id);
$cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self})); $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
var bindings = {}; var bindings = {};

View File

@ -415,8 +415,8 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
*/ */
do_search: function (domains, contexts, groupbys) { do_search: function (domains, contexts, groupbys) {
return this.rpc('/web/session/eval_domain_and_context', { return this.rpc('/web/session/eval_domain_and_context', {
domains: [this.dataset.get_domain()].concat(domains), domains: _([this.dataset.get_domain()].concat(domains)).compact(),
contexts: [this.dataset.get_context()].concat(contexts), contexts: _([this.dataset.get_context()].concat(contexts)).compact(),
group_by_seq: groupbys group_by_seq: groupbys
}, $.proxy(this, 'do_actual_search')); }, $.proxy(this, 'do_actual_search'));
}, },
@ -541,6 +541,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
if (_.isEmpty(records)) { if (_.isEmpty(records)) {
records = this.groups.get_records(); records = this.groups.get_records();
} }
records = _(records).compact();
var count = 0, sums = {}; var count = 0, sums = {};
_(columns).each(function (column) { _(columns).each(function (column) {
@ -1235,6 +1236,9 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L
}, },
get_records: function () { get_records: function () {
if (_(this.children).isEmpty()) { if (_(this.children).isEmpty()) {
if (!this.datagroup.length) {
return;
}
return { return {
count: this.datagroup.length, count: this.datagroup.length,
values: this.datagroup.aggregates values: this.datagroup.aggregates

View File

@ -862,6 +862,8 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{
console.log('Todo'); console.log('Todo');
}, },
on_sidebar_import: function() { on_sidebar_import: function() {
var import_view = new db.web.DataImport(this, this.dataset);
import_view.start();
}, },
on_sidebar_export: function() { on_sidebar_export: function() {
var export_view = new db.web.DataExport(this, this.dataset); var export_view = new db.web.DataExport(this, this.dataset);

View File

@ -800,13 +800,15 @@
></textarea> ></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"/> <img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>
</t> </t>
<t t-name="FieldDate"> <t t-name="web.datetimepicker">
<t t-call="FieldChar"/> <div class="oe_datepicker_root">
<img class="oe_input_icon oe_datepicker_trigger" src="/web/static/src/img/ui/field_calendar.png" <input type="text" size="1" style="width: 100%"/>
title="Select date" width="16" height="16" border="0"/> <img class="oe_input_icon oe_datepicker_trigger" src="/web/static/src/img/ui/field_calendar.png"
<div class="oe_datepicker ui-widget-content ui-corner-all" style="display: none; position: absolute; z-index: 1;"> title="Select date" width="16" height="16" border="0"/>
<div class="oe_datepicker_container"/> <div class="oe_datepicker ui-widget-content ui-corner-all" style="display: none; position: absolute; z-index: 1;">
<button type="button" class="oe_datepicker_close ui-state-default ui-priority-primary ui-corner-all" style="float: right;">Done</button> <div class="oe_datepicker_container"/>
<button type="button" class="oe_datepicker_close ui-state-default ui-priority-primary ui-corner-all" style="float: right;">Done</button>
</div>
</div> </div>
</t> </t>
<t t-name="FieldSelection"> <t t-name="FieldSelection">
@ -1051,6 +1053,18 @@
<t t-if="filters.length" t-raw="filters.render(defaults)"/> <t t-if="filters.length" t-raw="filters.render(defaults)"/>
</div> </div>
</t> </t>
<t t-name="SearchView.date">
<label t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
t-att-title="attrs.help"
t-att-for="element_id">
<t t-esc="attrs.string || attrs.name"/>
<span t-if="attrs.help">?</span>
</label>
<div style="white-space: nowrap;">
<span t-att-id="element_id"></span>
<t t-if="filters.length" t-raw="filters.render(defaults)"/>
</div>
</t>
<t t-name="SearchView.field.selection"> <t t-name="SearchView.field.selection">
<label t-att-title="attrs.help" <label t-att-title="attrs.help"
t-att-class="'oe_label' + (attrs.help ? '_help' : '')" t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
@ -1142,11 +1156,8 @@
<t t-name="SearchView.extended_search.proposition.char"> <t t-name="SearchView.extended_search.proposition.char">
<input t-att-id="element_id" class="field_char"/> <input t-att-id="element_id" class="field_char"/>
</t> </t>
<t t-name="SearchView.extended_search.proposition.datetime"> <t t-name="SearchView.extended_search.proposition.empty">
<input t-att-id="element_id" class="field_datetime"/> <span t-att-id="element_id"></span>
</t>
<t t-name="SearchView.extended_search.proposition.date">
<input t-att-id="element_id" class="field_date"/>
</t> </t>
<t t-name="SearchView.extended_search.proposition.integer"> <t t-name="SearchView.extended_search.proposition.integer">
<input type="number" t-att-id="element_id" class="field_integer" step="1"/> <input type="number" t-att-id="element_id" class="field_integer" step="1"/>
@ -1372,6 +1383,79 @@
</table> </table>
</form> </form>
</t> </t>
<t t-name="ImportView">
<a id="importview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Import</a>
</t>
<t t-name="ImportDataView">
<form name="import_data" id="import_data" action="" method="post" enctype="multipart/form-data">
<input type="hidden" name="session_id" t-att-value="session.session_id"/>
<h2 class="separator horizontal">1. Import a .CSV file</h2>
<p>Select a .CSV file to import. If you need a sample of file to import,
you should use the export tool with the "Import Compatible" option.
</p>
<p>
<label for="csvfile">CSV File:</label>
<input type="file" id="csvfile" size="50" name="csvfile"/>
</p>
<h2 class="separator horizontal">2. Check your file format</h2>
<div id="result"></div>
<fieldset>
<legend style="cursor:pointer;">Import Options</legend>
<table style="display:none">
<tr>
<td colspan="4">
<label for="file_has_headers">Does your file have titles?</label>
<input type="checkbox" checked="checked"
id="file_has_headers"/>
</td>
</tr>
<tr>
<td><label for="csv_separator">Separator:</label></td>
<td><input type="text" name="csvsep" id="csv_separator" value=","/></td>
<td><label for="csv_delimiter">Delimiter:</label></td>
<td><input type="text" name="csvdel" id="csv_delimiter" value='"'/></td>
</tr>
<tr>
<td><label for="csv_encoding">Encoding:</label></td>
<td>
<select name="csvcode" id="csv_encoding">
<option value="utf-8">UTF-8</option>
<option value="latin1">Latin 1</option>
</select>
</td>
<td><label for="csv_skip" title="For use if CSV files have titles on multiple lines, skips more than a single line during import">
Lines to skip<sup>?</sup>:</label></td>
<td><input type="number" id="csv_skip" value="0" min="0"/></td>
</tr>
</table>
</fieldset>
</form>
</t>
<table t-name="ImportView.result"
class="oe_import_grid" width="100%" style="margin: 5px 0;">
<tr t-if="headers" class="oe_import_grid-header">
<td t-foreach="headers" t-as="header" class="oe_import_grid-cell">
<t t-esc="header"/></td>
</tr>
<tr>
<td t-foreach="records[0]" t-as="column">
<input class="sel_fields"/>
</td>
</tr>
<tr t-foreach="records" t-as="record" class="oe_import_grid-row">
<td t-foreach="record" t-as="cell" class="oe_import_grid-cell">
<t t-esc="cell"/></td>
</tr>
</table>
<t t-name="ImportView.error">
<p style="white-space:pre-line;">The import failed due to:<t t-esc="error.message"/></p>
<t t-if="error.preview">
<p>Here is a preview of the file we could not import:</p>
<pre><t t-esc="error.preview"/></pre>
</t>
</t>
<t t-name="About-Page"> <t t-name="About-Page">
<div> <div>
<h1>OpenERP Web</h1> <h1>OpenERP Web</h1>

View File

@ -7,6 +7,7 @@ import logging
import logging.config import logging.config
import werkzeug.serving import werkzeug.serving
import werkzeug.contrib.fixers
path_root = os.path.dirname(os.path.abspath(__file__)) path_root = os.path.dirname(os.path.abspath(__file__))
path_addons = os.path.join(path_root, 'addons') path_addons = os.path.join(path_root, 'addons')
@ -14,8 +15,6 @@ if path_addons not in sys.path:
sys.path.insert(0, path_addons) sys.path.insert(0, path_addons)
optparser = optparse.OptionParser() optparser = optparse.OptionParser()
optparser.add_option("-p", "--port", dest="socket_port", default=8002,
help="listening port", type="int", metavar="NUMBER")
optparser.add_option("-s", "--session-path", dest="session_storage", optparser.add_option("-s", "--session-path", dest="session_storage",
default=os.path.join(tempfile.gettempdir(), "oe-sessions"), default=os.path.join(tempfile.gettempdir(), "oe-sessions"),
help="directory used for session storage", metavar="DIR") help="directory used for session storage", metavar="DIR")
@ -27,25 +26,37 @@ optparser.add_option("--db-filter", dest="dbfilter", default='.*',
help="Filter listed database", metavar="REGEXP") help="Filter listed database", metavar="REGEXP")
optparser.add_option('--addons-path', dest='addons_path', default=path_addons, optparser.add_option('--addons-path', dest='addons_path', default=path_addons,
help="Path do addons directory", metavar="PATH") help="Path do addons directory", metavar="PATH")
optparser.add_option('--no-serve-static', dest='serve_static',
default=True, action='store_false', server_options = optparse.OptionGroup(optparser, "Server configuration")
help="Do not serve static files via this server") server_options.add_option("-p", "--port", dest="socket_port", default=8002,
optparser.add_option('--reloader', dest='reloader', help="listening port", type="int", metavar="NUMBER")
default=False, action='store_true', server_options.add_option('--reloader', dest='reloader',
help="Reload application when python files change") default=False, action='store_true',
optparser.add_option("--log-level", dest="log_level", help="Reload application when python files change")
default='debug', help="Log level", metavar="LOG_LEVEL") server_options.add_option('--no-serve-static', dest='serve_static',
optparser.add_option("--log-config", dest="log_config", default=True, action='store_false',
default='', help="Log config file", metavar="LOG_CONFIG") help="Do not serve static files via this server")
optparser.add_option('--multi-threaded', dest='threaded', server_options.add_option('--multi-threaded', dest='threaded',
default=False, action='store_true', default=False, action='store_true',
help="Use multiple threads to handle requests") help="Spawn one thread per HTTP request")
server_options.add_option('--proxy-mode', dest='proxy_mode',
default=False, action='store_true',
help="Enable correct behavior when behind a reverse proxy")
optparser.add_option_group(server_options)
logging_opts = optparse.OptionGroup(optparser, "Logging")
logging_opts.add_option("--log-level", dest="log_level", type="choice",
default='debug', help="Global logging level", metavar="LOG_LEVEL",
choices=['debug', 'info', 'warning', 'error', 'critical'])
logging_opts.add_option("--log-config", dest="log_config",
help="Logging configuration file", metavar="FILE")
optparser.add_option_group(logging_opts)
import web.common.dispatch import web.common.dispatch
if __name__ == "__main__": if __name__ == "__main__":
(options, args) = optparser.parse_args(sys.argv[1:]) (options, args) = optparser.parse_args(sys.argv[1:])
options.backend = 'rpc' options.backend = 'xmlrpc'
os.environ["TZ"] = "UTC" os.environ["TZ"] = "UTC"
@ -56,6 +67,9 @@ if __name__ == "__main__":
app = web.common.dispatch.Root(options) app = web.common.dispatch.Root(options)
if options.proxy_mode:
app = werkzeug.contrib.fixers.ProxyFix(app)
werkzeug.serving.run_simple( werkzeug.serving.run_simple(
'0.0.0.0', options.socket_port, app, '0.0.0.0', options.socket_port, app,
use_reloader=options.reloader, threaded=options.threaded) use_reloader=options.reloader, threaded=options.threaded)