diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index bb9fcaa0dbd..9ea84a3501f 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -20,7 +20,6 @@ This module provides the core of the OpenERP Web Client. "static/lib/datejs/sugarpak.js", "static/lib/datejs/extras.js", "static/lib/jquery/jquery.js", - "static/lib/jquery.MD5/jquery.md5.js", "static/lib/jquery.form/jquery.form.js", "static/lib/jquery.validate/jquery.validate.js", "static/lib/jquery.ba-bbq/jquery.ba-bbq.js", @@ -66,6 +65,7 @@ This module provides the core of the OpenERP Web Client. "static/lib/jquery.tipsy/tipsy.css", "static/lib/jquery.textext/jquery.textext.css", "static/lib/fontawesome/css/font-awesome.css", + "static/lib/bootstrap/css/bootstrap.css", "static/src/css/base.css", "static/src/css/data_export.css", "static/lib/cleditor/jquery.cleditor.css", @@ -89,5 +89,4 @@ This module provides the core of the OpenERP Web Client. "static/test/list-editable.js", "static/test/mutex.js" ], - 'bootstrap': True, } diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 58abda3fc30..8f7c5d4f70d 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -3,6 +3,7 @@ import ast import base64 import csv +import functools import glob import itertools import logging @@ -35,7 +36,9 @@ from openerp.tools.translate import _ from openerp.tools import config from openerp import http -from openerp.http import request +from openerp.http import request, serialize_exception as _serialize_exception + +_logger = logging.getLogger(__name__) #---------------------------------------------------------- # OpenERP Web helpers @@ -89,17 +92,29 @@ 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': +def serialize_exception(f): + @functools.wraps(f) + def wrap(*args, **kwargs): 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 + return f(*args, **kwargs) + except Exception, e: + _logger.exception("An exception occured during an http request") + se = _serialize_exception(e) + error = { + 'code': 200, + 'message': "OpenERP Server Error", + 'data': se + } + return werkzeug.exceptions.InternalServerError(simplejson.dumps(error)) + return wrap + +def redirect_with_hash(url, code=303): + 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): @@ -546,14 +561,14 @@ class Home(http.Controller): def redirect(db): query = dict(urlparse.parse_qsl(request.httprequest.query_string, keep_blank_values=True)) - query.update({'db': db}) + query['db'] = db redirect = request.httprequest.path + '?' + urllib.urlencode(query) return redirect_with_hash(redirect) if db is None and guessed_db is not None: return redirect(guessed_db) - if db is not None and db != guessed_db: + if db is not None and db != request.session.db: request.session.logout() request.session.db = db guessed_db = db @@ -1224,6 +1239,7 @@ class Binary(http.Controller): return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read() @http.route('/web/binary/saveas', type='http', auth="user") + @serialize_exception def saveas(self, model, field, id=None, filename_field=None, **kw): """ Download link for files stored as binary fields. @@ -1257,6 +1273,7 @@ class Binary(http.Controller): ('Content-Disposition', content_disposition(filename))]) @http.route('/web/binary/saveas_ajax', type='http', auth="user") + @serialize_exception def saveas_ajax(self, data, token): jdata = simplejson.loads(data) model = jdata['model'] @@ -1290,6 +1307,7 @@ class Binary(http.Controller): cookies={'fileToken': token}) @http.route('/web/binary/upload', type='http', auth="user") + @serialize_exception def upload(self, callback, ufile): # TODO: might be useful to have a configuration flag for max-length file uploads out = """ - - - - - - \ No newline at end of file diff --git a/addons/web/static/lib/jquery.MD5/tests/tests.js b/addons/web/static/lib/jquery.MD5/tests/tests.js deleted file mode 100644 index ab70f2f991b..00000000000 --- a/addons/web/static/lib/jquery.MD5/tests/tests.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * jQuery MD5 Plugin Tests 1.1 - * https://github.com/blueimp/jQuery-MD5 - * - * Copyright 2010, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://creativecommons.org/licenses/MIT/ - */ - -/*global jQuery, module, test, strictEqual */ - -(function ($) { - 'use strict'; - - module('Hex-encoded MD5'); - - test('Creating hex-encoded MD5 hash of an ASCII value', function () { - strictEqual($.md5('value'), '2063c1608d6e0baf80249c42e2be5804'); - }); - - test('Creating hex-encoded MD5 hash of an UTF-8 value', function () { - strictEqual($.md5('日本'), '4dbed2e657457884e67137d3514119b3'); - }); - - module('Hex-encoded HMAC-MD5'); - - test('Creating hex-encoded HMAC-MD5 hash of an ASCII value and key', function () { - strictEqual($.md5('value', 'key'), '01433efd5f16327ea4b31144572c67f6'); - }); - - test('Creating hex-encoded HMAC-MD5 hash of an UTF-8 value and key', function () { - strictEqual($.md5('日本', '日本'), 'c78b8c7357926981cc04740bd3e9d015'); - }); - - module('Raw MD5'); - - test('Creating raw MD5 hash of an ASCII value', function () { - strictEqual($.md5('value', null, true), ' c\xc1`\x8dn\x0b\xaf\x80$\x9cB\xe2\xbeX\x04'); - }); - - test('Creating raw MD5 hash of an UTF-8 value', function () { - strictEqual($.md5('日本', null, true), 'M\xbe\xd2\xe6WEx\x84\xe6q7\xd3QA\x19\xb3'); - }); - - module('Raw HMAC-MD5'); - - test('Creating raw HMAC-MD5 hash of an ASCII value and key', function () { - strictEqual($.md5('value', 'key', true), '\x01C>\xfd_\x162~\xa4\xb3\x11DW,g\xf6'); - }); - - test('Creating raw HMAC-MD5 hash of an UTF-8 value and key', function () { - strictEqual($.md5('日本', '日本', true), '\xc7\x8b\x8csW\x92i\x81\xcc\x04t\x0b\xd3\xe9\xd0\x15'); - }); - -}(typeof jQuery === 'function' ? jQuery : this)); \ No newline at end of file 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..010585ee4a0 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -111,6 +111,8 @@ } .openerp h4 { margin: 4px 0; + font-weight: bold; + font-size: inherit; } .openerp a.button:link, .openerp a.button:visited, .openerp button, .openerp input[type='submit'], .openerp .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button { display: inline-block; @@ -199,6 +201,7 @@ } .openerp.ui-dialog { display: none; + height: auto !important; padding: 6px; background-color: rgba(60, 60, 60, 0.7); border: 1px solid; @@ -241,6 +244,7 @@ } .openerp.ui-dialog .ui-dialog-content { background: white; + width: auto !important; } .openerp.ui-dialog .ui-dialog-buttonpane { border-top: 1px solid #e0e0e0; @@ -529,6 +533,15 @@ .openerp .oe_grey { color: #aaaaaa; } +.openerp .oe_error_detail hr { + display: block; + -webkit-margin-before: 0.5em; + -webkit-margin-after: 0.5em; + -webkit-margin-start: auto; + -webkit-margin-end: auto; + border-style: inset; + border-width: 1px; +} .openerp .oe_tag { border: 1px solid #afafb6; font-size: 11px; @@ -680,6 +693,9 @@ .openerp .oe_notebook_page { padding: 0; } +.openerp .oe_notebook_page > label:not([for]) { + font-weight: normal !important; +} .openerp div.ui-tabs { padding: 3px 0px 3px 0px; } @@ -739,9 +755,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; @@ -979,7 +992,7 @@ } .openerp .oe_login .oe_login_footer a { color: #eeeeee; - margin: 0 8px; + margin: 0 6px; } .openerp .oe_login .oe_login_footer a:hover { text-decoration: underline; @@ -1025,19 +1038,7 @@ .openerp .oe_content_full_screen .oe_topbar, .openerp .oe_content_full_screen .oe_leftbar { display: none; } -.openerp .oe_topbar { - width: 100%; - height: 32px; - background-color: #414141; - background-color: #454343; - background-image: -webkit-gradient(linear, left top, left bottom, from(#646060), to(#262626)); - background-image: -webkit-linear-gradient(top, #646060, #262626); - background-image: -moz-linear-gradient(top, #646060, #262626); - background-image: -ms-linear-gradient(top, #646060, #262626); - background-image: -o-linear-gradient(top, #646060, #262626); - background-image: linear-gradient(to bottom, #646060, #262626); -} -.openerp .oe_topbar .oe_topbar_anonymous_login { +.openerp .navbar .oe_topbar_anonymous_login { background-color: #dc5f59; color: #eeeeee; background-color: #be4343; @@ -1048,11 +1049,11 @@ background-image: -o-linear-gradient(top, #fc8787, maroon); background-image: linear-gradient(to bottom, #fc8787, maroon); } -.openerp .oe_topbar .oe_topbar_anonymous_login a { +.openerp .navbar .oe_topbar_anonymous_login a { display: block; padding: 5px 10px 7px; line-height: 20px; - height: 20px; + height: 30px; text-decoration: none; color: white; background: transparent; @@ -1062,7 +1063,7 @@ -o-transition: all 0.2s ease-out; transition: all 0.2s ease-out; } -.openerp .oe_topbar .oe_topbar_anonymous_login a:hover { +.openerp .navbar .oe_topbar_anonymous_login a:hover { background: rgba(0, 0, 0, 0.1); color: white; text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.2); @@ -1070,9 +1071,9 @@ -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset; } -.openerp .oe_topbar .oe_topbar_item { +.openerp .navbar .oe_topbar_item { display: block; - padding: 5px 10px 7px; + padding: 5px 10px 26px; line-height: 20px; height: 20px; text-decoration: none; @@ -1085,7 +1086,7 @@ -o-transition: all 0.2s ease-out; transition: all 0.2s ease-out; } -.openerp .oe_topbar .oe_topbar_item:hover { +.openerp .navbar .oe_topbar_item:hover { background: rgba(0, 0, 0, 0.2); text-shadow: black 0px 0px 3px; color: white; @@ -1093,7 +1094,7 @@ -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; } -.openerp .oe_topbar .oe_topbar_avatar { +.openerp .navbar .oe_topbar_avatar { width: 24px; height: 24px; margin: -2px 2px 0 0; @@ -1101,13 +1102,13 @@ -webkit-border-radius: 3px; border-radius: 3px; } -.openerp .oe_topbar .oe_topbar_avatar { +.openerp .navbar .oe_topbar_avatar { vertical-align: top; } -.openerp .oe_topbar .oe_dropdown_arrow:after { +.openerp .navbar .oe_dropdown_arrow:after { border-top: 4px solid white; } -.openerp .oe_topbar .oe_dropdown_menu { +.openerp .navbar .dropdown_menu { top: 32px; background: #333333; background: rgba(37, 37, 37, 0.9); @@ -1122,14 +1123,14 @@ -moz-background-clip: padding-box; background-clip: padding-box; } -.openerp .oe_topbar .oe_dropdown_menu li { +.openerp .navbar .dropdown_menu li { float: none; padding: 3px 12px; } -.openerp .oe_topbar .oe_dropdown_menu li a { +.openerp .navbar .dropdown_menu li a { color: #eeeeee; } -.openerp .oe_topbar .oe_dropdown_menu li:hover { +.openerp .navbar .dropdown_menu li:hover { background-color: #212121; background-image: -webkit-gradient(linear, left top, left bottom, from(#292929), to(#191919)); background-image: -webkit-linear-gradient(top, #292929, #191919); @@ -1141,21 +1142,21 @@ -webkit-box-shadow: none; box-shadow: none; } -.openerp .oe_topbar .oe_topbar_name { +.openerp .navbar .oe_topbar_name { max-width: 150px; - overflow: hidden; display: inline-block; - max-height: 100%; + height: 15px; text-overflow: ellipsis; white-space: nowrap; + overflow: hidden; } -.openerp .oe_menu { +.openerp .navbar-nav { float: left; padding: 0; margin: 0; font-size: 13px; } -.openerp .oe_menu > li { +.openerp .navbar-nav > li { list-style-type: none; padding: 0; margin: 0; @@ -1163,13 +1164,13 @@ display: block; color: #eeeeee; } -.openerp .oe_menu > li > a { +.openerp .navbar-nav > li > a { display: block; - padding: 5px 10px 7px; + padding: 5px 10px 26px !important; line-height: 20px; height: 20px; text-decoration: none; - color: #eeeeee; + color: #eeeeee !important; vertical-align: top; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -webkit-transition: all 0.2s ease-out; @@ -1178,7 +1179,7 @@ -o-transition: all 0.2s ease-out; transition: all 0.2s ease-out; } -.openerp .oe_menu > li > a:hover { +.openerp .navbar-nav > li > a:hover { background: rgba(0, 0, 0, 0.2); text-shadow: black 0px 0px 3px; color: white; @@ -1186,7 +1187,7 @@ -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; } -.openerp .oe_menu > li > .oe_active { +.openerp .navbar-nav > li > .active { background: rgba(0, 0, 0, 0.3); text-shadow: black 0px 0px 3px; -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; @@ -1217,7 +1218,7 @@ width: 220px; background: #f0eeee; border-right: 1px solid #afafb6; - text-shadow: 0 1px 1px white; + text-shadow: none; padding-bottom: 16px; } .openerp a.oe_logo { @@ -1269,37 +1270,9 @@ color: #7c7bad; } .openerp .oe_secondary_submenu { - padding: 2px 0 8px 0; - margin: 0; -} -.openerp .oe_secondary_submenu li { - position: relative; - margin: 0; - padding: 1px 0 1px 20px !important; - list-style-type: none; -} -.openerp .oe_secondary_submenu li a { - display: block; - color: #4c4c4c; - padding: 2px 4px 2px 0; -} -.openerp .oe_secondary_submenu li .oe_menu_label { - position: absolute; - top: 1px; - right: 1px; - font-size: 10px; - background: #7c7bad; - color: white; - padding: 2px 4px; - margin: 1px 6px 0 0; - border: 1px solid lightgrey; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + margin-bottom: 10px !important; + line-height: 1.1em; + margin-top: 3px; } .openerp .oe_secondary_submenu .oe_menu_text { white-space: nowrap; @@ -1307,15 +1280,9 @@ display: inline-block; text-overflow: ellipsis; max-width: 85%; + margin-top: 1px; } -.openerp .oe_secondary_submenu .oe_menu_counter { - float: right; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - margin: 1px; - padding: 1px 4px; - border: none; -} -.openerp .oe_secondary_submenu .oe_menu_counter:hover { +.openerp .oe_secondary_submenu .badge:hover { cursor: pointer; -webkit-transform: scale(1.1); -moz-transform: scale(1.1); @@ -1323,33 +1290,6 @@ -o-transform: scale(1.1); transform: scale(1.1); } -.openerp .oe_secondary_submenu .oe_active { - border-top: 1px solid lightgrey; - border-bottom: 1px solid #dedede; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2); - -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2); - background: #7c7bad; -} -.openerp .oe_secondary_submenu .oe_active a { - color: white; -} -.openerp .oe_secondary_submenu .oe_active .oe_menu_label { - background: #eeeeee; - color: #7c7bad; - text-shadow: 0 1px 1px white; - -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -} -.openerp .oe_secondary_submenu .oe_active .oe_menu_counter { - background: #eeeeee; - color: #7c7bad; - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); -} .openerp .oe_secondary_submenu .oe_menu_toggler:before { width: 0; height: 0; @@ -1374,9 +1314,6 @@ border-right: 4px solid transparent; border-top: 4px solid #4c4c4c; } -.openerp .oe_secondary_submenu .oe_secondary_submenu { - margin-left: -20px; -} .openerp .oe_secondary_submenu .oe_secondary_submenu li { margin-left: 20px; } @@ -1443,6 +1380,7 @@ .openerp .oe_application .oe_breadcrumb_title > * { display: inline-block; overflow: hidden; + font-weight: bold; } .openerp .oe_view_manager { display: table; @@ -1485,6 +1423,7 @@ font-size: 18px; margin: 0; float: left; + line-height: 30px; } .openerp .oe_view_manager table.oe_view_manager_header h2 a { color: #7c7bad; @@ -1655,9 +1594,12 @@ .openerp .oe_view_manager_inline > .oe_view_manager_header, .openerp .oe_view_manager_inlineview > .oe_view_manager_header { display: none; } -.openerp .oe_popup_form > .oe_formview > .oe_form_pager { +.openerp .oe_popup_form .oe_formview .oe_form_pager { display: none !important; } +.openerp .oe_popup_form table label { + font-weight: normal; +} .openerp .oe_popup_list_pager { float: right; } @@ -1683,6 +1625,16 @@ font-size: 12px; line-height: 18px; } +.openerp .oe_searchview input:not([type]), .openerp .oe_searchview input[type="text"], .openerp .oe_searchview input[type="number"] { + width: 156px; + height: 22px; +} +.openerp .oe_searchview input[type="checkbox"] { + margin: 3px 3px 3px 4px; +} +.openerp .oe_searchview select { + margin: 2px 4px 2px 0; +} .openerp .oe_searchview.oe_focused { border-color: #a6a6fe; -moz-box-shadow: 0 1px 2px #a6a6fe inset; @@ -1907,6 +1859,7 @@ box-sizing: border-box; display: table-cell; width: 50%; + padding-left: 2px; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section ul { margin: 0 8px 8px; @@ -1942,7 +1895,7 @@ line-height: 18px; } .openerp .oe_searchview .oe_searchview_drawer form button { - margin: 0 0 8px 0; + margin: 0 0 8px -3px; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom { padding: 0 8px 8px 8px; @@ -1970,6 +1923,9 @@ top: 0; right: 5px; } +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom label { + font-weight: normal; +} .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_dashboard form { display: none; margin-top: 2px; @@ -1997,6 +1953,7 @@ } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced li:first-child .searchview_extended_prop_or { visibility: hidden; + margin-left: -14px; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced .searchview_extended_prop_or { opacity: 0.5; @@ -2188,6 +2145,12 @@ background: url(/web/static/src/img/form_sheetbg.png); border-bottom: 1px solid #dddddd; } +.openerp .oe_application .oe_form_sheetbg .oe_subtotal_footer label { + font-weight: bold; +} +.openerp .oe_application .oe_form_sheetbg table label { + font-weight: normal; +} .openerp .oe_application .oe_form_sheet { border: 1px solid #c8c8d3; -moz-box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); @@ -2276,7 +2239,7 @@ padding: 2px 11px 2px 0px !important; } .openerp .oe_form .oe_subtotal_footer label.oe_form_label_help { - font-weight: normal; + font-weight: normal !important; } .openerp .oe_form .oe_form_box_info { background: #ffee99; @@ -2308,7 +2271,8 @@ .openerp .oe_form td.oe_form_group_cell_label label { line-height: 18px; display: block; - min-width: 140px; + min-width: 150px; + font-weight: bold !important; } .openerp .oe_form td.oe_form_group_cell + .oe_form_group_cell { padding: 2px 0 2px 8px; @@ -2321,13 +2285,12 @@ padding-left: 20px; } .openerp .oe_form .oe_form_label_help[for], .openerp .oe_form .oe_form_label[for] { - font-weight: bold; white-space: nowrap; padding-right: 8px; } .openerp .oe_form .oe_form_label_help[for] span, .openerp .oe_form .oe_form_label[for] span { font-size: 80%; - color: darkgreen; + color: darkGreen; vertical-align: top; position: relative; top: -4px; @@ -2405,6 +2368,18 @@ height: inherit !important; font-size: inherit; } +.openerp .oe_form .oe_title h1, .openerp .oe_form h1 { + font-weight: bold; + font-size: 2em; +} +.openerp .oe_form h2 { + font-size: 1.5em; +} +.openerp .oe_form label { + font-weight: bold; + margin-bottom: 0px; + display: inline; +} .openerp .oe_form .oe_form_field { width: 100%; display: inline-block; @@ -2542,6 +2517,9 @@ margin: 0; padding: 0; } +.openerp .oe_form .oe_form_field_binary { + display: inline-block; +} .openerp .oe_form .oe_form_field_image { padding: 0; position: relative; @@ -2573,13 +2551,14 @@ display: inline-block; clear: both; width: 100%; + margin-bottom: -15px; } .openerp .oe_fileupload .oe_add { float: left; position: relative; width: 100%; left: 2px; - top: 7px; + margin: 9px 0; overflow: hidden; } .openerp .oe_fileupload .oe_add button { @@ -2591,9 +2570,6 @@ } .openerp .oe_fileupload .oe_add button.oe_attach { width: 24px; - overflow: hidden; - width: 24px; - overflow: hidden; background: transparent; color: #7c7bad; box-shadow: none; @@ -2642,6 +2618,9 @@ .openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) { background: #f4f5fa; } +.openerp .oe_form_field_many2one { + display: inline-block; +} .openerp .oe_form_field_many2one td:first-child { position: relative; } @@ -2704,9 +2683,13 @@ padding-left: 24px; margin: 0; position: relative; + line-height: normal; + font-size: 100%; + font-weight: normal; } .openerp ul.oe_form_status li > .arrow, .openerp ul.oe_form_status_clickable li > .arrow { width: 17px; + height: 30px; display: inline-block; vertical-align: top; overflow: hidden; @@ -3204,9 +3187,6 @@ .openerp .oe_form_field_many2one .oe_m2o_cm_button { line-height: 18px; } - .openerp .oe_secondary_submenu { - line-height: 14px; - } .openerp .oe_webclient .oe_star_on, .openerp .oe_webclient .oe_star_off { top: 0px; } @@ -3402,7 +3382,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; @@ -3454,6 +3434,151 @@ div.ui-widget-overlay { overflow: hidden !important; } } + +.nav li > a { + padding: 3px 4px 2px 18px; + color: #4c4c4c; +} +.nav nav-pills.nav-stacked > li > ul { + padding-left: 16px; +} + +.nav-pills > li.active > a, a.list-group-item.active > a { + background-color: #7c7bad; + color: white; + border-radius: 0; +} +.nav-pills > li.active a:hover, .nav-pills > li.active a:focus, a.list-group-item.active a:hover, a.list-group-item.active a:focus { + background-color: #7c7bad; +} +.nav-pills > li.active .badge, a.list-group-item.active .badge { + background-color: white; + color: #7c7bad; + text-shadow: none; +} + +.badge { + font-weight: normal; + font-size: 11px; + background-color: #7c7bad; +} + +button, body { + line-height: normal; +} + +h1, h2 { + font-weight: bold; +} + +h3 { + font-size: 1.17em; + font-weight: bold; +} + +p { + display: block; + -webkit-margin-before: 1em; + -webkit-margin-after: 1em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; +} + +pre { + background-color: white; + border: none; + padding: 10px 0 3px 0; +} + +.jqstooltip { + height: auto !important; + width: auto !important; +} + +h5 { + font-weight: bold; + font-size: smaller; +} + +.oe_form .oe_subtype label, .oe_subtype label { + font-weight: normal; +} + +.oe_msg_subtype_check { + margin: 3px 3px 0 !important; +} + +.oe_view_manager_new .oe_form_nosheet { + margin-top: 8px; +} +.oe_view_manager_new .oe_form_nosheet .oe_form_label { + font-weight: normal; +} + +.navbar { + min-height: 32px; + margin-bottom: 0px; + border: none; + z-index: 1; + background-color: #414141; + background-color: #454343; + background-image: -webkit-gradient(linear, left top, left bottom, from(#646060), to(#262626)); + background-image: -webkit-linear-gradient(top, #646060, #262626); + background-image: -moz-linear-gradient(top, #646060, #262626); + background-image: -ms-linear-gradient(top, #646060, #262626); + background-image: -o-linear-gradient(top, #646060, #262626); + background-image: linear-gradient(to bottom, #646060, #262626); +} + +.navbar-default .navbar-nav li a:hover, .navbar-default .navbar-nav li a:focus { + background: rgba(0, 0, 0, 0.3); +} +.navbar-default .navbar-nav .open > a, .navbar-default .navbar-nav a:hover, .navbar-default .navbar-nav a:focus { + background: rgba(0, 0, 0, 0.3) !important; +} +.navbar-default .navbar-nav .dropdown > a .caret { + border-top-color: #777777 !important; + border-bottom-color: #777777 !important; +} + +.navbar-nav li a { + padding: 4px 32px 4px 12px; +} + +.dropdown-menu { + font-size: 13px; + padding: 4px 0; + background: #333333 !important; + background: rgba(37, 37, 37, 0.9) !important; + border-color: #999999; + border-color: rgba(0, 0, 0, 0.2); + background-color: #414141; + text-shadow: none; + background-color: #454343; + background-image: -webkit-gradient(linear, left top, left bottom, from(#646060), to(#262626)); + background-image: -webkit-linear-gradient(top, #646060, #262626); + background-image: -moz-linear-gradient(top, #646060, #262626); + background-image: -ms-linear-gradient(top, #646060, #262626); + background-image: -o-linear-gradient(top, #646060, #262626); + background-image: linear-gradient(to bottom, #646060, #262626); + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.dropdown-menu li a, .dropdown-menu li a:hover, .dropdown-menu li a:focus { + color: #eeeeee; +} + +.ui-icon { + width: 18px; + height: 18px; +} + +input[type="radio"], input[type="checkbox"] { + margin-right: 4px; + margin-left: 4px; +} + .blockUI.blockOverlay { background-color: black; opacity: 0.6; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index e8eab4544a6..e9a51370192 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -204,6 +204,8 @@ $sheet-padding: 16px vertical-align: middle h4 margin: 4px 0 + font-weight: bold + font-size: inherit // }}} // Button style {{{ a.button:link, a.button:visited, button, input[type='submit'], .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button @@ -263,6 +265,7 @@ $sheet-padding: 16px // Modal box &.ui-dialog display: none + height: auto !important padding: 6px //overflow: hidden background-color: rgba(60,60,60,0.7) @@ -287,6 +290,7 @@ $sheet-padding: 16px border: none .ui-dialog-content background: white + width: auto !important .ui-dialog-buttonpane border-top: 1px solid #e0e0e0 background: #f5f7f9 @@ -467,7 +471,16 @@ $sheet-padding: 16px max-width: 650px .oe_grey color: #aaa - + // Added for generic error message and customize bootstrap3
,
+ .oe_error_detail + hr + display: block + -webkit-margin-before: 0.5em + -webkit-margin-after: 0.5em + -webkit-margin-start: auto + -webkit-margin-end: auto + border-style: inset + border-width: 1px // }}} // Tags (for many2many tags, among others) {{{ @@ -589,6 +602,10 @@ $sheet-padding: 16px cursor: default .oe_notebook_page padding: 0 + //Customize label weight according bootstrap3 + > label:not([for]) + font-weight: normal !important + // End of customize div.ui-tabs padding: 3px 0px 3px 0px .ui-tabs-hide @@ -630,9 +647,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 @@ -814,7 +828,7 @@ $sheet-padding: 16px text-align: center a color: #eee - margin: 0 8px + margin: 0 6px &:hover text-decoration: underline span @@ -857,11 +871,7 @@ $sheet-padding: 16px // }}} // Top Menu {{{ - .oe_topbar - width: 100% - height: 32px - background-color: #414141 - @include vertical-gradient(#646060, #262626) + .navbar .oe_topbar_anonymous_login background-color: #dc5f59 color: #eeeeee @@ -870,7 +880,7 @@ $sheet-padding: 16px display: block padding: 5px 10px 7px line-height: 20px - height: 20px + height: 30px text-decoration: none color: white background: transparent @@ -882,7 +892,7 @@ $sheet-padding: 16px @include box-shadow(0 1px 2px rgba(0,0,0,0.2) inset) .oe_topbar_item display: block - padding: 5px 10px 7px + padding: 5px 10px 26px line-height: 20px height: 20px text-decoration: none @@ -905,7 +915,7 @@ $sheet-padding: 16px vertical-align: top .oe_dropdown_arrow:after border-top: 4px solid white - .oe_dropdown_menu + .dropdown_menu top: 32px background: #333 background: rgba(37,37,37,0.9) @@ -925,17 +935,17 @@ $sheet-padding: 16px @include box-shadow(none) .oe_topbar_name max-width: 150px - overflow: hidden display: inline-block - max-height: 100% + height: 15px // Customize to manage caret when topbar_name is over sized. text-overflow: ellipsis white-space: nowrap + overflow: hidden // Customize to bring ellipsis when topbar_name is over sized. // oe menu is the list of the buttons on the left side of the bar. // So why aren't the buttons oe_topbar_items ? This sad state of affairs // is a leftover from an uncomplete refactoring when the left and top menu // were merged. You are welcome to refactor and clean this up - .oe_menu + .navbar-nav float: left padding: 0 margin: 0 @@ -949,11 +959,11 @@ $sheet-padding: 16px color: #eeeeee > a display: block - padding: 5px 10px 7px + padding: 5px 10px 26px !important line-height: 20px height: 20px text-decoration: none - color: #eee + color: #eee !important vertical-align: top text-shadow: 0 1px 1px rgba(0,0,0,0.2) @include transition(all 0.2s ease-out) @@ -962,7 +972,7 @@ $sheet-padding: 16px text-shadow: black 0px 0px 3px color: white @include box-shadow(0 1px 2px rgba(0,0,0,0.4) inset) - > .oe_active + > .active background: rgba(0,0,0,0.30) text-shadow: black 0px 0px 3px @include box-shadow(0 1px 2px rgba(0,0,0,0.4) inset) @@ -990,7 +1000,7 @@ $sheet-padding: 16px width: 220px background: #f0eeee border-right: 1px solid $tag-border - text-shadow: 0 1px 1px white + text-shadow: none padding-bottom: 16px a.oe_logo position: relative @@ -1033,62 +1043,20 @@ $sheet-padding: 16px margin-left: 8px color: $link-color .oe_secondary_submenu - padding: 2px 0 8px 0 - margin: 0 - li - position: relative - margin: 0 - padding: 1px 0 1px 20px !important - list-style-type: none - a - display: block - color: #4c4c4c - padding: 2px 4px 2px 0 - .oe_menu_label - position: absolute - top: 1px - right: 1px - font-size: 10px - background: $link-color - color: white - padding: 2px 4px - margin: 1px 6px 0 0 - border: 1px solid lightGray - text-shadow: 0 1px 1px rgba(0,0,0,0.2) - @include radius(4px) - @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, 0.2)) + margin-bottom: 10px !important + line-height: 1.1em + margin-top: 3px .oe_menu_text white-space: nowrap overflow: hidden display: inline-block text-overflow: ellipsis max-width: 85% - .oe_menu_counter - float: right - text-shadow: 0 1px 1px rgba(0,0,0,0.2) - margin: 1px - padding: 1px 4px - border: none + margin-top: 1px + .badge &:hover cursor: pointer @include transform(scale(1.1)) - .oe_active - border-top: 1px solid lightGray - border-bottom: 1px solid #dedede - text-shadow: 0 1px 1px rgba(0,0,0,0.2) - @include box-shadow((inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2))) - background: $link-color - a - color: white - .oe_menu_label - background: #eee - color: $link-color - text-shadow: 0 1px 1px white - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2)) - .oe_menu_counter - background: #eee - color: $link-color - @include box-shadow(inset 0 1px 2px rgba(0, 0, 0, 0.2)) .oe_menu_toggler:before width: 0 height: 0 @@ -1110,11 +1078,8 @@ $sheet-padding: 16px border-left: 4px solid transparent border-right: 4px solid transparent border-top: 4px solid #4c4c4c - .oe_secondary_submenu - margin-left: -20px .oe_secondary_submenu li margin-left: 20px - // }}} // About openerp {{{ .oe_about @@ -1160,6 +1125,7 @@ $sheet-padding: 16px .oe_breadcrumb_title > * display: inline-block overflow: hidden + font-weight: bold // }}} // ViewManager common {{{ .oe_view_manager @@ -1198,6 +1164,7 @@ $sheet-padding: 16px font-size: 18px margin: 0 float: left + line-height: 30px a color: $link-color .oe_dropdown_menu @@ -1326,8 +1293,13 @@ $sheet-padding: 16px display: none // }}} // FormPopup {{{ - .oe_popup_form > .oe_formview > .oe_form_pager - display: none !important + .oe_popup_form + .oe_formview .oe_form_pager + display: none !important + // Customize label weight for popup wizard appear from another wizard according bootstrap3 + table label + font-weight: normal + // End of customize .oe_popup_list_pager float: right // }}} @@ -1348,6 +1320,15 @@ $sheet-padding: 16px height: 14px font-size: 12px line-height: 18px + //Customize searchview input, select + input:not([type]), input[type="text"], input[type="number"] + width: 156px + height: 22px + input[type="checkbox"] + margin: 3px 3px 3px 4px + select + margin: 2px 4px 2px 0 + //End of customize &.oe_focused border-color: $tag-border-selected @include box-shadow(0 1px 2px $tag-border-selected inset) @@ -1512,6 +1493,7 @@ $sheet-padding: 16px @include box-sizing(border) display: table-cell width: 50% + padding-left: 2px // Managed padding-left according bootstrap3 ul margin: 0 8px 8px padding: 0 @@ -1541,7 +1523,7 @@ $sheet-padding: 16px margin: 4px 0 line-height: 18px button - margin: 0 0 8px 0 + margin: 0 0 8px -3px // Managed margin-left according bootstrap3 .oe_searchview_custom padding: 0 8px 8px 8px div @@ -1561,6 +1543,10 @@ $sheet-padding: 16px position: absolute top: 0 right: 5px + //Customize for searchview label + label + font-weight: normal + //End of Customize .oe_searchview_dashboard form display: none @@ -1586,9 +1572,10 @@ $sheet-padding: 16px white-space: nowrap &:first-child .searchview_extended_prop_or visibility: hidden + margin-left: -14px .searchview_extended_prop_or opacity: 0.5 - margin-left: -14px + margin-left: -14px //Customize 'or' in searchview .oe_opened h4:before content: "▾ " @@ -1735,6 +1722,12 @@ $sheet-padding: 16px .oe_form_sheetbg background: url(/web/static/src/img/form_sheetbg.png) border-bottom: 1px solid #ddd + // Customize font weight according bootstrap3 + .oe_subtotal_footer label + font-weight: bold + table label + font-weight: normal + // End of customize .oe_form_sheet border: 1px solid #c8c8d3 //$tag-border @include box-shadow(0 4px 20px rgba(0,0,0,0.15)) @@ -1802,7 +1795,7 @@ $sheet-padding: 16px font-weight: bold !important padding: 2px 11px 2px 0px !important label.oe_form_label_help - font-weight: normal + font-weight: normal !important .oe_form_box_info background: #fe9 border-bottom: 1px solid #cb6 @@ -1829,7 +1822,8 @@ $sheet-padding: 16px label line-height: 18px display: block - min-width: 140px + min-width: 150px + font-weight: bold !important td.oe_form_group_cell + .oe_form_group_cell padding: 2px 0 2px 8px .oe_form_group @@ -1841,7 +1835,6 @@ $sheet-padding: 16px // FormView.label {{{ .oe_form .oe_form_label_help[for], .oe_form_label[for] - font-weight: bold white-space: nowrap padding-right: 8px span @@ -1910,6 +1903,17 @@ $sheet-padding: 16px input height: inherit !important font-size: inherit + // Customize for formview + .oe_title h1, h1 + font-weight: bold + font-size: 2em + h2 + font-size: 1.5em + label + font-weight: bold + margin-bottom: 0px + display: inline + // End of customize .oe_form_field width: 100% display: inline-block @@ -2025,6 +2029,8 @@ $sheet-padding: 16px margin: 0 padding: 0 .oe_form + .oe_form_field_binary + display: inline-block .oe_form_field_image padding: 0 position: relative @@ -2048,12 +2054,13 @@ $sheet-padding: 16px display: inline-block clear: both width: 100% + margin-bottom: -15px .oe_add float: left position: relative width: 100% left: +2px - top: +7px + margin: 9px 0 /* Customize margin according bootstrap3 */ overflow: hidden button display: inline @@ -2063,9 +2070,6 @@ $sheet-padding: 16px vertical-align: middle button.oe_attach width: 24px - overflow: hidden - width: 24px - overflow: hidden background: transparent color: #7C7BAD box-shadow: none @@ -2107,6 +2111,7 @@ $sheet-padding: 16px // }}} // FormView.many2one {{{ .oe_form_field_many2one + display: inline-block td:first-child position: relative span.oe_m2o_drop_down_button @@ -2155,8 +2160,14 @@ $sheet-padding: 16px padding-left: 24px margin: 0 position: relative + // Customize for statusbar label + line-height: normal + font-size: 100% + font-weight: normal + // End of customize > .arrow width: 17px + height: 30px display: inline-block vertical-align: top overflow: hidden @@ -2525,8 +2536,6 @@ $sheet-padding: 16px top: -1px .oe_form_field_many2one .oe_m2o_cm_button line-height: 18px - .oe_secondary_submenu - line-height: 14px .oe_webclient .oe_star_on, .oe_star_off top: 0px @@ -2684,7 +2693,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 @@ -2731,6 +2740,121 @@ div.ui-widget-overlay overflow: hidden !important // }}} +// Customize bootstrap3 for leftbar +.nav + li > a + padding: 3px 4px 2px 18px + color: #4c4c4c + nav-pills.nav-stacked > li > ul + padding-left: 16px +.nav-pills > li.active,a.list-group-item.active + > a + background-color: #7c7bad + color: white + border-radius: 0 + a:hover,a:focus + background-color: #7c7bad + .badge + background-color: white + color: #7c7bad + text-shadow: none +.badge + font-weight: normal + font-size: 11px + background-color: #7c7bad +// End of customize + +// Customize for global tags +button, body + line-height: normal +h1,h2 + font-weight: bold +h3 + font-size: 1.17em + font-weight: bold +p + display: block + -webkit-margin-before: 1em + -webkit-margin-after: 1em + -webkit-margin-start: 0px + -webkit-margin-end: 0px +pre + background-color: #FFFFFF + border: none + padding: 10px 0 3px 0 +// End of customize + +// Customize for kanban tooltip +.jqstooltip + height: auto !important + width: auto !important +// End of kanban tooltip + +// Customize for chatter +h5 + font-weight: bold + font-size: smaller +.oe_form .oe_subtype label, .oe_subtype label + font-weight: normal +.oe_msg_subtype_check + margin: 3px 3px 0 !important +// End of customize + +// Minimized unused white space and label weight for wizard popup +.oe_view_manager_new .oe_form_nosheet + margin-top: 8px + .oe_form_label + font-weight: normal +// End of customize + +// navbar hack {{{ +.navbar + min-height: 32px + margin-bottom: 0px + border: none + z-index: 1 + background-color: #414141 + @include vertical-gradient(#646060, #262626) +.navbar-default + .navbar-nav + li + a:hover,a:focus + background: rgba(0, 0, 0, 0.3) + .open > a,a:hover,a:focus + background: rgba(0, 0, 0, 0.3) !important + .dropdown > a .caret + border-top-color: #777 !important + border-bottom-color: #777 !important +.navbar-nav + li + a + padding: 4px 32px 4px 12px +.dropdown-menu + font-size: 13px + padding: 4px 0 + background: #333333 !important + background: rgba(37, 37, 37, 0.9) !important + border-color: #999999 + border-color: rgba(0, 0, 0, 0.2) + background-color: #414141 + text-shadow: none + @include vertical-gradient(#646060, #262626) + @include radius(3px) + li + a,a:hover,a:focus + color: #eeeeee +// End hack }}} + +// Hack for ui icon {{{ +.ui-icon + width: 18px + height: 18px +// End hack}}} + +input[type="radio"], input[type="checkbox"] + margin-right: 4px + margin-left: 4px + .blockUI.blockOverlay background-color: black opacity: 0.6000000238418579 diff --git a/addons/web/static/src/css/data_export.css b/addons/web/static/src/css/data_export.css index 3fa24265b45..b9b885cd8e9 100644 --- a/addons/web/static/src/css/data_export.css +++ b/addons/web/static/src/css/data_export.css @@ -1,6 +1,11 @@ .openerp .oe_export { width: 100%; } +/* Customize according bootstrap3 */ +.openerp .oe_export label { + font-weight: normal; +} +/* End of customize */ .openerp .oe_export_row tr{ background-color: #FFFFFF; font-size: 0.8em; diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index f019972fd14..abbd4767361 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -442,10 +442,10 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.$el.html(QWeb.render("DatabaseManager", { widget : self })); $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self })); $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self })); - $('ul.oe_secondary_submenu > li:first').addClass('oe_active'); + $('ul.oe_secondary_submenu > li:first').addClass('active'); $('ul.oe_secondary_submenu > li').bind('click', function (event) { var menuitem = $(this); - menuitem.addClass('oe_active').siblings().removeClass('oe_active'); + menuitem.addClass('active').siblings().removeClass('active'); var form_id =menuitem.find('a').attr('href'); $(form_id).show().siblings().hide(); event.preventDefault(); @@ -969,7 +969,7 @@ instance.web.Menu = instance.web.Widget.extend({ this.needaction_data = data; _.each(this.needaction_data, function (item, menu_id) { var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]'); - $item.find('.oe_menu_counter').remove(); + $item.find('.badge').remove(); if (item.needaction_counter && item.needaction_counter > 0) { $item.append(QWeb.render("Menu.needaction_counter", { widget : item })); } @@ -982,8 +982,8 @@ instance.web.Menu = instance.web.Widget.extend({ reflow: function() { var self = this; this.$el.height('auto').show(); - var $more_container = this.$('.oe_menu_more_container').hide(); - var $more = this.$('.oe_menu_more'); + var $more_container = this.$('#menu_more_container').hide(); + var $more = this.$('#menu_more'); $more.children('li').insertBefore($more_container); var $toplevel_items = this.$el.children('li').not($more_container).hide(); $toplevel_items.each(function() { @@ -1026,8 +1026,8 @@ instance.web.Menu = instance.web.Widget.extend({ } // Activate current main menu - this.$el.find('.oe_active').removeClass('oe_active'); - $main_menu.addClass('oe_active'); + this.$el.find('.active').removeClass('active'); + $main_menu.addClass('active'); // Show current sub menu this.$secondary_menus.find('.oe_secondary_menu').hide(); @@ -1037,13 +1037,13 @@ instance.web.Menu = instance.web.Widget.extend({ this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length); // Activate current menu item and show parents - this.$secondary_menus.find('.oe_active').removeClass('oe_active'); + this.$secondary_menus.find('.active').removeClass('active'); if ($main_menu !== $clicked_menu) { $clicked_menu.parents().show(); if ($clicked_menu.is('.oe_menu_toggler')) { $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle(); } else { - $clicked_menu.parent().addClass('oe_active'); + $clicked_menu.parent().addClass('active'); } } }, @@ -1129,7 +1129,7 @@ instance.web.Menu = instance.web.Widget.extend({ }, on_menu_click: function(ev) { ev.preventDefault(); - var needaction = $(ev.target).is('div.oe_menu_counter'); + var needaction = $(ev.target).is('div#menu_counter'); this.menu_click($(ev.currentTarget).data('menu'), needaction); }, }); @@ -1143,7 +1143,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ start: function() { var self = this; this._super.apply(this, arguments); - this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) { + this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) { ev.preventDefault(); var f = self['on_menu_' + $(this).data('menu')]; if (f) { @@ -1223,6 +1223,26 @@ instance.web.UserMenu = instance.web.Widget.extend({ }, }); +instance.web.FullscreenWidget = instance.web.Widget.extend({ + /** + * Widgets extending the FullscreenWidget will be displayed fullscreen, + * and will have a fixed 1:1 zoom level on mobile devices. + */ + start: function(){ + if(!$('#oe-fullscreenwidget-viewport').length){ + $('head').append(''); + } + instance.webclient.set_content_full_screen(true); + return this._super(); + }, + destroy: function(){ + instance.webclient.set_content_full_screen(false); + $('#oe-fullscreenwidget-viewport').remove(); + return this._super(); + }, + +}); + instance.web.Client = instance.web.Widget.extend({ init: function(parent, origin) { instance.client = instance.webclient = this; @@ -1284,7 +1304,7 @@ instance.web.Client = instance.web.Widget.extend({ self.action_manager.appendTo(self.$('.oe_application')); }, toggle_bars: function(value) { - this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value); + this.$('tr:has(td.navbar),.oe_leftbar').toggle(value); }, has_uncommitted_changes: function() { return false; diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index 37a217a8664..81e029f5f87 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 */ +$.browser = $.browser || {}; +if(navigator.appVersion.indexOf("MSIE") !== -1) { + $.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 04f657d91f7..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 @@ -1090,6 +1101,157 @@ instance.web.DropMisordered = instance.web.Class.extend({ } }); +instance.web.SimpleIndexedDB = instance.web.Class.extend({ + /** + * A simple wrapper around IndexedDB that provides an asynchronous + * localStorage-like Key-Value api that persists between browser + * restarts. + * + * It will not work if the browser doesn't support a recent version + * of IndexedDB, and it may fail if the user refuses db access. + * + * All instances of SimpleIndexedDB will by default refer to the same + * IndexedDB database, if you want to pick another one, use the 'name' + * option on instanciation. + */ + init: function(opts){ + var self = this; + var opts = opts || {}; + + this.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; + this.db = undefined; + this._ready = new $.Deferred(); + + if(this.indexedDB && this.indexedDB.open){ + var request = this.indexedDB.open( opts.name || "SimpleIndexedDB" ,1); + + request.onerror = function(event){ + self.db = null; + self._ready.reject(event.target.error); + }; + + request.onsuccess = function(event){ + self.db = request.result; + self._ready.resolve(); + }; + + request.onupgradeneeded = function(event){ + self.db = event.target.result; + + var objectStore = self.db.createObjectStore("keyvals", { keyPath:"key" }); + self._ready.resolve(); + }; + }else{ + this.db = null; + this._ready.reject({type:'UnknownError', message:'IndexedDB is not supported by your Browser'}); + } + }, + + /** + * returns true if the browser supports the necessary IndexedDB API + * (but doesn't tell if the db can be created, check ready() for that) + */ + isSupportedByBrowser: function(){ + return this.indexedDB && this.indexedDB.open; + }, + + /** + * returns a deferred that resolves when the db has been successfully + * initialized. done/failed callbacks are optional. + */ + ready: function(done,failed){ + this._ready.then(done,failed); + return this._ready.promise(); + }, + + /** + * fetches the value associated to 'key' in the db. if the key doesn't + * exists, it returns undefined. The returned value is provided as a + * deferred, or as the first parameter of the optional 'done' callback. + */ + getItem: function(key,done,failed){ + var self = this; + var def = new $.Deferred(); + def.then(done,failed); + + this._ready.then(function(){ + var request = self.db.transaction(["keyvals"],"readwrite") + .objectStore("keyvals") + .get(key); + + request.onsuccess = function(){ + def.resolve(request.result ? request.result.value : undefined); + }; + + request.onerror = function(event){ + def.reject(event.target.error); + }; + },function(){ + def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'}); + }); + + return def.promise(); + }, + + /** + * Associates a value to 'key' in the db, overwriting previous value if + * necessary. Contrary to localStorage, the value is not limited to strings and + * can be any javascript object, even with cyclic references ! + * + * Be sure to check for failure as the user may refuse to have data localy stored. + */ + setItem: function(key,value,done,failed){ + var self = this; + var def = new $.Deferred(); + def.then(done,failed); + + this._ready.then(function(){ + var request = self.db.transaction(["keyvals"],"readwrite") + .objectStore("keyvals") + .put( {key:key, value:value} ); + + request.onsuccess = function(){ + def.resolve(); + }; + + request.onerror = function(event){ + def.reject(event.target.error); + }; + },function(){ + def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'}); + }); + + return def.promise(); + }, + + /** + * Removes the value associated with 'key' from the db. + */ + removeItem: function(key,done,failed){ + var self = this; + var def = new $.Deferred(); + def.then(done,failed); + + this._ready.then(function(){ + var request = self.db.transaction(["keyvals"],"readwrite") + .objectStore("keyvals") + .delete(key); + + request.onsuccess = function(){ + def.resolve(); + }; + + request.onerror = function(event){ + def.reject(event.target.error); + }; + },function(){ + def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'}); + }); + + return def.promise(); + }, +}); + })(); // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: 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/search.js b/addons/web/static/src/js/search.js index 56067707978..9c713e07669 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -71,7 +71,10 @@ my.SearchQuery = B.Collection.extend({ }, add: function (values, options) { options = options || {}; - if (!(values instanceof Array)) { + + if (!values) { + values = []; + } else if (!(values instanceof Array)) { values = [values]; } @@ -87,6 +90,10 @@ my.SearchQuery = B.Collection.extend({ } B.Collection.prototype.add.call(this, model, options); }, this); + // warning: in backbone 1.0+ add is supposed to return the added models, + // but here toggle may delegate to add and return its value directly. + // return value of neither seems actually used but should be tested + // before change, probably return this; }, toggle: function (value, options) { 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 2cb318dcb17..f20e9053867 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -399,9 +399,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.dataset.index = this.dataset.ids.length - 1; break; } - this.reload(); + var def = this.reload(); this.trigger('pager_action_executed'); + return def; } + return $.when(); }, init_pager: function() { var self = this; @@ -416,8 +418,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.$el.find('.oe_form_pager').replaceWith(this.$pager); } this.$pager.on('click','a[data-pager-action]',function() { - var action = $(this).data('pager-action'); - self.execute_pager_action(action); + var $el = $(this); + if ($el.attr("disabled")) + return; + var action = $el.data('pager-action'); + var def = $.when(self.execute_pager_action(action)); + $el.attr("disabled"); + def.always(function() { + $el.removeAttr("disabled"); + }); }); this.do_update_pager(); }, @@ -722,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) { @@ -891,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: @@ -3071,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); }); }, @@ -3583,6 +3588,67 @@ instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({ }, }); +/** + * Abstract-ish ListView.List subclass adding an "Add an item" row to replace + * the big ugly button in the header. + * + * Requires the implementation of a ``is_readonly`` method (usually a proxy to + * the corresponding field's readonly or effective_readonly property) to + * decide whether the special row should or should not be inserted. + * + * Optionally an ``_add_row_class`` attribute can be set for the class(es) to + * set on the insertion row. + */ +instance.web.form.AddAnItemList = instance.web.ListView.List.extend({ + pad_table_to: function (count) { + if (!this.view.is_action_enabled('create') || this.is_readonly()) { + this._super(count); + return; + } + + this._super(count > 0 ? count - 1 : 0); + + var self = this; + var columns = _(this.columns).filter(function (column) { + return column.invisible !== '1'; + }).length; + if (this.options.selectable) { columns++; } + if (this.options.deletable) { columns++; } + + var $cell = $('', { + colspan: columns, + 'class': this._add_row_class || '' + }).append( + $('', {href: '#'}).text(_t("Add an item")) + .mousedown(function () { + // FIXME: needs to be an official API somehow + if (self.view.editor.is_editing()) { + self.view.__ignore_blur = true; + } + }) + .click(function (e) { + e.preventDefault(); + e.stopPropagation(); + // FIXME: there should also be an API for that one + if (self.view.editor.form.__blur_timeout) { + clearTimeout(self.view.editor.form.__blur_timeout); + self.view.editor.form.__blur_timeout = false; + } + self.view.ensure_saved().done(function () { + self.view.do_add_record(); + }); + })); + + var $padding = this.$current.find('tr:not([data-id]):first'); + var $newrow = $('').append($cell); + if ($padding.length) { + $padding.before($newrow); + } else { + this.$current.append($newrow) + } + } +}); + /* # Values: (0, 0, { fields }) create # (1, ID, { fields }) update @@ -4161,62 +4227,11 @@ instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({ } } }); -instance.web.form.One2ManyList = instance.web.ListView.List.extend({ - pad_table_to: function (count) { - if (!this.view.is_action_enabled('create')) { - this._super(count); - } else { - this._super(count > 0 ? count - 1 : 0); - } - - // magical invocation of wtf does that do - if (this.view.o2m.get('effective_readonly')) { - return; - } - - var self = this; - var columns = _(this.columns).filter(function (column) { - return column.invisible !== '1'; - }).length; - if (this.options.selectable) { columns++; } - if (this.options.deletable) { columns++; } - - if (!this.view.is_action_enabled('create')) { - return; - } - - var $cell = $('', { - colspan: columns, - 'class': 'oe_form_field_one2many_list_row_add' - }).append( - $('', {href: '#'}).text(_t("Add an item")) - .mousedown(function () { - // FIXME: needs to be an official API somehow - if (self.view.editor.is_editing()) { - self.view.__ignore_blur = true; - } - }) - .click(function (e) { - e.preventDefault(); - e.stopPropagation(); - // FIXME: there should also be an API for that one - if (self.view.editor.form.__blur_timeout) { - clearTimeout(self.view.editor.form.__blur_timeout); - self.view.editor.form.__blur_timeout = false; - } - self.view.ensure_saved().done(function () { - self.view.do_add_record(); - }); - })); - - var $padding = this.$current.find('tr:not([data-id]):first'); - var $newrow = $('').append($cell); - if ($padding.length) { - $padding.before($newrow); - } else { - this.$current.append($newrow); - } - } +instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({ + _add_row_class: 'oe_form_field_one2many_list_row_add', + is_readonly: function () { + return this.view.o2m.get('effective_readonly'); + }, }); instance.web.form.One2ManyFormView = instance.web.FormView.extend({ @@ -4428,7 +4443,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instan this.$el.addClass('oe_form_field oe_form_field_many2many'); this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { - 'addable': this.get("effective_readonly") ? null : _t("Add"), + 'addable': null, 'deletable': this.get("effective_readonly") ? false : true, 'selectable': this.multi_selection, 'sortable': false, @@ -4495,6 +4510,11 @@ instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({ * @extends instance.web.ListView */ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{ + init: function (parent, dataset, view_id, options) { + this._super(parent, dataset, view_id, _.extend(options || {}, { + ListType: instance.web.form.Many2ManyList, + })); + }, do_add_record: function () { var pop = new instance.web.form.SelectCreatePopup(this); pop.select_element( @@ -4544,6 +4564,12 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in }, is_action_enabled: function () { return true; }, }); +instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({ + _add_row_class: 'oe_form_field_many2many_list_row_add', + is_readonly: function () { + return this.view.m2m_field.get('effective_readonly'); + } +}); instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, { disable_utility_classes: true, @@ -5547,18 +5573,18 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ var self = this; var selection_unfolded = []; var selection_folded = []; + var fold_field = this.options.fold_field; var calculation = _.bind(function() { if (this.field.type == "many2one") { - return self.get_distant_fields().then(function(fields) { + 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;}); + .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')) { + if (fold_field && record[fold_field] && record.id != self.get('value')) { selection_folded.push([record.id, name]); } else { selection_unfolded.push([record.id, name]); @@ -5566,7 +5592,7 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ }); }); }); - }); + }); } else { // For field type selection filter values according to // statusbar_visible attribute of the field. For example: @@ -5588,12 +5614,18 @@ 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.options.fold_field) { + this.distant_fields = {} + } if (this.distant_fields) { return $.when(this.distant_fields); } - return new instance.web.Model(self.field.relation).call("fields_get", [["fold"]]).then(function(fields) { + return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) { self.distant_fields = fields; return 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 ecdbaf8b465..37369377bf1 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -147,6 +147,19 @@ instance.web.ActionManager = instance.web.Widget.extend({ this.inner_action = last_widget.action; } }, + add_breadcrumb_url: function (url, label) { + // Add a pseudo breadcrumb that will redirect to an url + this.push_breadcrumb({ + show: function() { + instance.web.redirect(url); + }, + hide: function() {}, + destroy: function() {}, + get_title: function() { + return label; + } + }); + }, get_title: function() { var titles = []; for (var i = 0; i < this.breadcrumbs.length; i += 1) { @@ -217,6 +230,12 @@ instance.web.ActionManager = instance.web.Widget.extend({ do_load_state: function(state, warm) { var self = this, action_loaded; + if (!warm && 'return_label' in state) { + var return_url = state.return_url || document.referrer; + if (return_url) { + this.add_breadcrumb_url(return_url, state.return_label); + } + } if (state.action) { if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) { var action_client = { @@ -225,7 +244,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ params: state, _push_me: state._push_me, }; - this.null_action(); + if (warm) { + this.null_action(); + } action_loaded = this.do_action(action_client); } else { var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action; @@ -245,7 +266,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ add_context.active_ids = [state.active_id]; } add_context.params = state; - this.null_action(); + if (warm) { + this.null_action(); + } action_loaded = this.do_action(state.action, { additional_context: add_context }); $.when(action_loaded || null).done(function() { instance.webclient.menu.has_been_loaded.done(function() { @@ -258,7 +281,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ } } else if (state.model && state.id) { // TODO handle context & domain ? - this.null_action(); + if (warm) { + this.null_action(); + } var action = { res_model: state.model, res_id: state.id, @@ -268,7 +293,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ action_loaded = this.do_action(action); } else if (state.sa) { // load session action - this.null_action(); + if (warm) { + this.null_action(); + } action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).then(function(action) { if (action) { return self.do_action(action); @@ -510,7 +537,11 @@ instance.web.ActionManager = instance.web.Widget.extend({ }); }, ir_actions_act_url: function (action) { - window.open(action.url, action.target === 'self' ? '_self' : '_blank'); + if (action.target === 'self') { + instance.web.redirect(action.url); + } else { + window.open(action.url, '_blank'); + } return $.when(); }, }); @@ -1178,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 }); @@ -1354,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/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 60a344467c0..ae7a4f279b0 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -320,7 +320,7 @@
Database Management
-
-
    +