summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorXavier Morel <xmo@openerp.com>2013-03-13 10:49:09 +0100
committerXavier Morel <xmo@openerp.com>2013-03-13 10:49:09 +0100
commit94c2c42be6fc0bdf55421b3fe4c1d4c7db1f4bed (patch)
tree395948faff8d6874a5094dc632b238a8eff32b4e /addons
parenta67b8721848083a9336317139e508c338d925f73 (diff)
parentf79f6a5a25a0708e662f2deed4a54c704c6872b5 (diff)
[MERGE] from 7.0
bzr revid: xmo@openerp.com-20130313094909-u21ee88l2lak9p2x
Diffstat (limited to 'addons')
-rw-r--r--addons/web/__openerp__.py1
-rw-r--r--addons/web/controllers/main.py65
-rw-r--r--addons/web/doc/search_view.rst11
-rw-r--r--addons/web/http.py8
-rw-r--r--addons/web/static/lib/datejs/globalization/ar-SY.js30
-rwxr-xr-xaddons/web/static/lib/jquery.placeholder/jquery.placeholder.js157
-rw-r--r--addons/web/static/src/css/base.sass13
-rw-r--r--addons/web/static/src/js/chrome.js97
-rw-r--r--addons/web/static/src/js/corelib.js18
-rw-r--r--addons/web/static/src/js/coresetup.js4
-rw-r--r--addons/web/static/src/js/data.js47
-rw-r--r--addons/web/static/src/js/search.js189
-rw-r--r--addons/web/static/src/js/view_form.js170
-rw-r--r--addons/web/static/src/js/view_list.js2
-rw-r--r--addons/web/static/src/js/view_list_editable.js3
-rw-r--r--addons/web/static/src/js/views.js21
-rw-r--r--addons/web/static/src/xml/base.xml62
-rw-r--r--addons/web/static/test/search.js301
-rw-r--r--addons/web_graph/static/src/css/graph.css4
-rw-r--r--addons/web_graph/static/src/js/graph.js2
-rw-r--r--addons/web_kanban/static/src/css/kanban.css24
-rw-r--r--addons/web_kanban/static/src/css/kanban.sass4
22 files changed, 958 insertions, 275 deletions
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 "<html><head><script>window.location = '%s#' + location.hash;</script></head></html>" % 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('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
css = "\n ".join('<link rel="stylesheet" href="%s">' % 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
+ <openerp.web.search.Input.visible>`, 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 <openerp.web.search.Input.visible>` 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 = $('<input>').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<len; ++i) {
var record = this.cache[i];
// if record we call the button upon is in the cache
@@ -871,8 +887,15 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
break;
}
}
+ },
+ call_button: function (method, args) {
+ this.evict_record(args[0][0]);
return this._super(method, args);
},
+ exec_workflow: function (id, signal) {
+ this.evict_record(id);
+ return this._super(id, signal);
+ },
alter_ids: function(n_ids) {
this._super(n_ids);
this.trigger("dataset_changed", n_ids);
@@ -903,9 +926,9 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
return this._super.apply(this, arguments);
}
},
- create: function(data) {
+ create: function(data, options) {
if (this.create_function) {
- return this.create_function(data, this._super);
+ return this.create_function(data, options, this._super);
} else {
return this._super.apply(this, arguments);
}
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
index edbec1626b0..33e1fba3813 100644
--- a/addons/web/static/src/js/search.js
+++ b/addons/web/static/src/js/search.js
@@ -324,7 +324,10 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
e.preventDefault();
break;
}
- }
+ },
+ 'autocompleteopen': function () {
+ this.$el.autocomplete('widget').css('z-index', 3);
+ },
},
/**
* @constructs instance.web.SearchView
@@ -352,7 +355,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
this.has_defaults = !_.isEmpty(this.defaults);
this.inputs = [];
- this.controls = {};
+ this.controls = [];
this.headless = this.options.hidden && !this.has_defaults;
@@ -492,6 +495,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*/
complete_global_search: function (req, resp) {
$.when.apply(null, _(this.inputs).chain()
+ .filter(function (input) { return input.visible(); })
.invoke('complete', req.term)
.value()).then(function () {
resp(_(_(arguments).compact()).flatten(true));
@@ -580,18 +584,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*
* @param {Array} items a list of nodes to convert to widgets
* @param {Object} fields a mapping of field names to (ORM) field attributes
- * @param {String} [group_name] name of the group to put the new controls in
+ * @param {Object} [group] group to put the new controls in
*/
- make_widgets: function (items, fields, group_name) {
- group_name = group_name || null;
- if (!(group_name in this.controls)) {
- this.controls[group_name] = [];
+ make_widgets: function (items, fields, group) {
+ if (!group) {
+ group = new instance.web.search.Group(
+ this, 'q', {attrs: {string: _t("Filters")}});
}
- var self = this, group = this.controls[group_name];
+ var self = this;
var filters = [];
_.each(items, function (item) {
if (filters.length && item.tag !== 'filter') {
- group.push(new instance.web.search.FilterGroup(filters, this));
+ group.push(new instance.web.search.FilterGroup(filters, group));
filters = [];
}
@@ -599,15 +603,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
case 'separator': case 'newline':
break;
case 'filter':
- filters.push(new instance.web.search.Filter(item, this));
+ filters.push(new instance.web.search.Filter(item, group));
break;
case 'group':
- self.make_widgets(item.children, fields, item.attrs.string);
+ self.make_widgets(item.children, fields,
+ new instance.web.search.Group(group, 'w', item));
break;
case 'field':
- group.push(this.make_field(item, fields[item['attrs'].name]));
+ var field = this.make_field(
+ item, fields[item['attrs'].name], group);
+ group.push(field);
// filters
- self.make_widgets(item.children, fields, group_name);
+ self.make_widgets(item.children, fields, group);
break;
}
}, this);
@@ -622,12 +629,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*
* @param {Object} item fields_view_get node for the field
* @param {Object} field fields_get result for the field
+ * @param {Object} [parent]
* @returns instance.web.search.Field
*/
- make_field: function (item, field) {
+ make_field: function (item, field, parent) {
var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]);
if(obj) {
- return new (obj) (item, field, this);
+ return new (obj) (item, field, parent || this);
} else {
console.group('Unknown field type ' + field.type);
console.error('View node', item);
@@ -862,13 +870,18 @@ instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web
* @constructs instance.web.search.Widget
* @extends instance.web.Widget
*
- * @param view the ancestor view of this widget
+ * @param parent parent of this widget
*/
- init: function (view) {
- this._super(view);
- this.view = view;
+ init: function (parent) {
+ this._super(parent);
+ var ancestor = parent;
+ do {
+ this.view = ancestor;
+ } while (!(ancestor instanceof instance.web.SearchView)
+ && (ancestor = (ancestor.getParent && ancestor.getParent())));
}
});
+
instance.web.search.add_expand_listener = function($root) {
$root.find('a.searchview_group_string').click(function (e) {
$root.toggleClass('folded expanded');
@@ -877,13 +890,24 @@ instance.web.search.add_expand_listener = function($root) {
});
};
instance.web.search.Group = instance.web.search.Widget.extend({
- template: 'SearchView.group',
- init: function (view_section, view, fields) {
- this._super(view);
- this.attrs = view_section.attrs;
- this.lines = view.make_widgets(
- view_section.children, fields);
- }
+ init: function (parent, icon, node) {
+ this._super(parent);
+ var attrs = node.attrs;
+ this.modifiers = attrs.modifiers =
+ attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
+ this.attrs = attrs;
+ this.icon = icon;
+ this.name = attrs.string;
+ this.children = [];
+
+ this.view.controls.push(this);
+ },
+ push: function (input) {
+ this.children.push(input);
+ },
+ visible: function () {
+ return !this.modifiers.invisible;
+ },
});
instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{
@@ -892,12 +916,12 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
* @constructs instance.web.search.Input
* @extends instance.web.search.Widget
*
- * @param view
+ * @param parent
*/
- init: function (view) {
- this._super(view);
+ init: function (parent) {
+ this._super(parent);
+ this.load_attrs({});
this.view.inputs.push(this);
- this.style = undefined;
},
/**
* Fetch auto-completion values for the widget.
@@ -945,15 +969,30 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
"get_domain not implemented for widget " + this.attrs.type);
},
load_attrs: function (attrs) {
- if (attrs.modifiers) {
- attrs.modifiers = JSON.parse(attrs.modifiers);
- attrs.invisible = attrs.modifiers.invisible || false;
- if (attrs.invisible) {
- this.style = 'display: none;'
+ attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
+ this.attrs = attrs;
+ },
+ /**
+ * Returns whether the input is "visible". The default behavior is to
+ * query the ``modifiers.invisible`` flag on the input's description or
+ * view node.
+ *
+ * @returns {Boolean}
+ */
+ visible: function () {
+ if (this.attrs.modifiers.invisible) {
+ return false;
+ }
+ var parent = this;
+ while ((parent = parent.getParent()) &&
+ ( (parent instanceof instance.web.search.Group)
+ || (parent instanceof instance.web.search.Input))) {
+ if (!parent.visible()) {
+ return false;
}
}
- this.attrs = attrs;
- }
+ return true;
+ },
});
instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
template: 'SearchView.filters',
@@ -967,17 +1006,19 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
* @extends instance.web.search.Input
*
* @param {Array<instance.web.search.Filter>} 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<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' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + 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("<span class='oe_i'>%s</span> %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('<option value="80">80</option>' +
- '<option value="100">100</option>' +
'<option value="200">200</option>' +
'<option value="500">500</option>' +
+ '<option value="2000">2000</option>' +
'<option value="NaN">' + _t("Unlimited") + '</option>')
.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 @@
<td>
<p>
<t t-js="d">
- var message = d.message ? d.message : d.error.data.message;
+ var message = d.message ? d.message : d.error.data.fault_code;
d.html_error = context.engine.tools.html_escape(message)
.replace(/\n/g, '<br/>');
</t>
@@ -71,9 +71,9 @@
</div>
<ul>
<li>Username</li>
- <li><input name="login" type="text" t-att-value="widget.selected_login || ''" autofocus="autofocus"/></li>
+ <li><input name="login" type="text" value="" autofocus="autofocus"/></li>
<li>Password</li>
- <li><input name="password" type="password" t-att-value="widget.selected_password || ''"/></li>
+ <li><input name="password" type="password" value=""/></li>
<li><button name="submit">Log in</button></li>
</ul>
</form>
@@ -118,7 +118,8 @@
you will be able to install your first application.
</p>
<p class="oe_grey" style="margin: 10px">
- 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.
</p>
<table class="db_option_table" style="margin: 10px">
<tr>
@@ -129,7 +130,9 @@
</tr>
<tr>
<td><label for="db_name">Select a database name:</label></td>
- <td><input type="text" name="db_name" class="required" matches="^[a-zA-Z0-9][a-zA-Z0-9_-]+$" autofocus="true" placeholder="e.g. mycompany"/></td>
+ <td>
+ <input type="text" name="db_name" class="required" matches="^[a-zA-Z0-9][a-zA-Z0-9_-]+$" autofocus="true" placeholder="e.g. mycompany"/>
+ </td>
</tr>
<tr>
<td><label for="demo_data">Load demonstration data:</label></td>
@@ -1047,17 +1050,22 @@
</t>
<t t-name="FieldText">
<div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style">
- <textarea rows="6"
- t-att-name="widget.name"
- class="field_text"
- t-att-tabindex="widget.node.attrs.tabindex"
- t-att-autofocus="widget.node.attrs.autofocus"
- t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
- t-att-maxlength="widget.field.size"
- ></textarea><img class="oe_field_translate oe_input_icon"
- t-if="widget.field.translate and !widget.get('effective_readonly')"
- t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
- />
+ <t t-if="!widget.get('effective_readonly')">
+ <textarea rows="6"
+ t-att-name="widget.name"
+ class="field_text"
+ t-att-tabindex="widget.node.attrs.tabindex"
+ t-att-autofocus="widget.node.attrs.autofocus"
+ t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
+ t-att-maxlength="widget.field.size"
+ ></textarea><img class="oe_field_translate oe_input_icon"
+ t-if="widget.field.translate and !widget.get('effective_readonly')"
+ t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
+ />
+ </t>
+ <t t-if="widget.get('effective_readonly')">
+ <span class="oe_form_text_content"></span>
+ </t>
</div>
</t>
<t t-name="FieldTextHtml">
@@ -1076,8 +1084,9 @@
t-att-name="widget.name"
t-att-placeholder="placeholder"
class="oe_datepicker_master"
- /><img class="oe_input_icon oe_datepicker_trigger" t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
- title="Select date" width="16" height="16" border="0"/>
+ /><img class="oe_input_icon oe_datepicker_trigger" draggable="false"
+ t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
+ title="Select date" width="16" height="16" border="0"/>
</span>
</t>
<t t-name="FieldDate">
@@ -1111,7 +1120,7 @@
</t>
<t t-if="!widget.get('effective_readonly')">
<a t-if="! widget.options.no_open" href="#" tabindex="-1"
- class="oe_m2o_cm_button oe_e">/</a>
+ class="oe_m2o_cm_button oe_e" draggable="false">/</a>
<div>
<input type="text"
t-att-id="widget.id_for_label"
@@ -1120,7 +1129,7 @@
t-att-placeholder="widget.node.attrs.placeholder"
/>
<span class="oe_m2o_drop_down_button">
- <img t-att-src='_s + "/web/static/src/img/down-arrow.png"'/>
+ <img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/>
</span>
</div>
</t>
@@ -1515,7 +1524,7 @@
<t t-esc="attrs.string"/>
</button>
<ul t-name="SearchView.filters">
- <li t-foreach="widget.filters" t-as="filter"
+ <li t-foreach="widget.filters" t-as="filter" t-if="filter.visible()"
t-att-title="filter.attrs.string ? filter.attrs.help : undefined">
<t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
</li>
@@ -1603,15 +1612,6 @@
</div>
</div>
</t>
-<t t-name="SearchView.group">
- <t t-call="SearchView.util.expand">
- <t t-set="expand" t-value="attrs.expand"/>
- <t t-set="label" t-value="attrs.string"/>
- <t t-set="content">
- <t t-call="SearchView.render_lines"/>
- </t>
- </t>
-</t>
<div t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section">
</div>
@@ -1842,7 +1842,7 @@
<tr>
<td t-foreach="records[0]" t-as="column">
<input class="sel_fields" placeholder="--- Don't Import ---"/><span class="oe_m2o_drop_down_button">
- <img t-att-src='_s + "/web/static/src/img/down-arrow.png"' /></span>
+ <img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/></span>
</td>
</tr>
<tr t-foreach="records" t-as="record" class="oe_import_grid-row">
diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js
index 904af63f590..5c02d1f16db 100644
--- a/addons/web/static/test/search.js
+++ b/addons/web/static/test/search.js
@@ -1,4 +1,4 @@
-openerp.testing.section('query', {
+openerp.testing.section('search.query', {
dependencies: ['web.search']
}, function (test) {
test('Adding a facet to the query creates a facet and a value', function (instance) {
@@ -180,7 +180,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
});
return view;
};
-openerp.testing.section('defaults', {
+openerp.testing.section('search.defaults', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
@@ -362,7 +362,7 @@ openerp.testing.section('defaults', {
"should not accept multiple default values");
})
});
-openerp.testing.section('completions', {
+openerp.testing.section('search.completions', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@@ -615,7 +615,7 @@ openerp.testing.section('completions', {
return f.complete("bob");
});
});
-openerp.testing.section('search-serialization', {
+openerp.testing.section('search.serialization', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@@ -855,11 +855,11 @@ openerp.testing.section('search-serialization', {
test('FilterGroup', {asserts: 6}, function (instance) {
var view = {inputs: [], query: {on: function () {}}};
var filter_a = new instance.web.search.Filter(
- {attrs: {name: 'a', context: 'c1', domain: 'd1'}}, view);
+ {attrs: {name: 'a', context: '{"c1": True}', domain: 'd1'}}, view);
var filter_b = new instance.web.search.Filter(
- {attrs: {name: 'b', context: 'c2', domain: 'd2'}}, view);
+ {attrs: {name: 'b', context: '{"c2": True}', domain: 'd2'}}, view);
var filter_c = new instance.web.search.Filter(
- {attrs: {name: 'c', context: 'c3', domain: 'd3'}}, view);
+ {attrs: {name: 'c', context: '{"c3": True}', domain: 'd3'}}, view);
var group = new instance.web.search.FilterGroup(
[filter_a, filter_b, filter_c], view);
return group.facet_for_defaults({a: true, c: true})
@@ -880,7 +880,7 @@ openerp.testing.section('search-serialization', {
equal(context.__ref, 'compound_context',
"context should be compound");
deepEqual(context.__contexts, [
- 'c1', 'c3'
+ '{"c1": True}', '{"c3": True}'
], "context should merge all filter contexts");
ok(!context.get_eval_context(), "context should have no evaluation context");
});
@@ -922,7 +922,7 @@ openerp.testing.section('search-serialization', {
return $.when(t1, t2);
});
});
-openerp.testing.section('removal', {
+openerp.testing.section('search.removal', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@@ -945,7 +945,7 @@ openerp.testing.section('removal', {
});
});
});
-openerp.testing.section('drawer', {
+openerp.testing.section('search.drawer', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@@ -961,7 +961,7 @@ openerp.testing.section('drawer', {
});
});
});
-openerp.testing.section('filters', {
+openerp.testing.section('search.filters', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
@@ -1046,7 +1046,104 @@ openerp.testing.section('filters', {
});
});
});
-openerp.testing.section('saved_filters', {
+openerp.testing.section('search.groupby', {
+ dependencies: ['web.search'],
+ rpc: 'mock',
+ templates: true,
+}, function (test) {
+ test('basic', {
+ asserts: 7,
+ setup: function (instance, $s, mock) {
+ mock('dummy.model:fields_view_get', function () {
+ return {
+ type: 'search',
+ fields: {},
+ arch: [
+ '<search>',
+ '<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
+ '<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
+ '<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
+ '</search>'
+ ].join(''),
+ }
+ });
+ }
+ }, function (instance, $fix) {
+ var view = makeSearchView(instance);
+ return view.appendTo($fix)
+ .done(function () {
+ // 3 filters, 1 filtergroup group, 1 custom filter, 1 advanced, 1 Filters
+ equal(view.inputs.length, 7,
+ 'should have 7 inputs total');
+ var group = _.find(view.inputs, function (f) {
+ return f instanceof instance.web.search.GroupbyGroup
+ });
+ ok(group, "should have a GroupbyGroup input");
+ strictEqual(group.getParent(), view,
+ "group's parent should be view");
+
+ group.toggle(group.filters[0]);
+ group.toggle(group.filters[2]);
+
+ var results = view.build_search_data();
+ deepEqual(results.errors, [], "should have no errors");
+ deepEqual(results.domains, [], "should have no domain");
+ deepEqual(results.contexts, [
+ new instance.web.CompoundContext(
+ "{'group_by': 'foo'}", "{'group_by': 'baz'}")
+ ], "should have compound contexts");
+ deepEqual(results.groupbys, [
+ "{'group_by': 'foo'}",
+ "{'group_by': 'baz'}"
+ ], "should have sequence of contexts")
+ });
+ });
+ test('unified multiple groupby groups', {
+ asserts: 4,
+ setup: function (instance, $s, mock) {
+ mock('dummy.model:fields_view_get', function () {
+ return {
+ type: 'search',
+ fields: {},
+ arch: [
+ '<search>',
+ '<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
+ '<separator/>',
+ '<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
+ '<separator/>',
+ '<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
+ '</search>'
+ ].join(''),
+ }
+ });
+ }
+ }, function (instance, $fix) {
+ var view = makeSearchView(instance);
+ return view.appendTo($fix)
+ .done(function () {
+ // 3 filters, 3 filtergroups, 1 custom filter, 1 advanced, 1 Filters
+ equal(view.inputs.length, 9, "should have 9 inputs total");
+
+ var groups = _.filter(view.inputs, function (f) {
+ return f instanceof instance.web.search.GroupbyGroup
+ });
+ equal(groups.length, 3, "should have 3 GroupbyGroups");
+
+ groups[0].toggle(groups[0].filters[0]);
+ groups[2].toggle(groups[2].filters[0]);
+ equal(view.query.length, 1,
+ "should have unified groupby groups in single facet");
+ deepEqual(view.build_search_data(), {
+ errors: [],
+ domains: [],
+ contexts: [new instance.web.CompoundContext(
+ "{'group_by': 'foo'}", "{'group_by': 'baz'}")],
+ groupbys: [ "{'group_by': 'foo'}", "{'group_by': 'baz'}" ],
+ }, "should only have contexts & groupbys in search data");
+ });
+ });
+});
+openerp.testing.section('search.filters.saved', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@@ -1127,8 +1224,30 @@ openerp.testing.section('saved_filters', {
"should have selected second filter");
});
});
+ test('creation', {asserts: 2}, function (instance, $fix, mock) {
+ // force a user context
+ instance.session.user_context = {foo: 'bar'};
+
+ var view = makeSearchView(instance);
+ var done = $.Deferred();
+ mock('ir.filters:get_filters', function () { return []; });
+ mock('ir.filters:create_or_replace', function (args) {
+ var filter = args[0];
+ deepEqual(filter.context, {}, "should have empty context");
+ deepEqual(filter.domain, [], "should have empty domain");
+ done.resolve();
+ });
+ return view.appendTo($fix)
+ .then(function () {
+ $fix.find('.oe_searchview_custom input#oe_searchview_custom_input')
+ .text("filter name")
+ .end()
+ .find('.oe_searchview_custom button').click();
+ return done.promise();
+ });
+ });
});
-openerp.testing.section('advanced', {
+openerp.testing.section('search.advanced', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@@ -1209,3 +1328,159 @@ openerp.testing.section('advanced', {
});
// TODO: UI tests?
});
+openerp.testing.section('search.invisible', {
+ dependencies: ['web.search'],
+ rpc: 'mock',
+ templates: true,
+}, function (test) {
+ var registerTestField = function (instance, methods) {
+ instance.web.search.fields.add('test', 'instance.testing.TestWidget');
+ instance.testing = {
+ TestWidget: instance.web.search.Field.extend(methods),
+ };
+ };
+ var makeView = function (instance, mock, fields, arch, defaults) {
+ mock('ir.filters:get_filters', function () { return []; });
+ mock('test.model:fields_get', function () { return fields; });
+ mock('test.model:fields_view_get', function () {
+ return { type: 'search', fields: fields, arch: arch };
+ });
+ var ds = new instance.web.DataSet(null, 'test.model');
+ return new instance.web.SearchView(null, ds, false, defaults);
+ };
+ // Invisible fields should not auto-complete
+ test('invisible-field-no-autocomplete', {asserts: 1}, function (instance, $fix, mock) {
+ registerTestField(instance, {
+ complete: function () {
+ return $.when([{label: this.attrs.string}]);
+ },
+ });
+ var view = makeView(instance, mock, {
+ field0: {type: 'test', string: 'Field 0'},
+ field1: {type: 'test', string: 'Field 1'},
+ }, ['<search>',
+ '<field name="field0"/>',
+ '<field name="field1" modifiers="{&quot;invisible&quot;: true}"/>',
+ '</search>'].join());
+ return view.appendTo($fix)
+ .then(function () {
+ var done = $.Deferred();
+ view.complete_global_search({term: 'test'}, function (comps) {
+ done.resolve(comps);
+ });
+ return done;
+ }).then(function (completions) {
+ deepEqual(completions, [{label: 'Field 0'}],
+ "should only complete the visible field");
+ });
+ });
+ // Invisible filters should not appear in the drawer
+ test('invisible-filter-no-drawer', {asserts: 4}, function (instance, $fix, mock) {
+ var view = makeView(instance, mock, {}, [
+ '<search>',
+ '<filter string="filter 0"/>',
+ '<filter string="filter 1" modifiers="{&quot;invisible&quot;: true}"/>',
+ '</search>'].join());
+ return view.appendTo($fix)
+ .then(function () {
+ var $fs = $fix.find('.oe_searchview_filters ul');
+ strictEqual($fs.children().length,
+ 1,
+ "should only display one filter");
+ strictEqual(_.str.trim($fs.children().text()),
+ "filter 0",
+ "should only display filter 0");
+ var done = $.Deferred();
+ view.complete_global_search({term: 'filter'}, function (comps) {
+ done.resolve();
+ strictEqual(comps.length, 1, "should only complete visible filter");
+ strictEqual(comps[0].label, "Filter on: filter 0",
+ "should complete filter 0");
+ });
+ return done;
+ });
+ });
+ // Invisible filter groups should not appear in the drawer
+ // Group invisibility should be inherited by children
+ test('group-invisibility', {asserts: 6}, function (instance, $fix, mock) {
+ registerTestField(instance, {
+ complete: function () {
+ return $.when([{label: this.attrs.string}]);
+ },
+ });
+ var view = makeView(instance, mock, {
+ field0: {type: 'test', string: 'Field 0'},
+ field1: {type: 'test', string: 'Field 1'},
+ }, [
+ '<search>',
+ '<group string="Visibles">',
+ '<field name="field0"/>',
+ '<filter string="Filter 0"/>',
+ '</group>',
+ '<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
+ '<field name="field1"/>',
+ '<filter string="Filter 1"/>',
+ '</group>',
+ '</search>'
+ ].join(''));
+ return view.appendTo($fix)
+ .then(function () {
+ strictEqual($fix.find('.oe_searchview_filters h3').length,
+ 1,
+ "should only display one group");
+ strictEqual($fix.find('.oe_searchview_filters h3').text(),
+ 'w Visibles',
+ "should only display the Visibles group (and its icon char)");
+
+ var $fs = $fix.find('.oe_searchview_filters ul');
+ strictEqual($fs.children().length, 1,
+ "should only have one filter in the drawer");
+ strictEqual(_.str.trim($fs.text()), "Filter 0",
+ "should have filter 0 as sole filter");
+
+ var done = $.Deferred();
+ view.complete_global_search({term: 'filter'}, function (compls) {
+ done.resolve();
+ strictEqual(compls.length, 2,
+ "should have 2 completions");
+ deepEqual(_.pluck(compls, 'label'),
+ ['Field 0', 'Filter on: Filter 0'],
+ "should complete on field 0 and filter 0");
+ });
+ return done;
+ });
+ });
+ // Default on invisible fields should still work, for fields and filters both
+ test('invisible-defaults', {asserts: 1}, function (instance, $fix, mock) {
+ var view = makeView(instance, mock, {
+ field: {type: 'char', string: "Field"},
+ field2: {type: 'char', string: "Field 2"},
+ }, [
+ '<search>',
+ '<field name="field2"/>',
+ '<filter name="filter2" string="Filter"',
+ ' domain="[[\'qwa\', \'=\', 42]]"/>',
+ '<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
+ '<field name="field"/>',
+ '<filter name="filter" string="Filter"',
+ ' domain="[[\'whee\', \'=\', \'42\']]"/>',
+ '</group>',
+ '</search>'
+ ].join(''), {field: "foo", filter: true});
+
+ return view.appendTo($fix)
+ .then(function () {
+ deepEqual(view.build_search_data(), {
+ errors: [],
+ groupbys: [],
+ contexts: [],
+ domains: [
+ // Generated from field
+ [['field', 'ilike', 'foo']],
+ // generated from filter
+ "[['whee', '=', '42']]"
+ ],
+ }, "should yield invisible fields selected by defaults");
+ });
+ });
+});
diff --git a/addons/web_graph/static/src/css/graph.css b/addons/web_graph/static/src/css/graph.css
index a9fff591e39..2fd4b5953cf 100644
--- a/addons/web_graph/static/src/css/graph.css
+++ b/addons/web_graph/static/src/css/graph.css
@@ -1,5 +1,5 @@
.openerp a.dropdown-menu-icon {
- z-index: 10;
+ z-index: 1;
position: absolute;
color: #4c4c4c;
right: 8px;
@@ -23,7 +23,7 @@
padding: 8px;
border: 1px solid #afafb6;
background: white;
- z-index: 900;
+ z-index: 1;
min-width: 160px;
overflow-x: hidden;
-moz-border-radius: 3px;
diff --git a/addons/web_graph/static/src/js/graph.js b/addons/web_graph/static/src/js/graph.js
index 0ae3f7c1508..8710b840209 100644
--- a/addons/web_graph/static/src/js/graph.js
+++ b/addons/web_graph/static/src/js/graph.js
@@ -246,7 +246,7 @@ instance.web_graph.GraphView = instance.web.View.extend({
var result = [];
var ticks = {};
- return this.alive(obj.call("fields_view_get", [view_id, 'graph']).then(function(tmp) {
+ return this.alive(obj.call("fields_view_get", [view_id, 'graph', context]).then(function(tmp) {
view_get = tmp;
fields = view_get['fields'];
var toload = _.select(group_by, function(x) { return fields[x] === undefined });
diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css
index eaba760292c..a8ed25694ba 100644
--- a/addons/web_kanban/static/src/css/kanban.css
+++ b/addons/web_kanban/static/src/css/kanban.css
@@ -7,6 +7,15 @@
background: url(/web/static/src/img/form_sheetbg.png);
width: 100%;
}
+.openerp .oe_kanban_view .oe_kanban_group_length {
+ text-align: center;
+ display: none;
+}
+.openerp .oe_kanban_view .oe_kanban_group_length .oe_tag {
+ position: relative;
+ top: 8px;
+ font-weight: bold;
+}
.openerp .oe_kanban_view .ui-sortable-placeholder {
border: 1px solid rgba(0, 0, 0, 0.1);
visibility: visible !important;
@@ -92,15 +101,6 @@
overflow: hidden;
text-overflow: ellipsis;
}
-.openerp .oe_kanban_view .oe_kanban_group_length {
- text-align: center;
- display: none;
-}
-.openerp .oe_kanban_view .oe_kanban_group_length .oe_tag {
- position: relative;
- top: +8px;
- font-weight: bold;
-}
.openerp .oe_kanban_view .oe_fold_column .oe_kanban_group_length {
position: absolute;
top: -1px;
@@ -108,9 +108,6 @@
float: right;
display: block;
}
-.openerp .oe_kanban_view .oe_kanban_header:hover .oe_kanban_group_length {
- display: none;
-}
.openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_column, .openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_group_header {
width: 185px;
min-width: 185px;
@@ -150,8 +147,7 @@
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_title, .openerp .oe_kanban_view .oe_kanban_group_folded.oe_kanban_column *, .openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_aggregates, .openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_add {
display: none;
}
-.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_title_vertical,
-.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_length {
+.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_title_vertical, .openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_length {
display: block;
}
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_dropdown_kanban {
diff --git a/addons/web_kanban/static/src/css/kanban.sass b/addons/web_kanban/static/src/css/kanban.sass
index ec964c8819f..2182fb693ea 100644
--- a/addons/web_kanban/static/src/css/kanban.sass
+++ b/addons/web_kanban/static/src/css/kanban.sass
@@ -130,8 +130,8 @@
position: absolute
top: -1px
right: -14px
- display: block
float: right
+ display: block
&.oe_kanban_grouped
.oe_kanban_column, .oe_kanban_group_header
width: 185px
@@ -197,7 +197,7 @@
top: -8px
.oe_kanban_header .oe_dropdown_toggle
top: -2px
- height: 14px;
+ height: 14px
.oe_kanban_card, .oe_dropdown_toggle
cursor: pointer
display: inline-block