summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2019-06-04 15:11:08 +0200
committerSébastien Theys <seb@odoo.com>2019-06-04 22:14:13 +0200
commit76cf2ae82a45b9ffcf63b524944a42b7f5192b0c (patch)
tree89540178c9e62def90377eb50ea8f7e1c089aff6
parent191c556d6b50b35178ea5bb91fb998bcffb86367 (diff)
[IMP] web_editor, *: improve media dialog attachment upload
* = web_unsplash, website Remove iframe upload and replace it with a proper RPC. The route is now only accepting one file at a time, which makes it easier to maintain, and allows to process the first file as soon as it is ready, instead of having to wait on everything. Also fix a bug where a document could not be uploaded because it would try to parse it as an image. Remove filter after search result from the JS and add the corresponding conditions in the domain to avoid processing and returning unnecessary records. If multiple attachments exist with the same URL, now display them all to avoid confusion. Compute all the necessary values in the python model instead of in the JS. Allow SVG in the widget (the server already accepted them before). Disable multiple image upload when not in multiImages mode. task-1930726 PR: #31208
-rw-r--r--addons/web_editor/controllers/main.py134
-rw-r--r--addons/web_editor/models/ir_attachment.py21
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js10
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/media.js240
-rw-r--r--addons/web_editor/static/src/xml/wysiwyg.xml15
-rw-r--r--addons/web_editor/static/tests/test_utils.js13
-rw-r--r--addons/web_editor/static/tests/wysiwyg_tests.js37
-rw-r--r--addons/web_unsplash/controllers/main.py4
-rw-r--r--addons/web_unsplash/static/src/js/unsplash_image_widget.js4
-rw-r--r--addons/website/static/src/js/editor/snippets.options.js2
-rw-r--r--odoo/fields.py2
-rw-r--r--odoo/tools/image.py4
12 files changed, 233 insertions, 253 deletions
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index 41bb7fceb54..85797ba2e23 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import base64
import io
-import json
import logging
import re
import time
@@ -11,7 +9,8 @@ from PIL import Image, ImageFont, ImageDraw
from lxml import etree
from odoo.http import request
-from odoo import http, tools
+from odoo import http, tools, _
+from odoo.exceptions import UserError
logger = logging.getLogger(__name__)
@@ -153,86 +152,21 @@ class Web_Editor(http.Controller):
return True
- #------------------------------------------------------
- # upload an image as base64
- #------------------------------------------------------
- @http.route('/web_editor/add_image_base64', type='json', auth='user', methods=['POST'], website=True)
- def add_image_base64(self, res_model, res_id, image_base64, filename, disable_optimization=None, **kwargs):
- attachment = self._image_to_attachment(res_model, res_id, image_base64, filename, filename, disable_optimization=disable_optimization)
- return attachment.read(['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'])[0]
-
- def _image_to_attachment(self, res_model, res_id, image_base64, name, datas_fname, disable_optimization=None):
- Attachments = request.env['ir.attachment']
+ @http.route('/web_editor/attachment/add_data', type='json', auth='user', methods=['POST'], website=True)
+ def add_data(self, name, data, disable_optimization=False, res_id=False, res_model='ir.ui.view', filters=False, **kwargs):
if not disable_optimization:
- image_base64 = tools.image_process(image_base64, verify_resolution=True)
- attachment = Attachments.create({
- 'name': name,
- 'datas_fname': datas_fname,
- 'datas': image_base64,
- 'public': res_model == 'ir.ui.view',
- 'res_id': res_id,
- 'res_model': res_model,
- })
- attachment.generate_access_token()
- return attachment
-
- #------------------------------------------------------
- # add attachment (images or link)
- #------------------------------------------------------
- @http.route('/web_editor/attachment/add', type='http', auth='user', methods=['POST'], website=True)
- def attach(self, upload=None, url=None, disable_optimization=None, filters=None, **kwargs):
- # the upload argument doesn't allow us to access the files if more than
- # one file is uploaded, as upload references the first file
- # therefore we have to recover the files from the request object
- Attachments = request.env['ir.attachment'] # registry for the attachment table
-
- res_model = kwargs.get('res_model', 'ir.ui.view')
- if res_model != 'ir.ui.view' and kwargs.get('res_id'):
- res_id = int(kwargs['res_id'])
- else:
- res_id = None
-
- uploads = []
- message = None
- if not upload: # no image provided, storing the link and the image name
- name = url.split("/").pop() # recover filename
- datas_fname = name
- if filters:
- datas_fname = filters + '_' + datas_fname
- attachment = Attachments.create({
- 'name': name,
- 'datas_fname': datas_fname,
- 'type': 'url',
- 'url': url,
- 'public': res_model == 'ir.ui.view',
- 'res_id': res_id,
- 'res_model': res_model,
- })
- attachment.generate_access_token()
- uploads += attachment.read(['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'])
- else: # images provided
try:
- attachments = request.env['ir.attachment']
- for c_file in request.httprequest.files.getlist('upload'):
- image_base64 = base64.b64encode(c_file.read())
- name = c_file.filename
- datas_fname = name
- if filters:
- datas_fname = filters + '_' + datas_fname
- attachments += self._image_to_attachment(res_model, res_id, image_base64, name, datas_fname, disable_optimization=disable_optimization)
- uploads += attachments.read(['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'])
- except Exception as e:
- logger.exception("Failed to upload image to attachment")
- message = str(e)
-
- return """<script type='text/javascript'>
- window.attachments = %s;
- window.error = %s;
- </script>""" % (json.dumps(uploads), json.dumps(message))
+ data = tools.image_process(data, verify_resolution=True)
+ except OSError:
+ pass # not an image
+ attachment = self._attachment_create(name=name, data=data, res_id=res_id, res_model=res_model, filters=filters)
+ return attachment._get_media_info()
+
+ @http.route('/web_editor/attachment/add_url', type='json', auth='user', methods=['POST'], website=True)
+ def add_url(self, url, res_id=False, res_model='ir.ui.view', filters=False, **kwargs):
+ attachment = self._attachment_create(url=url, res_id=res_id, res_model=res_model, filters=filters)
+ return attachment._get_media_info()
- #------------------------------------------------------
- # remove attachment (images or link)
- #------------------------------------------------------
@http.route('/web_editor/attachment/remove', type='json', auth='user', website=True)
def remove(self, ids, **kwargs):
""" Removes a web-based image attachment if it is used by no view (template)
@@ -264,11 +198,12 @@ class Web_Editor(http.Controller):
attachments_to_remove.unlink()
return removal_blocked_by
- ## This route is used from CropImageDialog to get image info.
- ## It is used to display the original image when we crop a previously
- ## cropped image
@http.route('/web_editor/get_image_info', type='json', auth='user', website=True)
def get_image_info(self, image_id=None, xml_id=None):
+ """This route is used from CropImageDialog to get image info.
+ It is used to display the original image when we crop a previously
+ cropped image.
+ """
if xml_id:
record = request.env['ir.attachment'].get_attachment_by_key(xml_id)
elif image_id:
@@ -284,6 +219,39 @@ class Web_Editor(http.Controller):
result['originalSrc'] = record.url
return result
+ def _attachment_create(self, name='', data=False, url=False, res_id=False, res_model='ir.ui.view', filters=None):
+ """Create and return a new attachment."""
+ if not name and url:
+ name = url.split("/").pop()
+
+ if res_model != 'ir.ui.view' and res_id:
+ res_id = int(res_id)
+ else:
+ res_id = False
+
+ if filters:
+ name = filters + '_' + name
+
+ attachment_data = {
+ 'name': name,
+ 'public': res_model == 'ir.ui.view',
+ 'res_id': res_id,
+ 'res_model': res_model,
+ }
+
+ if data:
+ attachment_data['datas'] = data
+ elif url:
+ attachment_data.update({
+ 'type': 'url',
+ 'url': url,
+ })
+ else:
+ raise UserError(_("You need to specify either data or url to create an attachment."))
+
+ attachment = request.env['ir.attachment'].create(attachment_data)
+ return attachment
+
@http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True)
def get_assets_editor_resources(self, key, get_views=True, get_scss=True, get_js=True, bundles=False, bundles_restriction=[], only_user_custom_files=True):
"""
diff --git a/addons/web_editor/models/ir_attachment.py b/addons/web_editor/models/ir_attachment.py
index 0c5f0e53de8..83f8766b42e 100644
--- a/addons/web_editor/models/ir_attachment.py
+++ b/addons/web_editor/models/ir_attachment.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from werkzeug import url_quote
+
from odoo import api, models, fields
@@ -9,6 +11,7 @@ class IrAttachment(models.Model):
_inherit = "ir.attachment"
local_url = fields.Char("Attachment URL", compute='_compute_local_url')
+ image_src = fields.Char(compute='_compute_image_src')
@api.one
def _compute_local_url(self):
@@ -16,3 +19,21 @@ class IrAttachment(models.Model):
self.local_url = self.url
else:
self.local_url = '/web/image/%s?unique=%s' % (self.id, self.checksum)
+
+ @api.multi
+ @api.depends('mimetype', 'url', 'name')
+ def _compute_image_src(self):
+ for attachment in self:
+ if attachment.mimetype not in ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'image/svg+xml']:
+ attachment.image_src = False
+ else:
+ attachment.image_src = attachment.url or '/web/image/%s/%s' % (
+ attachment.id,
+ url_quote(attachment.name or ''),
+ )
+
+ @api.multi
+ def _get_media_info(self):
+ """Return a dict with the values that we need on the media dialog."""
+ self.ensure_one()
+ return self.read(['id', 'name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src'])[0]
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js b/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
index 3561f5b071c..a88fc50fc7f 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
@@ -148,12 +148,12 @@ var DropzonePlugin = Plugins.dropzone.extend({
});
return this._rpc({
- route: '/web_editor/add_image_base64',
+ route: '/web_editor/attachment/add_data',
params: {
- res_model: options.res_model,
- res_id: options.res_id || 0,
- image_base64: imageBase64.split(';base64,').pop(),
- filename: fileName,
+ 'data': imageBase64.split(';base64,').pop(),
+ 'name': fileName,
+ 'res_id': options.res_id || 0,
+ 'res_model': options.res_model,
},
});
},
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
index 8e923acc6db..f398e23bff9 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
@@ -3,7 +3,9 @@ odoo.define('wysiwyg.widgets.media', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
+var dom = require('web.dom');
var fonts = require('wysiwyg.fonts');
+var utils = require('web.utils');
var Widget = require('web.Widget');
var concurrency = require('web.concurrency');
@@ -113,7 +115,7 @@ var FileWidget = SearchableMediaWidget.extend({
}),
existingAttachmentsTemplate: undefined,
- IMAGE_MIMETYPES: ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png'],
+ IMAGE_MIMETYPES: ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'image/svg+xml'],
NUMBER_OF_ATTACHMENTS_TO_DISPLAY: 30,
/**
@@ -132,6 +134,8 @@ var FileWidget = SearchableMediaWidget.extend({
this.attachments = [];
this.selectedAttachments = [];
+
+ this._onUploadURLButtonClick = dom.makeAsyncHandler(this._onUploadURLButtonClick);
},
/**
* @override
@@ -173,9 +177,9 @@ var FileWidget = SearchableMediaWidget.extend({
o.id = +o.url.match(/\/web\/content\/(\d+)/, '')[1];
}
if (o.url) {
- self._toggleImage(_.find(self.attachments, function (attachment) {
+ self._selectAttachement(_.find(self.attachments, function (attachment) {
return attachment.url === o.url;
- }) || o, true);
+ }) || o);
}
return def;
@@ -207,40 +211,28 @@ var FileWidget = SearchableMediaWidget.extend({
args: [],
kwargs: {
domain: this._getAttachmentsDomain(needle),
- fields: ['name', 'datas_fname', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'],
+ fields: ['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src'],
order: [{name: 'id', asc: false}],
context: this.options.context,
},
}).then(function (attachments) {
self.attachments = _.chain(attachments)
- .filter(function (r) {
- return (r.type === "binary" || r.url && r.url.length > 0);
- })
- .uniq(function (r) {
- return (r.url || r.id);
- })
.sortBy(function (r) {
if (_.any(self.options.firstFilters, function (filter) {
var regex = new RegExp(filter, 'i');
- return r.name.match(regex) || r.datas_fname && r.datas_fname.match(regex);
+ return r.name.match(regex) || r.name && r.name.match(regex);
})) {
return -1;
}
if (_.any(self.options.lastFilters, function (filter) {
var regex = new RegExp(filter, 'i');
- return r.name.match(regex) || r.datas_fname && r.datas_fname.match(regex);
+ return r.name.match(regex) || r.name && r.name.match(regex);
})) {
return 1;
}
return 0;
})
.value();
-
- _.each(self.attachments, function (attachment) {
- // Name is added for SEO purposes
- attachment.src = attachment.url || _.str.sprintf('/web/image/%s/%s', attachment.id, encodeURI(attachment.name));
- attachment.isDocument = !(/gif|jpe|jpg|png/.test(attachment.mimetype));
- });
if (!noRender) {
self._renderImages();
}
@@ -304,9 +296,10 @@ var FileWidget = SearchableMediaWidget.extend({
domain = ['|', ['public', '=', true]].concat(domain);
domain = domain.concat(this.options.mimetypeDomain);
if (needle && needle.length) {
- domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
+ domain.push('|', ['name', 'ilike', needle], ['name', 'ilike', needle]);
}
- domain.push('|', ['datas_fname', '=', false], '!', ['datas_fname', '=like', '%.crop'], '!', ['name', '=like', '%.crop']);
+ domain.push('|', ['name', '=', false], '!', ['name', '=like', '%.crop'], '!', ['name', '=like', '%.crop']);
+ domain.push('|', ['type', '=', 'binary'], ['url', '!=', false]);
return domain;
},
/**
@@ -321,13 +314,24 @@ var FileWidget = SearchableMediaWidget.extend({
},
/**
* @private
+ * @param {object} attachment
+ */
+ _handleNewAttachment: function (attachment) {
+ this.attachments.unshift(attachment);
+ this._renderImages();
+ this._selectAttachement(attachment);
+ },
+ /**
+ * @private
+ * @returns {Promise}
*/
_loadMoreImages: function (forceSearch) {
this.numberOfAttachmentsToDisplay += 10;
if (!forceSearch) {
this._renderImages();
+ return Promise.resolve();
} else {
- this.search(this.$('.o_we_search').val() || '');
+ return this.search(this.$('.o_we_search').val() || '');
}
},
/**
@@ -348,8 +352,6 @@ var FileWidget = SearchableMediaWidget.extend({
_renderImages: function () {
var attachments = this.attachments.slice(0, this.numberOfAttachmentsToDisplay);
- this.$errorText.empty();
-
// Render menu & content
this.$('.existing-attachments').replaceWith(
this._renderExisting(attachments)
@@ -374,12 +376,12 @@ var FileWidget = SearchableMediaWidget.extend({
}
var img = this.selectedAttachments[0];
- if (!img) {
+ if (!img || !img.id) {
return Promise.resolve(this.media);
}
var prom;
- if (!img.access_token) {
+ if (!img.public && !img.access_token) {
prom = this._rpc({
model: 'ir.attachment',
method: 'generate_access_token',
@@ -390,19 +392,20 @@ var FileWidget = SearchableMediaWidget.extend({
}
return Promise.resolve(prom).then(function () {
- if (!img.isDocument) {
- if (img.access_token && self.options.res_model !== 'ir.ui.view') {
- img.src += _.str.sprintf('?access_token=%s', img.access_token);
+ if (img.image_src) {
+ var src = img.image_src;
+ if (!img.public && img.access_token) {
+ src += _.str.sprintf('?access_token=%s', img.access_token);
}
if (!self.$media.is('img')) {
+
// Note: by default the images receive the bootstrap opt-in
// img-fluid class. We cannot make them all responsive
// by design because of libraries and client databases img.
self.$media = $('<img/>', {class: 'img-fluid o_we_custom_image'});
self.media = self.$media[0];
}
- self.$media.attr('src', img.src);
-
+ self.$media.attr('src', src);
} else {
if (!self.$media.is('a')) {
$('.note-control-selection').hide();
@@ -410,7 +413,7 @@ var FileWidget = SearchableMediaWidget.extend({
self.media = self.$media[0];
}
var href = '/web/content/' + img.id + '?';
- if (img.access_token && self.options.res_model !== 'ir.ui.view') {
+ if (!img.public && img.access_token) {
href += _.str.sprintf('access_token=%s&', img.access_token);
}
href += 'unique=' + img.checksum + '&download=true';
@@ -436,19 +439,22 @@ var FileWidget = SearchableMediaWidget.extend({
});
},
/**
+ * @param {object} attachment
+ * @param {boolean} [save=true] to save the given attachment in the DOM and
+ * and to close the media dialog
* @private
*/
- _toggleImage: function (attachment, doubleClick) {
+ _selectAttachement: function (attachment, save) {
if (this.options.multiImages) {
- // if the clicked image is already selected, then unselect it
- // unless it was a double click
+ // if the clicked image is already selected then unselect it
+ // unless it was a save request (then keep the current selection)
var index = this.selectedAttachments.indexOf(attachment);
if (index !== -1) {
- if (!doubleClick) {
+ if (!save) {
this.selectedAttachments.splice(index, 1);
}
} else {
- // if the clicked image is not selected, then select it
+ // if the clicked image is not selected, add it to selection
this.selectedAttachments.push(attachment);
}
} else {
@@ -456,6 +462,9 @@ var FileWidget = SearchableMediaWidget.extend({
this.selectedAttachments = [attachment];
}
this._highlightSelected();
+ if (save) {
+ this.trigger_up('save_request');
+ }
},
/**
* Updates the add by URL UI.
@@ -473,73 +482,6 @@ var FileWidget = SearchableMediaWidget.extend({
this.$urlSuccess.toggleClass('d-none', !isURL);
this.$urlError.toggleClass('d-none', emptyValue || isURL);
},
- /**
- * @private
- */
- _uploadFile: function () {
- return this._mutex.exec(this._uploadImageIframe.bind(this));
- },
- /**
- * @returns {Promise}
- */
- _uploadImageIframe: function () {
- var self = this;
- return new Promise(function (resolve) {
-
- /**
- * @todo file upload cannot be handled with _rpc smoothly. This uses the
- * form posting in iframe trick to handle the upload.
- */
- var $iframe = self.$('iframe');
- $iframe.on('load', function () {
- var iWindow = $iframe[0].contentWindow;
-
- var attachments = iWindow.attachments || [];
- var error = iWindow.error;
-
- self.$('.well > span').remove();
- self.$('.well > div').show();
- _.each(attachments, function (attachment) {
- // Name is added for SEO purposes
- attachment.src = attachment.url || _.str.sprintf('/web/image/%s/%s', attachment.id, encodeURI(attachment.name));
- attachment.isDocument = !(/gif|jpe|jpg|png/.test(attachment.mimetype));
- });
- if (error || !attachments.length) {
- _processFile(null, error || !attachments.length);
- }
- self.attachments = attachments;
- for (var i = 0 ; i < attachments.length ; i++) {
- _processFile(attachments[i], error);
- }
-
- if (self.options.onUpload) {
- self.options.onUpload(attachments);
- }
-
- resolve();
-
- function _processFile(attachment, error) {
- var $button = self.$uploadButton;
- if (!error) {
- $button.addClass('btn-success');
- self._toggleImage(attachment, true);
- } else {
- $button.addClass('btn-danger');
- self.$el.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
- self.$errorText.text(error);
- }
-
- if (!self.options.multiImages) {
- self.trigger_up('save_request');
- }
- }
- });
- self.$el.submit();
-
- self.$fileInput.val('');
- });
- },
-
//--------------------------------------------------------------------------
// Handlers
@@ -548,36 +490,74 @@ var FileWidget = SearchableMediaWidget.extend({
/**
* @private
*/
- _onAttachmentClick: function (ev, doubleClick) {
+ _onAttachmentClick: function (ev, save) {
var $attachment = $(ev.currentTarget);
var attachment = _.find(this.attachments, {id: $attachment.data('id')});
- this._toggleImage(attachment, doubleClick);
+ this._selectAttachement(attachment, save);
},
/**
* @private
*/
_onAttachmentDblClick: function (ev) {
this._onAttachmentClick(ev, true);
- this.trigger_up('save_request');
},
/**
* @private
+ * @returns {Promise}
*/
_onFileInputChange: function () {
- this.$el.addClass('nosave');
- this.$form.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
- this.$errorText.empty();
- this.$uploadButton.removeClass('btn-danger btn-success');
- this._uploadFile();
+ return this._mutex.exec(this._addData.bind(this));
+ },
+ /**
+ * Uploads the files that are currently selected on the file input, which
+ * creates new attachments. Then inserts them on the media dialog and
+ * selects them. If multiImages is not set, also triggers up the
+ * save_request event to insert the attachment in the DOM.
+ *
+ * @private
+ * @returns {Promise}
+ */
+ _addData: function () {
+ var self = this;
+ var uploadMutex = new concurrency.Mutex();
+
+ // Upload the smallest file first to block the user the least possible.
+ var files = _.sortBy(this.$fileInput[0].files, 'size');
+
+ _.each(files, function (file) {
+ // Upload one file at a time: no need to parallel as upload is
+ // limited by bandwidth.
+ uploadMutex.exec(function () {
+ return utils.getDataURLFromFile(file).then(function (result) {
+ return self._rpc({
+ route: '/web_editor/attachment/add_data',
+ params: {
+ 'name': file.name,
+ 'data': result.split(',')[1],
+ 'res_id': self.options.res_id,
+ 'res_model': self.options.res_model,
+ 'filters': self.options.firstFilters.join('_'),
+ },
+ }).then(function (attachment) {
+ self._handleNewAttachment(attachment);
+ });
+ });
+ });
+ });
+ return uploadMutex.getUnlockedDef().then(function () {
+ if (!self.options.multiImages) {
+ self.trigger_up('save_request');
+ }
+ });
},
/**
* @private
*/
_onRemoveClick: function (ev) {
var self = this;
+ ev.stopPropagation();
Dialog.confirm(this, _t("Are you sure you want to delete this file ?"), {
confirm_callback: function () {
- var $helpBlock = self.$errorText.empty();
var $a = $(ev.currentTarget).closest('.o_existing_attachment_cell');
var id = parseInt($a.data('id'), 10);
var attachment = _.findWhere(self.attachments, {id: id});
@@ -589,10 +569,10 @@ var FileWidget = SearchableMediaWidget.extend({
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.attachments = _.without(self.attachments, attachment);
- self._renderImages();
+ $a.closest('.o_existing_attachment_cell').remove();
return;
}
- $helpBlock.replaceWith(QWeb.render('wysiwyg.widgets.image.existing.error', {
+ self.$errorText.replaceWith(QWeb.render('wysiwyg.widgets.image.existing.error', {
views: prevented[id],
widget: self,
}));
@@ -603,10 +583,8 @@ var FileWidget = SearchableMediaWidget.extend({
/**
* @private
*/
- _onURLInputChange: function (ev) {
- var $input = $(ev.currentTarget);
-
- var inputValue = $input.val();
+ _onURLInputChange: function () {
+ var inputValue = this.$urlInput.val();
var emptyValue = (inputValue === '');
var isURL = /^.+\..+$/.test(inputValue); // TODO improve
@@ -631,9 +609,33 @@ var FileWidget = SearchableMediaWidget.extend({
},
/**
* @private
+ * @returns {Promise}
*/
_onUploadURLButtonClick: function () {
- this._uploadFile();
+ return this._mutex.exec(this._addUrl.bind(this));
+ },
+ /**
+ * @private
+ * @returns {Promise}
+ */
+ _addUrl: function () {
+ var self = this;
+ return this._rpc({
+ route: '/web_editor/attachment/add_url',
+ params: {
+ 'url': this.$urlInput.val(),
+ 'res_id': this.options.res_id,
+ 'res_model': this.options.res_model,
+ 'filters': this.options.firstFilters.join('_'),
+ },
+ }).then(function (attachment) {
+ self.$urlInput.val('');
+ self._onURLInputChange();
+ self._handleNewAttachment(attachment);
+ if (!self.options.multiImages) {
+ self.trigger_up('save_request');
+ }
+ });
},
/**
* @private
diff --git a/addons/web_editor/static/src/xml/wysiwyg.xml b/addons/web_editor/static/src/xml/wysiwyg.xml
index 9f3101cb1c6..a9e42a88231 100644
--- a/addons/web_editor/static/src/xml/wysiwyg.xml
+++ b/addons/web_editor/static/src/xml/wysiwyg.xml
@@ -95,15 +95,7 @@
<!-- File choosing part of the Media Dialog -->
<t t-name="wysiwyg.widgets.file">
- <t t-set="iframeName" t-value="_.uniqueId('fileframe-')"/>
- <form method="POST"
- action="/web_editor/attachment/add"
- enctype="multipart/form-data"
- t-att-target="iframeName">
- <input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
- <input type="hidden" name="filters" t-att-value="widget.options.firstFilters.join('_')"/>
- <input t-if="widget.options.res_id" type="hidden" name="res_id" t-att-value="widget.options.res_id"/>
- <input t-if="widget.options.res_model" type="hidden" name="res_model" t-att-value="widget.options.res_model"/>
+ <form>
<t t-call="wysiwyg.widgets.files.submenu"/>
<div class="form-text o_we_error_text"/>
<div class="existing-attachments"/>
@@ -113,13 +105,12 @@
<span><i>All images have been loaded</i></span>
</div>
</div>
- <iframe class="o_file_upload_iframe d-none" t-att-name="iframeName"/>
</form>
</t>
<t t-name="wysiwyg.widgets.files.submenu">
<div class="form-inline align-items-center py-4">
- <input type="file" class="d-none o_file_input" name="upload" t-att-accept="widget.options.accept" multiple="multiple"/>
+ <input type="file" class="d-none o_file_input" name="upload" t-att-accept="widget.options.accept" t-att-multiple="widget.options.multiImages &amp;&amp; 'multiple'"/>
<input type="hidden" name="disable_optimization" value=""/>
<div class="btn-group">
@@ -209,7 +200,7 @@
<li class="o_existing_attachment_cell position-relative m-2" t-att-data-id="attachment.id">
<t t-call="wysiwyg.widgets.file.existing.remove"/>
<div t-attf-class="d-flex align-items-center h-100">
- <img class="img img-fluid o_we_attachment_highlight" t-att-src="attachment.src + '?width=256&amp;height=192'" t-att-alt="attachment.name" t-att-title="attachment.name"/>
+ <img class="img img-fluid o_we_attachment_highlight" t-att-src="attachment.image_src + '?width=256&amp;height=192'" t-att-alt="attachment.name" t-att-title="attachment.name"/>
</div>
</li>
</t>
diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js
index 27ba86b52ff..7d2a4b37bbb 100644
--- a/addons/web_editor/static/tests/test_utils.js
+++ b/addons/web_editor/static/tests/test_utils.js
@@ -347,14 +347,21 @@ function wysiwygData(data) {
string: "res_model",
type: 'char',
},
+ public: {
+ string: "public",
+ type: 'boolean',
+ },
access_token: {
string: "access_token",
type: 'char',
},
+ image_src: {
+ string: "image_src",
+ type: 'char',
+ },
},
records: [{
id: 1,
- public: true,
name: 'image',
datas_fname: 'image.png',
mimetype: 'image/png',
@@ -363,7 +370,9 @@ function wysiwygData(data) {
type: 'url',
res_id: 0,
res_model: false,
- access_token: false
+ public: true,
+ access_token: false,
+ image_src: '/web_editor/static/src/img/transparent.png',
}],
generate_access_token: function () {
return;
diff --git a/addons/web_editor/static/tests/wysiwyg_tests.js b/addons/web_editor/static/tests/wysiwyg_tests.js
index be62dbb4192..e03fb3888fb 100644
--- a/addons/web_editor/static/tests/wysiwyg_tests.js
+++ b/addons/web_editor/static/tests/wysiwyg_tests.js
@@ -3378,28 +3378,6 @@ var altDialogSaved;
var cropDialogOpened;
QUnit.module('Media', {
beforeEach: function () {
- $('body').on('submit.WysiwygTests', function (ev) {
- ev.preventDefault();
- var $from = $(ev.target);
- var iframe = $from.find('iframe[name="' + $from.attr('target') + '"]')[0];
- if (iframe) {
- iframe.contentWindow.attachments = [{
- id: 1,
- public: true,
- name: 'image',
- datas_fname: 'image.png',
- mimetype: 'image/png',
- checksum: false,
- url: '/web_editor/static/src/img/transparent.png',
- type: 'url',
- res_id: 0,
- res_model: false,
- access_token: false
- }];
- $(iframe).trigger('load');
- }
- });
-
this.data = {
debug: false,
wysiwygOptions: {
@@ -3415,6 +3393,21 @@ QUnit.module('Media', {
if (route.indexOf('/web_editor/static/src/img/') === 0) {
return Promise.resolve();
}
+ if (route === '/web_editor/attachment/add_url') {
+ return Promise.resolve({
+ id: 1,
+ public: true,
+ name: 'image',
+ mimetype: 'image/png',
+ checksum: false,
+ url: '/web_editor/static/src/img/transparent.png',
+ image_src: '/web_editor/static/src/img/transparent.png',
+ type: 'url',
+ res_id: 0,
+ res_model: false,
+ access_token: false,
+ });
+ }
return this._super(route, args);
},
};
diff --git a/addons/web_unsplash/controllers/main.py b/addons/web_unsplash/controllers/main.py
index c9a2deb631e..98b3cec976e 100644
--- a/addons/web_unsplash/controllers/main.py
+++ b/addons/web_unsplash/controllers/main.py
@@ -97,7 +97,7 @@ class Web_Unsplash(http.Controller):
logger.exception("Timeout: " + str(e))
continue
- image_base64 = tools.image_process(image_base64)
+ image_base64 = tools.image_process(image_base64, verify_resolution=True)
mimetype = guess_mimetype(base64.b64decode(image_base64))
# append image extension in name
query += mimetypes.guess_extension(mimetype) or ''
@@ -116,7 +116,7 @@ class Web_Unsplash(http.Controller):
'res_model': res_model,
})
attachment.generate_access_token()
- uploads.extend(attachment.read(['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token']))
+ uploads.append(attachment._get_media_info())
# Notifies Unsplash from an image download. (API requirement)
self._notify_download(value.get('download_url'))
diff --git a/addons/web_unsplash/static/src/js/unsplash_image_widget.js b/addons/web_unsplash/static/src/js/unsplash_image_widget.js
index 53259e6278b..8244f12b4b9 100644
--- a/addons/web_unsplash/static/src/js/unsplash_image_widget.js
+++ b/addons/web_unsplash/static/src/js/unsplash_image_widget.js
@@ -92,10 +92,6 @@ widgetsMedia.ImageWidget.include({
query: self._unsplash.query,
}
}).then(function (images) {
- _.each(images, function (image) {
- image.src = image.url;
- image.isDocument = !(/gif|jpe|jpg|png/.test(image.mimetype));
- });
self.attachments = images;
self.selectedAttachments = images;
return _super.apply(self, args);
diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js
index eb7f014307a..2798ca38f5c 100644
--- a/addons/website/static/src/js/editor/snippets.options.js
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -1008,7 +1008,7 @@ options.registry.gallery = options.Class.extend({
for (var i = 0 ; i < attachments.length; i++) {
$('<img/>', {
class: 'img img-fluid',
- src: attachments[i].src,
+ src: attachments[i].image_src,
'data-index': ++index,
}).appendTo($container);
}
diff --git a/odoo/fields.py b/odoo/fields.py
index 8baf2fa2edd..cc2932ef899 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -1817,7 +1817,7 @@ class Binary(Field):
return None
# Detect if the binary content is an SVG for restricting its upload
# only to system users.
- if value[:1] == b'P': # Fast detection of first 6 bits of '<' (0x3C)
+ if value[:1] in (b'P', 'P'): # Fast detection of first 6 bits of '<' (0x3C)
decoded_value = base64.b64decode(value)
# Full mimetype detection
if (guess_mimetype(decoded_value).startswith('image/svg') and
diff --git a/odoo/tools/image.py b/odoo/tools/image.py
index c714d912eb9..6f02f3c3d65 100644
--- a/odoo/tools/image.py
+++ b/odoo/tools/image.py
@@ -60,7 +60,7 @@ class ImageProcess():
self.base64_source = base64_source or False
self.operationsCount = 0
- if not base64_source or base64_source[:1] == b'P':
+ if not base64_source or base64_source[:1] in (b'P', 'P'):
# don't process empty source or SVG
self.image = False
else:
@@ -318,7 +318,7 @@ def is_image_size_above(base64_source, size=IMAGE_BIG_SIZE):
"""
if not base64_source:
return False
- if base64_source[:1] == b'P':
+ if base64_source[:1] in (b'P', 'P'):
# False for SVG
return False
image = base64_to_image(base64_source)