[MERGE] from 7.0

bzr revid: xmo@openerp.com-20130313094909-u21ee88l2lak9p2x
This commit is contained in:
Xavier Morel 2013-03-13 10:49:09 +01:00
commit 94c2c42be6
22 changed files with 958 additions and 275 deletions

View File

@ -26,6 +26,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/spinjs/spin.js", "static/lib/spinjs/spin.js",
"static/lib/jquery.autosize/jquery.autosize.js", "static/lib/jquery.autosize/jquery.autosize.js",
"static/lib/jquery.blockUI/jquery.blockUI.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/js/jquery-ui-1.9.1.custom.js",
"static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js", "static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js",
"static/lib/jquery.ui.notify/js/jquery.notify.js", "static/lib/jquery.ui.notify/js/jquery.notify.js",

View File

@ -15,6 +15,8 @@ import simplejson
import time import time
import urllib import urllib
import urllib2 import urllib2
import urlparse
import xmlrpclib
import zlib import zlib
from xml.etree import ElementTree from xml.etree import ElementTree
from cStringIO import StringIO from cStringIO import StringIO
@ -91,16 +93,50 @@ def db_list(req):
dbs = [i for i in dbs if re.match(r, i)] dbs = [i for i in dbs if re.match(r, i)]
return dbs return dbs
def db_monodb(req): def db_monodb_redirect(req):
# if only one db exists, return it else return False 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: try:
dbs = db_list(req) dbs = db_list(req)
if len(dbs) == 1:
return dbs[0]
except Exception: except Exception:
# ignore access denied # ignore access denied
pass dbs = []
return False
# 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): def module_topological_sort(modules):
""" Return a list of module names sorted so that their dependencies of the """ 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 return r
def manifest_list(req, extension, mods=None, db=None): 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: if not req.debug:
path = '/web/webclient/' + extension path = '/web/webclient/' + extension
if mods is not None: if mods is not None:
@ -299,12 +339,7 @@ def manifest_list(req, extension, mods=None, db=None):
path += '?' + urllib.urlencode({'db': db}) path += '?' + urllib.urlencode({'db': db})
return [path] return [path]
files = manifest_glob(req, extension, addons=mods, db=db) files = manifest_glob(req, extension, addons=mods, db=db)
i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \ return [wp for _fp, wp in files]
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]
def get_last_modified(files): def get_last_modified(files):
""" Returns the modification time of the most recently modified """ Returns the modification time of the most recently modified
@ -534,6 +569,10 @@ class Home(openerpweb.Controller):
@openerpweb.httprequest @openerpweb.httprequest
def index(self, req, s_action=None, db=None, **kw): 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)) 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)) 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') saved_actions = req.httpsession.get('saved_actions')
if not saved_actions: if not saved_actions:
saved_actions = {"next":0, "actions":{}} saved_actions = {"next":1, "actions":{}}
req.httpsession['saved_actions'] = saved_actions req.httpsession['saved_actions'] = saved_actions
# we don't allow more than 10 stored actions # we don't allow more than 10 stored actions
if len(saved_actions["actions"]) >= 10: if len(saved_actions["actions"]) >= 10:

View File

@ -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 items, it *should* prefix those with a section title using its own
name. This has no technical consequence but is clearer for users. 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 Providing drawer/supplementary UI
+++++++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++++++
@ -145,6 +151,11 @@ started only once (per view).
dynamically collects, lays out and renders filters? => dynamically collects, lays out and renders filters? =>
exercises drawer thingies 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 Converting from facet objects
+++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++

View File

@ -92,6 +92,14 @@ class WebRequest(object):
if not self.session: if not self.session:
self.session = session.OpenERPSession() self.session = session.OpenERPSession()
self.httpsession[self.session_id] = self.session 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.context = self.params.pop('context', {})
self.debug = self.params.pop('debug', False) is not False self.debug = self.params.pop('debug', False) is not False
# Determine self.lang # Determine self.lang

View File

@ -11,8 +11,8 @@ Date.CultureInfo = {
firstLetterDayNames: ["أ", "ا", "ث", "أ", "خ", "ج", "س"], firstLetterDayNames: ["أ", "ا", "ث", "أ", "خ", "ج", "س"],
/* Month Name Strings */ /* Month Name Strings */
monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
abbreviatedMonthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], abbreviatedMonthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
/* AM/PM Designators */ /* AM/PM Designators */
amDesignator: "ص", amDesignator: "ص",
@ -82,18 +82,18 @@ Date.CultureInfo = {
* As well, please review the list of "Future Strings" section below. * As well, please review the list of "Future Strings" section below.
*/ */
regexPatterns: { regexPatterns: {
jan: /^كانون الثاني/i, jan: /^يناير/i,
feb: /^شباط/i, feb: /^فبراير/i,
mar: /^آذار/i, mar: /^مارس/i,
apr: /^نيسان/i, apr: /^أبريل/i,
may: /^أيار/i, may: /^مايو/i,
jun: /^حزيران/i, jun: /^يونيو/i,
jul: /^تموز/i, jul: /^يوليو/i,
aug: /^آب/i, aug: /^أغسطس/i,
sep: /^أيلول/i, sep: /^سبتمبر/i,
oct: /^تشرين الأول/i, oct: /^أكتوبر/i,
nov: /^تشرين الثاني/i, nov: /^نوفمبر/i,
dec: /^كانون الأول/i, dec: /^ديسمبر/i,
sun: /^الاحد/i, sun: /^الاحد/i,
mon: /^ا(1)?/i, mon: /^ا(1)?/i,
@ -192,4 +192,4 @@ Date.CultureInfo = {
* end end * end end
* long long * long long
* short short * short short
*/ */

View File

@ -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));

View File

@ -1436,7 +1436,7 @@ $sheet-padding: 16px
.oe_searchview_drawer .oe_searchview_drawer
position: absolute position: absolute
z-index: 100 z-index: 2
// detach drawer from field slightly // detach drawer from field slightly
margin-top: 4px margin-top: 4px
top: 100% top: 100%
@ -1832,6 +1832,12 @@ $sheet-padding: 16px
.oe_form .oe_form
.oe_form_field_text .oe_form_field_text
width: 100% 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_char input,
.oe_form_field_url input, .oe_form_field_url input,
.oe_form_field_email input, .oe_form_field_email input,
@ -2502,6 +2508,9 @@ div.ui-widget-overlay
// Internet Explorer 9+ specifics {{{ // Internet Explorer 9+ specifics {{{
.openerp_ie .openerp_ie
.placeholder
color: $tag-border !important
font-style: italic !important
.oe_form_field_boolean input .oe_form_field_boolean input
background: #fff background: #fff
.db_option_table .oe_form_field_selection .db_option_table .oe_form_field_selection
@ -2517,6 +2526,8 @@ div.ui-widget-overlay
button.oe_highlight button.oe_highlight
padding-top: 0 padding-top: 0
padding-bottom: 0 padding-bottom: 0
.oe_view_manager_view_kanban
display: table-cell
.oe_view_manager_buttons .oe_view_manager_buttons
button.oe_write_full button.oe_write_full
padding-top: 0 padding-top: 0

View File

@ -530,7 +530,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
'login': 'admin', 'login': 'admin',
'password': form_obj['create_admin_pwd'], 'password': form_obj['create_admin_pwd'],
'login_successful': function() { '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, _push_me: false,
@ -641,6 +650,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage
instance.web.Login = instance.web.Widget.extend({ instance.web.Login = instance.web.Widget.extend({
template: "Login", template: "Login",
remember_credentials: true, remember_credentials: true,
events: {
'change input[name=db],select[name=db]': function(ev) {
this.set('database_selector', $(ev.currentTarget).val());
},
},
init: function(parent, action) { init: function(parent, action) {
this._super(parent); this._super(parent);
@ -652,18 +666,18 @@ instance.web.Login = instance.web.Widget.extend({
if (_.isEmpty(this.params)) { if (_.isEmpty(this.params)) {
this.params = $.bbq.getState(true); 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) { if (this.params.login_successful) {
this.on('login_successful', this, 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() { start: function() {
var self = this; var self = this;
@ -671,10 +685,10 @@ instance.web.Login = instance.web.Widget.extend({
self.$el.find('.oe_login_manage_db').click(function() { self.$el.find('.oe_login_manage_db').click(function() {
self.do_action("database_manager"); self.do_action("database_manager");
}); });
self.on('change:database_selector', this, function() {
this.database_selected(this.get('database_selector'));
});
var d = $.when(); var d = $.when();
if ($.deparam.querystring().db) {
self.params.db = $.deparam.querystring().db;
}
if ($.param.fragment().token) { if ($.param.fragment().token) {
self.params.token = $.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) { if (self.params.db && self.params.login && self.params.password) {
d = self.do_login(self.params.db, self.params.login, self.params.password); d = self.do_login(self.params.db, self.params.login, self.params.password);
} else { } else {
if (self.params.db) { d = self.rpc("/web/database/get_list", {})
self.on_db_loaded([self.params.db]) .done(self.on_db_loaded)
} else { .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; 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) { on_db_loaded: function (result) {
var self = this;
this.db_list = result; 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})); this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
if(this.db_list.length === 0) { if(this.db_list.length === 0) {
this.do_action("database_manager"); this.do_action("database_manager");
@ -732,17 +774,11 @@ instance.web.Login = instance.web.Widget.extend({
self.hide_error(); self.hide_error();
self.$(".oe_login_pane").fadeOut("slow"); self.$(".oe_login_pane").fadeOut("slow");
return this.session.session_authenticate(db, login, password).then(function() { return this.session.session_authenticate(db, login, password).then(function() {
if (self.has_local_storage) { self.remember_last_used_database(db);
if(self.remember_credentials) { if (self.has_local_storage && self.remember_credentials) {
localStorage.setItem('last_db_login_success', db); localStorage.setItem(db + '|last_login', login);
localStorage.setItem('last_login_login_success', login); if (self.session.debug) {
if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { localStorage.setItem(db + '|last_password', password);
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.trigger('login_successful'); self.trigger('login_successful');
@ -800,6 +836,9 @@ instance.web.Reload = function(parent, action) {
var sobj = $.deparam(l.search.substr(1)); var sobj = $.deparam(l.search.substr(1));
sobj.ts = new Date().getTime(); sobj.ts = new Date().getTime();
if (params.url_search) {
sobj = _.extend(sobj, params.url_search);
}
var search = '?' + $.param(sobj); var search = '?' + $.param(sobj);
var hash = l.hash; var hash = l.hash;
@ -1303,7 +1342,7 @@ instance.web.WebClient = instance.web.Client.extend({
}, },
logo_edit: function(ev) { logo_edit: function(ev) {
var self = this; 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) { self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
result.res_id = res['company_id'][0]; result.res_id = res['company_id'][0];
result.target = "new"; result.target = "new";

View File

@ -232,13 +232,17 @@ instance.web.ParentedMixin = {
Utility method to only execute asynchronous actions if the current Utility method to only execute asynchronous actions if the current
object has not been destroyed. object has not been destroyed.
@param {Promise} promise The promise representing the asynchronous action. @param {$.Deferred} promise The promise representing the asynchronous
@param {bool} reject Defaults to false. If true, the returned promise will be action.
rejected with no arguments if the current object is destroyed. If false, @param {bool} [reject=false] If true, the returned promise will be
the returned promise will never be resolved nor rejected. rejected with no arguments if the current
@returns {Promise} A promise that will mirror the given promise if everything goes object is destroyed. If false, the
fine but will either be rejected with no arguments or never resolved if the returned promise will never be resolved
current object is destroyed. 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) { alive: function(promise, reject) {
var def = $.Deferred(); var def = $.Deferred();

View File

@ -89,6 +89,10 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
}); });
}, },
session_is_valid: function() { session_is_valid: function() {
var db = $.deparam.querystring().db;
if (db && this.db !== db) {
return false;
}
return !!this.uid; return !!this.uid;
}, },
/** /**

View File

@ -481,9 +481,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
* Creates a new record in db * Creates a new record in db
* *
* @param {Object} data field values to set on the new record * @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} * @returns {$.Deferred}
*/ */
create: function(data) { create: function(data, options) {
return this._model.call('create', [data], {context: this.get_context()}); 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 {Number|String} id identifier for the record to alter
* @param {Object} data field values to write into the record * @param {Object} data field values to write into the record
* @param {Function} callback function called with operation result * @param {Object} options Dictionary that can contain the following keys:
* @param {Function} error_callback function called in case of write error * - 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} * @returns {$.Deferred}
*/ */
write: function (id, data, options) { write: function (id, data, options) {
@ -732,10 +737,13 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
self.last_default_get = res; self.last_default_get = res;
}); });
}, },
create: function(data) { create: function(data, options) {
var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, var cached = {
defaults: this.last_default_get}; id:_.uniqueId(this.virtual_id_prefix),
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)})); 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); this.cache.push(cached);
return $.Deferred().resolve(cached.id).promise(); return $.Deferred().resolve(cached.id).promise();
}, },
@ -762,7 +770,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
cached = {id: id, values: {}}; cached = {id: id, values: {}};
this.cache.push(cached); this.cache.push(cached);
} }
$.extend(cached.values, record.values); $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
if (dirty) if (dirty)
this.trigger("dataset_changed", id, data, options); this.trigger("dataset_changed", id, data, options);
return $.Deferred().resolve(true).promise(); return $.Deferred().resolve(true).promise();
@ -860,8 +868,16 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
} }
return completion.promise(); 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) { for(var i=0, len=this.cache.length; i<len; ++i) {
var record = this.cache[i]; var record = this.cache[i];
// if record we call the button upon is in the cache // if record we call the button upon is in the cache
@ -871,8 +887,15 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
break; break;
} }
} }
},
call_button: function (method, args) {
this.evict_record(args[0][0]);
return this._super(method, args); return this._super(method, args);
}, },
exec_workflow: function (id, signal) {
this.evict_record(id);
return this._super(id, signal);
},
alter_ids: function(n_ids) { alter_ids: function(n_ids) {
this._super(n_ids); this._super(n_ids);
this.trigger("dataset_changed", 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); return this._super.apply(this, arguments);
} }
}, },
create: function(data) { create: function(data, options) {
if (this.create_function) { if (this.create_function) {
return this.create_function(data, this._super); return this.create_function(data, options, this._super);
} else { } else {
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
} }

View File

@ -324,7 +324,10 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
e.preventDefault(); e.preventDefault();
break; break;
} }
} },
'autocompleteopen': function () {
this.$el.autocomplete('widget').css('z-index', 3);
},
}, },
/** /**
* @constructs instance.web.SearchView * @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.has_defaults = !_.isEmpty(this.defaults);
this.inputs = []; this.inputs = [];
this.controls = {}; this.controls = [];
this.headless = this.options.hidden && !this.has_defaults; 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) { complete_global_search: function (req, resp) {
$.when.apply(null, _(this.inputs).chain() $.when.apply(null, _(this.inputs).chain()
.filter(function (input) { return input.visible(); })
.invoke('complete', req.term) .invoke('complete', req.term)
.value()).then(function () { .value()).then(function () {
resp(_(_(arguments).compact()).flatten(true)); 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 {Array} items a list of nodes to convert to widgets
* @param {Object} fields a mapping of field names to (ORM) field attributes * @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) { make_widgets: function (items, fields, group) {
group_name = group_name || null; if (!group) {
if (!(group_name in this.controls)) { group = new instance.web.search.Group(
this.controls[group_name] = []; this, 'q', {attrs: {string: _t("Filters")}});
} }
var self = this, group = this.controls[group_name]; var self = this;
var filters = []; var filters = [];
_.each(items, function (item) { _.each(items, function (item) {
if (filters.length && item.tag !== 'filter') { 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 = []; filters = [];
} }
@ -599,15 +603,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
case 'separator': case 'newline': case 'separator': case 'newline':
break; break;
case 'filter': case 'filter':
filters.push(new instance.web.search.Filter(item, this)); filters.push(new instance.web.search.Filter(item, group));
break; break;
case 'group': 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; break;
case 'field': 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 // filters
self.make_widgets(item.children, fields, group_name); self.make_widgets(item.children, fields, group);
break; break;
} }
}, this); }, 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} item fields_view_get node for the field
* @param {Object} field fields_get result for the field * @param {Object} field fields_get result for the field
* @param {Object} [parent]
* @returns instance.web.search.Field * @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]); var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]);
if(obj) { if(obj) {
return new (obj) (item, field, this); return new (obj) (item, field, parent || this);
} else { } else {
console.group('Unknown field type ' + field.type); console.group('Unknown field type ' + field.type);
console.error('View node', item); 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 * @constructs instance.web.search.Widget
* @extends instance.web.Widget * @extends instance.web.Widget
* *
* @param view the ancestor view of this widget * @param parent parent of this widget
*/ */
init: function (view) { init: function (parent) {
this._super(view); this._super(parent);
this.view = view; 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) { instance.web.search.add_expand_listener = function($root) {
$root.find('a.searchview_group_string').click(function (e) { $root.find('a.searchview_group_string').click(function (e) {
$root.toggleClass('folded expanded'); $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({ instance.web.search.Group = instance.web.search.Widget.extend({
template: 'SearchView.group', init: function (parent, icon, node) {
init: function (view_section, view, fields) { this._super(parent);
this._super(view); var attrs = node.attrs;
this.attrs = view_section.attrs; this.modifiers = attrs.modifiers =
this.lines = view.make_widgets( attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
view_section.children, fields); 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# */{ 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 * @constructs instance.web.search.Input
* @extends instance.web.search.Widget * @extends instance.web.search.Widget
* *
* @param view * @param parent
*/ */
init: function (view) { init: function (parent) {
this._super(view); this._super(parent);
this.load_attrs({});
this.view.inputs.push(this); this.view.inputs.push(this);
this.style = undefined;
}, },
/** /**
* Fetch auto-completion values for the widget. * 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); "get_domain not implemented for widget " + this.attrs.type);
}, },
load_attrs: function (attrs) { load_attrs: function (attrs) {
if (attrs.modifiers) { attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
attrs.modifiers = JSON.parse(attrs.modifiers); this.attrs = attrs;
attrs.invisible = attrs.modifiers.invisible || false; },
if (attrs.invisible) { /**
this.style = 'display: none;' * 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# */{ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
template: 'SearchView.filters', template: 'SearchView.filters',
@ -967,17 +1006,19 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
* @extends instance.web.search.Input * @extends instance.web.search.Input
* *
* @param {Array<instance.web.search.Filter>} filters elements of the group * @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, // If all filters are group_by and we're not initializing a GroupbyGroup,
// create a GroupbyGroup instead of the current FilterGroup // create a GroupbyGroup instead of the current FilterGroup
if (!(this instanceof instance.web.search.GroupbyGroup) && if (!(this instanceof instance.web.search.GroupbyGroup) &&
_(filters).all(function (f) { _(filters).all(function (f) {
return f.attrs.context && f.attrs.context.group_by; })) { if (!f.attrs.context) { return false; }
return new instance.web.search.GroupbyGroup(filters, view); 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.filters = filters;
this.view.query.on('add remove change reset', this.proxy('search_change')); 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; var self = this;
item = item.toLowerCase(); item = item.toLowerCase();
var facet_values = _(this.filters).chain() var facet_values = _(this.filters).chain()
.filter(function (filter) { return filter.visible(); })
.filter(function (filter) { .filter(function (filter) {
var at = { var at = {
string: filter.attrs.string || '', 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({ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
icon: 'w', icon: 'w',
completion_label: _lt("Group by: %s"), completion_label: _lt("Group by: %s"),
init: function (filters, view) { init: function (filters, parent) {
this._super(filters, view); this._super(filters, parent);
// Not flanders: facet unicity is handled through the // Not flanders: facet unicity is handled through the
// (category, field) pair of facet attributes. This is all well and // (category, field) pair of facet attributes. This is all well and
// good for regular filter groups where a group matches a facet, but for // 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 // view which proxies to the first GroupbyGroup, so it can be used
// for every GroupbyGroup and still provides the various methods needed // for every GroupbyGroup and still provides the various methods needed
// by the search view. Use weirdo name to avoid risks of conflicts // by the search view. Use weirdo name to avoid risks of conflicts
if (!this.getParent()._s_groupby) { if (!this.view._s_groupby) {
this.getParent()._s_groupby = { this.view._s_groupby = {
help: "See GroupbyGroup#init", help: "See GroupbyGroup#init",
get_context: this.proxy('get_context'), get_context: this.proxy('get_context'),
get_domain: this.proxy('get_domain'), get_domain: this.proxy('get_domain'),
@ -1141,14 +1183,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
} }
}, },
match_facet: function (facet) { match_facet: function (facet) {
return facet.get('field') === this.getParent()._s_groupby; return facet.get('field') === this.view._s_groupby;
}, },
make_facet: function (values) { make_facet: function (values) {
return { return {
category: _t("GroupBy"), category: _t("GroupBy"),
icon: this.icon, icon: this.icon,
values: values, 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 * @extends instance.web.search.Input
* *
* @param node * @param node
* @param view * @param parent
*/ */
init: function (node, view) { init: function (node, parent) {
this._super(view); this._super(parent);
this.load_attrs(node.attrs); this.load_attrs(node.attrs);
}, },
facet_for: function () { return $.when(null); }, 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 view_section
* @param field * @param field
* @param view * @param parent
*/ */
init: function (view_section, field, view) { init: function (view_section, field, parent) {
this._super(view); this._super(parent);
this.load_attrs(_.extend({}, field, view_section.attrs)); this.load_attrs(_.extend({}, field, view_section.attrs));
}, },
facet_for: function (value) { 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} name the field's name
* @param {String} operator the field's operator (either attribute-specified or default operator for the field * @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 * @returns {Array<Array>} domain to include in the resulting search
*/ */
make_domain: function (name, operator, facet) { 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({ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
default_operator: {}, default_operator: {},
init: function (view_section, field, view) { init: function (view_section, field, parent) {
this._super(view_section, field, view); this._super(view_section, field, parent);
this.model = new instance.web.Model(this.attrs.relation); this.model = new instance.web.Model(this.attrs.relation);
}, },
complete: function (needle) { complete: function (needle) {
@ -1673,6 +1715,13 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
if (!_.isEmpty(results.group_by)) { if (!_.isEmpty(results.group_by)) {
results.context.group_by = 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 = { var filter = {
name: $name.val(), name: $name.val(),
user_id: private_filter ? instance.session.uid : false, 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; var running_count = 0;
// get total filters count // get total filters count
var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; }; 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() .flatten()
.filter(is_group) .filter(is_group)
.map(function (i) { return i.filters.length; }) .map(function (i) { return i.filters.length; })
.sum() .sum()
.value(); .value();
var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) { var col1 = [], col2 = visible_filters.map(function (group) {
var filters = _(inputs).filter(is_group); var filters = _(group.children).filter(is_group);
return { return {
name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group, name: _.str.sprintf("<span class='oe_i'>%s</span> %s",
filters: filters, group.icon, group.name),
length: _(filters).chain().map(function (i) { filters: filters,
return i.filters.length; }).sum().value() length: _(filters).chain().map(function (i) {
}; return i.filters.length; }).sum().value()
}); };
}).value();
while (col2.length) { while (col2.length) {
// col1 + group should be smaller than col2 + group // col1 + group should be smaller than col2 + group

View File

@ -810,7 +810,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
try { try {
var form_invalid = false, var form_invalid = false,
values = {}, values = {},
first_invalid_field = null; first_invalid_field = null,
readonly_values = {};
for (var f in self.fields) { for (var f in self.fields) {
if (!self.fields.hasOwnProperty(f)) { continue; } if (!self.fields.hasOwnProperty(f)) { continue; }
f = self.fields[f]; f = self.fields[f];
@ -819,11 +820,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (!first_invalid_field) { if (!first_invalid_field) {
first_invalid_field = f; 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 // Special case 'id' field, do not save this field
// on 'create' : save all non readonly fields // on 'create' : save all non readonly fields
// on 'edit' : save non readonly modified 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) { if (form_invalid) {
@ -836,7 +841,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var save_deferral; var save_deferral;
if (!self.datarecord.id) { if (!self.datarecord.id) {
// Creation save // 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); return self.record_created(r, prepend_on_create);
}, null); }, null);
} else if (_.isEmpty(values)) { } else if (_.isEmpty(values)) {
@ -844,7 +849,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
save_deferral = $.Deferred().resolve({}).promise(); save_deferral = $.Deferred().resolve({}).promise();
} else { } else {
// Write save // 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); return self.record_saved(r);
}, null); }, null);
} }
@ -1887,10 +1892,11 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) { 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.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() { start: function() {
this._super.apply(this, arguments); 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); this.$el.click(this.on_click);
if (this.node.attrs.help || instance.session.debug) { if (this.node.attrs.help || instance.session.debug) {
this.do_attach_tooltip(); this.do_attach_tooltip();
@ -1916,18 +1922,20 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
modal: true, modal: true,
buttons: [ buttons: [
{text: _t("Cancel"), click: function() { {text: _t("Cancel"), click: function() {
def.resolve();
$(this).dialog("close"); $(this).dialog("close");
} }
}, },
{text: _t("Ok"), click: function() { {text: _t("Ok"), click: function() {
self.on_confirmed().done(function() { var self2 = this;
def.resolve(); self.on_confirmed().always(function() {
$(self2).dialog("close");
}); });
$(this).dialog("close");
} }
} }
] ],
beforeClose: function() {
def.resolve();
},
}); });
return def.promise(); return def.promise();
} else { } 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, { instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
template: 'FieldChar', template: 'FieldChar',
widget_class: 'oe_form_field_char', widget_class: 'oe_form_field_char',
@ -2382,7 +2402,6 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
type_of_date: "datetime", type_of_date: "datetime",
events: { events: {
'change .oe_datepicker_master': 'change_datetime', 'change .oe_datepicker_master': 'change_datetime',
'dragstart img.oe_datepicker_trigger': function () { return false; },
}, },
init: function(parent) { init: function(parent) {
this._super(parent); this._super(parent);
@ -2546,42 +2565,45 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
}, },
'change textarea': 'store_dom_value', 'change textarea': 'store_dom_value',
}, },
init: function (field_manager, node) {
this._super(field_manager, node);
},
initialize_content: function() { initialize_content: function() {
var self = this; var self = this;
this.$textarea = this.$el.find('textarea'); if (! this.get("effective_readonly")) {
this.auto_sized = false; this.$textarea = this.$el.find('textarea');
this.default_height = this.$textarea.css('height'); this.auto_sized = false;
if (this.get("effective_readonly")) { this.default_height = this.$textarea.css('height');
this.$textarea.attr('disabled', 'disabled'); 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 () { commit_value: function () {
this.store_dom_value(); if (! this.get("effective_readonly") && this.$textarea) {
this.store_dom_value();
}
return this._super(); return this._super();
}, },
store_dom_value: function () { 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() { render_value: function() {
var show_value = instance.web.format_value(this.get('value'), this, ''); if (! this.get("effective_readonly")) {
if (show_value === '') { var show_value = instance.web.format_value(this.get('value'), this, '');
this.$textarea.css('height', parseInt(this.default_height)+"px"); if (show_value === '') {
} this.$textarea.css('height', parseInt(this.default_height)+"px");
this.$textarea.val(show_value); }
if (! this.auto_sized) { this.$textarea.val(show_value);
this.auto_sized = true; if (! this.auto_sized) {
this.$textarea.autosize(); this.auto_sized = true;
this.$textarea.autosize();
} else {
this.$textarea.trigger("autosize");
}
} else { } else {
this.$textarea.trigger("autosize"); var txt = this.get("value");
this.$(".oe_form_text_content").text(txt);
} }
}, },
is_syntax_valid: function() { 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(); return this.get('value') === '' || this._super();
}, },
focus: function($el) { focus: function($el) {
this.$textarea[0].focus(); if (!this.get("effective_readonly") && this.$textarea) {
this.$textarea[0].focus();
}
}, },
set_dimensions: function (height, width) { set_dimensions: function (height, width) {
this._super(height, width); this._super(height, width);
this.$textarea.css({ if (!this.get("effective_readonly") && this.$textarea) {
width: width, this.$textarea.css({
minHeight: height width: width,
}); minHeight: height
});
}
}, },
}); });
@ -2970,8 +2996,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
e.stopPropagation(); 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) { init: function(field_manager, node) {
this._super(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")) if (!this.get("effective_readonly"))
this.render_editable(); 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() { init_error_displayer: function() {
// nothing // nothing
}, },
@ -3263,7 +3307,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
}, },
focus: function () { focus: function () {
if (!this.get('effective_readonly')) { if (!this.get('effective_readonly')) {
this.$input[0].focus(); this.$input && this.$input[0].focus();
} }
}, },
_quick_create: function() { _quick_create: function() {
@ -3682,8 +3726,8 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
var pop = new instance.web.form.FormOpenPopup(this); var pop = new instance.web.form.FormOpenPopup(this);
pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
title: _t("Open: ") + self.o2m.string, title: _t("Open: ") + self.o2m.string,
create_function: function(data) { create_function: function(data, options) {
return self.o2m.dataset.create(data).done(function(r) { return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", 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(); this.o2m.trigger_on_change();
}, },
is_valid: function () { 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 // If the form has not been modified, the view can only be valid
// NB: is_dirty will also be set on defaults/onchanges/whatever? // NB: is_dirty will also be set on defaults/onchanges/whatever?
// oe_form_dirty seems to only be set on actual user actions // 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, title: _t("Create: ") + self.o2m.string,
initial_view: "form", initial_view: "form",
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
create_function: function(data, callback, error_callback) { create_function: function(data, options) {
return self.o2m.dataset.create(data).done(function(r) { return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", r); self.o2m.dataset.trigger("dataset_changed", r);
}).done(callback).fail(error_callback); });
}, },
read_function: function() { read_function: function() {
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); 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")) if (this.get("effective_readonly"))
return; return;
var self = this; var self = this;
var ignore_blur = false;
self.$text = this.$("textarea"); self.$text = this.$("textarea");
self.$text.textext({ self.$text.textext({
plugins : 'tags arrow autocomplete', plugins : 'tags arrow autocomplete',
@ -4008,6 +4057,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
if (data.id) { if (data.id) {
self.add_id(data.id); self.add_id(data.id);
} else { } else {
ignore_blur = true;
data.action(); data.action();
} }
}, },
@ -4058,10 +4108,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
self.$text self.$text
.focusin(function () { .focusin(function () {
self.trigger('focused'); self.trigger('focused');
ignore_blur = false;
}) })
.focusout(function() { .focusout(function() {
self.$text.trigger("setInputData", ""); self.$text.trigger("setInputData", "");
self.trigger('blurred'); if (!ignore_blur) {
self.trigger('blurred');
}
}).keydown(function(e) { }).keydown(function(e) {
if (e.which === $.ui.keyCode.TAB && self._drop_shown) { if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
self.$text.textext()[0].autocomplete().selectFromDropdown(); 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, { 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.created_elements = [];
this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context); this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
this.dataset.read_function = this.options.read_function; 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; 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.trigger('create_completed saved', r);
self.created_elements.push(r); self.created_elements.push(r);
}); });
@ -5216,11 +5270,11 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
this.calc_domain(); this.calc_domain();
this.on("change:value", this, this.get_selection); this.on("change:value", this, this.get_selection);
this.on("change:evaluated_selection_domain", this, this.get_selection); this.on("change:evaluated_selection_domain", this, this.get_selection);
this.get_selection();
this.on("change:selection", this, function() { this.on("change:selection", this, function() {
this.selection = this.get("selection"); this.selection = this.get("selection");
this.render_value(); this.render_value();
}); });
this.get_selection();
if (this.options.clickable) { if (this.options.clickable) {
this.$el.on('click','li',this.on_click_stage); 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) { 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) { 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);
}, },
}); });

View File

@ -321,9 +321,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
.appendTo($this.empty()) .appendTo($this.empty())
.click(function (e) {e.stopPropagation();}) .click(function (e) {e.stopPropagation();})
.append('<option value="80">80</option>' + .append('<option value="80">80</option>' +
'<option value="100">100</option>' +
'<option value="200">200</option>' + '<option value="200">200</option>' +
'<option value="500">500</option>' + '<option value="500">500</option>' +
'<option value="2000">2000</option>' +
'<option value="NaN">' + _t("Unlimited") + '</option>') '<option value="NaN">' + _t("Unlimited") + '</option>')
.change(function () { .change(function () {
var val = parseInt($select.val(), 10); var val = parseInt($select.val(), 10);

View File

@ -96,7 +96,8 @@ openerp.web.list_editable = function (instance) {
}); });
}, },
editable: function () { editable: function () {
return !this.options.disable_editable_mode return !this.grouped
&& !this.options.disable_editable_mode
&& (this.fields_view.arch.attrs.editable && (this.fields_view.arch.attrs.editable
|| this._context_editable || this._context_editable
|| this.options.editable); || this.options.editable);

View File

@ -1446,11 +1446,11 @@ instance.web.View = instance.web.Widget.extend({
* Performs a fields_view_get and apply postprocessing. * Performs a fields_view_get and apply postprocessing.
* return a {$.Deferred} resolved with the fvg * 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 {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 {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 {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 {String} [args.view_type] type of view to be loaded if view_id is null
* @param {Boolean} [args.toolbar=false] get the toolbar definition * @param {Boolean} [args.toolbar=false] get the toolbar definition
*/ */
instance.web.fields_view_get = function(args) { instance.web.fields_view_get = function(args) {
@ -1555,12 +1555,17 @@ instance.web.xml_to_str = function(node) {
} else { } else {
throw new Error(_t("Could not serialize XML")); throw new Error(_t("Could not serialize XML"));
} }
// Browsers won't deal with self closing tags except br, hr, input, ... // Browsers won't deal with self closing tags except void elements:
// http://stackoverflow.com/questions/97522/what-are-all-the-valid-self-closing-elements-in-xhtml-as-implemented-by-the-maj // 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 // 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) { 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; return str;
}; };

View File

@ -40,7 +40,7 @@
<td> <td>
<p> <p>
<t t-js="d"> <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) d.html_error = context.engine.tools.html_escape(message)
.replace(/\n/g, '<br/>'); .replace(/\n/g, '<br/>');
</t> </t>
@ -71,9 +71,9 @@
</div> </div>
<ul> <ul>
<li>Username</li> <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>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> <li><button name="submit">Log in</button></li>
</ul> </ul>
</form> </form>
@ -118,7 +118,8 @@
you will be able to install your first application. you will be able to install your first application.
</p> </p>
<p class="oe_grey" style="margin: 10px"> <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> </p>
<table class="db_option_table" style="margin: 10px"> <table class="db_option_table" style="margin: 10px">
<tr> <tr>
@ -129,7 +130,9 @@
</tr> </tr>
<tr> <tr>
<td><label for="db_name">Select a database name:</label></td> <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>
<tr> <tr>
<td><label for="demo_data">Load demonstration data:</label></td> <td><label for="demo_data">Load demonstration data:</label></td>
@ -1047,17 +1050,22 @@
</t> </t>
<t t-name="FieldText"> <t t-name="FieldText">
<div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style"> <div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style">
<textarea rows="6" <t t-if="!widget.get('effective_readonly')">
t-att-name="widget.name" <textarea rows="6"
class="field_text" t-att-name="widget.name"
t-att-tabindex="widget.node.attrs.tabindex" class="field_text"
t-att-autofocus="widget.node.attrs.autofocus" t-att-tabindex="widget.node.attrs.tabindex"
t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''" t-att-autofocus="widget.node.attrs.autofocus"
t-att-maxlength="widget.field.size" t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
></textarea><img class="oe_field_translate oe_input_icon" t-att-maxlength="widget.field.size"
t-if="widget.field.translate and !widget.get('effective_readonly')" ></textarea><img class="oe_field_translate oe_input_icon"
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0" 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> </div>
</t> </t>
<t t-name="FieldTextHtml"> <t t-name="FieldTextHtml">
@ -1076,8 +1084,9 @@
t-att-name="widget.name" t-att-name="widget.name"
t-att-placeholder="placeholder" t-att-placeholder="placeholder"
class="oe_datepicker_master" class="oe_datepicker_master"
/><img class="oe_input_icon oe_datepicker_trigger" t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"' /><img class="oe_input_icon oe_datepicker_trigger" draggable="false"
title="Select date" width="16" height="16" border="0"/> t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
title="Select date" width="16" height="16" border="0"/>
</span> </span>
</t> </t>
<t t-name="FieldDate"> <t t-name="FieldDate">
@ -1111,7 +1120,7 @@
</t> </t>
<t t-if="!widget.get('effective_readonly')"> <t t-if="!widget.get('effective_readonly')">
<a t-if="! widget.options.no_open" href="#" tabindex="-1" <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> <div>
<input type="text" <input type="text"
t-att-id="widget.id_for_label" t-att-id="widget.id_for_label"
@ -1120,7 +1129,7 @@
t-att-placeholder="widget.node.attrs.placeholder" t-att-placeholder="widget.node.attrs.placeholder"
/> />
<span class="oe_m2o_drop_down_button"> <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> </span>
</div> </div>
</t> </t>
@ -1515,7 +1524,7 @@
<t t-esc="attrs.string"/> <t t-esc="attrs.string"/>
</button> </button>
<ul t-name="SearchView.filters"> <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-att-title="filter.attrs.string ? filter.attrs.help : undefined">
<t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/> <t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
</li> </li>
@ -1603,15 +1612,6 @@
</div> </div>
</div> </div>
</t> </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 t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section">
</div> </div>
@ -1842,7 +1842,7 @@
<tr> <tr>
<td t-foreach="records[0]" t-as="column"> <td t-foreach="records[0]" t-as="column">
<input class="sel_fields" placeholder="--- Don't Import ---"/><span class="oe_m2o_drop_down_button"> <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> </td>
</tr> </tr>
<tr t-foreach="records" t-as="record" class="oe_import_grid-row"> <tr t-foreach="records" t-as="record" class="oe_import_grid-row">

View File

@ -1,4 +1,4 @@
openerp.testing.section('query', { openerp.testing.section('search.query', {
dependencies: ['web.search'] dependencies: ['web.search']
}, function (test) { }, function (test) {
test('Adding a facet to the query creates a facet and a value', function (instance) { 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; return view;
}; };
openerp.testing.section('defaults', { openerp.testing.section('search.defaults', {
dependencies: ['web.search'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true, templates: true,
@ -362,7 +362,7 @@ openerp.testing.section('defaults', {
"should not accept multiple default values"); "should not accept multiple default values");
}) })
}); });
openerp.testing.section('completions', { openerp.testing.section('search.completions', {
dependencies: ['web.search'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true templates: true
@ -615,7 +615,7 @@ openerp.testing.section('completions', {
return f.complete("bob"); return f.complete("bob");
}); });
}); });
openerp.testing.section('search-serialization', { openerp.testing.section('search.serialization', {
dependencies: ['web.search'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true templates: true
@ -855,11 +855,11 @@ openerp.testing.section('search-serialization', {
test('FilterGroup', {asserts: 6}, function (instance) { test('FilterGroup', {asserts: 6}, function (instance) {
var view = {inputs: [], query: {on: function () {}}}; var view = {inputs: [], query: {on: function () {}}};
var filter_a = new instance.web.search.Filter( 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( 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( 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( var group = new instance.web.search.FilterGroup(
[filter_a, filter_b, filter_c], view); [filter_a, filter_b, filter_c], view);
return group.facet_for_defaults({a: true, c: true}) return group.facet_for_defaults({a: true, c: true})
@ -880,7 +880,7 @@ openerp.testing.section('search-serialization', {
equal(context.__ref, 'compound_context', equal(context.__ref, 'compound_context',
"context should be compound"); "context should be compound");
deepEqual(context.__contexts, [ deepEqual(context.__contexts, [
'c1', 'c3' '{"c1": True}', '{"c3": True}'
], "context should merge all filter contexts"); ], "context should merge all filter contexts");
ok(!context.get_eval_context(), "context should have no evaluation context"); ok(!context.get_eval_context(), "context should have no evaluation context");
}); });
@ -922,7 +922,7 @@ openerp.testing.section('search-serialization', {
return $.when(t1, t2); return $.when(t1, t2);
}); });
}); });
openerp.testing.section('removal', { openerp.testing.section('search.removal', {
dependencies: ['web.search'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true templates: true
@ -945,7 +945,7 @@ openerp.testing.section('removal', {
}); });
}); });
}); });
openerp.testing.section('drawer', { openerp.testing.section('search.drawer', {
dependencies: ['web.search'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true templates: true
@ -961,7 +961,7 @@ openerp.testing.section('drawer', {
}); });
}); });
}); });
openerp.testing.section('filters', { openerp.testing.section('search.filters', {
dependencies: ['web.search'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true, 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'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true templates: true
@ -1127,8 +1224,30 @@ openerp.testing.section('saved_filters', {
"should have selected second filter"); "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'], dependencies: ['web.search'],
rpc: 'mock', rpc: 'mock',
templates: true templates: true
@ -1209,3 +1328,159 @@ openerp.testing.section('advanced', {
}); });
// TODO: UI tests? // 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");
});
});
});

View File

@ -1,5 +1,5 @@
.openerp a.dropdown-menu-icon { .openerp a.dropdown-menu-icon {
z-index: 10; z-index: 1;
position: absolute; position: absolute;
color: #4c4c4c; color: #4c4c4c;
right: 8px; right: 8px;
@ -23,7 +23,7 @@
padding: 8px; padding: 8px;
border: 1px solid #afafb6; border: 1px solid #afafb6;
background: white; background: white;
z-index: 900; z-index: 1;
min-width: 160px; min-width: 160px;
overflow-x: hidden; overflow-x: hidden;
-moz-border-radius: 3px; -moz-border-radius: 3px;

View File

@ -246,7 +246,7 @@ instance.web_graph.GraphView = instance.web.View.extend({
var result = []; var result = [];
var ticks = {}; 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; view_get = tmp;
fields = view_get['fields']; fields = view_get['fields'];
var toload = _.select(group_by, function(x) { return fields[x] === undefined }); var toload = _.select(group_by, function(x) { return fields[x] === undefined });

View File

@ -7,6 +7,15 @@
background: url(/web/static/src/img/form_sheetbg.png); background: url(/web/static/src/img/form_sheetbg.png);
width: 100%; 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 { .openerp .oe_kanban_view .ui-sortable-placeholder {
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
visibility: visible !important; visibility: visible !important;
@ -92,15 +101,6 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; 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 { .openerp .oe_kanban_view .oe_fold_column .oe_kanban_group_length {
position: absolute; position: absolute;
top: -1px; top: -1px;
@ -108,9 +108,6 @@
float: right; float: right;
display: block; 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 { .openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_column, .openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_group_header {
width: 185px; width: 185px;
min-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 { .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; 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_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_length {
display: block; display: block;
} }
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_dropdown_kanban { .openerp .oe_kanban_view .oe_kanban_group_folded .oe_dropdown_kanban {

View File

@ -130,8 +130,8 @@
position: absolute position: absolute
top: -1px top: -1px
right: -14px right: -14px
display: block
float: right float: right
display: block
&.oe_kanban_grouped &.oe_kanban_grouped
.oe_kanban_column, .oe_kanban_group_header .oe_kanban_column, .oe_kanban_group_header
width: 185px width: 185px
@ -197,7 +197,7 @@
top: -8px top: -8px
.oe_kanban_header .oe_dropdown_toggle .oe_kanban_header .oe_dropdown_toggle
top: -2px top: -2px
height: 14px; height: 14px
.oe_kanban_card, .oe_dropdown_toggle .oe_kanban_card, .oe_dropdown_toggle
cursor: pointer cursor: pointer
display: inline-block display: inline-block