diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 58abda3fc30..41bb6ccfca7 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -90,16 +90,12 @@ db_list = http.db_list db_monodb = http.db_monodb def redirect_with_hash(url, code=303): - redirect_code = "" % url - if request.httprequest.user_agent.browser == 'msie': - try: - version = float(request.httprequest.user_agent.version) - if version < 10: - return redirect_code - except Exception: - pass - elif request.httprequest.user_agent.browser == 'safari': - return redirect_code + if request.httprequest.user_agent.browser in ('msie', 'safari'): + # Most IE and Safari versions decided not to preserve location.hash upon + # redirect. And even if IE10 pretends to support it, it still fails + # inexplicably in case of multiple redirects (and we do have some). + # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/ + return "" % url return werkzeug.utils.redirect(url, code) def module_topological_sort(modules): diff --git a/addons/web/static/lib/underscore.string/lib/underscore.string.js b/addons/web/static/lib/underscore.string/lib/underscore.string.js index b0be69bf793..87611173bac 100644 --- a/addons/web/static/lib/underscore.string/lib/underscore.string.js +++ b/addons/web/static/lib/underscore.string/lib/underscore.string.js @@ -1,12 +1,11 @@ -// Underscore.string -// (c) 2010 Esa-Matti Suuronen -// Underscore.strings is freely distributable under the terms of the MIT license. -// Documentation: https://github.com/epeli/underscore.string -// Some code is borrowed from MooTools and Alexandru Marasteanu. +// Underscore.string +// (c) 2010 Esa-Matti Suuronen +// Underscore.string is freely distributable under the terms of the MIT license. +// Documentation: https://github.com/epeli/underscore.string +// Some code is borrowed from MooTools and Alexandru Marasteanu. +// Version '2.3.2' -// Version 2.2.0rc - -(function(root){ +!function(root, String){ 'use strict'; // Defining helper functions. @@ -16,36 +15,51 @@ var nativeTrimLeft = String.prototype.trimLeft; var parseNumber = function(source) { return source * 1 || 0; }; - - var strRepeat = function(str, qty, separator){ - // ~~var — is the fastest available way to convert anything to Integer in javascript. - // We'll use it extensively in this lib. - str += ''; qty = ~~qty; - for (var repeat = []; qty > 0; repeat[--qty] = str) {} - return repeat.join(separator == null ? '' : separator); - }; - var slice = function(a){ - return Array.prototype.slice.call(a); - }; - - var defaultToWhiteSpace = function(characters){ - if (characters != null) { - return '[' + _s.escapeRegExp(''+characters) + ']'; + var strRepeat = function(str, qty){ + if (qty < 1) return ''; + var result = ''; + while (qty > 0) { + if (qty & 1) result += str; + qty >>= 1, str += str; } - return '\\s'; + return result; }; - + + var slice = [].slice; + + var defaultToWhiteSpace = function(characters) { + if (characters == null) + return '\\s'; + else if (characters.source) + return characters.source; + else + return '[' + _s.escapeRegExp(characters) + ']'; + }; + + // Helper for toBoolean + function boolMatch(s, matchers) { + var i, matcher, down = s.toLowerCase(); + matchers = [].concat(matchers); + for (i = 0; i < matchers.length; i += 1) { + matcher = matchers[i]; + if (!matcher) continue; + if (matcher.test && matcher.test(s)) return true; + if (matcher.toLowerCase() === down) return true; + } + } + var escapeChars = { lt: '<', gt: '>', quot: '"', - apos: "'", - amp: '&' + amp: '&', + apos: "'" }; - + var reversedEscapeChars = {}; - for(var key in escapeChars){ reversedEscapeChars[escapeChars[key]] = key; } + for(var key in escapeChars) reversedEscapeChars[escapeChars[key]] = key; + reversedEscapeChars["'"] = '#39'; // sprintf() for JavaScript 0.7-beta1 // http://www.diveintojavascript.com/projects/javascript-sprintf @@ -175,28 +189,28 @@ var _s = { - VERSION: '2.2.0rc', + VERSION: '2.3.0', isBlank: function(str){ + if (str == null) str = ''; return (/^\s*$/).test(str); }, stripTags: function(str){ - return (''+str).replace(/<\/?[^>]+>/g, ''); + if (str == null) return ''; + return String(str).replace(/<\/?[^>]+>/g, ''); }, - capitalize : function(str) { - str += ''; - return str.charAt(0).toUpperCase() + str.substring(1); + capitalize : function(str){ + str = str == null ? '' : String(str); + return str.charAt(0).toUpperCase() + str.slice(1); }, chop: function(str, step){ - str = str+''; - step = ~~step || str.length; - var arr = []; - for (var i = 0; i < str.length; i += step) - arr.push(str.slice(i,i + step)); - return arr; + if (str == null) return []; + str = String(str); + step = ~~step; + return step > 0 ? str.match(new RegExp('.{1,' + step + '}', 'g')) : [str]; }, clean: function(str){ @@ -204,22 +218,47 @@ }, count: function(str, substr){ - str += ''; substr += ''; - return str.split(substr).length - 1; + if (str == null || substr == null) return 0; + + str = String(str); + substr = String(substr); + + var count = 0, + pos = 0, + length = substr.length; + + while (true) { + pos = str.indexOf(substr, pos); + if (pos === -1) break; + count++; + pos += length; + } + + return count; }, chars: function(str) { - return (''+str).split(''); + if (str == null) return []; + return String(str).split(''); + }, + + swapCase: function(str) { + if (str == null) return ''; + return String(str).replace(/\S/g, function(c){ + return c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase(); + }); }, escapeHTML: function(str) { - return (''+str).replace(/[&<>"']/g, function(match){ return '&' + reversedEscapeChars[match] + ';'; }); + if (str == null) return ''; + return String(str).replace(/[&<>"']/g, function(m){ return '&' + reversedEscapeChars[m] + ';'; }); }, unescapeHTML: function(str) { - return (''+str).replace(/\&([^;]+);/g, function(entity, entityCode){ + if (str == null) return ''; + return String(str).replace(/\&([^;]+);/g, function(entity, entityCode){ var match; - + if (entityCode in escapeChars) { return escapeChars[entityCode]; } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) { @@ -233,31 +272,8 @@ }, escapeRegExp: function(str){ - // From MooTools core 1.2.4 - return str.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1'); - }, - - insert: function(str, i, substr){ - var arr = _s.chars(str); - arr.splice(~~i, 0, ''+substr); - return arr.join(''); - }, - - include: function(str, needle){ - return !!~(''+str).indexOf(needle); - }, - - join: function() { - var args = slice(arguments); - return args.join(args.shift()); - }, - - lines: function(str) { - return (''+str).split("\n"); - }, - - reverse: function(str){ - return _s.chars(str).reverse().join(''); + if (str == null) return ''; + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); }, splice: function(str, i, howmany, substr){ @@ -266,31 +282,62 @@ return arr.join(''); }, + insert: function(str, i, substr){ + return _s.splice(str, i, 0, substr); + }, + + include: function(str, needle){ + if (needle === '') return true; + if (str == null) return false; + return String(str).indexOf(needle) !== -1; + }, + + join: function() { + var args = slice.call(arguments), + separator = args.shift(); + + if (separator == null) separator = ''; + + return args.join(separator); + }, + + lines: function(str) { + if (str == null) return []; + return String(str).split("\n"); + }, + + reverse: function(str){ + return _s.chars(str).reverse().join(''); + }, + startsWith: function(str, starts){ - str += ''; starts += ''; - return str.length >= starts.length && str.substring(0, starts.length) === starts; + if (starts === '') return true; + if (str == null || starts == null) return false; + str = String(str); starts = String(starts); + return str.length >= starts.length && str.slice(0, starts.length) === starts; }, endsWith: function(str, ends){ - str += ''; ends += ''; - return str.length >= ends.length && str.substring(str.length - ends.length) === ends; + if (ends === '') return true; + if (str == null || ends == null) return false; + str = String(str); ends = String(ends); + return str.length >= ends.length && str.slice(str.length - ends.length) === ends; }, succ: function(str){ - str += ''; - var arr = _s.chars(str); - arr.splice(str.length-1, 1, String.fromCharCode(str.charCodeAt(str.length-1) + 1)); - return arr.join(''); + if (str == null) return ''; + str = String(str); + return str.slice(0, -1) + String.fromCharCode(str.charCodeAt(str.length-1) + 1); }, titleize: function(str){ - return (''+str).replace(/\b./g, function(ch){ return ch.toUpperCase(); }); + if (str == null) return ''; + str = String(str).toLowerCase(); + return str.replace(/(?:^|\s|-)\S/g, function(c){ return c.toUpperCase(); }); }, camelize: function(str){ - return _s.trim(str).replace(/[-_\s]+(.)?/g, function(match, chr){ - return chr && chr.toUpperCase(); - }); + return _s.trim(str).replace(/[-_\s]+(.)?/g, function(match, c){ return c ? c.toUpperCase() : ""; }); }, underscored: function(str){ @@ -298,45 +345,41 @@ }, dasherize: function(str){ - return _s.trim(str).replace(/[_\s]+/g, '-').replace(/([A-Z])/g, '-$1').replace(/-+/g, '-').toLowerCase(); + return _s.trim(str).replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase(); }, classify: function(str){ - str += ''; - return _s.titleize(str.replace(/_/g, ' ')).replace(/\s/g, '') + return _s.titleize(String(str).replace(/[\W_]/g, ' ')).replace(/\s/g, ''); }, humanize: function(str){ - return _s.capitalize(this.underscored(str).replace(/_id$/,'').replace(/_/g, ' ')); + return _s.capitalize(_s.underscored(str).replace(/_id$/,'').replace(/_/g, ' ')); }, trim: function(str, characters){ - str += ''; - if (!characters && nativeTrim) { return nativeTrim.call(str); } + if (str == null) return ''; + if (!characters && nativeTrim) return nativeTrim.call(str); characters = defaultToWhiteSpace(characters); - return str.replace(new RegExp('\^' + characters + '+|' + characters + '+$', 'g'), ''); + return String(str).replace(new RegExp('\^' + characters + '+|' + characters + '+$', 'g'), ''); }, ltrim: function(str, characters){ - str+=''; - if (!characters && nativeTrimLeft) { - return nativeTrimLeft.call(str); - } + if (str == null) return ''; + if (!characters && nativeTrimLeft) return nativeTrimLeft.call(str); characters = defaultToWhiteSpace(characters); - return str.replace(new RegExp('^' + characters + '+'), ''); + return String(str).replace(new RegExp('^' + characters + '+'), ''); }, rtrim: function(str, characters){ - str+=''; - if (!characters && nativeTrimRight) { - return nativeTrimRight.call(str); - } + if (str == null) return ''; + if (!characters && nativeTrimRight) return nativeTrimRight.call(str); characters = defaultToWhiteSpace(characters); - return str.replace(new RegExp(characters + '+$'), ''); + return String(str).replace(new RegExp(characters + '+$'), ''); }, truncate: function(str, length, truncateStr){ - str += ''; truncateStr = truncateStr || '...'; + if (str == null) return ''; + str = String(str); truncateStr = truncateStr || '...'; length = ~~length; return str.length > length ? str.slice(0, length) + truncateStr : str; }, @@ -344,57 +387,53 @@ /** * _s.prune: a more elegant version of truncate * prune extra chars, never leaving a half-chopped word. - * @author github.com/sergiokas + * @author github.com/rwz */ prune: function(str, length, pruneStr){ - str += ''; length = ~~length; - pruneStr = pruneStr != null ? ''+pruneStr : '...'; - - var pruned, borderChar, template = str.replace(/\W/g, function(ch){ - return (ch.toUpperCase() !== ch.toLowerCase()) ? 'A' : ' '; - }); - - borderChar = template.charAt(length); - - pruned = template.slice(0, length); - - // Check if we're in the middle of a word - if (borderChar && borderChar.match(/\S/)) - pruned = pruned.replace(/\s\S+$/, ''); - - pruned = _s.rtrim(pruned); - - return (pruned+pruneStr).length > str.length ? str : str.substring(0, pruned.length)+pruneStr; + if (str == null) return ''; + + str = String(str); length = ~~length; + pruneStr = pruneStr != null ? String(pruneStr) : '...'; + + if (str.length <= length) return str; + + var tmpl = function(c){ return c.toUpperCase() !== c.toLowerCase() ? 'A' : ' '; }, + template = str.slice(0, length+1).replace(/.(?=\W*\w*$)/g, tmpl); // 'Hello, world' -> 'HellAA AAAAA' + + if (template.slice(template.length-2).match(/\w\w/)) + template = template.replace(/\s*\S+$/, ''); + else + template = _s.rtrim(template.slice(0, template.length-1)); + + return (template+pruneStr).length > str.length ? str : str.slice(0, template.length)+pruneStr; }, words: function(str, delimiter) { + if (_s.isBlank(str)) return []; return _s.trim(str, delimiter).split(delimiter || /\s+/); }, pad: function(str, length, padStr, type) { - str += ''; - + str = str == null ? '' : String(str); + length = ~~length; + var padlen = 0; - length = ~~length; - - if (!padStr) { + if (!padStr) padStr = ' '; - } else if (padStr.length > 1) { + else if (padStr.length > 1) padStr = padStr.charAt(0); - } - + switch(type) { case 'right': - padlen = (length - str.length); + padlen = length - str.length; return str + strRepeat(padStr, padlen); case 'both': - padlen = (length - str.length); - return strRepeat(padStr, Math.ceil(padlen/2)) + - str + - strRepeat(padStr, Math.floor(padlen/2)); + padlen = length - str.length; + return strRepeat(padStr, Math.ceil(padlen/2)) + str + + strRepeat(padStr, Math.floor(padlen/2)); default: // 'left' - padlen = (length - str.length); + padlen = length - str.length; return strRepeat(padStr, padlen) + str; } }, @@ -419,76 +458,185 @@ }, toNumber: function(str, decimals) { - str += ''; - var num = parseNumber(parseNumber(str).toFixed(~~decimals)); - return num === 0 && !str.match(/^0+$/) ? Number.NaN : num; + if (!str) return 0; + str = _s.trim(str); + if (!str.match(/^-?\d+(?:\.\d+)?$/)) return NaN; + return parseNumber(parseNumber(str).toFixed(~~decimals)); + }, + + numberFormat : function(number, dec, dsep, tsep) { + if (isNaN(number) || number == null) return ''; + + number = number.toFixed(~~dec); + tsep = typeof tsep == 'string' ? tsep : ','; + + var parts = number.split('.'), fnums = parts[0], + decimals = parts[1] ? (dsep || '.') + parts[1] : ''; + + return fnums.replace(/(\d)(?=(?:\d{3})+$)/g, '$1' + tsep) + decimals; }, strRight: function(str, sep){ - str += ''; sep = sep != null ? ''+sep : sep; + if (str == null) return ''; + str = String(str); sep = sep != null ? String(sep) : sep; var pos = !sep ? -1 : str.indexOf(sep); return ~pos ? str.slice(pos+sep.length, str.length) : str; }, strRightBack: function(str, sep){ - str += ''; sep = sep != null ? ''+sep : sep; + if (str == null) return ''; + str = String(str); sep = sep != null ? String(sep) : sep; var pos = !sep ? -1 : str.lastIndexOf(sep); return ~pos ? str.slice(pos+sep.length, str.length) : str; }, strLeft: function(str, sep){ - str += ''; sep = sep != null ? ''+sep : sep; + if (str == null) return ''; + str = String(str); sep = sep != null ? String(sep) : sep; var pos = !sep ? -1 : str.indexOf(sep); return ~pos ? str.slice(0, pos) : str; }, strLeftBack: function(str, sep){ + if (str == null) return ''; str += ''; sep = sep != null ? ''+sep : sep; var pos = str.lastIndexOf(sep); return ~pos ? str.slice(0, pos) : str; }, - toSentence: function(array, separator, lastSeparator) { - separator || (separator = ', '); - lastSeparator || (lastSeparator = ' and '); - var length = array.length, str = ''; + toSentence: function(array, separator, lastSeparator, serial) { + separator = separator || ', '; + lastSeparator = lastSeparator || ' and '; + var a = array.slice(), lastMember = a.pop(); - for (var i = 0; i < length; i++) { - str += array[i]; - if (i === (length - 2)) { str += lastSeparator; } - else if (i < (length - 1)) { str += separator; } - } + if (array.length > 2 && serial) lastSeparator = _s.rtrim(separator) + lastSeparator; - return str; + return a.length ? a.join(separator) + lastSeparator + lastMember : lastMember; + }, + + toSentenceSerial: function() { + var args = slice.call(arguments); + args[3] = true; + return _s.toSentence.apply(_s, args); }, slugify: function(str) { - var from = "ąàáäâãćęèéëêìíïîłńòóöôõùúüûñçżź", - to = "aaaaaaceeeeeiiiilnooooouuuunczz", + if (str == null) return ''; + + var from = "ąàáäâãåæăćęèéëêìíïîłńòóöôõøśșțùúüûñçżź", + to = "aaaaaaaaaceeeeeiiiilnoooooosstuuuunczz", regex = new RegExp(defaultToWhiteSpace(from), 'g'); - str = (''+str).toLowerCase(); - - str = str.replace(regex, function(ch){ - var index = from.indexOf(ch); + str = String(str).toLowerCase().replace(regex, function(c){ + var index = from.indexOf(c); return to.charAt(index) || '-'; }); - return _s.trim(str.replace(/[^\w\s-]/g, '').replace(/[-\s]+/g, '-'), '-'); + return _s.dasherize(str.replace(/[^\w\s-]/g, '')); + }, + + surround: function(str, wrapper) { + return [wrapper, str, wrapper].join(''); + }, + + quote: function(str, quoteChar) { + return _s.surround(str, quoteChar || '"'); + }, + + unquote: function(str, quoteChar) { + quoteChar = quoteChar || '"'; + if (str[0] === quoteChar && str[str.length-1] === quoteChar) + return str.slice(1,str.length-1); + else return str; }, exports: function() { var result = {}; for (var prop in this) { - if (!this.hasOwnProperty(prop) || ~_s.words('include contains reverse').indexOf(prop)) continue; + if (!this.hasOwnProperty(prop) || prop.match(/^(?:include|contains|reverse)$/)) continue; result[prop] = this[prop]; } return result; }, - - repeat: strRepeat + + repeat: function(str, qty, separator){ + if (str == null) return ''; + + qty = ~~qty; + + // using faster implementation if separator is not needed; + if (separator == null) return strRepeat(String(str), qty); + + // this one is about 300x slower in Google Chrome + for (var repeat = []; qty > 0; repeat[--qty] = str) {} + return repeat.join(separator); + }, + + naturalCmp: function(str1, str2){ + if (str1 == str2) return 0; + if (!str1) return -1; + if (!str2) return 1; + + var cmpRegex = /(\.\d+)|(\d+)|(\D+)/g, + tokens1 = String(str1).toLowerCase().match(cmpRegex), + tokens2 = String(str2).toLowerCase().match(cmpRegex), + count = Math.min(tokens1.length, tokens2.length); + + for(var i = 0; i < count; i++) { + var a = tokens1[i], b = tokens2[i]; + + if (a !== b){ + var num1 = parseInt(a, 10); + if (!isNaN(num1)){ + var num2 = parseInt(b, 10); + if (!isNaN(num2) && num1 - num2) + return num1 - num2; + } + return a < b ? -1 : 1; + } + } + + if (tokens1.length === tokens2.length) + return tokens1.length - tokens2.length; + + return str1 < str2 ? -1 : 1; + }, + + levenshtein: function(str1, str2) { + if (str1 == null && str2 == null) return 0; + if (str1 == null) return String(str2).length; + if (str2 == null) return String(str1).length; + + str1 = String(str1); str2 = String(str2); + + var current = [], prev, value; + + for (var i = 0; i <= str2.length; i++) + for (var j = 0; j <= str1.length; j++) { + if (i && j) + if (str1.charAt(j - 1) === str2.charAt(i - 1)) + value = prev; + else + value = Math.min(current[j], current[j - 1], prev) + 1; + else + value = i + j; + + prev = current[j]; + current[j] = value; + } + + return current.pop(); + }, + + toBoolean: function(str, trueValues, falseValues) { + if (typeof str === "number") str = "" + str; + if (typeof str !== "string") return !!str; + str = _s.trim(str); + if (boolMatch(str, trueValues || ["true", "1"])) return true; + if (boolMatch(str, falseValues || ["false", "0"])) return false; + } }; // Aliases @@ -500,26 +648,26 @@ _s.rjust = _s.lpad; _s.ljust = _s.rpad; _s.contains = _s.include; + _s.q = _s.quote; + _s.toBool = _s.toBoolean; + + // Exporting // CommonJS module is defined if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - // Export module + if (typeof module !== 'undefined' && module.exports) module.exports = _s; - } - exports._s = _s; - } else if (typeof define === 'function' && define.amd) { - // Register as a named module with AMD. - define('underscore.string', function() { - return _s; - }); - - } else { - // Integrate with Underscore.js if defined - // or create our own underscore object. - root._ = root._ || {}; - root._.string = root._.str = _s; + exports._s = _s; } -}(this || window)); + // Register as a named module with AMD. + if (typeof define === 'function' && define.amd) + define('underscore.string', [], function(){ return _s; }); + + + // Integrate with Underscore.js if defined + // or create our own underscore object. + root._ = root._ || {}; + root._.string = root._.str = _s; +}(this, String); diff --git a/addons/web/static/lib/underscore/underscore.js b/addons/web/static/lib/underscore/underscore.js index 208d4cd890c..b50115df5c1 100644 --- a/addons/web/static/lib/underscore/underscore.js +++ b/addons/web/static/lib/underscore/underscore.js @@ -1,17 +1,14 @@ -// Underscore.js 1.3.1 -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore is freely distributable under the MIT license. -// Portions of Underscore are inspired or borrowed from Prototype, -// Oliver Steele's Functional, and John Resig's Micro-Templating. -// For all details and documentation: -// http://documentcloud.github.com/underscore +// Underscore.js 1.5.2 +// http://underscorejs.org +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. (function() { // Baseline setup // -------------- - // Establish the root object, `window` in the browser, or `global` on the server. + // Establish the root object, `window` in the browser, or `exports` on the server. var root = this; // Save the previous value of the `_` variable. @@ -24,10 +21,12 @@ var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. - var slice = ArrayProto.slice, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. @@ -46,7 +45,11 @@ nativeBind = FuncProto.bind; // Create a safe reference to the Underscore object for use below. - var _ = function(obj) { return new wrapper(obj); }; + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; // Export the Underscore object for **Node.js**, with // backwards-compatibility for the old `require()` API. If we're in @@ -58,11 +61,11 @@ } exports._ = _; } else { - root['_'] = _; + root._ = _; } // Current version. - _.VERSION = '1.3.1'; + _.VERSION = '1.5.2'; // Collection Functions // -------------------- @@ -75,14 +78,13 @@ if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; + for (var i = 0, length = obj.length; i < length; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; } } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; } } }; @@ -94,12 +96,13 @@ if (obj == null) return results; if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { - results[results.length] = iterator.call(context, value, index, list); + results.push(iterator.call(context, value, index, list)); }); - if (obj.length === +obj.length) results.length = obj.length; return results; }; + var reduceError = 'Reduce of empty array with no initial value'; + // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { @@ -117,7 +120,7 @@ memo = iterator.call(context, memo, value, index, list); } }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + if (!initial) throw new TypeError(reduceError); return memo; }; @@ -130,9 +133,22 @@ if (context) iterator = _.bind(iterator, context); return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } - var reversed = _.toArray(obj).reverse(); - if (context && !initial) iterator = _.bind(iterator, context); - return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; }; // Return the first value which passes a truth test. Aliased as `detect`. @@ -155,32 +171,30 @@ if (obj == null) return results; if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; + if (iterator.call(context, value, index, list)) results.push(value); }); return results; }; // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); }; // Determine whether all of the elements match a truth test. // Delegates to **ECMAScript 5**'s native `every` if available. // Aliased as `all`. _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); var result = true; if (obj == null) return result; if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); each(obj, function(value, index, list) { if (!(result = result && iterator.call(context, value, index, list))) return breaker; }); - return result; + return !!result; }; // Determine if at least one element in the object matches a truth test. @@ -197,23 +211,22 @@ return !!result; }; - // Determine if a given value is included in the array or object using `===`. - // Aliased as `contains`. - _.include = _.contains = function(obj, target) { - var found = false; - if (obj == null) return found; + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { + return any(obj, function(value) { return value === target; }); - return found; }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); return _.map(obj, function(value) { - return (_.isFunction(method) ? method || value : value[method]).apply(value, args); + return (isFunc ? method : value[method]).apply(value, args); }); }; @@ -222,23 +235,47 @@ return _.map(obj, function(value){ return value[key]; }); }; + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return _[first ? 'find' : 'filter'](obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) _.max = function(obj, iterator, context) { - if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; + var result = {computed : -Infinity, value: -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; - computed >= result.computed && (result = {value : value, computed : computed}); + computed > result.computed && (result = {value : value, computed : computed}); }); return result.value; }; // Return the minimum element (or element-based computation). _.min = function(obj, iterator, context) { - if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; + var result = {computed : Infinity, value: Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); @@ -246,83 +283,126 @@ return result.value; }; - // Shuffle an array. + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). _.shuffle = function(obj) { - var shuffled = [], rand; - each(obj, function(value, index, list) { - if (index == 0) { - shuffled[0] = value; - } else { - rand = Math.floor(Math.random() * (index + 1)); - shuffled[index] = shuffled[rand]; - shuffled[rand] = value; - } + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; }); return shuffled; }; + // Sample **n** random values from an array. + // If **n** is not specified, returns a single random element from the array. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (arguments.length < 2 || guard) { + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + // Sort the object's values by a criterion produced by an iterator. - _.sortBy = function(obj, iterator, context) { + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); return _.pluck(_.map(obj, function(value, index, list) { return { - value : value, - criteria : iterator.call(context, value, index, list) + value: value, + index: index, + criteria: iterator.call(context, value, index, list) }; }).sort(function(left, right) { - var a = left.criteria, b = right.criteria; - return a < b ? -1 : a > b ? 1 : 0; + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; }), 'value'); }; + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, value, context) { + var result = {}; + var iterator = value == null ? _.identity : lookupIterator(value); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + }; + // Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. - _.groupBy = function(obj, val) { - var result = {}; - var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; - each(obj, function(value, index) { - var key = iterator(value, index); - (result[key] || (result[key] = [])).push(value); - }); - return result; - }; + _.groupBy = group(function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); - // Use a comparator function to figure out at what index an object should - // be inserted so as to maintain order. Uses binary search. - _.sortedIndex = function(array, obj, iterator) { - iterator || (iterator = _.identity); + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, key, value) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); var low = 0, high = array.length; while (low < high) { - var mid = (low + high) >> 1; - iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; } return low; }; - // Safely convert anything iterable into a real, live array. - _.toArray = function(iterable) { - if (!iterable) return []; - if (iterable.toArray) return iterable.toArray(); - if (_.isArray(iterable)) return slice.call(iterable); - if (_.isArguments(iterable)) return slice.call(iterable); - return _.values(iterable); + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { - return _.toArray(obj).length; + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; }; // Array Functions // --------------- // Get the first element of an array. Passing **n** will return the first N - // values in the array. Aliased as `head`. The **guard** check allows it to work - // with `_.map`. - _.first = _.head = function(array, n, guard) { - return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + return (n == null) || guard ? array[0] : slice.call(array, 0, n); }; - // Returns everything but the last entry of the array. Especcialy useful on + // Returns everything but the last entry of the array. Especially useful on // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. The **guard** check allows it to work with // `_.map`. @@ -333,33 +413,45 @@ // Get the last element of an array. Passing **n** will return the last N // values in the array. The **guard** check allows it to work with `_.map`. _.last = function(array, n, guard) { - if ((n != null) && !guard) { - return slice.call(array, Math.max(array.length - n, 0)); - } else { + if (array == null) return void 0; + if ((n == null) || guard) { return array[array.length - 1]; + } else { + return slice.call(array, Math.max(array.length - n, 0)); } }; - // Returns everything but the first entry of the array. Aliased as `tail`. - // Especially useful on the arguments object. Passing an **index** will return - // the rest of the values in the array from that index onward. The **guard** + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** // check allows it to work with `_.map`. - _.rest = _.tail = function(array, index, guard) { - return slice.call(array, (index == null) || guard ? 1 : index); + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); }; // Trim out all falsy values from an array. _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); + return _.filter(array, _.identity); }; - // Return a completely flattened version of an array. + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + each(input, function(value) { + if (_.isArray(value) || _.isArguments(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. _.flatten = function(array, shallow) { - return _.reduce(array, function(memo, value) { - if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); - memo[memo.length] = value; - return memo; - }, []); + return flatten(array, shallow, []); }; // Return a version of the array that does not contain the specified value(s). @@ -370,17 +462,22 @@ // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. - _.uniq = _.unique = function(array, isSorted, iterator) { - var initial = iterator ? _.map(array, iterator) : array; - var result = []; - _.reduce(initial, function(memo, el, i) { - if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { - memo[memo.length] = el; - result[result.length] = array[i]; + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); } - return memo; - }, []); - return result; + }); + return results; }; // Produce an array that contains the union: each distinct element from all of @@ -390,8 +487,8 @@ }; // Produce an array that contains every item shared between all the - // passed-in arrays. (Aliased as "intersect" for back-compat.) - _.intersection = _.intersect = function(array) { + // passed-in arrays. + _.intersection = function(array) { var rest = slice.call(arguments, 1); return _.filter(_.uniq(array), function(item) { return _.every(rest, function(other) { @@ -403,20 +500,37 @@ // Take the difference between one array and a number of other arrays. // Only the elements present in just the first array will remain. _.difference = function(array) { - var rest = _.flatten(slice.call(arguments, 1)); - return _.filter(array, function(value){ return !_.include(rest, value); }); + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); }; // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { - var args = slice.call(arguments); - var length = _.max(_.pluck(args, 'length')); + var length = _.max(_.pluck(arguments, "length").concat(0)); var results = new Array(length); - for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } return results; }; + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), // we need this function. Return the position of the first occurrence of an // item in an array, or -1 if the item is not included in the array. @@ -425,22 +539,29 @@ // for **isSorted** to use binary search. _.indexOf = function(array, item, isSorted) { if (array == null) return -1; - var i, l; + var i = 0, length = array.length; if (isSorted) { - i = _.sortedIndex(array, item); - return array[i] === item ? i : -1; + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } } - if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); - for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < length; i++) if (array[i] === item) return i; return -1; }; // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. - _.lastIndexOf = function(array, item) { + _.lastIndexOf = function(array, item, from) { if (array == null) return -1; - if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); - var i = array.length; - while (i--) if (i in array && array[i] === item) return i; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; return -1; }; @@ -454,11 +575,11 @@ } step = arguments[2] || 1; - var len = Math.max(Math.ceil((stop - start) / step), 0); + var length = Math.max(Math.ceil((stop - start) / step), 0); var idx = 0; - var range = new Array(len); + var range = new Array(length); - while(idx < len) { + while(idx < length) { range[idx++] = start; start += step; } @@ -473,29 +594,38 @@ var ctor = function(){}; // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (!_.isFunction(func)) throw new TypeError; args = slice.call(arguments, 2); return bound = function() { if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); ctor.prototype = func.prototype; var self = new ctor; + ctor.prototype = null; var result = func.apply(self, args.concat(slice.call(arguments))); if (Object(result) === result) return result; return self; }; }; + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); + }; + }; + // Bind all of an object's methods to that object. Useful for ensuring that // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); + if (funcs.length === 0) throw new Error("bindAll must be passed function names"); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; @@ -514,7 +644,7 @@ // it with the arguments supplied. _.delay = function(func, wait) { var args = slice.call(arguments, 2); - return setTimeout(function(){ return func.apply(func, args); }, wait); + return setTimeout(function(){ return func.apply(null, args); }, wait); }; // Defers a function, scheduling it to run after the current call stack has @@ -524,41 +654,63 @@ }; // Returns a function, that, when invoked, will only be triggered at most once - // during a given window of time. - _.throttle = function(func, wait) { - var context, args, timeout, throttling, more; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : new Date; + timeout = null; + result = func.apply(context, args); + }; return function() { - context = this; args = arguments; - var later = function() { + var now = new Date; + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); timeout = null; - if (more) func.apply(context, args); - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - func.apply(context, args); + previous = now; + result = func.apply(context, args); + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); } - whenDone(); - throttling = true; + return result; }; }; // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for - // N milliseconds. - _.debounce = function(func, wait) { - var timeout; + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; return function() { - var context = this, args = arguments; + context = this; + args = arguments; + timestamp = new Date(); var later = function() { - timeout = null; - func.apply(context, args); + var last = (new Date()) - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) result = func.apply(context, args); + } }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) result = func.apply(context, args); + return result; }; }; @@ -569,7 +721,9 @@ return function() { if (ran) return memo; ran = true; - return memo = func.apply(this, arguments); + memo = func.apply(this, arguments); + func = null; + return memo; }; }; @@ -578,7 +732,8 @@ // conditionally execute the original function. _.wrap = function(func, wrapper) { return function() { - var args = [func].concat(slice.call(arguments, 0)); + var args = [func]; + push.apply(args, arguments); return wrapper.apply(this, args); }; }; @@ -598,9 +753,10 @@ // Returns a function that will only be executed after being called N times. _.after = function(times, func) { - if (times <= 0) return func(); return function() { - if (--times < 1) { return func.apply(this, arguments); } + if (--times < 1) { + return func.apply(this, arguments); + } }; }; @@ -612,13 +768,40 @@ _.keys = nativeKeys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + for (var key in obj) if (_.has(obj, key)) keys.push(key); return keys; }; // Retrieve the values of an object's properties. _.values = function(obj) { - return _.map(obj, _.identity); + var keys = _.keys(obj); + var length = keys.length; + var values = new Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = new Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; }; // Return a sorted list of the function names available on the object. @@ -634,18 +817,42 @@ // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } } }); return obj; }; + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + // Fill in a given object with default properties. _.defaults = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } } }); return obj; @@ -665,19 +872,16 @@ return obj; }; - // Internal recursive comparison function. - function eq(a, b, stack) { + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). if (a === b) return a !== 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; // Unwrap any wrapped objects. - if (a._chain) a = a._wrapped; - if (b._chain) b = b._wrapped; - // Invoke a custom `isEqual` method if one is provided. - if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); - if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. var className = toString.call(a); if (className != toString.call(b)) return false; @@ -707,14 +911,22 @@ if (typeof a != 'object' || typeof b != 'object') return false; // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = stack.length; + var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. - if (stack[length] == a) return true; + if (aStack[length] == a) return bStack[length] == b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; } // Add the first object to the stack of traversed objects. - stack.push(a); + aStack.push(a); + bStack.push(b); var size = 0, result = true; // Recursively compare objects and arrays. if (className == '[object Array]') { @@ -724,20 +936,17 @@ if (result) { // Deep compare the contents, ignoring non-numeric properties. while (size--) { - // Ensure commutative equality for sparse arrays. - if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; + if (!(result = eq(a[size], b[size], aStack, bStack))) break; } } } else { - // Objects with different constructors are not equivalent. - if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; // Deep compare objects. for (var key in a) { if (_.has(a, key)) { // Count the expected number of properties. size++; // Deep compare each member. - if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; } } // Ensure that both objects contain the same number of properties. @@ -749,18 +958,20 @@ } } // Remove the first object from the stack of traversed objects. - stack.pop(); + aStack.pop(); + bStack.pop(); return result; - } + }; // Perform a deep comparison to check if two objects are equal. _.isEqual = function(a, b) { - return eq(a, b, []); + return eq(a, b, [], []); }; // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { + if (obj == null) return true; if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; for (var key in obj) if (_.has(obj, key)) return false; return true; @@ -768,7 +979,7 @@ // Is a given value a DOM element? _.isElement = function(obj) { - return !!(obj && obj.nodeType == 1); + return !!(obj && obj.nodeType === 1); }; // Is a given value an array? @@ -782,35 +993,36 @@ return obj === Object(obj); }; - // Is a given variable an arguments object? - _.isArguments = function(obj) { - return toString.call(obj) == '[object Arguments]'; - }; + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. if (!_.isArguments(arguments)) { _.isArguments = function(obj) { return !!(obj && _.has(obj, 'callee')); }; } - // Is a given value a function? - _.isFunction = function(obj) { - return toString.call(obj) == '[object Function]'; + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); }; - // Is a given value a string? - _.isString = function(obj) { - return toString.call(obj) == '[object String]'; - }; - - // Is a given value a number? - _.isNumber = function(obj) { - return toString.call(obj) == '[object Number]'; - }; - - // Is the given value `NaN`? + // Is the given value `NaN`? (NaN is the only number which does not equal itself). _.isNaN = function(obj) { - // `NaN` is the only value for which `===` is not reflexive. - return obj !== obj; + return _.isNumber(obj) && obj != +obj; }; // Is a given value a boolean? @@ -818,16 +1030,6 @@ return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; }; - // Is a given value a date? - _.isDate = function(obj) { - return toString.call(obj) == '[object Date]'; - }; - - // Is the given value a regular expression? - _.isRegExp = function(obj) { - return toString.call(obj) == '[object RegExp]'; - }; - // Is a given value equal to null? _.isNull = function(obj) { return obj === null; @@ -838,7 +1040,8 @@ return obj === void 0; }; - // Has own property? + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). _.has = function(obj, key) { return hasOwnProperty.call(obj, key); }; @@ -859,20 +1062,66 @@ }; // Run a function **n** times. - _.times = function (n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; }; - // Escape a string for HTML interpolation. - _.escape = function(string) { - return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); }; - // Add your own custom functions to the Underscore object, ensuring that - // they're correctly added to the OOP wrapper as well. + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. _.mixin = function(obj) { - each(_.functions(obj), function(name){ - addToWrapper(name, _[name] = obj[name]); + each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; }); }; @@ -880,7 +1129,7 @@ // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { - var id = idCounter++; + var id = ++idCounter + ''; return prefix ? prefix + id : id; }; @@ -895,41 +1144,80 @@ // When customizing `templateSettings`, if you don't want to define an // interpolation, evaluation or escaping regex, we need one that is // guaranteed not to match. - var noMatch = /.^/; + var noMatch = /(.)^/; - // Within an interpolation, evaluation, or escaping, remove HTML escaping - // that had been previously added. - var unescape = function(code) { - return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' }; + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + // JavaScript micro-templating, similar to John Resig's implementation. // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. - _.template = function(str, data) { - var c = _.templateSettings; - var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + - 'with(obj||{}){__p.push(\'' + - str.replace(/\\/g, '\\\\') - .replace(/'/g, "\\'") - .replace(c.escape || noMatch, function(match, code) { - return "',_.escape(" + unescape(code) + "),'"; - }) - .replace(c.interpolate || noMatch, function(match, code) { - return "'," + unescape(code) + ",'"; - }) - .replace(c.evaluate || noMatch, function(match, code) { - return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('"; - }) - .replace(/\r/g, '\\r') - .replace(/\n/g, '\\n') - .replace(/\t/g, '\\t') - + "');}return __p.join('');"; - var func = new Function('obj', '_', tmpl); - if (data) return func(data, _); - return function(data) { - return func.call(this, data, _); + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; }; // Add a "chain" function, which will delegate to the wrapper. @@ -937,29 +1225,15 @@ return _(obj).chain(); }; - // The OOP Wrapper + // OOP // --------------- - // If Underscore is called as a function, it returns a wrapped object that // can be used OO-style. This wrapper holds altered versions of all the // underscore functions. Wrapped objects may be chained. - var wrapper = function(obj) { this._wrapped = obj; }; - - // Expose `wrapper.prototype` as `_.prototype` - _.prototype = wrapper.prototype; // Helper function to continue chaining intermediate results. - var result = function(obj, chain) { - return chain ? _(obj).chain() : obj; - }; - - // A method to easily add functions to the OOP wrapper. - var addToWrapper = function(name, func) { - wrapper.prototype[name] = function() { - var args = slice.call(arguments); - unshift.call(args, this._wrapped); - return result(func.apply(_, args), this._chain); - }; + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; }; // Add all of the Underscore functions to the wrapper object. @@ -968,32 +1242,35 @@ // Add all mutator Array functions to the wrapper. each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; - wrapper.prototype[name] = function() { - var wrapped = this._wrapped; - method.apply(wrapped, arguments); - var length = wrapped.length; - if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; - return result(wrapped, this._chain); + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); }; }); // Add all accessor Array functions to the wrapper. each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; - wrapper.prototype[name] = function() { - return result(method.apply(this._wrapped, arguments), this._chain); + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); }; }); - // Start chaining a wrapped Underscore object. - wrapper.prototype.chain = function() { - this._chain = true; - return this; - }; + _.extend(_.prototype, { - // Extracts the result from a wrapped and chained object. - wrapper.prototype.value = function() { - return this._wrapped; - }; + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); }).call(this); diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 87e129bb211..a9b4934e4da 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -739,9 +739,6 @@ display: block; color: #4c4c4c; text-decoration: none; - width: 200px; - text-overflow: ellipsis; - overflow: hidden; } .openerp .oe_dropdown_menu > li > a:hover { text-decoration: none; @@ -3402,7 +3399,7 @@ div.ui-widget-overlay { background: transparent; } .openerp_ie ul.oe_form_status li > .arrow span, .openerp_ie ul.oe_form_status_clickable li > .arrow span { - background-color: #fefefe !important; + background-color: #eeeeee !important; } .openerp_ie ul.oe_form_status li.oe_active > .label, .openerp_ie ul.oe_form_status_clickable li.oe_active > .label { border-bottom: 1px solid #729fcf; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index e8eab4544a6..57d2a855512 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -630,9 +630,6 @@ $sheet-padding: 16px display: block color: #4c4c4c text-decoration: none - width: 200px - text-overflow: ellipsis - overflow: hidden &:hover text-decoration: none .oe_dropdown_arrow:after @@ -2684,7 +2681,7 @@ div.ui-widget-overlay background: transparent > .arrow span - background-color: #fefefe !important + background-color: #eeeeee !important li.oe_active > .label border-bottom: 1px solid #729fcf diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index 37a217a8664..d62c04b0ecb 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -561,6 +561,10 @@ instance.web.TranslationDataBase.include({ }); /** Custom jQuery plugins */ +if(navigator.appVersion.indexOf("MSIE") !== -1) { + $.browser = $.browser || {}; + $.browser.msie = 1; +} $.fn.getAttributes = function() { var o = {}; if (this.length) { diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 76164657d84..984ded1fd5e 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -743,7 +743,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({ }); }, get_domain: function (other_domain) { - this._model.domain(other_domain); + return this._model.domain(other_domain); }, alter_ids: function (ids) { this._super(ids); @@ -780,6 +780,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this._super.apply(this, arguments); this.reset_ids([]); this.last_default_get = {}; + this.running_reads = []; }, default_get: function(fields, options) { var self = this; @@ -846,6 +847,9 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.to_write = []; this.cache = []; this.delete_all = false; + _.each(_.clone(this.running_reads), function(el) { + el.reject(); + }); }, read_ids: function (ids, fields, options) { var self = this; @@ -861,7 +865,6 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ to_get.push(id); } }); - var completion = $.Deferred(); var return_records = function() { var records = _.map(ids, function(id) { return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id}); @@ -896,10 +899,20 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ }, 0); }); } - completion.resolve(records); + return $.when(records); }; if(to_get.length > 0) { - var rpc_promise = this._super(to_get, fields, options).done(function(records) { + var def = $.Deferred(); + self.running_reads.push(def); + def.always(function() { + self.running_reads = _.without(self.running_reads, def); + }); + this._super(to_get, fields, options).then(function() { + def.resolve.apply(def, arguments); + }, function() { + def.reject.apply(def, arguments); + }); + return def.then(function(records) { _.each(records, function(record, index) { var id = to_get[index]; var cached = _.detect(self.cache, function(x) {return x.id === id;}); @@ -910,13 +923,11 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ cached.values = _.defaults(_.clone(cached.values), record); } }); - return_records(); + return return_records(); }); - $.when(rpc_promise).fail(function() {completion.reject();}); } else { - return_records(); + return return_records(); } - return completion.promise(); }, /** * Invalidates caching of a record in the dataset to ensure the next read diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js index e986a2f4112..4c114e28fe8 100644 --- a/addons/web/static/src/js/openerpframework.js +++ b/addons/web/static/src/js/openerpframework.js @@ -1155,7 +1155,8 @@ openerp.Model = openerp.Class.extend({ kwargs = args; args = []; } - return this.session().rpc('/web/dataset/call_kw', { + var call_kw = _.str.sprintf('/web/dataset/call_kw/%s/%s', this.name, method); + return this.session().rpc(call_kw, { model: this.name, method: method, args: args, @@ -1236,15 +1237,14 @@ openerp.TranslationDataBase = openerp.Class.extend(/** @lends instance.Translati openerp._t = new openerp.TranslationDataBase().build_translation_function(); openerp.get_cookie = function(c_name) { - if (document.cookie.length > 0) { - var c_start = document.cookie.indexOf(c_name + "="); - if (c_start != -1) { - c_start = c_start + c_name.length + 1; - var c_end = document.cookie.indexOf(";", c_start); - if (c_end == -1) { - c_end = document.cookie.length; - } - return unescape(document.cookie.substring(c_start, c_end)); + var cookies = document.cookie ? document.cookie.split('; ') : []; + for (var i = 0, l = cookies.length; i < l; i++) { + var parts = cookies[i].split('='); + var name = parts.shift(); + var cookie = parts.join('='); + + if (c_name && c_name === name) { + return cookie; } } return ""; @@ -1292,7 +1292,10 @@ openerp.str_to_datetime = function(str) { if ( !res ) { throw new Error("'" + str + "' is not a valid datetime"); } - var tmp = new Date(0); + var tmp = new Date(2000,0,1); + tmp.setUTCMonth(1970); + tmp.setUTCMonth(0); + tmp.setUTCDate(1); tmp.setUTCFullYear(parseFloat(res[1])); tmp.setUTCMonth(parseFloat(res[2]) - 1); tmp.setUTCDate(parseFloat(res[3])); @@ -1324,7 +1327,7 @@ openerp.str_to_date = function(str) { if ( !res ) { throw new Error("'" + str + "' is not a valid date"); } - var tmp = new Date(0); + var tmp = new Date(2000,0,1); tmp.setFullYear(parseFloat(res[1])); tmp.setMonth(parseFloat(res[2]) - 1); tmp.setDate(parseFloat(res[3])); diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index a7768f0ca24..f607b278437 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -58,8 +58,7 @@ openerp.testing = {}; } var fn, params; var needle = rparams.model + ':' + rparams.method; - if (url.url === '/web/dataset/call_kw' - && needle in this.responses) { + if (url.url.substr(0, 20) === '/web/dataset/call_kw' && needle in this.responses) { fn = this.responses[needle]; params = [ rparams.args || [], diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 94fc38faa23..8b985695860 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -731,12 +731,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var self = this; return this.save().done(function(result) { self.trigger("save", result); - self.to_view_mode(); - }).then(function(result) { - var parent = self.ViewManager.ActionManager.getParent(); - if(parent){ - parent.menu.do_reload_needaction(); - } + self.reload().then(function() { + self.to_view_mode(); + var parent = self.ViewManager.ActionManager.getParent(); + if(parent){ + parent.menu.do_reload_needaction(); + } + }); }); }, on_button_cancel: function(event) { @@ -900,17 +901,12 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM * @param {Object} r result of the write function. */ record_saved: function(r) { - var self = this; + this.trigger('record_saved', r); if (!r) { // should not happen in the server, but may happen for internal purpose - this.trigger('record_saved', r); return $.Deferred().reject(); - } else { - return $.when(this.reload()).then(function () { - self.trigger('record_saved', r); - return r; - }); } + return r; }, /** * Updates the form' dataset to contain the new record: @@ -3080,7 +3076,7 @@ instance.web.form.CompletionFieldMixin = { values.push({ label: _t("Search More..."), action: function() { - dataset.name_search(search_val, self.build_domain(), 'ilike', false).done(function(data) { + dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) { self._search_create_popup("search", data); }); }, @@ -5559,23 +5555,23 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ var calculation = _.bind(function() { if (this.field.type == "many2one") { - return self.get_distant_fields().then(function(fields) { - return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain")) - .read_slice(fields.fold ? ['fold'] : ['id'], {}).then(function (records) { - - var ids = _.map(records, function (val) {return val.id;}); - return self.dataset.name_get(ids).then(function (records_name) { - _.each(records, function (record) { - var name = _.find(records_name, function (val) {return val[0] == record.id;})[1]; - if (record.fold && record.id != self.get('value')) { - selection_folded.push([record.id, name]); - } else { - selection_unfolded.push([record.id, name]); - } - }); + /* :deprecated: fold feature will probably be removed */ + // return self.get_distant_fields().then(function(fields) { + self.distant_fields = {}; + return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain")) + .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) { + var ids = _.pluck(records, 'id'); + return self.dataset.name_get(ids).then(function (records_name) { + _.each(records, function (record) { + var name = _.find(records_name, function (val) {return val[0] == record.id;})[1]; + if (record.fold && record.id != self.get('value')) { + selection_folded.push([record.id, name]); + } else { + selection_unfolded.push([record.id, name]); + } }); }); - }); + }); } else { // For field type selection filter values according to // statusbar_visible attribute of the field. For example: @@ -5597,6 +5593,9 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ } }); }, + /* + * :deprecated: this feature will probably be removed with OpenERP v8 + */ get_distant_fields: function() { var self = this; if (this.distant_fields) { diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 99e87725dce..17c62917a18 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -508,7 +508,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi var reloaded = $.Deferred(); this.$el.find('.oe_list_content').append( this.groups.render(function () { - // Keep '=='. This is not a mistake, this is a wanted behaviour to match null & undefined if (self.dataset.index == null) { if (self.records.length) { self.dataset.index = 0; @@ -1474,14 +1473,13 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we var view = this.view, limit = view.limit(), - d = new $.Deferred(), page = this.datagroup.openable ? this.page : view.page; var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field";}), 'name'); var options = { offset: page * limit, limit: limit, context: {bin_size: true} }; //TODO xmo: investigate why we need to put the setTimeout - $.async_when().done(function() { - dataset.read_slice(fields, options).done(function (records) { + return $.async_when().then(function() { + return dataset.read_slice(fields, options).then(function (records) { // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently if (self.records.length) { self.records.reset(null, {silent: true}); @@ -1513,13 +1511,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.records.add(records, {silent: true}); list.render(); - d.resolve(list); if (_.isEmpty(records)) { view.no_result(); } + return list; }); }); - return d.promise(); }, setup_resequence_rows: function (list, dataset) { // drag and drop enabled if list is not sorted and there is a @@ -1600,11 +1597,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.render_groups(groups)); if (post_render) { post_render(); } }, function (dataset) { - self.render_dataset(dataset).done(function (list) { + self.render_dataset(dataset).then(function (list) { self.children[null] = list; self.elements = [list.$current.replaceAll($el)[0]]; self.setup_resequence_rows(list, dataset); + }).always(function() { if (post_render) { post_render(); } }); }); diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index d147c2b3d09..37369377bf1 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -1209,11 +1209,12 @@ instance.web.Sidebar = instance.web.Widget.extend({ var self = this; self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) { var ids = self.getParent().get_selected_ids(); + var domain; if (self.getParent().get_active_domain) { - var domain = self.getParent().get_active_domain(); + domain = self.getParent().get_active_domain(); } else { - var domain = $.Deferred().resolve(undefined); + domain = $.Deferred().resolve(undefined); } if (ids.length === 0) { instance.web.dialog($("
").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true }); @@ -1385,7 +1386,17 @@ instance.web.View = instance.web.Widget.extend({ return self.getParent().on_action_executed.apply(null, arguments); } }; - var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {}); + var context = dataset.get_context(); + if (action_data.model !== dataset.model) { + // filter out context keys that are specific to the action model. + // Wrong default_ and search_default values will no give the expected views + // Wrong group_by values will simply fail and forbid rendering of the destination view + context = _.object(_.reject(_.pairs(context.eval()), function(pair) { + return pair[0].match('^(?:(?:default_|search_default_).+|group_by|group_by_no_leaf)$') !== null; + })); + } + + context = new instance.web.CompoundContext(context, action_data.context || {}); var handler = function (action) { if (action && action.constructor == Object) { var ncontext = new instance.web.CompoundContext(context); diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css index 731fab430b9..fd535383fc9 100644 --- a/addons/web_kanban/static/src/css/kanban.css +++ b/addons/web_kanban/static/src/css/kanban.css @@ -690,7 +690,6 @@ .openerp_ie .oe_kanban_view .oe_kanban_group_title_vertical { -ms-writing-mode: lr-tb !important; background: #f0eeee; - top: -5px !important; } .openerp_ie .oe_kanban_view.oe_kanban_grouped .oe_kanban_group_header { height: 1%; diff --git a/addons/web_kanban/static/src/css/kanban.sass b/addons/web_kanban/static/src/css/kanban.sass index dfca3b09d50..679c273ba0d 100644 --- a/addons/web_kanban/static/src/css/kanban.sass +++ b/addons/web_kanban/static/src/css/kanban.sass @@ -572,7 +572,6 @@ .oe_kanban_group_title_vertical -ms-writing-mode: lr-tb !important background: rgb(240, 238, 238) - top: -5px !important &.oe_kanban_grouped .oe_kanban_group_header height: 1% diff --git a/addons/web_kanban_gauge/static/lib/justgage/justgage.js b/addons/web_kanban_gauge/static/lib/justgage/justgage.js index 24540beb293..3e23c12a29e 100644 --- a/addons/web_kanban_gauge/static/lib/justgage/justgage.js +++ b/addons/web_kanban_gauge/static/lib/justgage/justgage.js @@ -859,7 +859,7 @@ function getColor(val, pct, col, noGradient, custSec) { /** Fix Raphael display:none tspan dy attribute bug */ function setDy(elem, fontSize, txtYpos) { - if (!ie || ie > 9) { + if ((!ie || ie > 9) && elem.node.firstChild.attributes.dy) { elem.node.firstChild.attributes.dy.value = 0; } } diff --git a/addons/web_tests_demo/__init__.py b/addons/web_tests_demo/__init__.py index 137d472ec74..7bed44c4154 100644 --- a/addons/web_tests_demo/__init__.py +++ b/addons/web_tests_demo/__init__.py @@ -1,6 +1,6 @@ from openerp.osv import orm, fields -class TestObject(orm.Model): +class TestObject(orm.TransientModel): _name = 'web_tests_demo.model' _columns = {