From da5b6d7dbff9eac3ca6117e23eac78271e243546 Mon Sep 17 00:00:00 2001 From: Chintan Ambaliya Date: Thu, 19 Jul 2018 17:04:13 +0530 Subject: [PATCH] Added Upstream Patch for web, website --- addons/web/controllers/main.py | 2 + addons/web/models/ir_http.py | 4 +- addons/web/static/src/js/_deprecated/data.js | 13 +- .../static/src/js/views/basic/basic_model.js | 139 ++++++++++-------- .../js/views/list/list_editable_renderer.js | 12 +- .../tests/fields/relational_fields_tests.js | 130 ++++++++++++++++ addons/web/static/tests/views/form_tests.js | 36 +++++ addons/web/static/tests/views/kanban_tests.js | 50 +++++++ addons/website/static/src/js/menu/content.js | 1 + 9 files changed, 313 insertions(+), 74 deletions(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 720e2692..8e67d2da 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1542,6 +1542,8 @@ class ExcelExport(ExportFormat, http.Controller): if isinstance(cell_value, pycompat.string_types): cell_value = re.sub("\r", " ", pycompat.to_text(cell_value)) + # Excel supports a maximum of 32767 characters in each cell: + cell_value = cell_value[:32767] elif isinstance(cell_value, datetime.datetime): cell_style = datetime_style elif isinstance(cell_value, datetime.date): diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py index b2b9bc11..94431f3d 100644 --- a/addons/web/models/ir_http.py +++ b/addons/web/models/ir_http.py @@ -26,7 +26,7 @@ class Http(models.AbstractModel): "session_id": request.session.sid, "uid": request.session.uid, "is_system": request.env.user._is_system(), - "is_superuser": request.env.user._is_superuser(), + "is_superuser": request.env.user._is_superuser() if request.session.uid else False, "user_context": request.session.get_context() if request.session.uid else {}, "db": request.session.db, "server_version": version_info.get('server_version'), @@ -36,7 +36,7 @@ class Http(models.AbstractModel): "company_id": request.env.user.company_id.id if request.session.uid else None, "partner_id": request.env.user.partner_id.id if request.session.uid and request.env.user.partner_id else None, "user_companies": {'current_company': (user.company_id.id, user.company_id.name), 'allowed_companies': [(comp.id, comp.name) for comp in user.company_ids]} if display_switch_company_menu else False, - "currencies": self.get_currencies(), + "currencies": self.get_currencies() if request.session.uid else {}, "web.base.url": self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''), } diff --git a/addons/web/static/src/js/_deprecated/data.js b/addons/web/static/src/js/_deprecated/data.js index efb7c381..110a34db 100644 --- a/addons/web/static/src/js/_deprecated/data.js +++ b/addons/web/static/src/js/_deprecated/data.js @@ -260,14 +260,15 @@ var QueryGroup = Class.extend({ var count_key = (grouping_fields[0] && grouping_fields[0].split(':')[0]) + '_count'; var aggregates = {}; - _(fixed_group).each(function (value, key) { - if (key.indexOf('__') === 0 + for (var key in fixed_group) { + if (fixed_group.hasOwnProperty(key)) { + if (!(key.indexOf('__') === 0 || _.contains(grouping_fields, key) - || (key === count_key)) { - return; + || (key === count_key))) { + aggregates[key] = fixed_group[key] || 0; + } } - aggregates[key] = value || 0; - }); + } this.model = new Model( model, fixed_group.__context, fixed_group.__domain); diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 52c2f783..2b35e727 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -814,7 +814,8 @@ var BasicModel = AbstractModel.extend({ return this.mutex.exec(this._applyChange.bind(this, record_id, changes, options)); }, /** - * Reload all data for a given resource + * Reload all data for a given resource. At any time there is at most one + * reload operation active. * * @param {string} id local id for a resource * @param {Object} [options] @@ -823,64 +824,7 @@ var BasicModel = AbstractModel.extend({ * @returns {Deferred} resolves to the id of the resource */ reload: function (id, options) { - options = options || {}; - var element = this.localData[id]; - - if (element.type === 'record') { - if (!options.currentId && (('currentId' in options) || this.isNew(id))) { - var params = { - context: element.context, - fieldsInfo: element.fieldsInfo, - fields: element.fields, - viewType: element.viewType, - }; - return this._makeDefaultRecord(element.model, params); - } - if (!options.keepChanges) { - this.discardChanges(id, {rollback: false}); - } - } else if (element._changes) { - delete element.tempLimitIncrement; - _.each(element._changes, function (change) { - delete change.isNew; - }); - } - - if (options.context !== undefined) { - element.context = options.context; - } - if (options.domain !== undefined) { - element.domain = options.domain; - } - if (options.groupBy !== undefined) { - element.groupedBy = options.groupBy; - } - if (options.limit !== undefined) { - element.limit = options.limit; - } - if (options.offset !== undefined) { - this._setOffset(element.id, options.offset); - } - if (options.loadMoreOffset !== undefined) { - element.loadMoreOffset = options.loadMoreOffset; - } else { - // reset if not specified - element.loadMoreOffset = 0; - } - if (options.currentId !== undefined) { - element.res_id = options.currentId; - } - if (options.ids !== undefined) { - element.res_ids = options.ids; - element.count = element.res_ids.length; - } - if (element.type === 'record') { - element.offset = _.indexOf(element.res_ids, element.res_id); - } - var loadOptions = _.pick(options, 'fieldNames', 'viewType'); - return this._load(element, loadOptions).then(function (result) { - return result.id; - }); + return this.mutex.exec(this._reload.bind(this, id, options)); }, /** * In some case, we may need to remove an element from a list, without going @@ -2916,13 +2860,10 @@ var BasicModel = AbstractModel.extend({ continue; } changes = this._generateChanges(relRecord, options); - if (changes.id) { + if (!this.isNew(relRecord.id)) { // the subrecord already exists in db + commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id)); delete changes.id; - if (this.isNew(record.id)) { - // if the main record is new, link the subrecord to it - commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id)); - } if (!_.isEmpty(changes)) { commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes)); } @@ -4008,6 +3949,76 @@ var BasicModel = AbstractModel.extend({ }); }); }, + /** + * Reload all data for a given resource + * + * @private + * @param {string} id local id for a resource + * @param {Object} [options] + * @param {boolean} [options.keepChanges=false] if true, doesn't discard the + * changes on the record before reloading it + * @returns {Deferred} resolves to the id of the resource + */ + _reload: function (id, options) { + options = options || {}; + var element = this.localData[id]; + + if (element.type === 'record') { + if (!options.currentId && (('currentId' in options) || this.isNew(id))) { + var params = { + context: element.context, + fieldsInfo: element.fieldsInfo, + fields: element.fields, + viewType: element.viewType, + }; + return this._makeDefaultRecord(element.model, params); + } + if (!options.keepChanges) { + this.discardChanges(id, {rollback: false}); + } + } else if (element._changes) { + delete element.tempLimitIncrement; + _.each(element._changes, function (change) { + delete change.isNew; + }); + } + + if (options.context !== undefined) { + element.context = options.context; + } + if (options.domain !== undefined) { + element.domain = options.domain; + } + if (options.groupBy !== undefined) { + element.groupedBy = options.groupBy; + } + if (options.limit !== undefined) { + element.limit = options.limit; + } + if (options.offset !== undefined) { + this._setOffset(element.id, options.offset); + } + if (options.loadMoreOffset !== undefined) { + element.loadMoreOffset = options.loadMoreOffset; + } else { + // reset if not specified + element.loadMoreOffset = 0; + } + if (options.currentId !== undefined) { + element.res_id = options.currentId; + } + if (options.ids !== undefined) { + element.res_ids = options.ids; + element.count = element.res_ids.length; + } + if (element.type === 'record') { + element.offset = _.indexOf(element.res_ids, element.res_id); + } + var loadOptions = _.pick(options, 'fieldNames', 'viewType'); + return this._load(element, loadOptions).then(function (result) { + return result.id; + }); + }, /** * Allows to save a value in the specialData cache associated to a given * record and fieldName. If the value in the cache was already the given diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js index 7288ef48..cfb6e006 100644 --- a/addons/web/static/src/js/views/list/list_editable_renderer.js +++ b/addons/web/static/src/js/views/list/list_editable_renderer.js @@ -841,8 +841,16 @@ ListRenderer.include({ */ _onTrashIconClick: function (event) { event.stopPropagation(); - var id = $(event.target).closest('tr').data('id'); - this.trigger_up('list_record_delete', {id: id}); + var $row = $(event.target).closest('tr') + var id = $row.data('id'); + if ($row.hasClass('o_selected_row')) { + this.trigger_up('list_record_delete', {id: id}); + } else { + var self = this; + this.unselectRow().then(function () { + self.trigger_up('list_record_delete', {id: id}); + }); + } }, /** * When a click happens outside the list view, or outside a currently diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 6dca123a..251dea62 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -3206,6 +3206,136 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('onchange for embedded one2many in a one2many with a second page', function (assert) { + assert.expect(1); + + this.data.turtle.fields.partner_ids.type = 'one2many'; + this.data.turtle.records[0].partner_ids = [1]; + // we need a second page, so we set two records and only display one per page + this.data.partner.records[0].turtles = [1, 2]; + + this.data.partner.onchanges = { + turtles: function (obj) { + obj.turtles = [ + [5], + [1, 1, { + turtle_foo: "hop", + partner_ids: [[5], [4, 1]], + }], + [1, 2, { + turtle_foo: "blip", + partner_ids: [[5], [4, 2], [4, 4]], + }], + ]; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'write') { + var expectedResultTurtles = [ + [1, 1, { + turtle_foo: "hop", + }], + [1, 2, { + partner_ids: [[4, 2, false], [4, 4, false]], + turtle_foo: "blip", + }], + ]; + assert.deepEqual(args.args[1].turtles, expectedResultTurtles, + "the right values should be written"); + } + return this._super.apply(this, arguments); + } + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_data_cell').eq(1).click(); + var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); + $cell.val("hop").trigger('change'); + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + + QUnit.test('onchange for embedded one2many in a one2many updated by server', function (assert) { + // here we test that after an onchange, the embedded one2many field has + // been updated by a new list of ids by the server response, to this new + // list should be correctly sent back at save time + assert.expect(3); + + this.data.turtle.fields.partner_ids.type = 'one2many'; + this.data.partner.records[0].turtles = [2]; + this.data.turtle.records[1].partner_ids = [2]; + + this.data.partner.onchanges = { + turtles: function (obj) { + obj.turtles = [ + [5], + [1, 2, { + turtle_foo: "hop", + partner_ids: [[5], [4, 2], [4, 4]], + }], + ]; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + var expectedResultTurtles = [ + [1, 2, { + partner_ids: [[4, 2, false], [4, 4, false]], + turtle_foo: "hop", + }], + ]; + assert.deepEqual(args.args[1].turtles, expectedResultTurtles, + 'The right values should be written'); + } + return this._super.apply(this, arguments); + } + }); + + assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim(), "second record", + "the partner_ids should be as specified at initialization"); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_data_cell').eq(1).click(); + var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); + $cell.val("hop").trigger("change"); + form.$buttons.find('.o_form_button_save').click(); + + assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim().split(/\s+/), + [ "second", "record", "aaa" ], + 'The partner_ids should have been updated'); + + form.destroy(); + }); + QUnit.test('onchange for embedded one2many with handle widget', function (assert) { assert.expect(2); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index c2a3c2bd..81ee4542 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -3067,6 +3067,42 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('delete a line in a one2many while editing another line triggers a warning', function (assert) { + assert.expect(3); + + this.data.partner.records[0].p = [1, 2]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_data_cell').first().click(); // edit first row + form.$('input').val('').trigger('input'); + form.$('.fa-trash-o').eq(1).click(); // delete second row + + assert.strictEqual($('.modal').find('.modal-title').first().text(), "Warning", + "Clicking out of a dirty line while editing should trigger a warning modal."); + + $('.modal').find('.btn-primary').click(); // discard changes + + assert.strictEqual(form.$('.o_data_cell').first().text(), "first record", + "Value should have been reset to what it was before editing began."); + assert.strictEqual(form.$('.o_data_row').length, 1, + "The other line should have been deleted."); + form.destroy(); + }); + QUnit.test('properly apply onchange on many2many fields', function (assert) { assert.expect(14); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index fb6e7db0..2c5a2ddc 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -2840,6 +2840,56 @@ QUnit.module('Views', { testUtils.unpatch(mixins.ParentedMixin); }); + QUnit.test('grouped kanban becomes ungrouped when clearing domain then clearing groupby', function (assert) { + // in this test, we simulate that clearing the domain is slow, so that + // clearing the groupby does not corrupt the data handled while + // reloading the kanban view. + assert.expect(4); + + var def = $.Deferred(); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + domain: [['foo', '=', 'norecord']], + groupBy: ['bar'], + mockRPC: function (route, args) { + var result = this._super(route, args); + if (args.method === 'read_group') { + var isFirstUpdate = _.isEmpty(args.kwargs.domain) && + args.kwargs.groupby && + args.kwargs.groupby[0] === 'bar'; + if (isFirstUpdate) { + return def.then(_.constant(result)); + } + } + return result; + }, + }); + + assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'), + "the kanban view should be grouped"); + assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'), + "the kanban view should not be ungrouped"); + + kanban.update({domain: []}); // 1st update on kanban view + kanban.update({groupBy: false}); // 2n update on kanban view + def.resolve(); // simulate slow 1st update of kanban view + + assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'), + "the kanban view should not longer be grouped"); + assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'), + "the kanban view should have become ungrouped"); + + kanban.destroy(); + }); + }); }); diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js index 17a3d983..62ff15af 100644 --- a/addons/website/static/src/js/menu/content.js +++ b/addons/website/static/src/js/menu/content.js @@ -145,6 +145,7 @@ var PagePropertiesDialog = widget.Dialog.extend({ horizontal: 'auto', vertical: 'top', }, + widgetParent: 'body', }; if (self.page.date_publish) { datepickersOptions.defaultDate = time.str_to_datetime(self.page.date_publish);