diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py
index 678eacd7f3e..269aa13d58e 100644
--- a/addons/web/__openerp__.py
+++ b/addons/web/__openerp__.py
@@ -26,6 +26,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/spinjs/spin.js",
"static/lib/jquery.autosize/jquery.autosize.js",
"static/lib/jquery.blockUI/jquery.blockUI.js",
+ "static/lib/jquery.placeholder/jquery.placeholder.js",
"static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js",
"static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js",
"static/lib/jquery.ui.notify/js/jquery.notify.js",
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 87af7474daf..8f1855f205d 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -15,6 +15,8 @@ import simplejson
import time
import urllib
import urllib2
+import urlparse
+import xmlrpclib
import zlib
from xml.etree import ElementTree
from cStringIO import StringIO
@@ -91,16 +93,50 @@ def db_list(req):
dbs = [i for i in dbs if re.match(r, i)]
return dbs
-def db_monodb(req):
- # if only one db exists, return it else return False
+def db_monodb_redirect(req):
+ db = False
+ redirect = False
+
+ # 1 try the db in the url
+ db_url = req.params.get('db')
+ if db_url:
+ return (db_url, False)
+
try:
dbs = db_list(req)
- if len(dbs) == 1:
- return dbs[0]
except Exception:
# ignore access denied
- pass
- return False
+ dbs = []
+
+ # 2 use the database from the cookie if it's listable and still listed
+ cookie_db = req.httprequest.cookies.get('last_used_database')
+ if cookie_db in dbs:
+ db = cookie_db
+
+ # 3 use the first db
+ if dbs and not db:
+ db = dbs[0]
+
+ # redirect to the chosen db if multiple are available
+ if db and len(dbs) > 1:
+ query = dict(urlparse.parse_qsl(req.httprequest.query_string, keep_blank_values=True))
+ query.update({ 'db': db })
+ redirect = req.httprequest.path + '?' + urllib.urlencode(query)
+ return (db, redirect)
+
+def db_monodb(req):
+ # if only one db exists, return it else return False
+ return db_monodb_redirect(req)[0]
+
+def redirect_with_hash(req, url, code=303):
+ if req.httprequest.user_agent.browser == 'msie':
+ try:
+ version = float(req.httprequest.user_agent.version)
+ if version < 10:
+ return "
" % url
+ except Exception:
+ pass
+ return werkzeug.utils.redirect(url, code)
def module_topological_sort(modules):
""" Return a list of module names sorted so that their dependencies of the
@@ -291,6 +327,10 @@ def manifest_glob(req, extension, addons=None, db=None):
return r
def manifest_list(req, extension, mods=None, db=None):
+ """ list ressources to load specifying either:
+ mods: a comma separated string listing modules
+ db: a database name (return all installed modules in that database)
+ """
if not req.debug:
path = '/web/webclient/' + extension
if mods is not None:
@@ -299,12 +339,7 @@ def manifest_list(req, extension, mods=None, db=None):
path += '?' + urllib.urlencode({'db': db})
return [path]
files = manifest_glob(req, extension, addons=mods, db=db)
- i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
- req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
- if i_am_diabetic:
- return [wp for _fp, wp in files]
- else:
- return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
+ return [wp for _fp, wp in files]
def get_last_modified(files):
""" Returns the modification time of the most recently modified
@@ -534,6 +569,10 @@ class Home(openerpweb.Controller):
@openerpweb.httprequest
def index(self, req, s_action=None, db=None, **kw):
+ db, redir = db_monodb_redirect(req)
+ if redir:
+ return redirect_with_hash(req, redir)
+
js = "\n ".join('' % i for i in manifest_list(req, 'js', db=db))
css = "\n ".join('' % i for i in manifest_list(req, 'css', db=db))
@@ -869,7 +908,7 @@ class Session(openerpweb.Controller):
"""
saved_actions = req.httpsession.get('saved_actions')
if not saved_actions:
- saved_actions = {"next":0, "actions":{}}
+ saved_actions = {"next":1, "actions":{}}
req.httpsession['saved_actions'] = saved_actions
# we don't allow more than 10 stored actions
if len(saved_actions["actions"]) >= 10:
diff --git a/addons/web/doc/search_view.rst b/addons/web/doc/search_view.rst
index 16a91c5e9ac..12b0eaf3104 100644
--- a/addons/web/doc/search_view.rst
+++ b/addons/web/doc/search_view.rst
@@ -107,6 +107,12 @@ formatted differently). If an input *may* fetch multiple completion
items, it *should* prefix those with a section title using its own
name. This has no technical consequence but is clearer for users.
+.. note::
+
+ If a field is :js:func:`invisible
+ `, its completion function will
+ *not* be called.
+
Providing drawer/supplementary UI
+++++++++++++++++++++++++++++++++
@@ -145,6 +151,11 @@ started only once (per view).
dynamically collects, lays out and renders filters? =>
exercises drawer thingies
+.. note::
+
+ An :js:func:`invisible ` input
+ will not be inserted into the drawer.
+
Converting from facet objects
+++++++++++++++++++++++++++++
diff --git a/addons/web/http.py b/addons/web/http.py
index ed917c00563..05069574da1 100644
--- a/addons/web/http.py
+++ b/addons/web/http.py
@@ -92,6 +92,14 @@ class WebRequest(object):
if not self.session:
self.session = session.OpenERPSession()
self.httpsession[self.session_id] = self.session
+
+ # set db/uid trackers - they're cleaned up at the WSGI
+ # dispatching phase in openerp.service.wsgi_server.application
+ if self.session._db:
+ threading.current_thread().dbname = self.session._db
+ if self.session._uid:
+ threading.current_thread().uid = self.session._uid
+
self.context = self.params.pop('context', {})
self.debug = self.params.pop('debug', False) is not False
# Determine self.lang
diff --git a/addons/web/static/lib/datejs/globalization/ar-SY.js b/addons/web/static/lib/datejs/globalization/ar-SY.js
index 317881211e6..b504a21d324 100644
--- a/addons/web/static/lib/datejs/globalization/ar-SY.js
+++ b/addons/web/static/lib/datejs/globalization/ar-SY.js
@@ -11,8 +11,8 @@ Date.CultureInfo = {
firstLetterDayNames: ["أ", "ا", "ث", "أ", "خ", "ج", "س"],
/* Month Name Strings */
- monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
- abbreviatedMonthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
+ monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
+ abbreviatedMonthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
/* AM/PM Designators */
amDesignator: "ص",
@@ -82,18 +82,18 @@ Date.CultureInfo = {
* As well, please review the list of "Future Strings" section below.
*/
regexPatterns: {
- jan: /^كانون الثاني/i,
- feb: /^شباط/i,
- mar: /^آذار/i,
- apr: /^نيسان/i,
- may: /^أيار/i,
- jun: /^حزيران/i,
- jul: /^تموز/i,
- aug: /^آب/i,
- sep: /^أيلول/i,
- oct: /^تشرين الأول/i,
- nov: /^تشرين الثاني/i,
- dec: /^كانون الأول/i,
+ jan: /^يناير/i,
+ feb: /^فبراير/i,
+ mar: /^مارس/i,
+ apr: /^أبريل/i,
+ may: /^مايو/i,
+ jun: /^يونيو/i,
+ jul: /^يوليو/i,
+ aug: /^أغسطس/i,
+ sep: /^سبتمبر/i,
+ oct: /^أكتوبر/i,
+ nov: /^نوفمبر/i,
+ dec: /^ديسمبر/i,
sun: /^الاحد/i,
mon: /^ا(1)?/i,
@@ -192,4 +192,4 @@ Date.CultureInfo = {
* end end
* long long
* short short
- */
\ No newline at end of file
+ */
diff --git a/addons/web/static/lib/jquery.placeholder/jquery.placeholder.js b/addons/web/static/lib/jquery.placeholder/jquery.placeholder.js
new file mode 100755
index 00000000000..d8051fa269d
--- /dev/null
+++ b/addons/web/static/lib/jquery.placeholder/jquery.placeholder.js
@@ -0,0 +1,157 @@
+/*! http://mths.be/placeholder v2.0.7 by @mathias */
+;(function(window, document, $) {
+
+ var isInputSupported = 'placeholder' in document.createElement('input'),
+ isTextareaSupported = 'placeholder' in document.createElement('textarea'),
+ prototype = $.fn,
+ valHooks = $.valHooks,
+ hooks,
+ placeholder;
+
+ if (isInputSupported && isTextareaSupported) {
+
+ placeholder = prototype.placeholder = function() {
+ return this;
+ };
+
+ placeholder.input = placeholder.textarea = true;
+
+ } else {
+
+ placeholder = prototype.placeholder = function() {
+ var $this = this;
+ $this
+ .filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
+ .not('.placeholder')
+ .bind({
+ 'focus.placeholder': clearPlaceholder,
+ 'blur.placeholder': setPlaceholder
+ })
+ .data('placeholder-enabled', true)
+ .trigger('blur.placeholder');
+ return $this;
+ };
+
+ placeholder.input = isInputSupported;
+ placeholder.textarea = isTextareaSupported;
+
+ hooks = {
+ 'get': function(element) {
+ var $element = $(element);
+ return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
+ },
+ 'set': function(element, value) {
+ var $element = $(element);
+ if (!$element.data('placeholder-enabled')) {
+ return element.value = value;
+ }
+ if (value == '') {
+ element.value = value;
+ // Issue #56: Setting the placeholder causes problems if the element continues to have focus.
+ if (element != document.activeElement) {
+ // We can't use `triggerHandler` here because of dummy text/password inputs :(
+ setPlaceholder.call(element);
+ }
+ } else if ($element.hasClass('placeholder')) {
+ clearPlaceholder.call(element, true, value) || (element.value = value);
+ } else {
+ element.value = value;
+ }
+ // `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363
+ return $element;
+ }
+ };
+
+ isInputSupported || (valHooks.input = hooks);
+ isTextareaSupported || (valHooks.textarea = hooks);
+
+ $(function() {
+ // Look for forms
+ $(document).delegate('form', 'submit.placeholder', function() {
+ // Clear the placeholder values so they don't get submitted
+ var $inputs = $('.placeholder', this).each(clearPlaceholder);
+ setTimeout(function() {
+ $inputs.each(setPlaceholder);
+ }, 10);
+ });
+ });
+
+ // Clear placeholder values upon page reload
+ $(window).bind('beforeunload.placeholder', function() {
+ $('.placeholder').each(function() {
+ this.value = '';
+ });
+ });
+
+ }
+
+ function args(elem) {
+ // Return an object of element attributes
+ var newAttrs = {},
+ rinlinejQuery = /^jQuery\d+$/;
+ $.each(elem.attributes, function(i, attr) {
+ if (attr.specified && !rinlinejQuery.test(attr.name)) {
+ newAttrs[attr.name] = attr.value;
+ }
+ });
+ return newAttrs;
+ }
+
+ function clearPlaceholder(event, value) {
+ var input = this,
+ $input = $(input);
+ if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) {
+ if ($input.data('placeholder-password')) {
+ $input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
+ // If `clearPlaceholder` was called from `$.valHooks.input.set`
+ if (event === true) {
+ return $input[0].value = value;
+ }
+ $input.focus();
+ } else {
+ input.value = '';
+ $input.removeClass('placeholder');
+ input == document.activeElement && input.select();
+ }
+ }
+ }
+
+ function setPlaceholder() {
+ var $replacement,
+ input = this,
+ $input = $(input),
+ $origInput = $input,
+ id = this.id;
+ if (input.value == '') {
+ if (input.type == 'password') {
+ if (!$input.data('placeholder-textinput')) {
+ try {
+ $replacement = $input.clone().attr({ 'type': 'text' });
+ } catch(e) {
+ $replacement = $('').attr($.extend(args(this), { 'type': 'text' }));
+ }
+ $replacement
+ .removeAttr('name')
+ .data({
+ 'placeholder-password': true,
+ 'placeholder-id': id
+ })
+ .bind('focus.placeholder', clearPlaceholder);
+ $input
+ .data({
+ 'placeholder-textinput': $replacement,
+ 'placeholder-id': id
+ })
+ .before($replacement);
+ }
+ $input = $input.removeAttr('id').hide().prev().attr('id', id).show();
+ // Note: `$input[0] != input` now!
+ }
+ $input.addClass('placeholder');
+ $input[0].value = $input.attr('placeholder');
+ } else {
+ $input.removeClass('placeholder');
+ }
+ }
+
+}(this, document, jQuery));
\ No newline at end of file
diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass
index 550a746b7ad..8544cf0dd3c 100644
--- a/addons/web/static/src/css/base.sass
+++ b/addons/web/static/src/css/base.sass
@@ -1436,7 +1436,7 @@ $sheet-padding: 16px
.oe_searchview_drawer
position: absolute
- z-index: 100
+ z-index: 2
// detach drawer from field slightly
margin-top: 4px
top: 100%
@@ -1832,6 +1832,12 @@ $sheet-padding: 16px
.oe_form
.oe_form_field_text
width: 100%
+ .oe_form_text_content
+ text-overflow: ellipsis
+ display: inline-block
+ white-space: pre-wrap
+ overflow-x: hidden
+ width: 100%
.oe_form_field_char input,
.oe_form_field_url input,
.oe_form_field_email input,
@@ -2502,6 +2508,9 @@ div.ui-widget-overlay
// Internet Explorer 9+ specifics {{{
.openerp_ie
+ .placeholder
+ color: $tag-border !important
+ font-style: italic !important
.oe_form_field_boolean input
background: #fff
.db_option_table .oe_form_field_selection
@@ -2517,6 +2526,8 @@ div.ui-widget-overlay
button.oe_highlight
padding-top: 0
padding-bottom: 0
+ .oe_view_manager_view_kanban
+ display: table-cell
.oe_view_manager_buttons
button.oe_write_full
padding-top: 0
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js
index eaa4bcbfe1c..c3f9f6af357 100644
--- a/addons/web/static/src/js/chrome.js
+++ b/addons/web/static/src/js/chrome.js
@@ -530,7 +530,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
'login': 'admin',
'password': form_obj['create_admin_pwd'],
'login_successful': function() {
- self.do_action("reload");
+ var action = {
+ type: "ir.actions.client",
+ tag: 'reload',
+ params: {
+ url_search : {
+ db: form_obj['db_name'],
+ },
+ }
+ };
+ self.do_action(action);
},
},
_push_me: false,
@@ -641,6 +650,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage
instance.web.Login = instance.web.Widget.extend({
template: "Login",
remember_credentials: true,
+ events: {
+ 'change input[name=db],select[name=db]': function(ev) {
+ this.set('database_selector', $(ev.currentTarget).val());
+ },
+ },
init: function(parent, action) {
this._super(parent);
@@ -652,18 +666,18 @@ instance.web.Login = instance.web.Widget.extend({
if (_.isEmpty(this.params)) {
this.params = $.bbq.getState(true);
}
+ if (action && action.params && action.params.db) {
+ this.params.db = action.params.db;
+ } else if ($.deparam.querystring().db) {
+ this.params.db = $.deparam.querystring().db;
+ }
+ if (this.params.db) {
+ this.selected_db = this.params.db;
+ }
if (this.params.login_successful) {
this.on('login_successful', this, this.params.login_successful);
}
-
- if (this.has_local_storage && this.remember_credentials) {
- this.selected_db = localStorage.getItem('last_db_login_success');
- this.selected_login = localStorage.getItem('last_login_login_success');
- if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
- this.selected_password = localStorage.getItem('last_password_login_success');
- }
- }
},
start: function() {
var self = this;
@@ -671,10 +685,10 @@ instance.web.Login = instance.web.Widget.extend({
self.$el.find('.oe_login_manage_db').click(function() {
self.do_action("database_manager");
});
+ self.on('change:database_selector', this, function() {
+ this.database_selected(this.get('database_selector'));
+ });
var d = $.when();
- if ($.deparam.querystring().db) {
- self.params.db = $.deparam.querystring().db;
- }
if ($.param.fragment().token) {
self.params.token = $.param.fragment().token;
}
@@ -682,16 +696,44 @@ instance.web.Login = instance.web.Widget.extend({
if (self.params.db && self.params.login && self.params.password) {
d = self.do_login(self.params.db, self.params.login, self.params.password);
} else {
- if (self.params.db) {
- self.on_db_loaded([self.params.db])
- } else {
- d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed);
- }
+ d = self.rpc("/web/database/get_list", {})
+ .done(self.on_db_loaded)
+ .fail(self.on_db_failed)
+ .always(function() {
+ if (self.selected_db && self.has_local_storage && self.remember_credentials) {
+ self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
+ if (self.session.debug) {
+ self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
+ }
+ }
+ });
}
return d;
},
+ remember_last_used_database: function(db) {
+ // This cookie will be used server side in order to avoid db reloading on first visit
+ var ttl = 24 * 60 * 60 * 365;
+ document.cookie = [
+ 'last_used_database=' + db,
+ 'path=/',
+ 'max-age=' + ttl,
+ 'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString()
+ ].join(';');
+ },
+ database_selected: function(db) {
+ var params = $.deparam.querystring();
+ params.db = db;
+ this.remember_last_used_database(db);
+ this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
+ this.$('[name=login], [name=password]').prop('readonly', true);
+ instance.web.redirect('/?' + $.param(params));
+ },
on_db_loaded: function (result) {
+ var self = this;
this.db_list = result;
+ if (!this.selected_db) {
+ this.selected_db = result[0];
+ }
this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
if(this.db_list.length === 0) {
this.do_action("database_manager");
@@ -732,17 +774,11 @@ instance.web.Login = instance.web.Widget.extend({
self.hide_error();
self.$(".oe_login_pane").fadeOut("slow");
return this.session.session_authenticate(db, login, password).then(function() {
- if (self.has_local_storage) {
- if(self.remember_credentials) {
- localStorage.setItem('last_db_login_success', db);
- localStorage.setItem('last_login_login_success', login);
- if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
- localStorage.setItem('last_password_login_success', password);
- }
- } else {
- localStorage.setItem('last_db_login_success', '');
- localStorage.setItem('last_login_login_success', '');
- localStorage.setItem('last_password_login_success', '');
+ self.remember_last_used_database(db);
+ if (self.has_local_storage && self.remember_credentials) {
+ localStorage.setItem(db + '|last_login', login);
+ if (self.session.debug) {
+ localStorage.setItem(db + '|last_password', password);
}
}
self.trigger('login_successful');
@@ -800,6 +836,9 @@ instance.web.Reload = function(parent, action) {
var sobj = $.deparam(l.search.substr(1));
sobj.ts = new Date().getTime();
+ if (params.url_search) {
+ sobj = _.extend(sobj, params.url_search);
+ }
var search = '?' + $.param(sobj);
var hash = l.hash;
@@ -1303,7 +1342,7 @@ instance.web.WebClient = instance.web.Client.extend({
},
logo_edit: function(ev) {
var self = this;
- new self.alive(instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
+ self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
result.res_id = res['company_id'][0];
result.target = "new";
diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js
index 90a0eb0a5a0..c4bb81117f8 100644
--- a/addons/web/static/src/js/corelib.js
+++ b/addons/web/static/src/js/corelib.js
@@ -232,13 +232,17 @@ instance.web.ParentedMixin = {
Utility method to only execute asynchronous actions if the current
object has not been destroyed.
- @param {Promise} promise The promise representing the asynchronous action.
- @param {bool} reject Defaults to false. If true, the returned promise will be
- rejected with no arguments if the current object is destroyed. If false,
- the returned promise will never be resolved nor rejected.
- @returns {Promise} A promise that will mirror the given promise if everything goes
- fine but will either be rejected with no arguments or never resolved if the
- current object is destroyed.
+ @param {$.Deferred} promise The promise representing the asynchronous
+ action.
+ @param {bool} [reject=false] If true, the returned promise will be
+ rejected with no arguments if the current
+ object is destroyed. If false, the
+ returned promise will never be resolved
+ or rejected.
+ @returns {$.Deferred} A promise that will mirror the given promise if
+ everything goes fine but will either be rejected
+ with no arguments or never resolved if the
+ current object is destroyed.
*/
alive: function(promise, reject) {
var def = $.Deferred();
diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js
index 43d292b66db..dedb4a49710 100644
--- a/addons/web/static/src/js/coresetup.js
+++ b/addons/web/static/src/js/coresetup.js
@@ -89,6 +89,10 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
});
},
session_is_valid: function() {
+ var db = $.deparam.querystring().db;
+ if (db && this.db !== db) {
+ return false;
+ }
return !!this.uid;
},
/**
diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js
index bbff633c687..5f56c4bc5f9 100644
--- a/addons/web/static/src/js/data.js
+++ b/addons/web/static/src/js/data.js
@@ -481,9 +481,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
* Creates a new record in db
*
* @param {Object} data field values to set on the new record
+ * @param {Object} options Dictionary that can contain the following keys:
+ * - readonly_fields: Values from readonly fields that were updated by
+ * on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {$.Deferred}
*/
- create: function(data) {
+ create: function(data, options) {
return this._model.call('create', [data], {context: this.get_context()});
},
/**
@@ -491,8 +494,10 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
*
* @param {Number|String} id identifier for the record to alter
* @param {Object} data field values to write into the record
- * @param {Function} callback function called with operation result
- * @param {Function} error_callback function called in case of write error
+ * @param {Object} options Dictionary that can contain the following keys:
+ * - context: The context to use in the server-side call.
+ * - readonly_fields: Values from readonly fields that were updated by
+ * on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {$.Deferred}
*/
write: function (id, data, options) {
@@ -732,10 +737,13 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
self.last_default_get = res;
});
},
- create: function(data) {
- var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
- defaults: this.last_default_get};
- this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)}));
+ create: function(data, options) {
+ var cached = {
+ id:_.uniqueId(this.virtual_id_prefix),
+ values: _.extend({}, data, (options || {}).readonly_fields || {}),
+ defaults: this.last_default_get
+ };
+ this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
this.cache.push(cached);
return $.Deferred().resolve(cached.id).promise();
},
@@ -762,7 +770,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
cached = {id: id, values: {}};
this.cache.push(cached);
}
- $.extend(cached.values, record.values);
+ $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
if (dirty)
this.trigger("dataset_changed", id, data, options);
return $.Deferred().resolve(true).promise();
@@ -860,8 +868,16 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
}
return completion.promise();
},
- call_button: function (method, args) {
- var id = args[0][0], index;
+ /**
+ * Invalidates caching of a record in the dataset to ensure the next read
+ * of that record will hit the server.
+ *
+ * Of use when an action is going to remote-alter a record which will then
+ * need to be reloaded, e.g. action button.
+ *
+ * @param {Object} id record to remove from the BDS's cache
+ */
+ evict_record: function (id) {
for(var i=0, len=this.cache.length; i} filters elements of the group
- * @param {instance.web.SearchView} view view in which the filters are contained
+ * @param {instance.web.SearchView} parent parent in which the filters are contained
*/
- init: function (filters, view) {
+ init: function (filters, parent) {
// If all filters are group_by and we're not initializing a GroupbyGroup,
// create a GroupbyGroup instead of the current FilterGroup
if (!(this instanceof instance.web.search.GroupbyGroup) &&
_(filters).all(function (f) {
- return f.attrs.context && f.attrs.context.group_by; })) {
- return new instance.web.search.GroupbyGroup(filters, view);
+ if (!f.attrs.context) { return false; }
+ var c = instance.web.pyeval.eval('context', f.attrs.context);
+ return !_.isEmpty(c.group_by);})) {
+ return new instance.web.search.GroupbyGroup(filters, parent);
}
- this._super(view);
+ this._super(parent);
this.filters = filters;
this.view.query.on('add remove change reset', this.proxy('search_change'));
},
@@ -1096,6 +1137,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
var self = this;
item = item.toLowerCase();
var facet_values = _(this.filters).chain()
+ .filter(function (filter) { return filter.visible(); })
.filter(function (filter) {
var at = {
string: filter.attrs.string || '',
@@ -1122,8 +1164,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
icon: 'w',
completion_label: _lt("Group by: %s"),
- init: function (filters, view) {
- this._super(filters, view);
+ init: function (filters, parent) {
+ this._super(filters, parent);
// Not flanders: facet unicity is handled through the
// (category, field) pair of facet attributes. This is all well and
// good for regular filter groups where a group matches a facet, but for
@@ -1131,8 +1173,8 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
// view which proxies to the first GroupbyGroup, so it can be used
// for every GroupbyGroup and still provides the various methods needed
// by the search view. Use weirdo name to avoid risks of conflicts
- if (!this.getParent()._s_groupby) {
- this.getParent()._s_groupby = {
+ if (!this.view._s_groupby) {
+ this.view._s_groupby = {
help: "See GroupbyGroup#init",
get_context: this.proxy('get_context'),
get_domain: this.proxy('get_domain'),
@@ -1141,14 +1183,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
}
},
match_facet: function (facet) {
- return facet.get('field') === this.getParent()._s_groupby;
+ return facet.get('field') === this.view._s_groupby;
},
make_facet: function (values) {
return {
category: _t("GroupBy"),
icon: this.icon,
values: values,
- field: this.getParent()._s_groupby
+ field: this.view._s_groupby
};
}
});
@@ -1166,10 +1208,10 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc
* @extends instance.web.search.Input
*
* @param node
- * @param view
+ * @param parent
*/
- init: function (node, view) {
- this._super(view);
+ init: function (node, parent) {
+ this._super(parent);
this.load_attrs(node.attrs);
},
facet_for: function () { return $.when(null); },
@@ -1185,10 +1227,10 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
*
* @param view_section
* @param field
- * @param view
+ * @param parent
*/
- init: function (view_section, field, view) {
- this._super(view);
+ init: function (view_section, field, parent) {
+ this._super(parent);
this.load_attrs(_.extend({}, field, view_section.attrs));
},
facet_for: function (value) {
@@ -1228,7 +1270,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
*
* @param {String} name the field's name
* @param {String} operator the field's operator (either attribute-specified or default operator for the field
- * @param {Number|String} value parsed value for the field
+ * @param {Number|String} facet parsed value for the field
* @returns {Array} domain to include in the resulting search
*/
make_domain: function (name, operator, facet) {
@@ -1460,8 +1502,8 @@ instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @le
});
instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
default_operator: {},
- init: function (view_section, field, view) {
- this._super(view_section, field, view);
+ init: function (view_section, field, parent) {
+ this._super(view_section, field, parent);
this.model = new instance.web.Model(this.attrs.relation);
},
complete: function (needle) {
@@ -1673,6 +1715,13 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
if (!_.isEmpty(results.group_by)) {
results.context.group_by = results.group_by;
}
+ // Don't save user_context keys in the custom filter, otherwise end
+ // up with e.g. wrong uid or lang stored *and used in subsequent
+ // reqs*
+ var ctx = results.context;
+ _(_.keys(instance.session.user_context)).each(function (key) {
+ delete ctx[key];
+ });
var filter = {
name: $name.val(),
user_id: private_filter ? instance.session.uid : false,
@@ -1702,22 +1751,28 @@ instance.web.search.Filters = instance.web.search.Input.extend({
var running_count = 0;
// get total filters count
var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
- var filters_count = _(this.view.controls).chain()
+ var visible_filters = _(this.view.controls).chain().reject(function (group) {
+ return _(_(group.children).filter(is_group)).isEmpty()
+ || group.modifiers.invisible;
+ });
+ var filters_count = visible_filters
+ .pluck('children')
.flatten()
.filter(is_group)
.map(function (i) { return i.filters.length; })
.sum()
.value();
- var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) {
- var filters = _(inputs).filter(is_group);
- return {
- name: group === 'null' ? "q " + _t("Filters") : "w " + group,
- filters: filters,
- length: _(filters).chain().map(function (i) {
- return i.filters.length; }).sum().value()
- };
- });
+ var col1 = [], col2 = visible_filters.map(function (group) {
+ var filters = _(group.children).filter(is_group);
+ return {
+ name: _.str.sprintf("%s %s",
+ group.icon, group.name),
+ filters: filters,
+ length: _(filters).chain().map(function (i) {
+ return i.filters.length; }).sum().value()
+ };
+ }).value();
while (col2.length) {
// col1 + group should be smaller than col2 + group
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index ccfc17a0288..cd27bd68b27 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -810,7 +810,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
try {
var form_invalid = false,
values = {},
- first_invalid_field = null;
+ first_invalid_field = null,
+ readonly_values = {};
for (var f in self.fields) {
if (!self.fields.hasOwnProperty(f)) { continue; }
f = self.fields[f];
@@ -819,11 +820,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (!first_invalid_field) {
first_invalid_field = f;
}
- } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) {
+ } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
// Special case 'id' field, do not save this field
// on 'create' : save all non readonly fields
// on 'edit' : save non readonly modified fields
- values[f.name] = f.get_value();
+ if (!f.get("readonly")) {
+ values[f.name] = f.get_value();
+ } else {
+ readonly_values[f.name] = f.get_value();
+ }
}
}
if (form_invalid) {
@@ -836,7 +841,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var save_deferral;
if (!self.datarecord.id) {
// Creation save
- save_deferral = self.dataset.create(values).then(function(r) {
+ save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
return self.record_created(r, prepend_on_create);
}, null);
} else if (_.isEmpty(values)) {
@@ -844,7 +849,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
save_deferral = $.Deferred().resolve({}).promise();
} else {
// Write save
- save_deferral = self.dataset.write(self.datarecord.id, values, {}).then(function(r) {
+ save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
return self.record_saved(r);
}, null);
}
@@ -1887,10 +1892,11 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
}
- this.view.on('view_content_has_changed', this, this.check_disable);
},
start: function() {
this._super.apply(this, arguments);
+ this.view.on('view_content_has_changed', this, this.check_disable);
+ this.check_disable();
this.$el.click(this.on_click);
if (this.node.attrs.help || instance.session.debug) {
this.do_attach_tooltip();
@@ -1916,18 +1922,20 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
modal: true,
buttons: [
{text: _t("Cancel"), click: function() {
- def.resolve();
$(this).dialog("close");
}
},
{text: _t("Ok"), click: function() {
- self.on_confirmed().done(function() {
- def.resolve();
+ var self2 = this;
+ self.on_confirmed().always(function() {
+ $(self2).dialog("close");
});
- $(this).dialog("close");
}
}
- ]
+ ],
+ beforeClose: function() {
+ def.resolve();
+ },
});
return def.promise();
} else {
@@ -2218,6 +2226,18 @@ instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.Reini
},
});
+/**
+ Some hack to make placeholders work in ie9.
+*/
+if ($.browser.msie && $.browser.version === "9.0") {
+ document.addEventListener("DOMNodeInserted",function(event){
+ var nodename = event.target.nodeName.toLowerCase();
+ if ( nodename === "input" || nodename == "textarea" ) {
+ $(event.target).placeholder();
+ }
+ });
+}
+
instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
template: 'FieldChar',
widget_class: 'oe_form_field_char',
@@ -2382,7 +2402,6 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
type_of_date: "datetime",
events: {
'change .oe_datepicker_master': 'change_datetime',
- 'dragstart img.oe_datepicker_trigger': function () { return false; },
},
init: function(parent) {
this._super(parent);
@@ -2546,42 +2565,45 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
},
'change textarea': 'store_dom_value',
},
- init: function (field_manager, node) {
- this._super(field_manager, node);
- },
initialize_content: function() {
var self = this;
- this.$textarea = this.$el.find('textarea');
- this.auto_sized = false;
- this.default_height = this.$textarea.css('height');
- if (this.get("effective_readonly")) {
- this.$textarea.attr('disabled', 'disabled');
+ if (! this.get("effective_readonly")) {
+ this.$textarea = this.$el.find('textarea');
+ this.auto_sized = false;
+ this.default_height = this.$textarea.css('height');
+ if (this.get("effective_readonly")) {
+ this.$textarea.attr('disabled', 'disabled');
+ }
+ this.setupFocus(this.$textarea);
+ } else {
+ this.$textarea = undefined;
}
- this.setupFocus(this.$textarea);
},
commit_value: function () {
- this.store_dom_value();
+ if (! this.get("effective_readonly") && this.$textarea) {
+ this.store_dom_value();
+ }
return this._super();
},
store_dom_value: function () {
- if (!this.get('effective_readonly') && this.$('textarea').length) {
- this.internal_set_value(
- instance.web.parse_value(
- this.$textarea.val(),
- this));
- }
+ this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
},
render_value: function() {
- var show_value = instance.web.format_value(this.get('value'), this, '');
- if (show_value === '') {
- this.$textarea.css('height', parseInt(this.default_height)+"px");
- }
- this.$textarea.val(show_value);
- if (! this.auto_sized) {
- this.auto_sized = true;
- this.$textarea.autosize();
+ if (! this.get("effective_readonly")) {
+ var show_value = instance.web.format_value(this.get('value'), this, '');
+ if (show_value === '') {
+ this.$textarea.css('height', parseInt(this.default_height)+"px");
+ }
+ this.$textarea.val(show_value);
+ if (! this.auto_sized) {
+ this.auto_sized = true;
+ this.$textarea.autosize();
+ } else {
+ this.$textarea.trigger("autosize");
+ }
} else {
- this.$textarea.trigger("autosize");
+ var txt = this.get("value");
+ this.$(".oe_form_text_content").text(txt);
}
},
is_syntax_valid: function() {
@@ -2599,14 +2621,18 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function($el) {
- this.$textarea[0].focus();
+ if (!this.get("effective_readonly") && this.$textarea) {
+ this.$textarea[0].focus();
+ }
},
set_dimensions: function (height, width) {
this._super(height, width);
- this.$textarea.css({
- width: width,
- minHeight: height
- });
+ if (!this.get("effective_readonly") && this.$textarea) {
+ this.$textarea.css({
+ width: width,
+ minHeight: height
+ });
+ }
},
});
@@ -2970,8 +2996,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
e.stopPropagation();
}
},
- 'dragstart .oe_m2o_drop_down_button img': function () { return false; },
- 'dragstart .oe_m2o_cm_button': function () { return false; }
},
init: function(field_manager, node) {
this._super(field_manager, node);
@@ -3002,6 +3026,26 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
if (!this.get("effective_readonly"))
this.render_editable();
},
+ destroy_content: function () {
+ if (this.$drop_down) {
+ this.$drop_down.off('click');
+ delete this.$drop_down;
+ }
+ if (this.$input) {
+ this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
+ this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
+ 'focus focusout change keydown');
+ delete this.$input;
+ }
+ if (this.$follow_button) {
+ this.$follow_button.off('blur focus click');
+ delete this.$follow_button;
+ }
+ },
+ destroy: function () {
+ this.destroy_content();
+ return this._super();
+ },
init_error_displayer: function() {
// nothing
},
@@ -3263,7 +3307,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
},
focus: function () {
if (!this.get('effective_readonly')) {
- this.$input[0].focus();
+ this.$input && this.$input[0].focus();
}
},
_quick_create: function() {
@@ -3682,8 +3726,8 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
var pop = new instance.web.form.FormOpenPopup(this);
pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
title: _t("Open: ") + self.o2m.string,
- create_function: function(data) {
- return self.o2m.dataset.create(data).done(function(r) {
+ create_function: function(data, options) {
+ return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", r);
});
@@ -3742,8 +3786,12 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
this.o2m.trigger_on_change();
},
is_valid: function () {
- var form = this.editor.form;
-
+ var editor = this.editor;
+ var form = editor.form;
+ // If no edition is pending, the listview can not be invalid (?)
+ if (!editor.record) {
+ return true
+ }
// If the form has not been modified, the view can only be valid
// NB: is_dirty will also be set on defaults/onchanges/whatever?
// oe_form_dirty seems to only be set on actual user actions
@@ -3773,11 +3821,11 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
title: _t("Create: ") + self.o2m.string,
initial_view: "form",
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
- create_function: function(data, callback, error_callback) {
- return self.o2m.dataset.create(data).done(function(r) {
+ create_function: function(data, options) {
+ return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", r);
- }).done(callback).fail(error_callback);
+ });
},
read_function: function() {
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
@@ -3990,6 +4038,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
if (this.get("effective_readonly"))
return;
var self = this;
+ var ignore_blur = false;
self.$text = this.$("textarea");
self.$text.textext({
plugins : 'tags arrow autocomplete',
@@ -4008,6 +4057,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
if (data.id) {
self.add_id(data.id);
} else {
+ ignore_blur = true;
data.action();
}
},
@@ -4058,10 +4108,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
self.$text
.focusin(function () {
self.trigger('focused');
+ ignore_blur = false;
})
.focusout(function() {
self.$text.trigger("setInputData", "");
- self.trigger('blurred');
+ if (!ignore_blur) {
+ self.trigger('blurred');
+ }
}).keydown(function(e) {
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
self.$text.textext()[0].autocomplete().selectFromDropdown();
@@ -4268,6 +4321,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in
});
}
},
+ is_action_enabled: function () { return true; },
});
instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
@@ -4502,9 +4556,9 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
this.created_elements = [];
this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
this.dataset.read_function = this.options.read_function;
- this.dataset.create_function = function(data, sup) {
+ this.dataset.create_function = function(data, options, sup) {
var fct = self.options.create_function || sup;
- return fct.call(this, data).done(function(r) {
+ return fct.call(this, data, options).done(function(r) {
self.trigger('create_completed saved', r);
self.created_elements.push(r);
});
@@ -5216,11 +5270,11 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
this.calc_domain();
this.on("change:value", this, this.get_selection);
this.on("change:evaluated_selection_domain", this, this.get_selection);
- this.get_selection();
this.on("change:selection", this, function() {
this.selection = this.get("selection");
this.render_value();
});
+ this.get_selection();
if (this.options.clickable) {
this.$el.on('click','li',this.on_click_stage);
}
@@ -5339,10 +5393,10 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
});
},
parse_value: function(val, def) {
- return instance.web.parse_value(val, {type: "float"}, def);
+ return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
},
format_value: function(val, def) {
- return instance.web.format_value(val, {type: "float"}, def);
+ return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
},
});
diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js
index d23bedf1d1c..b69ec6dbdca 100644
--- a/addons/web/static/src/js/view_list.js
+++ b/addons/web/static/src/js/view_list.js
@@ -321,9 +321,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
.appendTo($this.empty())
.click(function (e) {e.stopPropagation();})
.append('' +
- '' +
'' +
'' +
+ '' +
'')
.change(function () {
var val = parseInt($select.val(), 10);
diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js
index f1bd7c4325e..4e042b60c5d 100644
--- a/addons/web/static/src/js/view_list_editable.js
+++ b/addons/web/static/src/js/view_list_editable.js
@@ -96,7 +96,8 @@ openerp.web.list_editable = function (instance) {
});
},
editable: function () {
- return !this.options.disable_editable_mode
+ return !this.grouped
+ && !this.options.disable_editable_mode
&& (this.fields_view.arch.attrs.editable
|| this._context_editable
|| this.options.editable);
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index f5638004b47..1ba65f7ec18 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -1446,11 +1446,11 @@ instance.web.View = instance.web.Widget.extend({
* Performs a fields_view_get and apply postprocessing.
* return a {$.Deferred} resolved with the fvg
*
- * @param {Object} [args]
+ * @param {Object} args
* @param {String|Object} args.model instance.web.Model instance or string repr of the model
- * @param {null|Object} args.context context if args.model is a string
- * @param {null|Number} args.view_id id of the view to be loaded, default view if null
- * @param {null|String} args.view_type type of view to be loaded if view_id is null
+ * @param {Object} [args.context] context if args.model is a string
+ * @param {Number} [args.view_id] id of the view to be loaded, default view if null
+ * @param {String} [args.view_type] type of view to be loaded if view_id is null
* @param {Boolean} [args.toolbar=false] get the toolbar definition
*/
instance.web.fields_view_get = function(args) {
@@ -1555,12 +1555,17 @@ instance.web.xml_to_str = function(node) {
} else {
throw new Error(_t("Could not serialize XML"));
}
- // Browsers won't deal with self closing tags except br, hr, input, ...
- // http://stackoverflow.com/questions/97522/what-are-all-the-valid-self-closing-elements-in-xhtml-as-implemented-by-the-maj
- //
+ // Browsers won't deal with self closing tags except void elements:
+ // http://www.w3.org/TR/html-markup/syntax.html
+ var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
+
// The following regex is a bit naive but it's ok for the xmlserializer output
str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
- return "<" + tag + attrs + ">" + tag + ">";
+ if (void_elements.indexOf(tag) < 0) {
+ return "<" + tag + attrs + ">" + tag + ">";
+ } else {
+ return match;
+ }
});
return str;
};
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 26b1cc09019..70d74d05747 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -40,7 +40,7 @@
@@ -118,7 +118,8 @@
you will be able to install your first application.
- By default, the master password is 'admin' if you did not changed it.
+ By default, the master password is 'admin'. This password
+ is required to created, delete dump or restore databases.