[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
This commit is contained in:
Sébastien Theys
2019-06-04 22:14:13 +02:00
parent 191c556d6b
commit 76cf2ae82a
12 changed files with 232 additions and 252 deletions
+50 -82
View File
@@ -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)
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()
return """<script type='text/javascript'>
window.attachments = %s;
window.error = %s;
</script>""" % (json.dumps(uploads), json.dumps(message))
@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):
"""
+21
View File
@@ -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]
@@ -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,
},
});
},
@@ -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
+3 -12
View File
@@ -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>
+11 -2
View File
@@ -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;
+15 -22
View File
@@ -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);
},
};
+2 -2
View File
@@ -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'))
@@ -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);
@@ -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);
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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)