diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-10-27 14:09:20 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-10-27 14:09:20 +0200 |
| commit | e918271f7600d9f77dc6631e48814f0daf951ad8 (patch) | |
| tree | 295629178de5fe3a1cf1dcb8d780661d82df1877 /addons/web/static | |
| parent | 2f0435f08112251abce2591f44e748896c030420 (diff) | |
| parent | 8fdac4b6b2f2fccafc95ea65b976c7e4028d9832 (diff) | |
[MERGE] forward port branch saas-16 up to 8fdac4b6b2f
Diffstat (limited to 'addons/web/static')
14 files changed, 219 insertions, 60 deletions
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index fc29ba77466..2534f8ae90a 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -43,6 +43,15 @@ var AbstractField = Widget.extend({ custom_events: { navigation_move: '_onNavigationMove', }, + + /** + * An object representing fields to be fetched by the model eventhough not present in the view + * This object contains "field name" as key and an object as value. + * That value object must contain the key "type" + * see FieldBinaryImage for an example. + */ + fieldDependencies: {}, + /** * If this flag is set to true, the field widget will be reset on every * change which is made in the view (if the view supports it). This is diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 549b2e256be..817c76323d5 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -20,6 +20,7 @@ var framework = require('web.framework'); var session = require('web.session'); var utils = require('web.utils'); var view_dialogs = require('web.view_dialogs'); +var field_utils = require('web.field_utils'); var qweb = core.qweb; var _t = core._t; @@ -1169,6 +1170,10 @@ var AbstractFieldBinary = AbstractField.extend({ }); var FieldBinaryImage = AbstractFieldBinary.extend({ + fieldDependencies: _.extend({}, AbstractFieldBinary.prototype.fieldDependencies, { + __last_update: {type: 'datetime'}, + }), + template: 'FieldBinaryImage', placeholder: "/web/static/src/img/placeholder.png", events: _.extend({}, AbstractFieldBinary.prototype.events, { @@ -1191,7 +1196,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({ id: JSON.stringify(this.res_id), field: this.nodeOptions.preview_image || this.name, // unique forces a reload of the image when the record has been updated - unique: (this.recordData.__last_update || '').replace(/[^0-9]/g, ''), + unique: field_utils.format.datetime(this.recordData.__last_update).replace(/[^0-9]/g, ''), }); } } diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js index 911e1c5582f..32d526e160d 100644 --- a/addons/web/static/src/js/services/data_manager.js +++ b/addons/web/static/src/js/services/data_manager.js @@ -388,6 +388,11 @@ return core.Class.extend({ } } } + + if (attrs.Widget.prototype.fieldDependencies) { + attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies; + } + return attrs; }, /** @@ -414,6 +419,15 @@ return core.Class.extend({ if (node.tag === 'field') { fieldsInfo[node.attrs.name] = self._processField(viewType, fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {}); + + if (fieldsInfo[node.attrs.name].fieldDependencies) { + var deps = fieldsInfo[node.attrs.name].fieldDependencies; + for (var dependency_name in deps) { + if (!(dependency_name in fieldsInfo)) { + fieldsInfo[dependency_name] = {'name': dependency_name, 'type': deps[dependency_name].type}; + } + } + } return false; } return node.tag !== 'arch'; 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 9f8ea3227f5..4412d29c455 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1109,6 +1109,10 @@ var BasicModel = AbstractModel.extend({ if (!options.doNotSetDirty) { record._isDirty = true; } + var initialData = {}; + this._visitChildren(record, function (elem) { + initialData[elem.id] = $.extend(true, {}, _.pick(elem, 'data', '_changes')); + }); // apply changes to local data for (var fieldName in changes) { @@ -1137,12 +1141,18 @@ var BasicModel = AbstractModel.extend({ } } } - var onchangeDef; + var onchangeDef = $.Deferred(); if (onChangeFields.length) { - onchangeDef = self._performOnChange(record, onChangeFields, options.viewType).then(function (result) { - delete record._warning; - return _.keys(changes).concat(Object.keys(result && result.value || {})); - }); + self._performOnChange(record, onChangeFields, options.viewType) + .then(function (result) { + delete record._warning; + onchangeDef.resolve(_.keys(changes).concat(Object.keys(result && result.value || {}))); + }).fail(function () { + self._visitChildren(record, function (elem) { + _.extend(elem, initialData[elem.id]); + }); + onchangeDef.resolve({}); + }); } else { onchangeDef = $.Deferred().resolve(_.keys(changes)); } @@ -2653,13 +2663,21 @@ var BasicModel = AbstractModel.extend({ } commands[fieldName].push(command); } else if (_.contains(addedIds, list.res_ids[i])) { - // this is a new id + // this is a new id (maybe existing in DB, but new in JS) relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]}); changes = this._generateChanges(relRecord, options); if ('id' in changes) { + // the subrecord already exists in db delete changes.id; - commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes)); + 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)); + } } else { + // the subrecord is new, so create it commands[fieldName].push(x2ManyCommands.create(relRecord.ref, changes)); } } @@ -2892,7 +2910,8 @@ var BasicModel = AbstractModel.extend({ * @returns {boolean} */ _isFieldProtected: function (record, fieldName, viewType) { - var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; + var fieldInfo = record.fieldsInfo && + (record.fieldsInfo[viewType || record.viewType][fieldName]); if (fieldInfo) { var rawModifiers = fieldInfo.modifiers || {}; var modifiers = this._evalModifiers(record, rawModifiers); @@ -3249,15 +3268,18 @@ var BasicModel = AbstractModel.extend({ }); return $.when.apply($, defs) .then(function () { - return self._performOnChange(record, fields_key).then(function () { + var def = $.Deferred(); + self._performOnChange(record, fields_key).always(function () { if (record._warning) { if (params.allowWarning) { delete record._warning; } else { - return $.Deferred().reject(); + def.reject(); } } + def.resolve(); }); + return def; }) .then(function () { return self._fetchRelationalData(record); @@ -3324,10 +3346,7 @@ var BasicModel = AbstractModel.extend({ * @param {string[]} fields changed fields * @param {string} [viewType] current viewType. If not set, we will assume * main viewType from the record - * @returns {Deferred} Note: this deferred cannot fail. It is either in the - * 'success' or 'pending' state. If the onchange rpc fails, it will be - * resolved with an empty dictionary. Note that the initial change was - * already applied. + * @returns {Deferred} */ _performOnChange: function (record, fields, viewType) { var self = this; @@ -3347,9 +3366,7 @@ var BasicModel = AbstractModel.extend({ var context = this._getContext(record, options); var currentData = this._generateOnChangeData(record, {changesOnly: false}); - var def = $.Deferred(); - - self._rpc({ + return self._rpc({ model: record.model, method: 'onchange', args: [idList, currentData, fields, onchangeSpec, context], @@ -3374,12 +3391,9 @@ var BasicModel = AbstractModel.extend({ record._domains = _.extend(record._domains, result.domain); } return self._applyOnChange(result.value, record).then(function () { - def.resolve(result); + return result; }); - }).fail(function () { - def.resolve({}); }); - return def; }, /** * Once a record is created and some data has been fetched, we need to do diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index f76f40460a1..2b17710935c 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -115,7 +115,7 @@ var CalendarController = AbstractController.extend({ */ _onChangeDate: function (event) { var modelData = this.model.get(); - if (modelData.target_date.isSame(event.data.date)) { + if (modelData.target_date.format('YYYY-MM-DD') === event.data.date.format('YYYY-MM-DD')) { // When clicking on same date, toggle between the two views switch (modelData.scale) { case 'month': this.model.setScale('week'); break; diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index b32374057bb..a0f6515b98c 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -255,6 +255,8 @@ return AbstractModel.extend({ */ setDate: function (start, highlight) { this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start; + this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes'); + switch (this.data.scale) { case 'month': this.data.start_date = this.data.start_date.clone().startOf('month').startOf('week'); @@ -391,6 +393,12 @@ return AbstractModel.extend({ firstDay: moment().startOf('week').isoWeekday(), }; }, + /** + * Return a domain from the date range + * + * @private + * @returns {Array} + */ _getRangeDomain: function () { // Build OpenERP Domain to filter object by this.mapping.date_start field // between given start, end dates. @@ -623,8 +631,10 @@ return AbstractModel.extend({ date_stop = date_start.clone().add(date_delay,'hours'); } - date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); - date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); + if (!all_day) { + date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); + date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); + } if (this.mapping.all_day && evt[this.mapping.all_day]) { date_stop.add(1, 'days'); diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index a6248c827a2..7b53b60f90c 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -367,12 +367,23 @@ var FormController = BasicController.extend({ this._disableButtons(); + function saveAndExecuteAction () { + return self.saveRecord(self.handle, { + stayInEdit: true, + }).then(function () { + // we need to reget the record to make sure we have changes made + // by the basic model, such as the new res_id, if the record is + // new. + var record = self.model.get(event.data.record.id); + return self._callButtonAction(attrs, record); + }); + } var attrs = event.data.attrs; if (attrs.confirm) { var d = $.Deferred(); - Dialog.confirm(this, attrs.confirm, { confirm_callback: function () { - self._callButtonAction(attrs, event.data.record); - }}).on("closed", null, function () { + Dialog.confirm(this, attrs.confirm, { + confirm_callback: saveAndExecuteAction, + }).on("closed", null, function () { d.resolve(); }); def = d.promise(); @@ -380,15 +391,7 @@ var FormController = BasicController.extend({ def = this._callButtonAction(attrs, event.data.record); } else if (!attrs.special || attrs.special === 'save') { // save the record but don't switch to readonly mode - def = this.saveRecord(this.handle, { - stayInEdit: true, - }).then(function () { - // we need to reget the record to make sure we have changes made - // by the basic model, such as the new res_id, if the record is - // new. - var record = self.model.get(event.data.record.id); - return self._callButtonAction(attrs, record); - }); + def = saveAndExecuteAction(); } def.always(this._enableButtons.bind(this)); diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 8b24d265be8..33c075fc77f 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -101,9 +101,6 @@ var ListRenderer = BasicRenderer.extend({ } else { data = this.state.data; } - if (data.length === 0) { - return; - } _.each(this.columns, this._computeColumnAggregates.bind(this, data)); }, @@ -145,7 +142,7 @@ var ListRenderer = BasicRenderer.extend({ } }); if (func === 'avg') { - aggregateValue = aggregateValue / count; + aggregateValue = count ? aggregateValue / count : aggregateValue; } column.aggregate = { help: attrs[func], diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 1e623e54d9a..99cc72144b6 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1465,8 +1465,9 @@ QUnit.module('basic_fields', { QUnit.module('FieldImage'); QUnit.test('image fields are correctly rendered', function (assert) { - assert.expect(5); + assert.expect(6); + this.data.partner.records[0].__last_update = '2017-02-08 10:00:00'; this.data.partner.records[0].document = 'myimage'; var form = createView({ @@ -1477,7 +1478,10 @@ QUnit.module('basic_fields', { '<field name="document" widget="image" options="{\'size\': [90, 90]}"/> ' + '</form>', res_id: 1, - mockRPC: function (route) { + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/read') { + assert.deepEqual(args.args[1], ['document', '__last_update', 'display_name'], "The fields document, display_name and __last_update should be present when reading an image"); + } if (route === 'data:image/png;base64,myimage') { assert.ok(true, "should called the correct route"); return $.when('wow'); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index b981a4241c6..f21db07f088 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -1070,7 +1070,9 @@ QUnit.module('relational_fields', { assert.deepEqual( obj.turtles, [ + [4, 2, false], [1, 2, {turtle_foo: 'blip'}], + [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], "should have properly created the x2many command list"); @@ -1099,7 +1101,9 @@ QUnit.module('relational_fields', { // unchanged state with the command 1, but this seems more // difficult. assert.deepEqual(args.args[0].turtles, [ + [4, 2, false], [1, 2, {turtle_foo: 'blip'}], + [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], 'should send proper commands to create method'); } @@ -6474,10 +6478,50 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('default value for nested one2manys (coming from onchange)', function (assert) { + assert.expect(3); + + this.data.partner.onchanges.p = function (obj) { + obj.p = [ + [5], + [0, 0, {turtles: [[5], [4, 1]]}], // link record 1 by default + ]; + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<sheet>' + + '<field name="p">' + + '<tree><field name="turtles"/></tree>' + + '</field>' + + '</sheet>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.strictEqual(args.args[0].p[0][0], 0, + "should send a command 0 (CREATE) for p"); + assert.deepEqual(args.args[0].p[0][2], {turtles: [[4, 1, false]]}, + "should send the correct values"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('.o_data_cell').text(), '1 record', + "should correctly display the value of the inner o2m"); + + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.test('display correct value after validation error', function (assert) { assert.expect(4); - this.data.partner.onchanges.turtles = true; + this.data.partner.onchanges.turtles = function () { }; var form = createView({ View: FormView, @@ -6494,17 +6538,17 @@ QUnit.module('relational_fields', { '</form>', mockRPC: function (route, args) { if (args.method === 'onchange') { - // we simulate a validation error. In the 'real' web client, - // the server error will be used by the session to display - // an error dialog. From the point of view of the basic - // model, the deferred is just rejected. - return $.Deferred().reject(); + if (args.args[1].turtles[0][2].turtle_foo === 'pinky') { + // we simulate a validation error. In the 'real' web client, + // the server error will be used by the session to display + // an error dialog. From the point of view of the basic + // model, the deferred is just rejected. + return $.Deferred().reject(); + } } if (args.method === 'write') { assert.deepEqual(args.args[1].turtles[0], [1,2,{turtle_foo: 'foo'}], - 'should send the "bad" value'); - // we simulate a validation error - return $.Deferred().reject(); + 'should send the "good" value'); } return this._super.apply(this, arguments); }, @@ -6514,21 +6558,25 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'blip', "initial text should be correct"); + + // click and edit value to 'foo', which will trigger onchange form.$('.o_data_row .o_data_cell:nth(0)').click(); form.$('.o_field_widget[name=turtle_foo]').val('foo').trigger('input'); + form.$el.click(); + assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'foo', + "field should have been changed to foo"); - // we try to validate the line. This triggers an onchange which will be - // rejected. The line will be returned to readonly mode, but with the - // new invalid value. + // click and edit value to 'pinky', which trigger a failed onchange + form.$('.o_data_row .o_data_cell:nth(0)').click(); + form.$('.o_field_widget[name=turtle_foo]').val('pinky').trigger('input'); form.$el.click(); assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'foo', - "turtle_foo text should now be foo (invalid value)"); + "turtle_foo text should now be set back to foo"); // we make sure here that when we save, the values are the current // values displayed in the field. form.$buttons.find('.o_form_button_save').click(); - assert.strictEqual(form.mode, 'edit', "form view should still be in edit mode"); form.destroy(); }); @@ -6628,7 +6676,6 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index dfcbdba5985..f7d3f2fb307 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -26,6 +26,9 @@ var MockServer = Class.extend({ if (!('display_name' in model.fields)) { model.fields.display_name = {string: "Display Name", type: "char"}; } + if (!('__last_update' in model.fields)) { + model.fields.__last_update = {string: "Last Modified on", type: "datetime"}; + } if (!('name' in model.fields)) { model.fields.name = {string: "Name", type: "char", default: "name"}; } diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 14b7ae42b03..b6cbcd7e03f 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -21,7 +21,8 @@ CalendarRenderer.include({ var createView = testUtils.createView; -var initialDate = new Date("2016-12-12T08:00:00Z"); +var initialDate = new Date(2016, 11, 12, 8, 0, 0); +initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000); QUnit.module('Views', { diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 1b32857c951..e4e33af0a8f 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -5355,6 +5355,53 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('buttons with "confirm" attribute save before calling the method', function (assert) { + assert.expect(9); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<header>' + + '<button name="post" class="p" string="Confirm" type="object" ' + + 'confirm="Very dangerous. U sure?"/>' + + '</header>' + + '<sheet>' + + '<field name="foo"/>' + + '</sheet>' + + '</form>', + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + intercepts: { + execute_action: function (event) { + assert.step('execute_action'); + }, + }, + }); + + // click on button, and cancel in confirm dialog + form.$('.o_statusbar_buttons button').click(); + assert.ok(form.$('.o_statusbar_buttons button').prop('disabled'), + 'button should be disabled'); + $('.modal .modal-footer button.btn-default').click(); + assert.ok(!form.$('.o_statusbar_buttons button').prop('disabled'), + 'button should no longer be disabled'); + + assert.verifySteps(['default_get']); + + // click on button, and click on ok in confirm dialog + form.$('.o_statusbar_buttons button').click(); + assert.verifySteps(['default_get']); + $('.modal .modal-footer button.btn-primary').click(); + + assert.verifySteps(['default_get', 'create', 'read', 'execute_action']); + + form.destroy(); + }); + QUnit.test('buttons are disabled until action is resolved (in dialogs)', function (assert) { assert.expect(3); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 67566692f92..32e3ad0046a 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -560,7 +560,7 @@ QUnit.module('Views', { }); QUnit.test('aggregates are computed correctly', function (assert) { - assert.expect(3); + assert.expect(4); var list = createView({ View: ListView, @@ -581,6 +581,11 @@ QUnit.module('Views', { $thead_selector.click(); assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32 as all records are selected"); + + // Let's update the view to dislay NO records + list.update({domain: ['&', ['bar', '=', false], ['int_field', '>', 0]]}); + assert.strictEqual(list.$('tfoot td:nth(2)').text(), "0", "total should have been recomputed to 0"); + list.destroy(); }); |
