diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 2303b6a68e8..678eacd7f3e 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -86,5 +86,4 @@ This module provides the core of the OpenERP Web Client. "static/test/mutex.js" ], 'bootstrap': True, - 'twitter': False, } diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index d5f5f896fd5..ab74611b8cb 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -522,7 +522,7 @@ html_template = """ diff --git a/addons/web/doc/module.rst b/addons/web/doc/module.rst index 1bdb7c14799..8306e885f65 100644 --- a/addons/web/doc/module.rst +++ b/addons/web/doc/module.rst @@ -1,3 +1,5 @@ +.. _module: + Building an OpenERP Web module ============================== diff --git a/addons/web/session.py b/addons/web/session.py index 30e9308d4e1..8a083e0dec9 100644 --- a/addons/web/session.py +++ b/addons/web/session.py @@ -85,7 +85,7 @@ class OpenERPSession(object): self.jsonp_requests = {} # FIXME use a LRU def send(self, service_name, method, *args): - code_string = "warning -- %s\n\n%s" + code_string = u"warning -- %s\n\n%s" try: return openerp.netsvc.dispatch_rpc(service_name, method, args) except openerp.osv.osv.except_osv, e: @@ -95,13 +95,13 @@ class OpenERPSession(object): except openerp.exceptions.AccessError, e: raise xmlrpclib.Fault(code_string % ("AccessError", e), '') except openerp.exceptions.AccessDenied, e: - raise xmlrpclib.Fault('AccessDenied', str(e)) + raise xmlrpclib.Fault('AccessDenied', openerp.tools.ustr(e)) except openerp.exceptions.DeferredException, e: formatted_info = "".join(traceback.format_exception(*e.traceback)) - raise xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info) + raise xmlrpclib.Fault(openerp.tools.ustr(e), formatted_info) except Exception, e: formatted_info = "".join(traceback.format_exception(*(sys.exc_info()))) - raise xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info) + raise xmlrpclib.Fault(openerp.tools.ustr(e), formatted_info) def proxy(self, service): return Service(self, service) diff --git a/addons/web/static/lib/datejs/globalization/am-ET.js b/addons/web/static/lib/datejs/globalization/am-ET.js new file mode 100644 index 00000000000..8b75ccfd3a5 --- /dev/null +++ b/addons/web/static/lib/datejs/globalization/am-ET.js @@ -0,0 +1,195 @@ +Date.CultureInfo = { + /* Culture Name */ + name: "am-ET", + englishName: "Amharic (Ethiopia)", + nativeName: "አምሃርኛ (ኢትዮጵያ)", + + /* Day Name Strings */ + dayNames: ["እሁድ", "ሰኞ", "ማክሰኞ", "ረብዑ", "ሃሙስ", "ዓርብ", "ቅዳሜ"], + abbreviatedDayNames: ["እሁድ", "ሰኞ", "ማክሰ", "ረብዑ", "ሃሙስ", "ዓርብ", "ቅዳሜ"], + shortestDayNames: ["እሁ", "ሰኞ", "ማክ", "ረብ", "ሃሙ", "ዓር", "ቅዳ"], + firstLetterDayNames: ["እ", "ሰ", "ማ", "ረ", "ሃ", "ዓ", "ቅ"], + + /* Month Name Strings */ + monthNames: ["ጃንዋሪ", "ፌብሩዋሪ", "ማርች", "አፕሪል", "ሜይ", "ጁን", "ጁላይ", "ኦገስት", "ሴፕቴምበር", "ኦክቶበር", "ኖቬምበር", "ዲሴምበር"], + abbreviatedMonthNames: ["ጃንዋ", "ፌብሩ", "ማርች", "አፕሪ", "ሜይ", "ጁን", "ጁላይ", "ኦገስ", "ሴፕቴ", "ኦክቶ", "ኖቬም", "ዲሴም"], + + /* AM/PM Designators */ + amDesignator: "ከቀኑ", + pmDesignator: "ከለሊቱ", + + firstDayOfWeek: 1, + twoDigitYearMax: 2029, + + /** + * The dateElementOrder is based on the order of the + * format specifiers in the formatPatterns.DatePattern. + * + * Example: +
+     shortDatePattern    dateElementOrder
+     ------------------  ----------------
+     "M/d/yyyy"          "mdy"
+     "dd/MM/yyyy"        "dmy"
+     "yyyy-MM-dd"        "ymd"
+     
+ * + * The correct dateElementOrder is required by the parser to + * determine the expected order of the date elements in the + * string being parsed. + */ + dateElementOrder: "dmy", + + /* Standard date and time format patterns */ + formatPatterns: { + shortDate: "dd/MM/yyyy", + longDate: "dd MMMM yyyy", + shortTime: "HH:mm", + longTime: "HH:mm:ss", + fullDateTime: "dd MMMM yyyy HH:mm:ss", + sortableDateTime: "yyyy-MM-ddTHH:mm:ss", + universalSortableDateTime: "yyyy-MM-dd HH:mm:ssZ", + rfc1123: "ddd, dd MMM yyyy HH:mm:ss GMT", + monthDay: "dd MMMM", + yearMonth: "MMMM yyyy" + }, + + /** + * NOTE: If a string format is not parsing correctly, but + * you would expect it parse, the problem likely lies below. + * + * The following regex patterns control most of the string matching + * within the parser. + * + * The Month name and Day name patterns were automatically generated + * and in general should be (mostly) correct. + * + * Beyond the month and day name patterns are natural language strings. + * Example: "next", "today", "months" + * + * These natural language string may NOT be correct for this culture. + * If they are not correct, please translate and edit this file + * providing the correct regular expression pattern. + * + * If you modify this file, please post your revised CultureInfo file + * to the Datejs Forum located at http://www.datejs.com/forums/. + * + * Please mark the subject of the post with [CultureInfo]. Example: + * Subject: [CultureInfo] Translated "da-DK" Danish(Denmark) + * + * We will add the modified patterns to the master source files. + * + * As well, please review the list of "Future Strings" section below. + */ + regexPatterns: { + jan: /^jan(uary)?/i, + feb: /^feb(ruary)?/i, + mar: /^mar(ch)?/i, + apr: /^apr(il)?/i, + may: /^may/i, + jun: /^jun(e)?/i, + jul: /^jul(y)?/i, + aug: /^aug(ust)?/i, + sep: /^sep(t(ember)?)?/i, + oct: /^oct(ober)?/i, + nov: /^nov(ember)?/i, + dec: /^dec(ember)?/i, + + sun: /^su(n(day)?)?/i, + mon: /^mo(n(day)?)?/i, + tue: /^tu(e(s(day)?)?)?/i, + wed: /^we(d(nesday)?)?/i, + thu: /^th(u(r(s(day)?)?)?)?/i, + fri: /^fr(i(day)?)?/i, + sat: /^sa(t(urday)?)?/i, + + future: /^next/i, + past: /^last|past|prev(ious)?/i, + add: /^(\+|aft(er)?|from|hence)/i, + subtract: /^(\-|bef(ore)?|ago)/i, + + yesterday: /^yes(terday)?/i, + today: /^t(od(ay)?)?/i, + tomorrow: /^tom(orrow)?/i, + now: /^n(ow)?/i, + + millisecond: /^ms|milli(second)?s?/i, + second: /^sec(ond)?s?/i, + minute: /^mn|min(ute)?s?/i, + hour: /^h(our)?s?/i, + week: /^w(eek)?s?/i, + month: /^m(onth)?s?/i, + day: /^d(ay)?s?/i, + year: /^y(ear)?s?/i, + + shortMeridian: /^(a|p)/i, + longMeridian: /^(a\.?m?\.?|p\.?m?\.?)/i, + timezone: /^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i, + ordinalSuffix: /^\s*(st|nd|rd|th)/i, + timeContext: /^\s*(\:|a(?!u|p)|p)/i + }, + + timezones: [{name:"UTC", offset:"-000"}, {name:"GMT", offset:"-000"}, {name:"EST", offset:"-0500"}, {name:"EDT", offset:"-0400"}, {name:"CST", offset:"-0600"}, {name:"CDT", offset:"-0500"}, {name:"MST", offset:"-0700"}, {name:"MDT", offset:"-0600"}, {name:"PST", offset:"-0800"}, {name:"PDT", offset:"-0700"}] +}; + +/******************** + ** Future Strings ** + ******************** + * + * The following list of strings may not be currently being used, but + * may be incorporated into the Datejs library later. + * + * We would appreciate any help translating the strings below. + * + * If you modify this file, please post your revised CultureInfo file + * to the Datejs Forum located at http://www.datejs.com/forums/. + * + * Please mark the subject of the post with [CultureInfo]. Example: + * Subject: [CultureInfo] Translated "da-DK" Danish(Denmark)b + * + * English Name Translated + * ------------------ ----------------- + * about about + * ago ago + * date date + * time time + * calendar calendar + * show show + * hourly hourly + * daily daily + * weekly weekly + * bi-weekly bi-weekly + * fortnight fortnight + * monthly monthly + * bi-monthly bi-monthly + * quarter quarter + * quarterly quarterly + * yearly yearly + * annual annual + * annually annually + * annum annum + * again again + * between between + * after after + * from now from now + * repeat repeat + * times times + * per per + * min (abbrev minute) min + * morning morning + * noon noon + * night night + * midnight midnight + * mid-night mid-night + * evening evening + * final final + * future future + * spring spring + * summer summer + * fall fall + * winter winter + * end of end of + * end end + * long long + * short short + */ diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 638f446c5ba..6cfee50d6a4 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1242,8 +1242,6 @@ .openerp .oe_secondary_submenu { padding: 2px 0 8px 0; margin: 0; - width: 220px; - display: inline-block; } .openerp .oe_secondary_submenu li { position: relative; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 42ade77ccbb..b4665808caf 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1009,8 +1009,6 @@ $sheet-padding: 16px .oe_secondary_submenu padding: 2px 0 8px 0 margin: 0 - width: 220px - display: inline-block li position: relative margin: 0 diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 1c3723d3e4e..f2de7e61eab 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -481,6 +481,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.do_action("reload"); }, }, + _push_me: false, }; self.do_action(client_action); }); @@ -868,7 +869,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.remove('oe_menu_counter'); + $item.find('.oe_menu_counter').remove(); if (item.needaction_counter && item.needaction_counter > 0) { $item.append(QWeb.render("Menu.needaction_counter", { widget : item })); } @@ -1395,13 +1396,9 @@ instance.web.WebClient = instance.web.Client.extend({ }); }, set_content_full_screen: function(fullscreen) { - if (fullscreen) { - $(".oe_webclient", this.$el).addClass("oe_content_full_screen"); - $("body").css({'overflow-y':'hidden'}); - } else { - $(".oe_webclient", this.$el).removeClass("oe_content_full_screen"); - $("body").css({'overflow-y':'scroll'}); - } + $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll'); + this.$('.oe_webclient').toggleClass( + 'oe_content_full_screen', fullscreen); }, has_uncommitted_changes: function() { var $e = $.Event('clear_uncommitted_changes'); diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 9432b02826f..851b60af229 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -166,9 +166,13 @@ instance.web.format_value = function (value, descriptor, value_if_empty) { value = Math.abs(value); pattern = '-' + pattern; } - return _.str.sprintf(pattern, - Math.floor(value), - Math.round((value % 1) * 60)); + var hour = Math.floor(value); + var min = Math.round((value % 1) * 60); + if (min == 60){ + min = 0; + hour = hour + 1; + } + return _.str.sprintf(pattern, hour, min); case 'many2one': // name_get value format return value[1] ? value[1].split("\n")[0] : value[1]; diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 2c1b5ec9d7d..554c4aa7db0 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -689,15 +689,14 @@ openerp.web.pyeval = function (instance) { }; instance.web.pyeval.context = function () { - return { - uid: py.float.fromJSON(instance.session.uid), + return _.extend({ datetime: datetime, context_today: context_today, time: time, relativedelta: relativedelta, current_date: py.PY_call( time.strftime, [py.str.fromJSON('%Y-%m-%d')]), - }; + }, instance.session.user_context); }; /** diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 50e768bac59..12acf4039f4 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -127,41 +127,26 @@ my.InputView = instance.web.Widget.extend({ events: { focus: function () { this.trigger('focused', this); }, blur: function () { this.$el.text(''); this.trigger('blurred', this); }, - keydown: 'onKeydown' + keydown: 'onKeydown', + paste: 'onPaste', }, getSelection: function () { // get Text node - var root = this.$el[0].childNodes[0]; + var root = this.el.childNodes[0]; if (!root || !root.textContent) { // if input does not have a child node, or the child node is an // empty string, then the selection can only be (0, 0) return {start: 0, end: 0}; } - if (window.getSelection) { - var domRange = window.getSelection().getRangeAt(0); - assert(domRange.startContainer === root, - "selection should be in the input view"); - assert(domRange.endContainer === root, - "selection should be in the input view"); - return { - start: domRange.startOffset, - end: domRange.endOffset - } - } else if (document.selection) { - var ieRange = document.selection.createRange(); - var rangeParent = ieRange.parentElement(); - assert(rangeParent === root, - "selection should be in the input view"); - var offsetRange = document.body.createTextRange(); - offsetRange = offsetRange.moveToElementText(rangeParent); - offsetRange.setEndPoint("EndToStart", ieRange); - var start = offsetRange.text.length; - return { - start: start, - end: start + ieRange.text.length - } + var range = window.getSelection().getRangeAt(0); + assert(range.startContainer === root, + "selection should be in the input view"); + assert(range.endContainer === root, + "selection should be in the input view"); + return { + start: range.startOffset, + end: range.endOffset } - throw new Error("Could not get caret position"); }, onKeydown: function (e) { var sel; @@ -199,6 +184,50 @@ my.InputView = instance.web.Widget.extend({ } break; } + }, + setCursorAtEnd: function () { + var sel = window.getSelection(); + sel.removeAllRanges(); + var range = document.createRange(); + // in theory, range.selectNodeContents should work here. In practice, + // MSIE9 has issues from time to time, instead of selecting the inner + // text node it would select the reference node instead (e.g. in demo + // data, company news, copy across the "Company News" link + the title, + // from about half the link to half the text, paste in search box then + // hit the left arrow key, getSelection would blow up). + // + // Explicitly selecting only the inner text node (only child node at + // this point, though maybe we should assert that) avoiids the issue + range.selectNode(this.el.childNodes[0]); + range.collapse(false); + sel.addRange(range); + }, + onPaste: function () { + // In MSIE and Webkit, it is possible to get various representations of + // the clipboard data at this point e.g. + // window.clipboardData.getData('Text') and + // event.clipboardData.getData('text/plain') to ensure we have a plain + // text representation of the object (and probably ensure the object is + // pastable as well, so nobody puts an image in the search view) + // (nb: since it's not possible to alter the content of the clipboard + // — at least in Webkit — to ensure only textual content is available, + // using this would require 1. getting the text data; 2. manually + // inserting the text data into the content; and 3. cancelling the + // paste event) + // + // But Firefox doesn't support the clipboard API (as of FF18) + // although it correctly triggers the paste event (Opera does not even + // do that) => implement lowest-denominator system where onPaste + // triggers a followup "cleanup" pass after the data has been pasted + setTimeout(function () { + // Read text content (ignore pasted HTML) + var data = this.$el.text(); + // paste raw text back in + this.$el.empty().text(data); + // Set the cursor at the end of the text, so the cursor is not lost + // in some kind of error-spawning limbo. + this.setCursorAtEnd(); + }.bind(this), 0); } }); my.FacetView = instance.web.Widget.extend({ @@ -1572,6 +1601,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ append_filter: function (filter) { var self = this; var key = this.key_for(filter); + var warning = _t("This filter is global and will be removed for everybody if you continue."); var $filter; if (key in this.$filters) { @@ -1589,6 +1619,9 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ $('x') .click(function (e) { e.stopPropagation(); + if (!(filter.user_id || confirm(warning))) { + return; + } self.model.call('unlink', [id]).done(function () { $filter.remove(); delete self.$filters[key]; @@ -1711,10 +1744,12 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ }); return $.when( this._super(), - new instance.web.Model(this.view.model).call('fields_get').done(function(data) { - self.fields = _.extend({ - id: { string: 'ID', type: 'id' } - }, data); + new instance.web.Model(this.view.model).call('fields_get', { + context: this.view.dataset.context + }).done(function(data) { + self.fields = _.extend({ + id: { string: 'ID', type: 'id' } + }, data); })).done(function () { self.append_proposition(); }); @@ -1781,6 +1816,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ this._super(parent); this.fields = _(fields).chain() .map(function(val, key) { return _.extend({}, val, {'name': key}); }) + .filter(function (field) { return !field.deprecated; }) .sortBy(function(field) {return field.string;}) .value(); this.attrs = {_: _, fields: this.fields, selected: null}; diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 01d4bf6dd33..4880b3882ff 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -573,19 +573,14 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM ] }); } - if (result.domain) { - function edit_domain(node) { - if (typeof node !== "object") { - return; - } - var new_domain = result.domain[node.attrs.name]; - if (new_domain) { - node.attrs.domain = new_domain; - } - _(node.children).each(edit_domain); - } - edit_domain(this.fields_view.arch); - } + + var fields = this.fields; + _(result.domain).each(function (domain, fieldname) { + var field = fields[fieldname]; + if (!field) { return; } + field.node.attrs.domain = domain; + }); + return $.Deferred().resolve(); } catch(e) { console.error(e); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 6d707bb4627..7579eb9a55f 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1414,8 +1414,6 @@ this.removeAttr('t-if'); - - this.removeAttr('t-if'); diff --git a/addons/web/static/test/formats.js b/addons/web/static/test/formats.js index 4536501d71d..ac602e4b693 100644 --- a/addons/web/static/test/formats.js +++ b/addons/web/static/test/formats.js @@ -68,6 +68,12 @@ openerp.testing.section('web-formats', { strictEqual( instance.web.format_value(-0.0085, {type:'float', widget:'float_time'}), '-00:01'); + strictEqual( + instance.web.format_value(4.9999, {type:'float', widget:'float_time'}), + '05:00'); + strictEqual( + instance.web.format_value(-6.9999, {type:'float', widget:'float_time'}), + '-07:00'); }); test("format_float", function (instance) { var fl = 12.1234; diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js index 1d9ced0ca6d..0ced5bcef2d 100644 --- a/addons/web_kanban/static/src/js/kanban.js +++ b/addons/web_kanban/static/src/js/kanban.js @@ -632,7 +632,7 @@ instance.web_kanban.KanbanGroup = instance.web.Widget.extend({ 'limit': self.view.limit, 'offset': self.dataset_offset += self.view.limit }).then(function(records) { - self.view.dataset.ids = ids.concat(self.view.dataset.ids); + self.view.dataset.ids = ids.concat(self.dataset.ids); self.do_add_records(records); self.compute_cards_auto_height(); return records;