summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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)