From c31bf95e4e5e01e9f433ac13c6be33f85a57f9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 10 May 2017 14:54:05 +0200 Subject: [PATCH] [FIX] pad: make it work with the new views The pad widget was partially updated to the new views, but was no longer working. In this commit, we rewrite the widget to make it work. Note that we did some functional changes: - we do only one rpc to check if the server is configured. the result is stored on the prototype, so we do not need to do it again - we do some rpcs with shadow: true, so we will not stop the rest of the ui if the server is slow to respond - we display a loading message in readonly, when the content is not loaded yet. --- addons/pad/static/src/js/pad.js | 229 ++++++++++++++++----------- addons/pad/static/tests/pad_tests.js | 151 ++++++++++++++++++ addons/pad/views/pad.xml | 6 + 3 files changed, 297 insertions(+), 89 deletions(-) create mode 100644 addons/pad/static/tests/pad_tests.js diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index 8fb09df0173..76f6ad9b9ae 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -3,116 +3,167 @@ odoo.define('pad.pad', function (require) { var AbstractField = require('web.AbstractField'); var core = require('web.core'); -var field_registry = require('web.field_registry'); +var fieldRegistry = require('web.field_registry'); var _t = core._t; - var FieldPad = AbstractField.extend({ template: 'FieldPad', content: "", events: { - 'click .oe_pad_switch': function() { - this.$el.toggleClass('oe_pad_fullscreen mb0'); - this.$('.oe_pad_switch').toggleClass('fa-expand fa-compress'); - }, + 'click .oe_pad_switch': '_onToggleFullScreen', }, supportedFieldTypes: ['char'], - init: function() { - var self = this; - this._super.apply(this, arguments); - this.configured = false; - this._configured_deferred = $.Deferred(); - this._url_request_deferred = $.Deferred(); + /** + * @override + */ + willStart: function () { + if (this.isPadConfigured === undefined) { + return this._rpc({ + method: 'pad_is_configured', + model: this.model, + }).then(function (result) { + // we write on the prototype to share the information between + // all pad widgets instances, across all actions + FieldPad.prototype.isPadConfigured = result; + }); + } + return $.when(); + }, + /** + * @override + */ + start: function () { + if (!this.isPadConfigured) { + this.$(".oe_unconfigured").removeClass('hidden'); + this.$(".oe_configured").addClass('hidden'); + return; + } + return this._super.apply(this, arguments); + }, - this.trigger_up('perform_model_rpc', { - method: 'pad_is_configured', - model: this.model, - on_success: function (data) { - self.set_configured(!!data); - self._configured_deferred.resolve(); - }, - on_fail: function () { - self.set_configured(true); - self._configured_deferred.resolve(); - }, - }); + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * If we had to generate an url, we wait for the generation to be completed, + * so the current record will be associated with the correct pad url. + * + * @override + */ + commitChanges: function () { + return this.urlDef; }, - render_readonly: function() { - var self = this; - this._configured_deferred.then(function() { - if (self.configured){ - if (_.str.startsWith(self.value, 'http')) { - self.trigger_up('perform_model_rpc', { - method: 'pad_get_content', - model: self.model, - args: [self.value], - on_success: function(data) { - self.$('.oe_pad_content').removeClass('oe_pad_loading').html('
'); - self.$('.oe_pad_readonly').html(data); - }, - on_fail: function() { - self.$('.oe_pad_content').text(_t('Unable to load pad')); - } - }); - } else { - self.$('.oe_pad_content').addClass('oe_pad_loading').show().text(_t("This pad will be initialized on first edit")); - } - } - }); + /** + * @override + */ + isSet: function () { + return true; }, - render_edit: function() { - var self = this; - this._configured_deferred.then(function() { - if (! self.value || !_.str.startsWith(self.value, 'http')) { - self.trigger_up('perform_model_rpc', { - method: 'pad_generate_url', - model: self.model, - context: { - model: self.model, - field_name: self.name, - object_id: self.model - }, - on_success: function(data) { - if (! data.url) { - self.set_configured(false); - } else { - self.set_value(data.url); - } - self._url_request_deferred.resolve(); - }, - }); - } else { + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Note that this method has some serious side effects: performing rpcs and + * setting the value of this field. This is not conventional and should not + * be copied in other code, unless really necessary. + * + * @override + * @private + */ + _renderEdit: function () { + if (_.str.startsWith(this.value, 'http')) { + // here, we have a valid url, so we can simply display an iframe + // with the correct src attribute + var userName = encodeURIComponent(this.getSession().userName); + var url = this.value + '?showChat=false&userName=' + userName; + var content = ''; + this.$('.oe_pad_content').html(content); + } else if (this.value) { + // it looks like the field does not contain a valid url, so we just + // display it (it cannot be edited in that case) + this.$('.oe_pad_content').text(this.value); + } else { + // It is totally discouraged to have a render method that does + // non-rendering work, especially since the work in question + // involves doing RPCs and changing the value of the field. + // However, this is kind of necessary in this case, because the + // value of the field is actually only the url of the pad. The + // actual content will be loaded in an iframe. We could do this + // work in the basic model, but the basic model does not know that + // this widget is in edit or readonly, and we really do not want to + // create a pad url everytime a task without a pad is viewed. + var self = this; + this.urlDef = this._rpc({ + method: 'pad_generate_url', + model: this.model, + context: { + model: this.model, + field_name: this.name, + object_id: this.res_id + }, + }, { + shadow: true + }).then(function (result) { // We need to write the url of the pad to trigger // the write function which updates the actual value // of the field to the value of the pad content - self.set_value(self.value); - self._url_request_deferred.resolve(); - } - self._url_request_deferred.then(function() { - if (_.str.startsWith(self.value, 'http')) { - var content = ''; - self.$('.oe_pad_content').html(content); - } - else { - self.$('.oe_pad_content').text(self.value); - } + self._setValue(result.url); }); - }); - }, - set_configured: function(configured) { - this.configured = configured; - if (!configured) { - this.$(".oe_unconfigured").removeClass('hidden'); - this.$(".oe_configured").addClass('hidden'); } }, - is_set: function() { - return true; + /** + * @override + * @private + */ + _renderReadonly: function () { + if (_.str.startsWith(this.value, 'http')) { + var self = this; + this.$('.oe_pad_content') + .addClass('oe_pad_loading') + .text(_t("Loading")); + this._rpc({ + method: 'pad_get_content', + model: this.model, + args: [this.value] + }, { + shadow: true + }).then(function (data) { + self.$('.oe_pad_content') + .removeClass('oe_pad_loading') + .html('
'); + self.$('.oe_pad_readonly').html(data); + }).fail(function () { + self.$('.oe_pad_content').text(_t('Unable to load pad')); + }); + } else { + this.$('.oe_pad_content') + .addClass('oe_pad_loading') + .show() + .text(_t("This pad will be initialized on first edit")); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _onToggleFullScreen: function () { + this.$el.toggleClass('oe_pad_fullscreen mb0'); + this.$('.oe_pad_switch').toggleClass('fa-expand fa-compress'); }, }); -field_registry.add('pad', FieldPad); +fieldRegistry.add('pad', FieldPad); + +return FieldPad; }); diff --git a/addons/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js new file mode 100644 index 00000000000..69342f5de6e --- /dev/null +++ b/addons/pad/static/tests/pad_tests.js @@ -0,0 +1,151 @@ +odoo.define('pad.pad_tests', function (require) { +"use strict"; + +var FieldPad = require('pad.pad'); +var FormView = require('web.FormView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +QUnit.module('pad widget', { + beforeEach: function () { + this.data = { + task: { + fields: { + description: {string: "Description", type: "char"}, + }, + records: [ + {id: 1, description: false}, + {id: 2, description: "https://pad.odoo.pad/p/test-03AK6RCJT"}, + ], + pad_is_configured: function () { + return true; + }, + pad_generate_url: function (route, args) { + return { + url:'https://pad.odoo.pad/p/test/' + args.context.object_id + }; + }, + pad_get_content: function () { + return "we should rewrite this server in haskell"; + }, + }, + }; + }, +}); + + QUnit.test('pad widget display help if server not configured', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'task', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'pad_is_configured') { + return $.when(false); + } + return this._super.apply(this, arguments); + }, + }); + assert.ok(form.$('p.oe_unconfigured').is(':visible'), + "help message should be visible"); + assert.notOk(form.$('p.oe_pad_content').is(':visible'), + "content should not be visible"); + form.$buttons.find('.o_form_button_edit').click(); + assert.ok(form.$('p.oe_unconfigured').is(':visible'), + "help message should be visible"); + assert.notOk(form.$('p.oe_pad_content').is(':visible'), + "content should not be visible"); + form.destroy(); + delete FieldPad.prototype.isPadConfigured; + }); + + QUnit.test('pad widget works, basic case', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'task', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (route === 'https://pad.odoo.pad/p/test/1?showChat=false&userName=batman') { + assert.ok(true, "should have an iframe with correct src"); + return $.when(true); + } + return this._super.apply(this, arguments); + }, + session: { + userName: "batman", + }, + }); + assert.notOk(form.$('p.oe_unconfigured').is(':visible'), + "help message should not be visible"); + assert.ok(form.$('.oe_pad_content').is(':visible'), + "content should be visible"); + assert.strictEqual(form.$('.oe_pad_content:contains(This pad will be)').length, 1, + "content should display a message when not initialized"); + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.oe_pad_content iframe').length, 1, + "should have an iframe"); + + form.destroy(); + delete FieldPad.prototype.isPadConfigured; + }); + + QUnit.test('pad widget works, with existing data', function (assert) { + assert.expect(2); + + var contentDef = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'task', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 2, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'pad_get_content') { + return contentDef.then(_.constant(result)); + } + return result; + }, + session: { + userName: "batman", + }, + }); + assert.strictEqual(form.$('.oe_pad_content').text(), "Loading", + "should display loading message"); + contentDef.resolve(); + assert.strictEqual(form.$('.oe_pad_content').text(), "we should rewrite this server in haskell", + "should display proper value"); + form.destroy(); + delete FieldPad.prototype.isPadConfigured; + }); + +}); diff --git a/addons/pad/views/pad.xml b/addons/pad/views/pad.xml index 56bc0b1a997..126f8c84f16 100644 --- a/addons/pad/views/pad.xml +++ b/addons/pad/views/pad.xml @@ -6,4 +6,10 @@ + +