[IMP] web: new widget for static image urls in char fields

This new widget allows definition of Images through static urls stored in a Char field.
It was implemented to provide a way to reduce databases footprint by using
static files instead of duplicating them in the databases (& their backups).
This commit is contained in:
Victor Feyens
2020-09-02 13:57:23 +00:00
parent d1eb6089fd
commit 1aa874aab3
3 changed files with 182 additions and 0 deletions
@@ -1955,6 +1955,45 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
},
});
var CharImageUrl = AbstractField.extend({
className: 'o_field_image',
description: _lt("Image"),
supportedFieldTypes: ['char'],
placeholder: "/web/static/src/img/placeholder.png",
_renderReadonly: function () {
var self = this;
const url = this.value;
if (url) {
var $img = $(qweb.render("FieldBinaryImage-img", {widget: this, url: url}));
// override css size attributes (could have been defined in css files)
// if specified on the widget
const width = this.nodeOptions.size ? this.nodeOptions.size[0] : this.attrs.width;
const height = this.nodeOptions.size ? this.nodeOptions.size[1] : this.attrs.height;
if (width) {
$img.attr('width', width);
$img.css('max-width', width + 'px');
}
if (height) {
$img.attr('height', height);
$img.css('max-height', height + 'px');
}
this.$('> img').remove();
this.$el.prepend($img);
$img.one('error', function () {
$img.attr('src', self.placeholder);
self.displayNotification({
type: 'info',
message: _t("Could not display the specified image url."),
});
});
}
return this._super.apply(this, arguments);
},
});
var KanbanFieldBinaryImage = FieldBinaryImage.extend({
// In kanban views, there is a weird logic to determine whether or not a
// click on a card should open the record in a form view. This logic checks
@@ -1966,6 +2005,17 @@ var KanbanFieldBinaryImage = FieldBinaryImage.extend({
events: {},
});
var KanbanCharImageUrl = CharImageUrl.extend({
// In kanban views, there is a weird logic to determine whether or not a
// click on a card should open the record in a form view. This logic checks
// if the clicked element has click handlers bound on it, and if so, does
// not open the record (assuming that the click will be handled by someone
// else). In the case of this widget, there are clicks handler but they
// only apply in edit mode, which is never the case in kanban views, so we
// simply remove them.
events: {},
});
var FieldBinaryFile = AbstractFieldBinary.extend({
description: _lt("File"),
template: 'FieldBinaryFile',
@@ -3539,6 +3589,8 @@ return {
AbstractFieldBinary: AbstractFieldBinary,
FieldBinaryImage: FieldBinaryImage,
KanbanFieldBinaryImage: KanbanFieldBinaryImage,
CharImageUrl: CharImageUrl,
KanbanCharImageUrl: KanbanCharImageUrl,
FieldBoolean: FieldBoolean,
BooleanToggle: BooleanToggle,
FieldChar: FieldChar,
@@ -43,7 +43,9 @@ registry
.add('CopyClipboardText', basic_fields.TextCopyClipboard)
.add('CopyClipboardChar', basic_fields.CharCopyClipboard)
.add('image', basic_fields.FieldBinaryImage)
.add('image_url', basic_fields.CharImageUrl)
.add('kanban.image', basic_fields.KanbanFieldBinaryImage)
.add('kanban.image_url', basic_fields.KanbanCharImageUrl)
.add('binary', basic_fields.FieldBinaryFile)
.add('pdf_viewer', basic_fields.FieldPdfViewer)
.add('monetary', basic_fields.FieldMonetary)
@@ -24,6 +24,9 @@ var _t = core._t;
// Base64 images for testing purpose
const MY_IMAGE = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
const PRODUCT_IMAGE = 'R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7';
const FR_FLAG_URL = '/base/static/img/country_flags/fr.png';
const EN_FLAG_URL = '/base/static/img/country_flags/gb.png';
QUnit.module('fields', {}, function () {
@@ -2889,6 +2892,131 @@ QUnit.module('basic_fields', {
form.destroy();
});
/**
* Same tests than for Image fields, but for Char fields with image_url widget.
*/
QUnit.module('FieldChar-ImageUrlWidget', {
beforeEach: function () {
// specific sixth partner data for image_url widget tests
this.data.partner.records.push({id: 6, bar: false, foo: FR_FLAG_URL, int_field: 5, qux: 0.0, timmy: []});
},
});
QUnit.test('image fields are correctly rendered', async function (assert) {
assert.expect(6);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/> ' +
'</form>',
res_id: 6,
async mockRPC(route, args) {
if (route === FR_FLAG_URL) {
assert.ok(true, "the correct route should have been called.");
}
return this._super.apply(this, arguments);
},
});
assert.hasClass(form.$('div[name="foo"]'), 'o_field_image',
"the widget should have the correct class");
assert.containsOnce(form, 'div[name="foo"] > img',
"the widget should contain an image");
assert.hasClass(form.$('div[name="foo"] > img'), 'img-fluid',
"the image should have the correct class");
assert.hasAttrValue(form.$('div[name="foo"] > img'), 'width', "90",
"the image should correctly set its attributes");
assert.strictEqual(form.$('div[name="foo"] > img').css('max-width'), "90px",
"the image should correctly set its attributes");
form.destroy();
});
QUnit.test('image_url widget in subviews are loaded correctly', async function (assert) {
assert.expect(6);
this.data.partner_type.fields.image = {name: 'image', type: 'char'};
this.data.partner_type.records[0].image = EN_FLAG_URL;
this.data.partner.records[5].timmy = [12];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/>' +
'<field name="timmy" widget="many2many">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form>' +
'<field name="image" widget="image_url"/>' +
'</form>' +
'</field>' +
'</form>',
res_id: 6,
async mockRPC(route) {
if (route === FR_FLAG_URL) {
assert.step("The view's image should have been fetched");
return 'wow';
}
if (route === EN_FLAG_URL) {
assert.step("The dialog's image should have been fetched");
return;
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(["The view's image should have been fetched"]);
assert.containsOnce(form, 'tr.o_data_row',
'There should be one record in the many2many');
// Actual flow: click on an element of the m2m to get its form view
await testUtils.dom.click(form.$('tbody td:contains(gold)'));
assert.strictEqual($('.modal').length, 1,
'The modal should have opened');
assert.verifySteps(["The dialog's image should have been fetched"]);
form.destroy();
});
QUnit.test('image fields in x2many list are loaded correctly', async function (assert) {
assert.expect(2);
this.data.partner_type.fields.image = {name: 'image', type: 'char'};
this.data.partner_type.records[0].image = EN_FLAG_URL;
this.data.partner.records[5].timmy = [12];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="timmy" widget="many2many">' +
'<tree>' +
'<field name="image" widget="image_url"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 6,
async mockRPC(route) {
if (route === EN_FLAG_URL) {
assert.ok(true, "The list's image should have been fetched");
return;
}
return this._super.apply(this, arguments);
},
});
assert.containsOnce(form, 'tr.o_data_row',
'There should be one record in the many2many');
form.destroy();
});
QUnit.module('JournalDashboardGraph', {
beforeEach: function () {
_.extend(this.data.partner.fields, {