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

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

View File

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

View File

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

View File

@ -190,6 +190,34 @@ class NetRPCConnector(Connector):
socket.disconnect()
return result
class LocalConnector(Connector):
"""
A type of connector that uses the XMLRPC protocol.
"""
PROTOCOL = 'local'
__logger = _getChildLogger(_logger, 'connector.local')
def __init__(self):
pass
def send(self, service_name, method, *args):
import openerp
# TODO Exception handling
# This will be changed to be xmlrpc compatible
# OpenERPWarning code 1
# OpenERPException code 2
try:
result = openerp.netsvc.dispatch_rpc(service_name, method, args, 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):
"""
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 {})
return records
def get_connector(hostname, protocol="xmlrpc", port="auto"):
def get_connector(hostname=None, protocol="xmlrpc", port="auto"):
"""
A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC.
@ -377,10 +405,12 @@ def get_connector(hostname, protocol="xmlrpc", port="auto"):
return XmlRPCConnector(hostname, port)
elif protocol == "netrpc":
return NetRPCConnector(hostname, port)
elif protocol == "local":
return LocalConnector()
else:
raise ValueError("You must choose xmlrpc or netrpc")
raise ValueError("You must choose xmlrpc or netrpc or local")
def get_connection(hostname, protocol="xmlrpc", port='auto', database=None,
def get_connection(hostname=None, protocol="xmlrpc", port='auto', database=None,
login=None, password=None, user_id=None):
"""
A shortcut method to easily create a connection to a remote OpenERP server.

View File

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

View File

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

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

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

View File

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

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,
Date.CultureInfo.formatPatterns.longTime));
if (tmp !== null)
return tmp;
return openerp.web.datetime_to_str(tmp);
tmp = Date.parse(value);
if (tmp !== null)
return tmp;
return openerp.web.datetime_to_str(tmp);
throw value + " is not a valid datetime";
case 'date':
var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate);
if (tmp !== null)
return tmp;
return openerp.web.date_to_str(tmp);
tmp = Date.parse(value);
if (tmp !== null)
return tmp;
return openerp.web.date_to_str(tmp);
throw value + " is not a valid date";
case 'time':
var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime);
if (tmp !== null)
return tmp;
return openerp.web.time_to_str(tmp);
tmp = Date.parse(value);
if (tmp !== null)
return tmp;
return openerp.web.time_to_str(tmp);
throw value + " is not a valid time";
}
return value;

View File

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

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import logging
import logging.config
import werkzeug.serving
import werkzeug.contrib.fixers
path_root = os.path.dirname(os.path.abspath(__file__))
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)
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",
default=os.path.join(tempfile.gettempdir(), "oe-sessions"),
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")
optparser.add_option('--addons-path', dest='addons_path', default=path_addons,
help="Path do addons directory", metavar="PATH")
optparser.add_option('--no-serve-static', dest='serve_static',
default=True, action='store_false',
help="Do not serve static files via this server")
optparser.add_option('--reloader', dest='reloader',
default=False, action='store_true',
help="Reload application when python files change")
optparser.add_option("--log-level", dest="log_level",
default='debug', help="Log level", metavar="LOG_LEVEL")
optparser.add_option("--log-config", dest="log_config",
default='', help="Log config file", metavar="LOG_CONFIG")
optparser.add_option('--multi-threaded', dest='threaded',
default=False, action='store_true',
help="Use multiple threads to handle requests")
server_options = optparse.OptionGroup(optparser, "Server configuration")
server_options.add_option("-p", "--port", dest="socket_port", default=8002,
help="listening port", type="int", metavar="NUMBER")
server_options.add_option('--reloader', dest='reloader',
default=False, action='store_true',
help="Reload application when python files change")
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=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',
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
if __name__ == "__main__":
(options, args) = optparser.parse_args(sys.argv[1:])
options.backend = 'rpc'
options.backend = 'xmlrpc'
os.environ["TZ"] = "UTC"
@ -56,6 +67,9 @@ if __name__ == "__main__":
app = web.common.dispatch.Root(options)
if options.proxy_mode:
app = werkzeug.contrib.fixers.ProxyFix(app)
werkzeug.serving.run_simple(
'0.0.0.0', options.socket_port, app,
use_reloader=options.reloader, threaded=options.threaded)