Merge pull request #1561 from aignatov-bio/ai-sci-3131-improve-image-file-upload

Improve the image file upload into RTE input fields [SCI-3131]
This commit is contained in:
aignatov-bio 2019-04-10 14:04:06 +02:00 committed by GitHub
commit f47d0e2932
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 585 additions and 424 deletions

View file

@ -146,7 +146,7 @@
var nameValid = textValidator(ev, $nameInput, 0,
<%= Constants::NAME_MAX_LENGTH %>);
var $descrTextarea = $form.find("#result_result_text_attributes_text");
var $tinyMCEInput = TinyMCE.getContent();
var $tinyMCEInput = TinyMCE.init('#result_result_text_attributes_text');;
textValidator(ev, $descrTextarea, 1, <%= Constants::RICH_TEXT_MAX_LENGTH %>, false, $tinyMCEInput);
break;
case ResultTypeEnum.COMMENT:

View file

@ -35,7 +35,7 @@
animateSpinner(null, false);
initNewReslutText();
}
})
});
});
}
@ -59,6 +59,7 @@
});
Results.toggleResultEditButtons(false);
TinyMCE.refresh();
TinyMCE.init('#result_result_text_attributes_text');
$('#result_name').focus();
});
}

View file

@ -49,7 +49,7 @@ var TinyMCE = (function() {
autosave_interval: '15s',
autosave_retention: '1440m',
removed_menuitems: 'newdocument',
object_resizing: false,
object_resizing: true,
elementpath: false,
forced_root_block: false,
default_link_target: '_blank',

View file

@ -0,0 +1,337 @@
/* eslint no-underscore-dangle: "off" */
/* eslint no-use-before-define: "off" */
/* eslint no-restricted-syntax: ["off", "BinaryExpression[operator='in']"] */
/* global tinymce I18n */
(function() {
'use strict';
tinymce.PluginManager.requireLangPack('customimageuploader');
tinymce.create('tinymce.plugins.CustomImageUploader', {
CustomImageUploader: function(ed, url) {
var form;
var iframe;
var win;
var throbber;
var editor = ed;
var textAreaElement = $('#' + ed.id);
function showDialog() {
var ctrl;
var cycle;
var el;
var body;
var containers;
var inputs;
win = editor.windowManager.open({
title: I18n.t('tiny_mce.upload_window_title'),
width: 520 + parseInt(editor.getLang(
'customimageuploader.delta_width', 0
), 10),
height: 180 + parseInt(editor.getLang(
'customimageuploader.delta_height', 0
), 10),
body: [
{ type: 'iframe', url: 'javascript:void(0)' },
{
type: 'textbox',
name: 'file',
classes: 'image-loader',
label: I18n.t('tiny_mce.upload_window_label'),
subtype: 'file'
},
{
type: 'container',
classes: 'error',
html: "<p style='color: #b94a48;'>&nbsp;</p>"
},
{ type: 'container', classes: 'throbber' }
],
buttons: [
{
text: I18n.t('tiny_mce.insert_btn'),
onclick: insertImage,
subtype: 'primary'
},
{
text: I18n.t('general.cancel'),
onclick: editor.windowManager.close
}
]
}, {
plugin_url: url
});
// Let's make image selection looks fancy
$('<div class="image-selection-container">'
+ '<div class="select_button btn btn-primary">' + I18n.t('tiny_mce.choose_file') + '</div>'
+ '<input type="text" placeholder="' + I18n.t('tiny_mce.no_image_chosen') + '" disabled></input>'
+ '</div>').insertAfter('.mce-image-loader')
.click(() => { $('.mce-image-loader').click(); })
.parent()
.css('height', '32px');
$('.mce-image-loader')
.change(e => {
$(e.target).next().find('input[type=text]')[0].value = e.target.value.split(/(\\|\/)/g).pop();
})
.parent().find('label')
.css('line-height', '32px')
.css('height', '32px');
el = win.getEl();
body = document.getElementById(el.id + '-body');
containers = body.getElementsByClassName('mce-container');
win.off('submit');
win.on('submit', insertImage);
iframe = win.find('iframe')[0];
form = createElement('form', {
action: editor.getParam(
'customimageuploader_form_url',
'/tinymce_assets'
),
target: iframe._id,
method: 'POST',
enctype: 'multipart/form-data',
accept_charset: 'UTF-8'
});
inputs = form.getElementsByTagName('input');
iframe.getEl().name = iframe._id;
// Create some needed hidden inputs
form.appendChild(
createElement(
'input',
{
type: 'hidden',
name: 'utf8',
value: '✓'
}
)
);
form.appendChild(
createElement(
'input',
{
type: 'hidden',
name: 'authenticity_token',
value: getMetaContents('csrf-token')
}
)
);
form.appendChild(
createElement(
'input',
{
type: 'hidden',
name: 'object_type',
value: $(editor.getElement()).data('object-type')
}
)
);
form.appendChild(
createElement(
'input',
{
type: 'hidden',
name: 'object_id',
value: $(editor.getElement()).data('object-id')
}
)
);
form.appendChild(
createElement(
'input',
{
type: 'hidden',
name: 'hint',
value: editor.getParam('uploadimage_hint', '')
}
)
);
for (cycle = 0; cycle < containers.length; cycle += 1) {
form.appendChild(containers[cycle]);
}
for (cycle = 0; cycle < inputs.length; cycle += 1) {
ctrl = inputs[cycle];
if (ctrl.tagName.toLowerCase() === 'input' && ctrl.type !== 'hidden') {
if (ctrl.type === 'file') {
ctrl.name = 'file';
tinymce.DOM.setStyles(ctrl, {
border: 0,
boxShadow: 'none',
webkitBoxShadow: 'none'
});
} else {
ctrl.name = 'alt';
}
}
}
body.appendChild(form);
}
function insertImage() {
var target = iframe.getEl();
if (getInputValue('file') === '') {
return handleError(I18n.t('tiny_mce.error_message'));
}
throbber = new top.tinymce.ui.Throbber(win.getEl());
throbber.show(throbber);
clearErrors();
/* Add event listeners.
* We remove the existing to avoid them being called twice in case
* of errors and re-submitting afterwards.
*/
if (target.attachEvent) {
target.detachEvent('onload', uploadDone);
target.attachEvent('onload', uploadDone);
} else {
target.removeEventListener('load', uploadDone);
target.addEventListener('load', uploadDone, false);
}
form.submit();
return true;
}
function uploadDone() {
var target = iframe.getEl();
var doc;
if (throbber) {
throbber.hide();
}
if (target.document || target.contentDocument) {
doc = target.contentDocument || target.contentWindow.document;
handleResponse(doc.getElementsByTagName('body')[0].innerHTML);
} else {
handleError(I18n.t('tiny_mce.server_not_respond'));
}
}
function handleResponse(ret) {
var json;
var errorJson;
try {
json = tinymce.util.JSON.parse(ret);
if (json.error) {
handleError(json.error.message);
} else {
editor.execCommand('mceInsertContent', false, buildHTML(json));
editor.windowManager.close();
updateActiveImages(ed);
}
} catch (e) {
// hack that gets the server error message
errorJson = JSON.parse($(ret).text());
handleError(errorJson.error[0]);
}
}
function clearErrors() {
var message = win.find('.error')[0].getEl();
if (message) {
message.getElementsByTagName('p')[0].innerHTML = '&nbsp;';
}
}
function handleError(error) {
var message = win.find('.error')[0].getEl();
if (message) {
message
.getElementsByTagName('p')[0]
.innerHTML = editor.translate(error);
}
}
function createElement(element, attributes) {
var elResult = document.createElement(element);
var property;
for (property in attributes) {
if (!(attributes[property] instanceof Function)) {
elResult[property] = attributes[property];
}
}
return elResult;
}
function buildHTML(json) {
var imgstr = "<img src='" + json.image.url + "'";
imgstr += " data-mce-token='" + json.image.token + "'";
imgstr += " alt='description-" + json.image.token + "' />";
return imgstr;
}
function getInputValue(name) {
var inputValues = form.getElementsByTagName('input');
var cycle;
for (cycle in inputValues) {
if (inputValues[cycle].name === name) {
return inputValues[cycle].value;
}
}
return '';
}
function getMetaContents(mn) {
var m = document.getElementsByTagName('meta');
var cycle;
for (cycle in m) {
if (m[cycle].name === mn) {
return m[cycle].content;
}
}
return null;
}
// Finding images in text
function updateActiveImages() {
var images;
iframe = $('#' + editor.id).prev().find('.mce-edit-area iframe').contents();
images = $.map($('img', iframe), e => {
return e.dataset.mceToken;
});
$('#' + editor.id).next()[0].value = JSON.stringify(images);
return JSON.stringify(images);
}
// Add a button that opens a window
editor.addButton('customimageuploader', {
tooltip: I18n.t('tiny_mce.upload_window_label'),
icon: 'image',
onclick: showDialog
});
// Adds a menu item to the tools menu
editor.addMenuItem('customimageuploader', {
text: I18n.t('tiny_mce.upload_window_label'),
icon: 'image',
context: 'insert',
onclick: showDialog
});
ed.on('NodeChange', () => {
updateActiveImages(ed);
});
textAreaElement.parent().find('input#tiny-mce-images').remove();
$('<input type="hidden" id="tiny-mce-images" name="tiny_mce_images" value="[]">').insertAfter(textAreaElement);
}
});
tinymce.PluginManager.add(
'customimageuploader',
tinymce.plugins.CustomImageUploader
);
})();

View file

@ -1,245 +0,0 @@
(function() {
'use strict';
tinymce.PluginManager.requireLangPack('customimageuploader');
tinymce.create('tinymce.plugins.CustomImageUploader', {
CustomImageUploader: function(ed, url) {
var form, iframe, win, throbber, editor = ed;
function showDialog() {
win = editor.windowManager.open({
title: "<%= I18n.t 'tiny_mce.upload_window_title' %>",
width: 520 + parseInt(editor.getLang(
'customimageuploader.delta_width', 0
), 10),
height: 180 + parseInt(editor.getLang(
'customimageuploader.delta_height', 0
), 10),
body: [
{type: 'iframe', url: 'javascript:void(0)'},
{type: 'textbox',
name: 'file',
label: "<%= I18n.t 'tiny_mce.upload_window_label' %>",
subtype: 'file'},
{type: 'container',
classes: 'error',
html: "<p style='color: #b94a48;'>&nbsp;</p>"},
{type: 'container', classes: 'throbber'},
],
buttons: [
{text: "<%= I18n.t 'tiny_mce.insert_btn' %>",
onclick: insertImage,
subtype: 'primary'},
{text: "<%= I18n.t 'general.cancel' %>",
onclick: editor.windowManager.close}
],
}, {
plugin_url: url
});
win.off('submit');
win.on('submit', insertImage);
iframe = win.find('iframe')[0];
form = createElement('form', {
action: editor.getParam('customimageuploader_form_url',
'<%= Rails.application
.routes
.url_helpers
.tiny_mce_assets_path %>'),
target: iframe._id,
method: 'POST',
enctype: 'multipart/form-data',
accept_charset: 'UTF-8',
});
iframe.getEl().name = iframe._id;
// Create some needed hidden inputs
form.appendChild(createElement('input',
{type: 'hidden',
name: 'utf8',
value: '✓'}));
form.appendChild(createElement('input',
{type: 'hidden',
name: 'authenticity_token',
value: getMetaContents('csrf-token')}));
form.appendChild(createElement('input',
{type: 'hidden',
name: 'object_type',
value: $(editor.getElement())
.data('object-type')}));
form.appendChild(createElement('input',
{type: 'hidden',
name: 'object_id',
value: $(editor.getElement())
.data('object-id')}));
form.appendChild(createElement('input',
{type: 'hidden',
name: 'hint',
value: editor.getParam('uploadimage_hint', '')}));
var el = win.getEl();
var body = document.getElementById(el.id + '-body');
// Copy everything TinyMCE made into our form
var containers = body.getElementsByClassName('mce-container');
for(var i = 0; i < containers.length; i++) {
form.appendChild(containers[i]);
}
var inputs = form.getElementsByTagName('input');
for(var i = 0; i < inputs.length; i++) {
var ctrl = inputs[i];
if(ctrl.tagName.toLowerCase() == 'input' && ctrl.type != 'hidden') {
if(ctrl.type == 'file') {
ctrl.name = 'file';
tinymce.DOM.setStyles(ctrl, {
'border': 0,
'boxShadow': 'none',
'webkitBoxShadow': 'none',
});
} else {
ctrl.name = 'alt';
}
}
}
body.appendChild(form);
}
function insertImage() {
if(getInputValue('file') == '') {
return handleError("<%= I18n.t 'tiny_mce.error_message' %>");
}
throbber = new top.tinymce.ui.Throbber(win.getEl());
throbber.show();
clearErrors();
/* Add event listeners.
* We remove the existing to avoid them being called twice in case
* of errors and re-submitting afterwards.
*/
var target = iframe.getEl();
if(target.attachEvent) {
target.detachEvent('onload', uploadDone);
target.attachEvent('onload', uploadDone);
} else {
target.removeEventListener('load', uploadDone);
target.addEventListener('load', uploadDone, false);
}
form.submit();
}
function uploadDone() {
if(throbber) {
throbber.hide();
}
var target = iframe.getEl();
if(target.document || target.contentDocument) {
var doc = target.contentDocument || target.contentWindow.document;
handleResponse(doc.getElementsByTagName("body")[0].innerHTML);
} else {
handleError("<%= I18n.t 'tiny_mce.server_not_respond' %>");
}
}
function handleResponse(ret) {
try {
var json = tinymce.util.JSON.parse(ret);
if(json['error']) {
handleError(json['error']['message']);
} else {
editor.execCommand('mceInsertContent', false, buildHTML(json));
editor.windowManager.close();
}
} catch(e) {
// hack that gets the server error message
var error_json = JSON.parse($(ret).text());
handleError(error_json['error'][0]);
}
}
function clearErrors() {
var message = win.find('.error')[0].getEl();
if(message)
message.getElementsByTagName('p')[0].innerHTML = '&nbsp;';
}
function handleError(error) {
var message = win.find('.error')[0].getEl();
if(message) {
message
.getElementsByTagName('p')[0]
.innerHTML = editor.translate(error);
}
}
function createElement(element, attributes) {
var el = document.createElement(element);
for(var property in attributes) {
if (!(attributes[property] instanceof Function)) {
el[property] = attributes[property];
}
}
return el;
}
function buildHTML(json) {
var imgstr = "<img src='" + json['image']['url'] + "'";
imgstr += " data-token='" + json['image']['token'] + "'"
imgstr += " alt='description-" + json['image']['token'] + "'/>";
return imgstr;
}
function getInputValue(name) {
var inputs = form.getElementsByTagName('input');
for(var i in inputs)
if(inputs[i].name == name)
return inputs[i].value;
return "";
}
function getMetaContents(mn) {
var m = document.getElementsByTagName('meta');
for(var i in m)
if(m[i].name == mn)
return m[i].content;
return null;
}
// Add a button that opens a window
editor.addButton('customimageuploader', {
tooltip: "<%= I18n.t 'tiny_mce.upload_window_label' %>",
icon: 'image',
onclick: showDialog
});
// Adds a menu item to the tools menu
editor.addMenuItem('customimageuploader', {
text: "<%= I18n.t 'tiny_mce.upload_window_label' %>",
icon: 'image',
context: 'insert',
onclick: showDialog
});
}
});
tinymce.PluginManager.add('customimageuploader',
tinymce.plugins.CustomImageUploader);
})();

View file

@ -21,4 +21,5 @@
@import "extend/bootstrap";
@import "themes/scinote";
@import "select2.min";
@import "my_modules/protocols/*"
@import "my_modules/protocols/*";
@import "hooks/*";

View file

@ -0,0 +1,37 @@
@import "constants";
@import "mixins";
.mce-image-loader {
display: none !important;
+ .image-selection-container {
height: 28px;
left: 140px;
position: absolute;
top: 0;
width: 338px;
.select_button {
background-color: $brand-success;
border-color: $brand-success;
color: $color-white;
cursor: pointer;
line-height: 20px;
padding: 6px 12px;
position: relative;
text-align: center;
width: 76px;
z-index: 2;
}
input[type=text] {
border: 1px solid $color-gainsboro;
border-radius: 4px;
height: 30px;
left: -5px;
padding-left: 10px;
position: relative;
width: calc(100% - 100px);
}
}
}

View file

@ -3,8 +3,6 @@
module Api
module V1
class ResultsController < BaseController
include TinyMceHelper
before_action :load_team
before_action :load_project
before_action :load_experiment
@ -52,15 +50,18 @@ module Api
tiny_mce_asset_params.each do |t|
image_params = t[:attributes]
token = image_params[:file_token]
unless result_text.text["[~tiny_mce_id:#{token}]"]
unless result_text.text["data-mce-token=\"#{token}\""]
raise ActiveRecord::RecordInvalid,
I18n.t('api.core.errors.result_wrong_tinymce.detail')
end
image = Paperclip.io_adapters.for(image_params[:file_data])
image.original_filename = image_params[:file_name]
tiny_img = TinyMceAsset.create!(image: image, team: @team)
result_text.text.sub!("[~tiny_mce_id:#{token}]",
"[~tiny_mce_id:#{tiny_img.id}]")
TinyMceAsset.create!(
image: image,
team: @team,
object: result_text,
saved: true
)
end
end
@result = Result.new(user: current_user,
@ -69,12 +70,12 @@ module Api
result_text: result_text,
last_modified_by: current_user)
@result.save! && result_text.save!
link_tiny_mce_assets(result_text.text, result_text)
end
end
def result_params
raise TypeError unless params.require(:data).require(:type) == 'results'
params.require(:data).require(:attributes).require(:name)
params.permit(data: { attributes: :name })[:data][:attributes]
end
@ -95,7 +96,7 @@ module Api
end
file_tokens = prms.map { |p| p[:attributes][:file_token] }
result_text_params[:text].scan(
/\[~tiny_mce_id:(\w+)\]/
/data-mce-token="(\w+)"/
).flatten.each do |token|
unless file_tokens.include?(token)
raise ActiveRecord::RecordInvalid,

View file

@ -6,7 +6,6 @@ class MyModulesController < ApplicationController
include Rails.application.routes.url_helpers
include ActionView::Helpers::UrlHelper
include ApplicationHelper
include TinyMceHelper
before_action :load_vars,
only: %i(show update destroy description due_date protocols
@ -158,7 +157,6 @@ class MyModulesController < ApplicationController
@my_module.assign_attributes(update_params)
@my_module.last_modified_by = current_user
description_changed = @my_module.description_changed?
if @my_module.archived_changed?(from: false, to: true)
saved = @my_module.archive(current_user)
@ -196,9 +194,9 @@ class MyModulesController < ApplicationController
)
end
else
saved = @my_module.save
if saved and description_changed then
TinyMceAsset.update_images(@my_module, params[:tiny_mce_images])
Activity.create(
type_of: :change_module_description,
project: @my_module.experiment.project,
@ -213,7 +211,6 @@ class MyModulesController < ApplicationController
)
end
end
respond_to do |format|
if restored
format.html do
@ -261,9 +258,10 @@ class MyModulesController < ApplicationController
respond_to do |format|
format.json do
if @my_module.update(description: params.require(:my_module)[:description])
TinyMceAsset.update_images(@my_module, params[:tiny_mce_images])
render json: {
html: custom_auto_link(
generate_image_tag_from_token(@my_module.description, @my_module),
@my_module.tinymce_render(:description),
simple_format: false,
tags: %w(img),
team: current_team
@ -282,9 +280,10 @@ class MyModulesController < ApplicationController
respond_to do |format|
format.json do
if protocol.update(description: params.require(:protocol)[:description])
TinyMceAsset.update_images(protocol, params[:tiny_mce_images])
render json: {
html: custom_auto_link(
generate_image_tag_from_token(protocol.description, protocol),
protocol.tinymce_render(:description),
simple_format: false,
tags: %w(img),
team: current_team

View file

@ -220,6 +220,7 @@ class ProtocolsController < ApplicationController
respond_to do |format|
if @protocol.save
TinyMceAsset.update_images(@protocol, params[:tiny_mce_images])
format.json do
render json: {
url: edit_protocol_path(

View file

@ -2,7 +2,6 @@ class ResultTextsController < ApplicationController
include ResultsHelper
include ActionView::Helpers::UrlHelper
include ApplicationHelper
include TinyMceHelper
include InputSanitizeHelper
include Rails.application.routes.url_helpers
@ -32,9 +31,6 @@ class ResultTextsController < ApplicationController
def create
@result_text = ResultText.new(result_params[:result_text_attributes])
# gerate a tag that replaces img tag in database
@result_text.text = parse_tiny_mce_asset_to_token(@result_text.text,
@result_text)
@result = Result.new(
user: current_user,
my_module: @my_module,
@ -46,7 +42,7 @@ class ResultTextsController < ApplicationController
respond_to do |format|
if @result.save && @result_text.save
# link tiny_mce_assets to the text result
link_tiny_mce_assets(@result_text.text, @result_text)
TinyMceAsset.update_images(@result_text, params[:tiny_mce_images])
result_annotation_notification
# Generate activity
@ -88,8 +84,6 @@ class ResultTextsController < ApplicationController
end
def edit
@result_text.text = generate_image_tag_from_token(@result_text.text,
@result_text)
respond_to do |format|
format.json {
render json: {
@ -106,8 +100,7 @@ class ResultTextsController < ApplicationController
update_params = result_params
@result.last_modified_by = current_user
@result.assign_attributes(update_params)
@result_text.text = parse_tiny_mce_asset_to_token(@result_text.text,
@result_text)
success_flash = t("result_texts.update.success_flash",
module: @my_module.name)
if @result.archived_changed?(from: false, to: true)
@ -115,6 +108,7 @@ class ResultTextsController < ApplicationController
success_flash = t("result_texts.archive.success_flash",
module: @my_module.name)
if saved
TinyMceAsset.update_images(@result_text, params[:tiny_mce_images])
Activity.create(
type_of: :archive_result,
project: @my_module.experiment.project,
@ -134,6 +128,7 @@ class ResultTextsController < ApplicationController
saved = @result.save
if saved then
TinyMceAsset.update_images(@result_text, params[:tiny_mce_images])
Activity.create(
type_of: :edit_result,
user: current_user,

View file

@ -1,7 +1,6 @@
class StepsController < ApplicationController
include ActionView::Helpers::TextHelper
include ApplicationHelper
include TinyMceHelper
include StepsActions
before_action :load_vars, only: %i(edit update destroy show toggle_step_state
@ -32,7 +31,6 @@ class StepsController < ApplicationController
def create
@step = Step.new(step_params)
# gerate a tag that replaces img tag in database
@step.description = parse_tiny_mce_asset_to_token(@step.description, @step)
@step.completed = false
@step.position = @protocol.number_of_steps
@step.protocol = @protocol
@ -61,7 +59,7 @@ class StepsController < ApplicationController
end
# link tiny_mce_assets to the step
link_tiny_mce_assets(@step.description, @step)
TinyMceAsset.update_images(@step, params[:tiny_mce_images])
create_annotation_notifications(@step)
@ -120,7 +118,6 @@ class StepsController < ApplicationController
end
def edit
@step.description = generate_image_tag_from_token(@step.description, @step)
respond_to do |format|
format.json do
render json: {
@ -159,14 +156,9 @@ class StepsController < ApplicationController
table.last_modified_by = current_user unless table.new_record?
table.team = current_team
end
# gerate a tag that replaces img tag in databases
@step.description = parse_tiny_mce_asset_to_token(
params[:step][:description],
@step
)
if @step.save
TinyMceAsset.update_images(@step, params[:tiny_mce_images])
@step.reload
# generates notification on step upadate

View file

@ -1,11 +1,12 @@
# frozen_string_literal: true
class TinyMceAssetsController < ApplicationController
before_action :find_object
def create
image = params.fetch(:file) { render_404 }
tiny_img = TinyMceAsset.new(image: image,
reference: @obj,
team_id: current_team.id)
team_id: current_team.id,
saved: false)
if tiny_img.save
render json: {
image: {
@ -20,12 +21,4 @@ class TinyMceAssetsController < ApplicationController
end
end
private
def find_object
obj_type = params.fetch(:object_type) { render_404 }
obj_id = params.fetch(:object_id) { render_404 }
render_404 unless %w(step result_text).include? obj_type
@obj = obj_type.classify.constantize.find_by_id(obj_id)
end
end

View file

@ -1,91 +0,0 @@
module TinyMceHelper
def parse_tiny_mce_asset_to_token(text, obj)
ids = []
html = Nokogiri::HTML(remove_pasted_tokens(text))
html.search('img').each do |img|
next unless img['data-token']
img_id = Base62.decode(img['data-token'])
ids << img_id
token = "[~tiny_mce_id:#{img_id}]"
img.replace(token)
next unless obj
tiny_img = TinyMceAsset.find_by_id(img_id)
tiny_img.reference = obj unless tiny_img.step || tiny_img.result_text
tiny_img.save
end
destroy_removed_tiny_mce_assets(ids, obj) if obj
html
end
# @param pdf_export_ready is needed for wicked_pdf in export report action
def generate_image_tag_from_token(text, obj, pdf_export_ready = false)
return unless text
regex = Constants::TINY_MCE_ASSET_REGEX
text.gsub(regex) do |el|
match = el.match(regex)
img = TinyMceAsset.find_by_id(match[1])
next unless img && check_image_permissions(obj, img)
if pdf_export_ready
tmp_f = Tempfile.open(img.image_file_name, Rails.root.join('tmp'))
begin
img.image.copy_to_local_file(:large, tmp_f.path)
encoded_image = Base64.strict_encode64(tmp_f.read)
"<img src='data:image/jpg;base64,#{encoded_image}'>"
ensure
tmp_f.close
tmp_f.unlink
end
else
image_tag(img.url,
class: 'img-responsive',
data: { token: Base62.encode(img.id) })
end
end
end
def link_tiny_mce_assets(text, ref)
ids = []
regex = Constants::TINY_MCE_ASSET_REGEX
text.gsub(regex) do |img|
match = img.match(regex)
tiny_img = TinyMceAsset.find_by_id(match[1])
next unless tiny_img
ids << tiny_img.id
tiny_img.public_send("#{ref.class.to_s.underscore}=".to_sym, ref)
tiny_img.save!
end
destroy_removed_tiny_mce_assets(ids, ref)
end
def replace_tiny_mce_assets(text, img_ids)
img_ids.each do |src_id, dest_id|
regex = /\[~tiny_mce_id:#{src_id}\]/
new_token = "[~tiny_mce_id:#{dest_id}]"
text.sub!(regex, new_token)
end
text
end
def destroy_removed_tiny_mce_assets(ids, ref)
# need to check if the array is empty because if we pass the empty array
# in the SQL query it will not work properly
if ids.empty?
ref.tiny_mce_assets.destroy_all
else
ref.tiny_mce_assets.where.not('id IN (?)', ids).destroy_all
end
end
def check_image_permissions(obj, img)
if obj.class == Step
img.step == obj
elsif obj.class == ResultText
img.result_text == obj
end
end
def remove_pasted_tokens(text)
regex = Constants::TINY_MCE_ASSET_REGEX
text.gsub(regex, ' ')
end
end

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
module TinyMceImages
extend ActiveSupport::Concern
included do
has_many :tiny_mce_assets,
as: :object,
class_name: :TinyMceAsset,
dependent: :destroy
def prepare_for_report(field)
description = self[field]
tiny_mce_assets.each do |tm_asset|
tmp_f = Tempfile.open(tm_asset.image_file_name, Rails.root.join('tmp'))
begin
tm_asset.image.copy_to_local_file(:large, tmp_f.path)
encoded_tm_asset = Base64.strict_encode64(tmp_f.read)
new_tm_asset = "<img class='img-responsive'
src='data:image/jpg;base64,#{encoded_tm_asset}' >"
html_description = Nokogiri::HTML(description)
tm_asset_to_update = html_description.css(
"img[data-mce-token=\"#{Base62.encode(tm_asset.id)}\"]"
)[0]
tm_asset_to_update.replace new_tm_asset
description = html_description.css('body').inner_html.to_s
ensure
tmp_f.close
tmp_f.unlink
end
end
description
end
def tinymce_render(field)
TinyMceAsset.generate_url(self[field])
end
end
end

View file

@ -1,5 +1,6 @@
class MyModule < ApplicationRecord
include ArchivableModel, SearchableModel
include TinyMceImages
enum state: Extends::TASKS_STATES

View file

@ -1,7 +1,7 @@
class Protocol < ApplicationRecord
include SearchableModel
include RenamingUtil
extend TinyMceHelper
include TinyMceImages
after_save :update_linked_children
after_destroy :decrement_linked_children
@ -333,13 +333,13 @@ class Protocol < ApplicationRecord
step2.tables << table2
end
# Copy tinyMce assets
# Copy steps tinyMce assets
cloned_img_ids = []
step.tiny_mce_assets.each do |tiny_img|
tiny_img2 = TinyMceAsset.new(
image: tiny_img.image,
estimated_size: tiny_img.estimated_size,
step: step2,
object: step2,
team: dest.team
)
tiny_img2.save
@ -347,9 +347,7 @@ class Protocol < ApplicationRecord
step2.tiny_mce_assets << tiny_img2
cloned_img_ids << [tiny_img.id, tiny_img2.id]
end
step2.update(
description: replace_tiny_mce_assets(step2.description, cloned_img_ids)
)
TinyMceAsset.reload_images(cloned_img_ids)
end
# Call clone helper

View file

@ -1,9 +1,10 @@
class ResultText < ApplicationRecord
include TinyMceImages
auto_strip_attributes :text, nullify: false
validates :text,
presence: true,
length: { maximum: Constants::RICH_TEXT_MAX_LENGTH }
validates :result, presence: true
belongs_to :result, inverse_of: :result_text, touch: true, optional: true
has_many :tiny_mce_assets, inverse_of: :result_text, dependent: :destroy
end

View file

@ -1,5 +1,6 @@
class Step < ApplicationRecord
include SearchableModel
include TinyMceImages
auto_strip_attributes :name, :description, nullify: false
validates :name,
@ -30,7 +31,6 @@ class Step < ApplicationRecord
has_many :tables, through: :step_tables
has_many :report_elements, inverse_of: :step,
dependent: :destroy
has_many :tiny_mce_assets, inverse_of: :step, dependent: :destroy
accepts_nested_attributes_for :checklists,
reject_if: :all_blank,

View file

@ -1,8 +1,11 @@
# frozen_string_literal: true
class TinyMceAsset < ApplicationRecord
attr_accessor :reference
before_create :set_reference, optional: true
after_create :update_estimated_size
after_create :update_estimated_size, :self_destruct
after_destroy :release_team_space
after_save :update_description
belongs_to :team, inverse_of: :tiny_mce_assets, optional: true
belongs_to :step, inverse_of: :tiny_mce_assets, touch: true, optional: true
@ -10,6 +13,10 @@ class TinyMceAsset < ApplicationRecord
inverse_of: :tiny_mce_assets,
touch: true,
optional: true
belongs_to :object, polymorphic: true,
optional: true,
inverse_of: :tiny_mce_assets
has_attached_file :image,
styles: { large: [Constants::LARGE_PIC_FORMAT, :jpg] },
convert_options: { large: '-quality 100 -strip' }
@ -21,20 +28,69 @@ class TinyMceAsset < ApplicationRecord
validates_attachment :image,
presence: true,
size: {
less_than: Rails.configuration.x.file_max_size_mb.megabytes
less_than: Rails.configuration.x\
.file_max_size_mb.megabytes
}
validates :estimated_size, presence: true
# When using S3 file upload, we can limit file accessibility with url signing
def self.update_images(object, images)
images = JSON.parse(images)
current_images = object.tiny_mce_assets.pluck(:id)
images_to_delete = current_images.reject do |x|
(images.include? Base62.encode(x))
end
images.each do |image|
image_to_update = find_by_id(Base62.decode(image))
image_to_update&.update(object: object, saved: true)
end
where(id: images_to_delete).destroy_all
rescue StandardError => e
Rails.logger.error e.message
end
def self.reload_images(images = [])
images.each do |image|
old_id = image.class == Array ? image[0] : image
new_id = image.class == Array ? image[1] : image
image_to_update = find_by_id(new_id)
next unless image_to_update
object_field = data_fields[image_to_update.object_type]
next unless image_to_update.object
old_description = Nokogiri::HTML(image_to_update.object[object_field])
description_image = old_description.css(
"img[data-mce-token=\"#{Base62.encode(old_id)}\"]"
)
description_image.attr('src').value = ''
description_image.attr('data-mce-token').value = Base62.encode(new_id)
description_image[0]['class'] = 'img-responsive'
new_description = old_description.css('body').inner_html.to_s
image_to_update.object.update(object_field => new_description)
end
end
def self.generate_url(description)
description = Nokogiri::HTML(description)
tm_assets = description.css('img')
tm_assets.each do |tm_asset|
asset_id = tm_asset.attr('data-mce-token')
new_asset_url = find_by_id(Base62.decode(asset_id))
if new_asset_url
tm_asset.attributes['src'].value = new_asset_url.url
tm_asset['class'] = 'img-responsive'
end
end
description.css('body').inner_html.to_s
end
def presigned_url(style = :large,
download: false,
timeout: Constants::URL_LONG_EXPIRE_TIME)
if stored_on_s3?
if download
download_arg = 'attachment; filename=' + URI.escape(image_file_name)
else
download_arg = nil
end
download_arg = if download
'attachment; filename=' + CGI.escape(image_file_name)
end
signer = Aws::S3::Presigner.new(client: S3_BUCKET.client)
signer.presigned_url(:get_object,
@ -65,21 +121,44 @@ class TinyMceAsset < ApplicationRecord
end
end
def self.delete_unsaved_image(id)
asset = find_by_id(id)
asset.destroy if asset && !asset.saved
end
def self.data_fields
{
'Step' => :description,
'ResultText' => :text,
'Protocol' => :description,
'MyModule' => :description
}
end
private
def update_description
TinyMceAsset.reload_images([id]) if object
end
def self_destruct
TinyMceAsset.delay(queue: :assets, run_at: 1.days.from_now).delete_unsaved_image(id)
end
def update_estimated_size
return if image_file_size.blank?
es = image_file_size * Constants::ASSET_ESTIMATED_SIZE_FACTOR
update(estimated_size: es)
Rails.logger.info "Asset #{id}: Estimated size successfully calculated"
# update team space taken
self.team.take_space(es)
self.team.save
team.take_space(es)
team.save
end
def release_team_space
self.team.release_space(estimated_size)
self.team.save
team.release_space(estimated_size)
team.save
end
def set_reference

View file

@ -4,7 +4,7 @@
<div id="my_module_description_view"
class="ql-editor tinymce-view"
data-placeholder="<%= t('my_modules.module_header.empty_description_edit_label') %>">
<%= custom_auto_link(generate_image_tag_from_token(@my_module.description, @my_module),
<%= custom_auto_link(@my_module.tinymce_render(:description),
simple_format: false,
tags: %w(img),
team: current_team) %>
@ -18,7 +18,7 @@
id: :my_module_description_textarea,
class: 'hidden',
hide_label: true,
value: sanitize_input(@my_module.description),
value: sanitize_input(@my_module.tinymce_render(:description)),
data: {
object_type: 'my_module',
object_id: @my_module.id,

View file

@ -76,7 +76,7 @@
<% if can_manage_module?(@my_module) %>
<%= render partial: "description_form" %>
<% elsif @my_module.description.present? %>
<%= custom_auto_link(generate_image_tag_from_token(@my_module.description, @my_module),
<%= custom_auto_link(@my_module.description,
simple_format: false,
tags: %w(img),
team: current_team) %>

View file

@ -34,7 +34,7 @@
<% if can_manage_module?(@my_module) %>
<%= render partial: "my_modules/protocols/protocol_description_form" %>
<% elsif @my_module.protocol.description.present? %>
<%= custom_auto_link(generate_image_tag_from_token(@my_module.protocol.description, @my_module.protocol),
<%= custom_auto_link(@my_module.protocol.description,
simple_format: false,
tags: %w(img),
team: current_team) %>

View file

@ -4,7 +4,7 @@
<div id="protocol_description_view"
class="ql-editor tinymce-view"
data-placeholder="<%= t('my_modules.protocols.protocol_status_bar.empty_description_edit_label') %>">
<%= custom_auto_link(generate_image_tag_from_token(@my_module.protocol.description, @my_module.protocol),
<%= custom_auto_link(@my_module.protocol.tinymce_render(:description),
simple_format: false,
tags: %w(img),
team: current_team) %>
@ -18,7 +18,7 @@
id: :protocol_description_textarea,
class: 'hidden',
hide_label: true,
value: sanitize_input(@my_module.protocol.description),
value: sanitize_input(@my_module.protocol.tinymce_render(:description)),
data: {
object_type: 'protocol',
object_id: @my_module.protocol.id,

View file

@ -1,5 +1,5 @@
<% if @protocol.description.present? %>
<strong><%= @protocol.description %></strong>
<strong><%= @protocol.tinymce_render(:description) %></strong>
<% else %>
<em><%= t("protocols.header.no_description") %></em>
<% end %>

View file

@ -85,7 +85,7 @@
<em><%= t("protocols.steps.no_description") %></em>
<% else %>
<div class="ql-editor">
<%= sanitize_input(generate_image_tag_from_token(step.description, step), ['img']) %>
<%= sanitize_input(step.tinymce_render(:description), ['img']) %>
</div>
<% end %>
</div>

View file

@ -38,7 +38,7 @@
<div class="row">
<div class="col-xs-12">
<% if my_module.description.present? %>
<%= custom_auto_link(my_module.description, team: current_team) %>
<%= custom_auto_link(my_module.tinymce_render(:description), team: current_team) %>
<% else %>
<em><%=t "projects.reports.elements.module.no_description" %></em>
<% end %>

View file

@ -24,7 +24,7 @@
<div class="report-element-body">
<div class="row">
<div class="col-xs-12 text-container ql-editor">
<%= custom_auto_link(generate_image_tag_from_token(result_text.text, result_text, pdf_export_ready),
<%= custom_auto_link(result_text.prepare_for_report(:text),
team: current_team,
simple_format: false,
tags: %w(img)) %>

View file

@ -31,7 +31,7 @@
<div class="row">
<div class="col-xs-12 ql-editor">
<% if strip_tags(step.description).present? %>
<%= custom_auto_link(generate_image_tag_from_token(step.description, step, pdf_export_ready),
<%= custom_auto_link(step.prepare_for_report(:description),
team: current_team,
simple_format: false,
tags: %w(img)) %>

View file

@ -4,7 +4,7 @@
<%= f.fields_for :result_text do |ff| %>
<div class="form-group">
<%= ff.tiny_mce_editor(:text,
value: @result.result_text.text,
value: @result.result_text.tinymce_render(:text),
data: { object_type: 'result_text',
object_id: @result.result_text.id }) %>
</div>

View file

@ -1,5 +1,5 @@
<div class="ql-editor">
<%= custom_auto_link(generate_image_tag_from_token(result.result_text.text, result.result_text),
<%= custom_auto_link(result.result_text.text,
simple_format: false,
tags: %w(img),
team: current_team) %>

View file

@ -32,7 +32,7 @@
<%= f.tiny_mce_editor(:description,
id: :step_description_textarea,
hide_label: false,
value: sanitize_input(@step.description),
value: sanitize_input(@step.tinymce_render(:description)),
data: {
object_type: 'step',
object_id: @step.id,

View file

@ -49,11 +49,11 @@
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<% if strip_tags(step.description).blank? %>
<% if step.description.blank? %>
<em><%= t('protocols.steps.no_description') %></em>
<% else %>
<div class="ql-editor">
<%= custom_auto_link(generate_image_tag_from_token(step.description, step),
<%= custom_auto_link(step.tinymce_render(:description),
simple_format: false,
tags: %w(img),
team: current_team) %>

View file

@ -221,7 +221,7 @@ class Constants
WHITELISTED_TAGS = %w(
a b strong i em li ul ol h1 del ins h2 h3 h4 h5 h6 br sub sup p code hr div
span u s blockquote pre col colgroup table thead tbody th tr td
span u s blockquote pre col colgroup table thead tbody th tr td img
).freeze
WHITELISTED_ATTRIBUTES = [

View file

@ -1971,6 +1971,8 @@ en:
tiny_mce:
upload_window_title: 'Insert an image from your computer'
upload_window_label: 'Choose an image'
choose_file: 'Choose file'
no_image_chosen: 'No image chosen'
insert_btn: 'Insert'
error_message: 'You must choose a file'
server_not_respond: "Didn't get a response from the server"

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddColumnSavedToTinyMceAsset < ActiveRecord::Migration[5.1]
def change
add_column :tiny_mce_assets, :saved, :boolean, default: true
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddPolymorphicToTinyMceAsset < ActiveRecord::Migration[5.1]
def change
add_reference :tiny_mce_assets, :object, polymorphic: true
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190304153544) do
ActiveRecord::Schema.define(version: 20190308092130) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -744,6 +744,10 @@ ActiveRecord::Schema.define(version: 20190304153544) do
t.integer "result_text_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "saved", default: true
t.string "object_type"
t.bigint "object_id"
t.index ["object_type", "object_id"], name: "index_tiny_mce_assets_on_object_type_and_object_id"
t.index ["result_text_id"], name: "index_tiny_mce_assets_on_result_text_id"
t.index ["step_id"], name: "index_tiny_mce_assets_on_step_id"
t.index ["team_id"], name: "index_tiny_mce_assets_on_team_id"

View file

@ -126,7 +126,9 @@ RSpec.describe "Api::V1::ResultsController", type: :request do
included: [
{ type: 'result_texts',
attributes: {
text: 'Result text 1 [~tiny_mce_id:a1]'
text: 'Result text 1 <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAA'\
'AACCAIAAAD91JpzAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAE0lE'\
'QVQIHWP8//8/AwMDExADAQAkBgMBOOSShwAAAABJRU5ErkJggg==" data-mce-token="a1">'
} },
{ type: 'tiny_mce_assets',
attributes: {