Merge branch 'activestorage_migration' into ok_SCI_3679

This commit is contained in:
Oleksii Kriuchykhin 2019-08-06 15:27:31 +02:00
commit eaf9d59819
166 changed files with 3024 additions and 997 deletions

5
.gitignore vendored
View file

@ -77,3 +77,8 @@ spec/addons
# RVM/rbenv ruby version for local development
.ruby-version
#ignore marvinJs
public/marvinjs
public/marvin4js-license.cxl

10
Gemfile
View file

@ -4,9 +4,7 @@ source 'http://rubygems.org'
ruby '2.6.3'
gem 'bootsnap', require: false
gem 'webpacker', '~> 3.5'
gem 'bootstrap-sass', '~> 3.3.7'
gem 'bootstrap_form', '~> 2.7.0'
gem 'devise', '~> 4.6.2'
@ -19,6 +17,7 @@ gem 'recaptcha', require: 'recaptcha/rails'
gem 'sanitize', '~> 4.4'
gem 'sassc-rails'
gem 'simple_token_authentication', '~> 1.15.1' # Token authentication for Devise
gem 'webpacker', '~> 3.5'
gem 'yomu'
# Gems for OAuth2 subsystem
@ -29,6 +28,7 @@ gem 'omniauth-linkedin-oauth2'
# Gems for API implementation
gem 'active_model_serializers', '~> 0.10.7'
gem 'json-jwt'
gem 'jsonapi-renderer', '= 0.2.0'
gem 'jwt', '~> 1.5'
gem 'kaminari'
gem 'rack-attack'
@ -78,16 +78,16 @@ gem 'sneaky-save', git: 'https://github.com/einzige/sneaky-save'
gem 'turbolinks', '~> 5.1.1'
gem 'underscore-rails'
gem 'wicked_pdf', '~> 1.1.0'
gem 'wkhtmltopdf-heroku'
gem 'wkhtmltopdf-heroku', '2.12.4'
gem 'aws-sdk-rails'
gem 'aws-sdk-s3'
gem 'mini_magick'
gem 'paperclip', '~> 6.1' # File attachment, image attachment library
gem 'delayed_job_active_record'
gem 'devise-async',
git: 'https://github.com/mhfs/devise-async.git',
branch: 'devise-4.x'
gem 'mini_magick'
gem 'paperclip', '~> 6.1' # File attachment, image attachment library
gem 'rufus-scheduler', '~> 3.5'
gem 'discard', '~> 1.0'

View file

@ -283,7 +283,7 @@ GEM
json_matchers (0.11.0)
json_schema
json_schema (0.20.6)
jsonapi-renderer (0.2.1)
jsonapi-renderer (0.2.0)
jwt (1.5.6)
kaminari (1.1.1)
activesupport (>= 4.1.0)
@ -558,7 +558,7 @@ GEM
websocket-extensions (0.1.4)
whacamole (1.2.0)
wicked_pdf (1.1.0)
wkhtmltopdf-heroku (2.12.5.0)
wkhtmltopdf-heroku (2.12.4.0)
xpath (3.2.0)
nokogiri (~> 1.8)
yomu (0.2.4)
@ -616,6 +616,7 @@ DEPENDENCIES
js_cookie_rails
json-jwt
json_matchers
jsonapi-renderer (= 0.2.0)
jwt (~> 1.5)
kaminari
listen (~> 3.0)
@ -671,7 +672,7 @@ DEPENDENCIES
webpacker (~> 3.5)
whacamole
wicked_pdf (~> 1.1.0)
wkhtmltopdf-heroku
wkhtmltopdf-heroku (= 2.12.4)
yomu
RUBY VERSION

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-2{fill:#505050}</style></defs><path fill="none" d="M0 0h24v24H0z" id="Trim_Area" data-name="Trim Area"/><g id="Icons"><path class="cls-2" d="M12 2.56l8 4.62v9.64l-8 4.62-8-4.62V7.18zm0-.95L3 6.8v10.4l9 5.19 9-5.19V6.8l-9-5.19z"/><path class="cls-2" d="M5.28 9.52l8.21-4.74-1-.58-7.21 4.16v1.16zM18.51 16V7.67l-1-.57v9.48l1-.58zm-13.23-.95v1.15l6.44 3.72 1-.58-7.44-4.29z"/></g></svg>

After

Width:  |  Height:  |  Size: 461 B

View file

@ -42,6 +42,7 @@
//= require shared/inline_editing
//= require activestorage
//= require turbolinks
//= require marvinjslauncher
// Initialize links for submitting forms. This is useful for submitting

View file

@ -0,0 +1,485 @@
/* global TinyMCE I18n animateSpinner importProtocolFromFile truncateLongString globalConstants */
/* global HelperModule */
/* eslint-disable no-use-before-define, no-alert, no-restricted-globals, no-underscore-dangle */
//= require my_modules
//= require protocols/import_export/import
// Currently selected row in "load from protocol" modal
var selectedRow = null;
function initEditMyModuleDescription() {
$('#my_module_description_view').on('click', function() {
TinyMCE.init('#my_module_description_textarea');
});
}
function initEditProtocolDescription() {
$('#protocol_description_view').on('click', function() {
TinyMCE.init('#protocol_description_textarea', refreshProtocolStatusBar);
});
}
// Initialize edit description modal window
function initEditDescription() {
var editDescriptionModal = $('#manage-module-description-modal');
var editDescriptionModalBody = editDescriptionModal.find('.modal-body');
$('.description-link')
.on('ajax:success', function(ev, data) {
var descriptionLink = $('.description-refresh');
// Set modal body & title
editDescriptionModalBody.html(data.html);
editDescriptionModal
.find('#manage-module-description-modal-label')
.text(data.title);
editDescriptionModalBody.find('form')
.on('ajax:success', function(ev2, data2) {
// Update module's description in the tab
descriptionLink.html(data2.description_label);
// Close modal
editDescriptionModal.modal('hide');
})
.on('ajax:error', function(ev2, data2) {
// Display errors if needed
$(this).renderFormErrors('my_module', data2.responseJSON);
});
// Show modal
editDescriptionModal.modal('show');
});
editDescriptionModal.on('hidden.bs.modal', function() {
editDescriptionModalBody.find('form').off('ajax:success ajax:error');
editDescriptionModalBody.html('');
});
}
function initCopyToRepository() {
var link = $("[data-action='copy-to-repository']");
var modal = $('#copy-to-repository-modal');
var modalBody = modal.find('.modal-body');
var submitBtn = modal.find(".modal-footer [data-action='submit']");
link
.on('ajax:success', function(e, data) {
modalBody.html(data.html);
modalBody.find("[data-role='copy-to-repository']")
.on('ajax:success', function(e2, data2) {
if (data2.refresh !== null) {
// Reload page
location.reload();
} else {
// Simply hide the modal
modal.modal('hide');
}
})
.on('ajax:error', function(e2, data2) {
// Display errors in form
submitBtn[0].disabled = false;
if (data2.status === 422) {
$(this).renderFormErrors('protocol', data2.responseJSON);
} else {
// Simply display global error
alert(data2.responseJSON.message);
}
});
modal.modal('show');
submitBtn[0].disabled = false;
})
.on('ajax:error', function() {});
submitBtn.on('click', function() {
// Submit the embedded form
submitBtn[0].disabled = true;
modalBody.find('form').submit();
});
modalBody.on('click', "[data-role='link-check']", function() {
var text = $(this).closest('.modal-body').find("[data-role='link-text']");
if ($(this).prop('checked')) {
text.show();
} else {
text.hide();
}
});
modal.on('hidden.bs.modal', function() {
modalBody.find("[data-role='copy-to-repository']")
.off('ajax:success ajax:error');
modalBody.html('');
});
}
function initLinkUpdate() {
var modal = $('#confirm-link-update-modal');
var modalTitle = modal.find('.modal-title');
var modalBody = modal.find('.modal-body');
var updateBtn = modal.find(".modal-footer [data-action='submit']");
$("[data-action='unlink'], [data-action='revert'], [data-action='update-parent'], [data-action='update-self']")
.on('ajax:success', function(e, data) {
modalTitle.html(data.title);
modalBody.html(data.message);
updateBtn.text(data.btn_text);
modal.attr('data-url', data.url);
modal.modal('show');
});
modal.on('hidden.bs.modal', function() {
modalBody.html('');
});
if (!$._data(updateBtn[0], 'events')) {
updateBtn.on('click', function() {
// POST via ajax
$.ajax({
url: modal.attr('data-url'),
type: 'POST',
dataType: 'json',
success: function() {
// Simply reload page
location.reload();
},
error: function(ev) {
// Display error message in alert()
alert(ev.responseJSON.message);
// Hide modal
modal.modal('hide');
}
});
});
}
$('[data-role="protocol-status-bar"] .preview-protocol').click(function(e) {
e.preventDefault();
});
}
function initLoadFromRepository() {
var modal = $('#load-from-repository-modal');
var modalBody = modal.find('.modal-body');
var loadBtn = modal.find(".modal-footer [data-action='submit']");
$("[data-action='load-from-repository']")
.on('ajax:success', function(e, data) {
modalBody.html(data.html);
// Disable load btn
loadBtn.attr('disabled', 'disabled');
modal.modal('show');
// Init Datatable on public tab
initLoadFromRepositoryTable(modalBody.find('#public-tab'));
modalBody.find("a[data-toggle='tab']")
.on('hide.bs.tab', function(el) {
// Destroy Handsontable in to-be-hidden tab
var content = $($(el.target).attr('href'));
destroyLoadFromRepositoryTable(content);
})
.on('shown.bs.tab', function(el) {
// Initialize Handsontable in to-be-shown tab
var content = $($(el.target).attr('href'));
initLoadFromRepositoryTable(content);
});
loadBtn.on('click', function() {
loadFromRepository();
});
});
modal.on('hidden.bs.modal', function() {
// Destroy the current Datatable
destroyLoadFromRepositoryTable(modalBody.find('.tab-pane.active'));
modalBody.find("a[data-toggle='tab']")
.off('hide.bs.tab shown.bs.tab');
loadBtn.off('click');
modalBody.html('');
});
}
function initLoadFromRepositoryTable(content) {
var tableEl = content.find("[data-role='datatable']");
var datatable = tableEl.DataTable({
order: [[1, 'asc']],
dom: 'RBfltpi',
buttons: [],
processing: true,
serverSide: true,
responsive: true,
ajax: {
url: tableEl.data('source'),
type: 'POST'
},
colReorder: {
fixedColumnsLeft: 1000000 // Disable reordering
},
columnDefs: [{
targets: 0,
searchable: false,
orderable: false,
sWidth: '1%',
render: function() {
return "<input type='radio'>";
}
}, {
targets: [1, 2, 3, 4, 5, 6],
searchable: true,
orderable: true
}],
columns: [
{ data: '0' },
{ data: '1' },
{ data: '2' },
{ data: '3' },
{ data: '4' },
{ data: '5' },
{ data: '6' }
],
oLanguage: {
sSearch: I18n.t('general.filter')
},
rowCallback: function(row, data) {
// Get row ID
var rowId = data.DT_RowId;
$(row).attr('data-row-id', rowId);
// If row ID is in the list of selected row IDs
if (rowId === selectedRow) {
$(row).find("input[type='radio']").prop('checked', true);
$(row).addClass('selected');
}
},
fnDrawCallback: function() {
animateSpinner(this, false);
},
preDrawCallback: function() {
animateSpinner(this);
}
});
// Handle click on table cells with radio buttons
tableEl.find('tbody').on('click', 'td', function() {
$(this).parent().find("input[type='radio']").trigger('click');
});
// Handle clicks on radio buttons
tableEl.find('tbody').on('click', "input[type='radio']", function(e) {
// Get row ID
var row = $(this).closest('tr');
var data = datatable.row(row).data();
var rowId = data.DT_RowId;
// Uncheck all radio buttons
tableEl.find("tbody input[type='radio']")
.prop('checked', false)
.closest('tr')
.removeClass('selected');
// Select the current row
row.find("input[type='radio']").prop('checked', true);
selectedRow = rowId;
row.addClass('selected');
// Enable load btn
content.closest('.modal')
.find(".modal-footer [data-action='submit']")
.removeAttr('disabled');
e.stopPropagation();
});
tableEl.find('tbody').on('click', "a[data-action='filter']", function(e) {
var link = $(this);
var query = link.attr('data-param');
// Re-search data
datatable.search(query).draw();
// Don't propagate this further up
e.stopPropagation();
return false;
});
}
function destroyLoadFromRepositoryTable(content) {
var tableEl = content.find("[data-role='datatable']");
// Unbind event listeners
tableEl.find('tbody').off('click', "a[data-action='filter']");
tableEl.find('tbody').off('click', "input[type='radio']");
tableEl.find('tbody').off('click', 'td');
// Destroy datatable
tableEl.DataTable().destroy();
tableEl.find('tbody').html('');
selectedRow = null;
// Disable load btn
content.closest('.modal')
.find(".modal-footer [data-action='submit']")
.attr('disabled', 'disabled');
}
function loadFromRepository() {
var modal = $('#load-from-repository-modal');
var checkLinked = $("[data-role='protocol-status-bar']")
.text();
var confirmMessage = '';
if (checkLinked.trim() !== '(unlinked)') {
confirmMessage = I18n.t('my_modules.protocols.load_from_repository_modal.import_to_linked_task_rep');
} else {
confirmMessage = I18n.t('my_modules.protocols.load_from_repository_modal.confirm_message');
}
if (selectedRow !== null && confirm(confirmMessage)) {
// POST via ajax
$.ajax({
url: modal.attr('data-url'),
type: 'POST',
dataType: 'json',
data: { source_id: selectedRow },
success: function() {
// Simply reload page
location.reload();
},
error: function(ev) {
// Display error message in alert()
alert(ev.responseJSON.message);
// Hide modal
modal.modal('hide');
}
});
}
}
function refreshProtocolStatusBar() {
// Get the status bar URL
var url = $("[data-role='protocol-status-bar-url']").attr('data-url');
// Fetch new updated at label
$.ajax({
url: url,
type: 'GET',
dataType: 'json',
success: function(data) {
$("[data-role='protocol-status-bar']").html(data.html);
initLinkUpdate();
}
});
}
function initImport() {
var fileInput = $("[data-action='load-from-file']");
// Make sure multiple selections of same file
// always prompt new modal
fileInput.find("input[type='file']").on('click', function() {
this.value = null;
});
// Hack to hide "No file chosen" tooltip
fileInput.attr('title', window.URL ? ' ' : '');
fileInput.on('change', function(ev) {
var importUrl = fileInput.attr('data-import-url');
importProtocolFromFile(
ev.target.files[0],
importUrl,
null,
true,
function(datas) {
var data = datas[0];
if (data.status === 'ok') {
// Simply reload page
location.reload();
} else if (data.status === 'locked') {
alert(I18n.t('my_modules.protocols.load_from_file_error_locked'));
} else {
if (data.status === 'size_too_large') {
alert(I18n.t('my_modules.protocols.load_from_file_size_error',
{ size: $(document.body).data('file-max-size-mb') }));
} else {
alert(I18n.t('my_modules.protocols.load_from_file_error'));
}
animateSpinner(null, false);
}
}
);
// Clear input on self
$(this).val('');
});
}
function initRecentProtocols() {
var recentProtocolContainer = $('.my-module-recent-protocols');
var dropDownList = recentProtocolContainer.find('.dropdown-menu');
recentProtocolContainer.find('.dropdown-button').click(function() {
dropDownList.find('.protocol').remove();
$.get('/protocols/recent_protocols', result => {
$.each(result, (i, protocol) => {
$('<div class="protocol"><i class="fas fa-file-alt"></i>'
+ truncateLongString(protocol.name, globalConstants.name_truncation_length)
+ '</div>').appendTo(dropDownList)
.click(() => {
$.post(recentProtocolContainer.data('updateUrl'), { source_id: protocol.id })
.success(() => {
location.reload();
})
.error(ev => {
HelperModule.flashAlertMsg(ev.responseJSON.message, 'warning');
});
});
});
});
});
// We use here ajax:success, because we want to check any change on this page
$(document).on('ajax:success', () => {
updateRecentProtocolsStatus();
});
}
function updateRecentProtocolsStatus() {
var recentProtocolContainer = $('.my-module-recent-protocols');
var steps = $('.step');
var protocolDescription = $('#protocol_description_view').html();
if (steps.length === 0 && protocolDescription.length === 0) {
recentProtocolContainer.css('display', '');
} else {
recentProtocolContainer.css('display', 'none');
}
}
/**
* Initializes page
*/
function init() {
initEditMyModuleDescription();
initEditProtocolDescription();
initEditDescription();
initCopyToRepository();
initLinkUpdate();
initLoadFromRepository();
refreshProtocolStatusBar();
initImport();
initRecentProtocols();
}
init();

View file

@ -500,7 +500,14 @@ function importProtocolFromFile(
var tinyMceAsset = {};
var fileRef = $(this).attr('fileRef');
tinyMceAsset.tokenId = $(this).attr('tokenId');
tinyMceAsset.fileName = $(this).children('fileName').text();
tinyMceAsset.fileType = $(this).children('fileType').text();
if ($(this).children('fileMetadata').html() !== undefined) {
tinyMceAsset.fileMetadata = $(this).children('fileMetadata').html()
.replace('<!--[CDATA[', '')
.replace(' ]]-->', '')
.replace(']]&gt;', '');
}
tinyMceAsset.bytes = getAssetBytes(
protocolFolders[index],
stepGuid,
@ -579,6 +586,12 @@ function importProtocolFromFile(
stepAssetJson.id = assetId;
stepAssetJson.fileName = fileName;
stepAssetJson.fileType = $(this).children('fileType').text();
if ($(this).children('fileMetadata').html() !== undefined) {
stepAssetJson.fileMetadata = $(this).children('fileMetadata').html()
.replace('<!--[CDATA[', '')
.replace(' ]]-->', '')
.replace(']]&gt;', '');
}
stepAssetJson.bytes = getAssetBytes(
protocolFolders[index],
stepGuid,

View file

@ -122,6 +122,7 @@
SmartAnnotation.preventPropagation('.atwho-user-popover');
TinyMCE.destroyAll();
DragNDropSteps.clearFiles();
MarvinJsEditor.initNewButton('.new-marvinjs-upload-button');
}, 1000);
})
@ -374,7 +375,7 @@
}
function initCallBacks() {
applyCreateWopiFileCallback();
applyCreateWopiFileCallback()
applyCheckboxCallBack();
applyStepCompletedCallBack();
applyEditCallBack();
@ -502,10 +503,9 @@
$('#new-step-main-tab a')
.on('shown.bs.tab', function() {
$('#step_name').focus();
TinyMCE.init('#step_description_textarea');
}).on('hidden.bs.tab', function() {
tinyMCE.editors.step_description_textarea.remove();
});
TinyMCE.init('#step_description_textarea');
})
TinyMCE.init('#step_description_textarea');
});
@ -536,7 +536,7 @@
var nameValid = textValidator(ev, $nameInput, 1,
<%= Constants::NAME_MAX_LENGTH %>);
var $descrTextarea = $form.find("#step_description_textarea");
var $tinyMCEInput = TinyMCE.getContent();
var $tinyMCEInput = tinyMCE.editors.step_description_textarea.getContent();
var descriptionValid = textValidator(ev, $descrTextarea, 0,
<%= Constants::RICH_TEXT_MAX_LENGTH %>, false, $tinyMCEInput);
var tableNamesValidArray = [];
@ -601,6 +601,7 @@
SmartAnnotation.preventPropagation('.atwho-user-popover');
tinyMCE.editors.step_description_textarea.remove();
MarvinJsEditor.initNewButton('.new-marvinjs-upload-button');
//Rerender tables
$new_step.find("div.step-result-hot-table").each(function() {
@ -611,6 +612,7 @@
FilePreviewModal.init();
$.initTooltips();
if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar();
if (typeof updateRecentProtocolsStatus === 'function') updateRecentProtocolsStatus();
},
error: function(xhr) {
if (xhr.responseJSON['assets.file']) {
@ -665,13 +667,20 @@
}
// Reorder attachments
global.reorderAttachments = function reorderAtt(stepId, sortType) {
global.reorderAttachments = function reorderAtt(elem, stepId, sortType) {
var label_value = $("#dd-att-step-" + stepId + "> .dropdown-menu > li > a[data-order=" + sortType + "]").html();
$("#dd-att-step-" + stepId + "-label").html(label_value);
$('#att-' + stepId + ' a.file-preview-link').each(function(){
var elm = $(this)
elm.parent().css('order', elm.attr('data-order-' + sortType));
})
});
$.post(
$(elem).closest('.dropdown-menu').data('stateSavePath'),
{ assets: { order: sortType } },
null,
'json',
);
}
// On init

View file

@ -45,7 +45,7 @@ function initializeHandsonTable(el) {
formulas: true
});
el.handsontable("getInstance").loadData(data);
el.handsontable("getInstance").sort(3, order);
el.handsontable('getInstance').getPlugin('columnSorting').sort(3, order);
// "Hack" to disable user sorting rows by clicking on
// header elements

View file

@ -85,6 +85,7 @@ function initInlineEditing(title) {
if (inputString.disabled) {
saveAllEditFields();
editBlock.dataset.editMode = 1;
$editBlock.closest('.inline_scroll_block').scrollTop(editBlock.offsetTop);
inputString.disabled = false;
$inputString.removeClass('hidden');
$editBlock.find('.view-mode').addClass('hidden');

View file

@ -2,8 +2,14 @@
/* eslint-disable no-restricted-globals, no-alert */
var Comments = (function() {
function changeCounter(comment, value) {
var currnetCount = $('#comment-counter-' + comment.closest('.comments-container').attr('data-object-id'));
currnetCount.html(parseInt(currnetCount.html(), 10) + value);
var currentCount = $('#comment-counter-' + comment.closest('.comments-container').attr('data-object-id'));
var newValue = parseInt(currentCount.html(), 10) + value;
currentCount.html(newValue);
if (newValue === 0) {
currentCount.addClass('hidden');
} else {
currentCount.removeClass('hidden');
}
}
function scrollBottom(container) {
@ -76,6 +82,7 @@ var Comments = (function() {
$el.find('#message').val('');
$el.find('.new-comment-button').removeClass('show');
newButton.disable = false;
$el.find('textarea').focus().blur();
})
.error((error) => {
errorField.html(error.responseJSON.errors.message);

View file

@ -1,12 +1,17 @@
/* eslint no-underscore-dangle: ["error", { "allowAfterThis": true }]*/
/* eslint no-use-before-define: ["error", { "functions": false }]*/
/* eslint-disable no-underscore-dangle */
/* global Uint8Array fabric tui animateSpinner I18n PerfectScrollbar*/
/* global Uint8Array fabric tui animateSpinner Assets
I18n PerfectScrollbar MarvinJsEditor refreshProtocolStatusBar */
var FilePreviewModal = (function() {
'use strict';
var readOnly = false;
var CHECK_READY_DELAY = 5000;
var CHECK_READY_TRIES_LIMIT = 60;
var checkReadyCntr;
function initPreviewModal(options = {}) {
var name;
@ -17,10 +22,11 @@ var FilePreviewModal = (function() {
$('.file-preview-link').off('click');
$('.file-preview-link').click(function(e) {
e.preventDefault();
name = $(this).find('p').text();
name = $(this).find('.attachment-label').text();
url = $(this).data('preview-url');
downloadUrl = $(this).attr('href');
openPreviewModal(name, url, downloadUrl);
return true;
});
}
@ -402,8 +408,33 @@ var FilePreviewModal = (function() {
imageEditor = {};
$('#tui-image-editor').html('');
$('#fileEditModal').modal('hide');
if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar();
});
if (data.mode === 'tinymce') {
$.ajax({
type: 'PUT',
url: data.url,
data: dataUpload,
contentType: false,
processData: false,
success: function(res) {
data.image.src = res.url;
}
}).done(function() { closeEditor(); });
} else {
$.ajax({
type: 'POST',
url: '/files/' + data.id + '/update_image',
data: dataUpload,
contentType: false,
processData: false,
success: function(res) {
$('#modal_link' + data.id).parent().html(res.html);
Assets.setupAssetsLoading();
}
}).done(function() { closeEditor(); });
}
if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar();
});
window.onresize = function() {
@ -420,8 +451,7 @@ var FilePreviewModal = (function() {
dataType: 'json',
success: function(data) {
var link = modal.find('.file-download-link');
modal.find('.file-preview-container').empty();
modal.find('.file-wopi-controls').empty();
clearPrevieModal();
if (Object.prototype.hasOwnProperty.call(data, 'wopi-controls')) {
modal.find('.file-wopi-controls').html(data['wopi-controls']);
}
@ -443,6 +473,7 @@ var FilePreviewModal = (function() {
if (!readOnly && data.editable) {
modal.find('.file-edit-link').css('display', '');
modal.find('.file-edit-link').off().click(function(ev) {
$.post('/files/' + data.id + '/start_edit_image');
ev.preventDefault();
ev.stopPropagation();
modal.modal('hide');
@ -452,6 +483,8 @@ var FilePreviewModal = (function() {
modal.find('.file-edit-link').css('display', 'none');
}
}
} else if (data.type === 'marvinjs') {
openMarvinEditModal(data, modal);
} else {
modal.find('.file-edit-link').css('display', 'none');
modal.find('.file-preview-container').html(data['preview-icon']);
@ -460,10 +493,13 @@ var FilePreviewModal = (function() {
modal.find('#wopi_file_edit_button').remove();
}
if (data.processing) {
checkFileReady(url, modal);
setTimeout(function() {
checkFileReady(url, modal);
}, CHECK_READY_DELAY);
}
modal.find('.file-name').text(name);
modal.find('.preview-close').click(function() {
checkReadyCntr = CHECK_READY_TRIES_LIMIT;
modal.modal('hide');
if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar();
});
@ -472,6 +508,7 @@ var FilePreviewModal = (function() {
ev.preventDefault();
});
$('.modal-backdrop').last().css('z-index', modal.css('z-index') - 1);
checkReadyCntr = 0;
},
error: function() {
// TODO
@ -492,9 +529,11 @@ var FilePreviewModal = (function() {
ev.preventDefault();
ev.stopPropagation();
});
setTimeout(function() {
checkFileReady(url, modal);
}, 10000);
if (checkReadyCntr < CHECK_READY_TRIES_LIMIT) {
setTimeout(function() {
checkFileReady(url, modal);
}, CHECK_READY_DELAY);
}
} else {
if (data.type === 'image') {
modal.find('.file-preview-container').empty();
@ -518,9 +557,45 @@ var FilePreviewModal = (function() {
.off();
}
});
checkReadyCntr += 1;
}
function clearPrevieModal() {
var modal = $('#filePreviewModal');
modal.find('.file-preview-container').empty();
modal.find('.file-wopi-controls').empty();
modal.find('.file-edit-link').css('display', 'none');
}
function openMarvinEditModal(data, modal) {
modal.find('.file-preview-container')
.append($('<img>')
.attr('src', data['large-preview-url'])
.attr('alt', data.name)
.click(function(ev) {
ev.stopPropagation();
}));
if (!readOnly && data.editable) {
modal.find('.file-edit-link').css('display', '');
modal.find('.file-edit-link').off().click(function(ev) {
ev.preventDefault();
ev.stopPropagation();
modal.modal('hide');
MarvinJsEditor.open({
mode: 'edit',
data: data.description,
name: data.name,
marvinUrl: data['update-url']
});
});
} else {
modal.find('.file-edit-link').css('display', 'none');
}
}
return Object.freeze({
init: initPreviewModal
init: initPreviewModal,
imageEditor: initImageEditor
});
}(window));

View file

@ -0,0 +1,318 @@
/* global TinyMCE, ChemicalizeMarvinJs, MarvinJSUtil, I18n, FilePreviewModal, tinymce */
/* global Results, Comments */
/* eslint-disable no-param-reassign */
/* eslint-disable wrap-iife */
/* eslint-disable no-use-before-define */
var marvinJsRemoteLastMrv;
var marvinJsRemoteEditor;
var MarvinJsEditor;
var MarvinJsEditorApi = (function() {
var marvinJsModal = $('#MarvinJsModal');
var marvinJsContainer = $('#marvinjs-editor');
var marvinJsObject = $('#marvinjs-sketch');
var emptySketch = '<cml><MDocument></MDocument></cml>';
var sketchName = marvinJsModal.find('.file-name input');
var marvinJsMode = marvinJsContainer.data('marvinjsMode');
// Facade api actions
var marvinJsExportImage = (childFuction, options = {}) => {
if (marvinJsMode === 'remote') {
remoteExportImage(childFuction, options);
} else {
localExportImage(childFuction, options);
}
};
// ///////////////
var loadEditor = () => {
if (marvinJsMode === 'remote') {
return marvinJsRemoteEditor;
}
return MarvinJSUtil.getEditor('#marvinjs-sketch');
};
var loadPackages = () => {
return MarvinJSUtil.getPackage('#marvinjs-sketch');
};
// Local marvinJS installation
var localExportImage = (childFuction, options = {}) => {
loadEditor().then(function(sketcherInstance) {
sketcherInstance.exportStructure('mrv').then(function(source) {
loadPackages().then(function(sketcherPackage) {
sketcherPackage.onReady(function() {
var exporter = createExporter(sketcherPackage, 'image/jpeg');
exporter.render(source).then(function(image) {
childFuction(source, image, options);
});
});
});
});
});
};
// Web services installation
var remoteImage = (childFuction, source, options = {}) => {
var params = {
carbonLabelVisible: false,
implicitHydrogen: 'TERMINAL_AND_HETERO',
displayMode: 'WIREFRAME',
width: 900,
height: 900
};
if (typeof (marvinJsRemoteEditor) === 'undefined') {
setTimeout(() => { remoteImage(childFuction, source, options); }, 100);
return false;
}
marvinJsRemoteEditor.exportMrvToImageDataUri(source, 'image/jpeg', params).then(function(image) {
childFuction(source, image, options);
});
return true;
};
var remoteExportImage = (childFuction, options = {}) => {
remoteImage(childFuction, marvinJsRemoteLastMrv, options);
};
// Support actions
function preloadActions(config) {
if (marvinJsMode === 'remote') {
if (config.mode === 'new' || config.mode === 'new-tinymce') {
marvinJsRemoteEditor.importStructure('mrv', emptySketch);
sketchName.val('');
} else if (config.mode === 'edit') {
marvinJsRemoteLastMrv = config.data;
marvinJsRemoteEditor.importStructure('mrv', config.data);
sketchName.val(config.name);
} else if (config.mode === 'edit-tinymce') {
marvinJsRemoteLastMrv = config.data;
$.get(config.marvinUrl, { object_type: 'TinyMceAsset' }, function(result) {
marvinJsRemoteEditor.importStructure('mrv', result.description);
sketchName.val(result.name);
});
}
marvinJsRemoteEditor.on('molchange', () => {
marvinJsRemoteEditor.exportStructure('mrv').then(function(source) {
marvinJsRemoteLastMrv = source;
});
});
} else {
loadEditor().then(function(marvin) {
if (config.mode === 'new' || config.mode === 'new-tinymce') {
marvin.importStructure('mrv', emptySketch);
sketchName.val(I18n.t('marvinjs.new_sketch'));
} else if (config.mode === 'edit') {
marvin.importStructure('mrv', config.data);
sketchName.val(config.name);
} else if (config.mode === 'edit-tinymce') {
$.get(config.marvinUrl, function(result) {
marvin.importStructure('mrv', result.description);
sketchName.val(result.name);
});
}
});
}
}
function createExporter(marvin, imageType) {
var inputFormat = 'mrv';
var settings = {
width: 900,
height: 900
};
var params = {
imageType: imageType,
settings: settings,
inputFormat: inputFormat
};
return new marvin.ImageExporter(params);
}
function TinyMceBuildHTML(json) {
var imgstr = "<img src='" + json.image.url + "'";
imgstr += " width='300' height='300'";
imgstr += " data-mce-token='" + json.image.token + "'";
imgstr += " data-source-type='" + json.image.source_type + "'";
imgstr += " alt='description-" + json.image.token + "' />";
return imgstr;
}
function saveFunction(source, image, config) {
$.post(config.marvinUrl, {
description: source,
object_id: config.objectId,
object_type: config.objectType,
name: sketchName.val(),
image: image
}, function(result) {
var newAsset = $(result.html);
var json;
if (config.objectType === 'Step') {
newAsset.find('.file-preview-link').css('top', '-300px');
newAsset.addClass('new').prependTo($(config.container));
setTimeout(function() {
newAsset.find('.file-preview-link').css('top', '0px');
}, 200);
} else if (config.objectType === 'Result') {
newAsset.prependTo($(config.container));
Results.expandResult(newAsset);
Comments.init();
} else if (config.objectType === 'TinyMceAsset') {
json = tinymce.util.JSON.parse(result);
config.editor.execCommand('mceInsertContent', false, TinyMceBuildHTML(json));
TinyMCE.updateImages(config.editor);
}
$(marvinJsModal).modal('hide');
FilePreviewModal.init();
});
}
function updateFunction(source, image, config) {
$.ajax({
url: config.marvinUrl,
data: {
description: source,
name: sketchName.val(),
object_type: config.objectType,
image: image
},
dataType: 'json',
type: 'PUT',
success: function(json) {
if (config.objectType === 'TinyMceAsset') {
config.image[0].src = json.url;
config.image[0].dataset.mceSrc = json.url;
$(marvinJsModal).modal('hide');
} else {
$(marvinJsModal).modal('hide');
$('#modal_link' + json.id + ' img').attr('src', json.url);
$('#modal_link' + json.id + ' .attachment-label').html(json.file_name);
}
}
});
}
// MarvinJS Methods
return {
enabled: function() {
return ($('#MarvinJsModal').length > 0);
},
open: function(config) {
if (!MarvinJsEditor.enabled()) {
$('#MarvinJsPromoModal').modal('show');
return false;
}
if (marvinJsMode === 'remote' && typeof (marvinJsRemoteEditor) === 'undefined') {
setTimeout(() => { MarvinJsEditor.open(config); }, 100);
return false;
}
preloadActions(config);
$(marvinJsModal).modal('show');
$(marvinJsObject)
.css('width', marvinJsContainer.width() + 'px')
.css('height', marvinJsContainer.height() + 'px');
marvinJsModal.find('.file-save-link').off('click').on('click', () => {
if (config.mode === 'new') {
MarvinJsEditor.save(config);
} else if (config.mode === 'edit') {
config.objectType = 'Asset';
MarvinJsEditor.update(config);
} else if (config.mode === 'new-tinymce') {
config.objectType = 'TinyMceAsset';
MarvinJsEditor.save(config);
} else if (config.mode === 'edit-tinymce') {
config.objectType = 'TinyMceAsset';
MarvinJsEditor.update(config);
}
});
return true;
},
initNewButton: function(selector) {
$(selector).off('click').on('click', function() {
var objectId = this.dataset.objectId;
var objectType = this.dataset.objectType;
var marvinUrl = this.dataset.marvinUrl;
var container = this.dataset.sketchContainer;
MarvinJsEditor.open({
mode: 'new',
objectId: objectId,
objectType: objectType,
marvinUrl: marvinUrl,
container: container
});
});
},
save: function(config) {
marvinJsExportImage(saveFunction, config);
},
update: function(config) {
marvinJsExportImage(updateFunction, config);
}
};
});
// TinyMCE plugin
(function() {
'use strict';
tinymce.PluginManager.requireLangPack('MarvinJsPlugin');
tinymce.create('tinymce.plugins.MarvinJsPlugin', {
MarvinJsPlugin: function(ed) {
var editor = ed;
function openMarvinJs() {
MarvinJsEditor.open({
mode: 'new-tinymce',
marvinUrl: '/tiny_mce_assets/marvinjs',
editor: editor
});
}
// Add a button that opens a window
editor.addButton('marvinjsplugin', {
tooltip: I18n.t('marvinjs.new_button'),
icon: 'marvinjs',
onclick: openMarvinJs
});
// Adds a menu item to the tools menu
editor.addMenuItem('marvinjsplugin', {
text: I18n.t('marvinjs.new_button'),
icon: 'marvinjs',
context: 'insert',
onclick: openMarvinJs
});
}
});
tinymce.PluginManager.add(
'marvinjsplugin',
tinymce.plugins.MarvinJsPlugin
);
})();
// Initialization
$(document).on('turbolinks:load', function() {
MarvinJsEditor = MarvinJsEditorApi();
if (MarvinJsEditor.enabled()) {
if ($('#marvinjs-editor')[0].dataset.marvinjsMode === 'remote') {
ChemicalizeMarvinJs.createEditor('#marvinjs-sketch').then(function(marvin) {
marvinJsRemoteEditor = marvin;
});
}
}
MarvinJsEditor.initNewButton('.new-marvinjs-upload-button');
});

View file

@ -88,7 +88,7 @@
form = createElement('form', {
action: editor.getParam(
'customimageuploader_form_url',
'/tinymce_assets'
'/tiny_mce_assets'
),
target: iframe._id,
method: 'POST',

View file

@ -23,5 +23,6 @@
@import "select2.min";
@import "extend/perfect-scrollbar";
@import "my_modules/protocols/*";
@import "my_modules/results/*";
@import "protocols/*";
@import "hooks/*";

View file

@ -43,3 +43,22 @@
.mce-top-part {
z-index: 5;
}
.mce-widget.mce-btn[aria-label="Restore last draft"] {
background: $brand-primary;
border-radius: 4px;
transition: $md-transaction;
i {
color: $color-white;
transition: $md-transaction;
}
&.mce-disabled {
background: transparent;
i {
color: inherit;
}
}
}

View file

@ -0,0 +1,166 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
// scss-lint:disable SelectorFormat
// scss-lint:disable ImportantRule
// scss-lint:disable IdSelector
@import "constants";
@import "mixins";
// MarvinJs modal
.modal-marvin-js {
background: transparent;
font-size: $font-size-large;
padding: 0 !important;
.preview-close {
background: transparent;
border: 0;
color: $color-white;
display: inline-block;
float: right;
}
.modal-dialog {
height: 100%;
margin: 0;
padding: 0;
width: auto;
}
.modal-content {
background: transparent;
border: 0;
box-shadow: none;
color: $color-white;
height: 100%;
width: auto;
}
.modal-header {
background: $color-black;
border: 0;
height: 60px;
line-height: 40px;
padding: 10px 15px;
text-align: center;
.file-save-link {
margin: 0 20px 0 0;
}
.file-name {
align-items: center;
display: flex;
float: left;
input {
border-radius: 5px;
box-shadow: none;
color: $color-black;
height: 40px;
margin-left: 5px;
outline: 0;
padding: 5px 10px;
position: relative;
width: 350px;
}
}
}
.modal-body {
height: calc(100% - 60px);
padding: 0;
#marvinjs-editor {
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
#marvinjs-sketch {
border-right: 1px solid $color-gainsboro;
float: left;
min-height: 450px;
min-width: 500px;
overflow: hidden;
}
}
.sketch-container {
@include md-card-style;
cursor: pointer;
margin: 10px;
overflow: hidden;
padding: 10px;
position: relative;
.sketch-image {
height: 100%;
width: 100%;
}
.sketch-name {
color: $brand-primary;
font-family: Lato;
font-size: 16px;
line-height: 18px;
margin: 10px auto;
overflow: hidden;
text-align: center;
width: 160px;
}
.sketch-object {
color: $color-emperor;
font-size: 12px;
opacity: .6;
text-align: center;
}
}
}
.file-save-link {
color: $color-white;
cursor: pointer;
display: inline-block;
float: right;
margin-right: 20px;
}
}
#new-step-sketch {
.sketch-container {
display: grid;
float: left;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
width: 100%;
}
}
.new-marvinjs-upload-button {
.new-marvinjs-upload-icon {
display: inline-block;
height: 22px;
width: 22px;
img {
height: 100%;
width: 100%;
}
}
}
.mce-i-marvinjs::before {
background-image: url("icon_small/marvinjs.svg");
content: "";
display: block;
height: 22px;
left: -3px;
line-height: 16px;
position: relative;
top: -3px;
width: 22px;
}

View file

@ -239,3 +239,49 @@
border-left: 0;
border-radius: 0 5px 5px 0;
}
.my-module-recent-protocols {
flex-grow: 1;
margin-bottom: 5px;
position: relative;
.btn-group {
align-items: center;
display: flex;
float: right;
height: 33px;
}
.title {
font-size: 14px;
}
.dropdown-button {
cursor: pointer;
padding: 10px 5px;
}
.dropdown-menu {
left: auto;
padding: 0;
right: 0;
width: 402px;
.protocol {
cursor: pointer;
display: inline-block;
float: left;
padding: 5px 10px;
transition: $md-transaction;
width: 200px;
&:hover {
background: $color-gainsboro;
}
.fas {
margin-right: 5px;
}
}
}
}

View file

@ -0,0 +1,17 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
// scss-lint:disable SelectorFormat
// scss-lint:disable ImportantRule
@import "constants";
@import "mixins";
#results-toolbar {
.help_tooltips {
.btn-default {
border: 0;
color: inherit;
margin-left: 10px;
}
}
}

View file

@ -66,10 +66,6 @@ label {
height: auto !important;
width: auto !important;
}
.ht_clone_top,.ht_clone_left,.ht_clone_corner {
display: none !important;
}
}

View file

@ -144,6 +144,21 @@
}
}
// Looks like PDF has some specail CSS rules, here is some hack
&.report {
display: block;
float: left;
width: 100%;
.avatar-placehodler {
float: left;
}
.content-placeholder {
float: left;
}
}
&[data-edit-mode="0"]:hover,
&[data-edit-mode="1"] {
.comment-right {
@ -224,13 +239,16 @@
.new-comment-button {
cursor: pointer;
font-size: 18px;
font-size: 14px;
line-height: 18px;
margin: 8px;
margin: 4px;
padding: 4px;
position: absolute;
right: -36px;
text-align: center;
top: 0;
transition: $md-transaction;
width: 26px;
&.show {
right: 0;

View file

@ -124,6 +124,35 @@
.pseudo-attachment-container {
display: flex;
justify-content: center;
overflow: hidden;
.file-preview-link {
position: relative;
}
&.new {
order: 0 !important;
.file-preview-link {
transition: .5s;
}
.attachment-placeholder {
border: 1px solid $brand-primary;
&::before {
background: $brand-primary;
border-radius: 0 5px;
bottom: 16px;
color: $color-white;
content: "NEW";
left: 8px;
line-height: 20px;
position: absolute;
width: 50px;
}
}
}
}
}
@ -131,10 +160,14 @@
@include md-card-style;
color: $color-silver-chalice;
height: 280px;
margin: 8px;
margin: 4px 8px 16px;
text-align: center;
width: 220px;
.attachment-thumbnail {
width: calc(100% - 20px);
}
a {
color: inherit;
}
@ -183,6 +216,7 @@
.remove-icon {
bottom: 15px;
cursor: pointer;
display: none;
position: relative;
right: 10px;

View file

@ -919,6 +919,7 @@ ul.content-activities {
align-items: center;
display: flex;
flex-wrap: wrap;
margin-bottom: 5px;
.protocol-button {
margin-bottom: 5px;

View file

@ -5,8 +5,11 @@
border: solid 1px;
border-color: $color-white;
border-radius: 3px;
float: left;
margin-bottom: 10px;
min-height: 100px;
padding: 3px;
width: 100%;
&:hover {
border-color: $color-gainsboro;
@ -63,4 +66,50 @@
.mce-toolbar {
background: $color-white !important;
}
.mce-stack-layout {
.tinymce-active-object-handler {
border-top: 1px solid rgb(226, 228, 231);
height: 33px;
width: 100%;
.tool-button {
border: 1px solid transparent;
cursor: pointer;
display: inline-block;
line-height: 27px;
margin: 2px;
text-align: center;
width: 30px;
&:hover {
border: 1px solid rgb(226, 228, 231);
}
}
.mce-i-donwload::before {
content: "\F019";
font-family: "Font Awesome 5 Free";
font-weight: 900;
line-height: 16px;
position: absolute;
}
.mce-i-pencil::before {
content: "\F303";
font-family: "Font Awesome 5 Free";
font-weight: 900;
line-height: 16px;
position: absolute;
}
.mce-i-image::before {
content: "\F03E";
font-family: "Font Awesome 5 Free";
font-weight: 900;
line-height: 16px;
position: absolute;
}
}
}
// scss-lint:enable ImportantRule

View file

@ -1,5 +1,8 @@
# frozen_string_literal: true
class AssetsController < ApplicationController
include WopiUtil
include AssetsActions
# include ActionView::Helpers
include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::TextHelper
@ -10,14 +13,14 @@ class AssetsController < ApplicationController
include FileIconsHelper
before_action :load_vars, except: :create_wopi_file
before_action :check_read_permission
before_action :check_edit_permission, only: :edit
def file_preview
response_json = {
'id' => @asset.id,
'type' => (@asset.image? ? 'image' : 'file'),
'type' => @asset.file.metadata[:asset_type] || (@asset.image? ? 'image' : 'file'),
'filename' => truncate(escape_input(@asset.file_name),
length: Constants::FILENAME_TRUNCATION_LENGTH),
'download-url' => download_asset_path(@asset, timestamp: Time.now.to_i)
@ -30,17 +33,25 @@ class AssetsController < ApplicationController
elsif @assoc.class == RepositoryCell
can_manage_repository_rows?(@repository.team)
end
if @asset.image?
if ['image/jpeg', 'image/pjpeg'].include? @asset.content_type
if response_json['type'] == 'image'
if ['image/jpeg', 'image/pjpeg'].include? @asset.file.content_type
response_json['quality'] = @asset.file_image_quality || 90
end
response_json.merge!(
'editable' => @asset.editable_image? && can_edit,
'mime-type' => @asset.file.content_type,
'large-preview-url' => @asset.large_preview
'large-preview-url' => rails_representation_url(@asset.large_preview)
)
elsif response_json['type'] == 'marvinjs'
response_json.merge!(
'editable' => can_edit,
'large-preview-url' => rails_representation_url(@asset.large_preview),
'update-url' => marvin_js_asset_path(@asset.id),
'description' => @asset.file.metadata[:description],
'name' => @asset.file.metadata[:name]
)
else
response_json['preview-icon'] = render_to_string(partial: 'shared/file_preview_icon.html.erb',
locals: { asset: @asset })
end
@ -115,6 +126,10 @@ class AssetsController < ApplicationController
render layout: false
end
def create_start_edit_image_activity
create_edit_image_activity(@asset, current_user, :start_editing)
end
def update_image
@asset = Asset.find(params[:id])
orig_file_size = @asset.file_size
@ -123,6 +138,7 @@ class AssetsController < ApplicationController
@asset.file.attach(io: params.require(:image), filename: orig_file_name)
@asset.save!
create_edit_image_activity(@asset, current_user, :finish_editing)
# release previous image space
@asset.team.release_space(orig_file_size)
# Post process file here

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
module AssetsActions
extend ActiveSupport::Concern
def create_edit_image_activity(asset, current_user, started_editing)
action = if started_editing == :start_editing
t('activities.file_editing.started')
elsif started_editing == :finish_editing
t('activities.file_editing.finished')
end
if asset.step.class == Step
protocol = asset.step.protocol
default_step_items =
{ step: asset.step.id,
step_position: { id: asset.step.id, value_for: 'position_plus_one' },
asset_name: { id: asset.id, value_for: 'file_file_name' },
action: action }
if protocol.in_module?
project = protocol.my_module.experiment.project
team = project.team
type_of = :edit_image_on_step
message_items = { my_module: protocol.my_module.id }
else
type_of = :edit_image_on_step_in_repository
project = nil
team = protocol.team
message_items = { protocol: protocol.id }
end
message_items = default_step_items.merge(message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: protocol,
team: team,
project: project,
message_items: message_items)
elsif asset.result.class == Result
my_module = asset.result.my_module
Activities::CreateActivityService
.call(activity_type: :edit_image_on_result,
owner: current_user,
subject: asset.result,
team: my_module.experiment.project.team,
project: my_module.experiment.project,
message_items: {
result: asset.result.id,
asset_name: { id: asset.id, value_for: 'file_file_name' },
action: action
})
end
end
end

View file

@ -0,0 +1,100 @@
# frozen_string_literal: true
class MarvinJsAssetsController < ApplicationController
before_action :load_vars, except: :create
before_action :load_create_vars, only: :create
before_action :check_read_permission
before_action :check_edit_permission, only: %i(update create)
def create
result = MarvinJsService.create_sketch(marvin_params, current_user, current_team)
if result[:asset] && marvin_params[:object_type] == 'Step'
render json: {
html: render_to_string(
partial: 'steps/attachments/item.html.erb',
locals: { asset: result[:asset],
i: 0,
assets_count: 0,
step: result[:object],
order_atoz: 0,
order_ztoa: 0 }
)
}
elsif result[:asset] && marvin_params[:object_type] == 'Result'
@my_module = result[:object].my_module
render json: {
html: render_to_string(
partial: 'my_modules/result.html.erb',
locals: { result: result[:object] }
)
}, status: :ok
elsif result[:asset]
render json: result[:asset]
else
render json: result[:asset].errors, status: :unprocessable_entity
end
end
def update
asset = MarvinJsService.update_sketch(marvin_params, current_user, current_team)
if asset
render json: { url: rails_representation_url(asset.medium_preview), id: asset.id, file_name: asset.file_name }
else
render json: { error: t('marvinjs.no_sketches_found') }, status: :unprocessable_entity
end
end
private
def load_vars
@asset = current_team.assets.find_by_id(params[:id])
return render_404 unless @asset
@assoc ||= @asset.step
@assoc ||= @asset.result
if @assoc.class == Step
@protocol = @assoc.protocol
elsif @assoc.class == Result
@my_module = @assoc.my_module
end
end
def load_create_vars
@assoc = Step.find_by_id(marvin_params[:object_id]) if marvin_params[:object_type] == 'Step'
@assoc = MyModule.find_by_id(params[:object_id]) if marvin_params[:object_type] == 'Result'
if @assoc.class == Step
@protocol = @assoc.protocol
elsif @assoc.class == MyModule
@my_module = @assoc
end
end
def check_read_permission
if @assoc.class == Step
return render_403 unless can_read_protocol_in_module?(@protocol) ||
can_read_protocol_in_repository?(@protocol)
elsif @assoc.class == Result || @assoc.class == MyModule
return render_403 unless can_read_experiment?(@my_module.experiment)
else
render_403
end
end
def check_edit_permission
if @assoc.class == Step
return render_403 unless can_manage_protocol_in_module?(@protocol) ||
can_manage_protocol_in_repository?(@protocol)
elsif @assoc.class == Result || @assoc.class == MyModule
return render_403 unless can_manage_module?(@my_module)
else
render_403
end
end
def marvin_params
params.permit(:id, :description, :object_id, :object_type, :name, :image)
end
end

View file

@ -278,6 +278,11 @@ class MyModulesController < ApplicationController
def protocols
@protocol = @my_module.protocol
@recent_protcols_positive = Protocol.recent_protocols(
current_user,
current_team,
Constants::RECENT_PROTOCOL_LIMIT
).any?
current_team_switch(@protocol.team)
end

View file

@ -106,6 +106,14 @@ class ProtocolsController < ApplicationController
end
end
def recent_protocols
render json: Protocol.recent_protocols(
current_user,
current_team,
Constants::RECENT_PROTOCOL_LIMIT
).select(:id, :name)
end
def linked_children
respond_to do |format|
format.json do

View file

@ -3,12 +3,11 @@ class StepsController < ApplicationController
include ApplicationHelper
include StepsActions
before_action :load_vars, only: %i(edit update destroy show toggle_step_state
checklistitem_state)
before_action :load_vars, only: %i(edit update destroy show toggle_step_state checklistitem_state update_view_state)
before_action :load_vars_nested, only: [:new, :create]
before_action :convert_table_contents_to_utf8, only: [:create, :update]
before_action :check_view_permissions, only: [:show]
before_action :check_view_permissions, only: %i(show update_view_state)
before_action :check_manage_permissions, only: %i(new create edit update
destroy)
before_action :check_complete_and_checkbox_permissions, only:
@ -214,6 +213,17 @@ class StepsController < ApplicationController
end
end
def update_view_state
view_state = @step.current_view_state(current_user)
view_state.state['assets']['sort'] = params.require(:assets).require(:order)
view_state.save! if view_state.changed?
respond_to do |format|
format.json do
render json: {}, status: :ok
end
end
end
def destroy
if @step.can_destroy?
# Update position on other steps of this module
@ -617,7 +627,11 @@ class StepsController < ApplicationController
:name,
:contents,
:_destroy
]
],
marvin_js_assets_attributes: %i(
id
_destroy
)
)
end

View file

@ -1,6 +1,11 @@
# frozen_string_literal: true
class TinyMceAssetsController < ApplicationController
before_action :load_vars, only: %i(marvinjs_show marvinjs_update download)
before_action :check_read_permission, only: %i(marvinjs_show marvinjs_update download)
before_action :check_edit_permission, only: %i(marvinjs_update)
def create
image = params.fetch(:file) { render_404 }
tiny_img = TinyMceAsset.new(team_id: current_team.id, saved: false)
@ -23,4 +28,91 @@ class TinyMceAssetsController < ApplicationController
}, status: :unprocessable_entity
end
end
def download
if @asset&.image&.attached?
redirect_to rails_blob_path(@asset.image, disposition: 'attachment')
else
render_404
end
end
def marvinjs_show
asset = current_team.tiny_mce_assets.find_by_id(Base62.decode(params[:id]))
return render_404 unless asset
render json: {
name: asset.image.metadata[:name],
description: asset.image.metadata[:description]
}
end
def marvinjs_create
result = MarvinJsService.create_sketch(marvin_params, current_user, current_team)
if result[:asset]
render json: {
image: {
url: rails_representation_url(result[:asset].preview),
token: Base62.encode(result[:asset].id),
source_type: result[:asset].image.metadata[:asset_type]
}
}, content_type: 'text/html'
else
render json: result[:asset].errors, status: :unprocessable_entity
end
end
def marvinjs_update
asset = MarvinJsService.update_sketch(marvin_params, current_user, current_team)
if asset
render json: { url: rails_representation_url(asset.preview), id: asset.id }
else
render json: { error: t('marvinjs.no_sketches_found') }, status: :unprocessable_entity
end
end
private
def load_vars
@asset = current_team.tiny_mce_assets.find_by_id(Base62.decode(params[:id]))
return render_404 unless @asset
@assoc = @asset.object
if @assoc.class == Step
@protocol = @assoc.protocol
elsif @assoc.class == Protocol
@protocol = @assoc
elsif @assoc.class == MyModule
@my_module = @assoc
elsif @assoc.class == ResultText
@my_module = @assoc.result.my_module
end
end
def check_read_permission
if @assoc.class == Step || @assoc.class == Protocol
return render_403 unless can_read_protocol_in_module?(@protocol) ||
can_read_protocol_in_repository?(@protocol)
elsif @assoc.class == ResultText || @assoc.class == MyModule
return render_403 unless can_read_experiment?(@my_module.experiment)
else
render_403
end
end
def check_edit_permission
if @assoc.class == Step || @assoc.class == Protocol
return render_403 unless can_manage_protocol_in_module?(@protocol) ||
can_manage_protocol_in_repository?(@protocol)
elsif @assoc.class == ResultText || @assoc.class == MyModule
return render_403 unless can_manage_module?(@my_module)
else
render_403
end
end
def marvin_params
params.permit(:id, :description, :object_id, :object_type, :name, :image)
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module MyModulesHelper
def ordered_step_of(my_module)
my_module.protocol.steps.order(:position)
@ -8,7 +10,15 @@ module MyModulesHelper
end
def ordered_assets(step)
step.assets.order(:file_updated_at)
view_state = step.current_view_state(current_user)
sort = case view_state.state.dig('assets', 'sort')
when 'old' then { created_at: :asc }
when 'atoz' then { file_file_name: :asc }
when 'ztoa' then { file_file_name: :desc }
else { created_at: :desc }
end
step.assets.order(sort)
end
def az_ordered_assets_index(step, asset_id)
@ -39,11 +49,10 @@ module MyModulesHelper
end
def is_steps_page?
action_name == "steps"
action_name == 'steps'
end
def is_results_page?
action_name == "results"
action_name == 'results'
end
end

View file

@ -7,7 +7,7 @@ module ProtocolStatusHelper
res << 'data-trigger="focus" data-placement="bottom" title="'
res << protocol_status_popover_title(parent) +
'" data-content="' + protocol_status_popover_content(parent) +
'">' + protocol_name(parent) + '</a>'
'">' + protocol_name(parent).truncate(Constants::NAME_TRUNCATION_LENGTH) + '</a>'
res.html_safe
end

View file

@ -241,7 +241,7 @@ class Asset < ApplicationRecord
download_blob_to_tempfile do |tmp_file|
to_asset.file.attach(io: tmp_file.open, filename: file_name)
end
new_asset.post_process_file(new_asset.team)
to_asset.post_process_file(to_asset.team)
end
def extract_image_quality
@ -257,7 +257,7 @@ class Asset < ApplicationRecord
end
def image?
content_type == %r{^image/#{Regexp.union(Constants::WHITELISTED_IMAGE_TYPES)}}
content_type =~ %r{^image/#{Regexp.union(Constants::WHITELISTED_IMAGE_TYPES)}}
end
def text?

View file

@ -5,7 +5,7 @@ class AssetTextDatum < ApplicationRecord
validates :data, presence: true
validates :asset, presence: true, uniqueness: true
belongs_to :asset, inverse_of: :asset_text_datum, optional: true
belongs_to :asset, inverse_of: :asset_text_datum
after_save :update_ts_index

View file

@ -7,7 +7,7 @@ class Checklist < ApplicationRecord
length: { maximum: Constants::TEXT_MAX_LENGTH }
validates :step, presence: true
belongs_to :step, inverse_of: :checklists, touch: true, optional: true
belongs_to :step, inverse_of: :checklists, touch: true
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',

View file

@ -7,8 +7,7 @@ class ChecklistItem < ApplicationRecord
validates :checked, inclusion: { in: [true, false] }
belongs_to :checklist,
inverse_of: :checklist_items,
optional: true
inverse_of: :checklist_items
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',

View file

@ -18,7 +18,7 @@ module TinyMceImages
description = TinyMceAsset.update_old_tinymce(description, self)
tiny_mce_assets.each do |tm_asset|
tm_asset_key = tm_asset.image.preview.key
tm_asset_key = tm_asset.preview.key
encoded_tm_asset = Base64.strict_encode64(tm_asset.image.service.download(tm_asset_key))
new_tm_asset_src = "data:image/jpg;base64,#{encoded_tm_asset}"
html_description = Nokogiri::HTML(description)
@ -73,7 +73,7 @@ module TinyMceImages
tiny_img_clone.transaction do
tiny_img_clone.save!
tiny_img_clone.image.attach(io: tiny_img.image.open, filename: tiny_img.image.filename.to_s)
tiny_img.duplicate_file(tiny_img_clone)
end
target.tiny_mce_assets << tiny_img_clone
@ -96,8 +96,6 @@ module TinyMceImages
next if asset && asset.object == self && asset.team_id != asset_team_id
new_image = asset.image
new_image_filename = new_image.file_name
else
# We need implement size and type checks here
new_image = URI.parse(image['src']).open
@ -111,7 +109,11 @@ module TinyMceImages
new_asset.transaction do
new_asset.save!
new_asset.image.attach(io: new_image, filename: new_image_filename)
if image['data-mce-token']
asset.duplicate_file(new_asset)
else
new_asset.image.attach(io: new_image, filename: new_image_filename)
end
end
image['src'] = ''

View file

@ -7,6 +7,18 @@ module ViewableModel
has_many :view_states, as: :viewable, dependent: :destroy
end
# This module requres that the class which includes it implements these methods:
# => default_view_state, returning hash with default state representation
# => validate_view_state(view_state), custom validator for the state hash
def default_view_state
raise NotImplementedError, 'default_view_state should be implemented!'
end
def validate_view_state(_view_state)
raise NotImplementedError, 'validate_view_state(view_state) should be implemented!'
end
def current_view_state(user)
state = view_states.where(user: user).take
state || view_states.create!(user: user, state: default_view_state)

View file

@ -3,15 +3,13 @@ class Experiment < ApplicationRecord
include SearchableModel
include SearchableByNameModel
belongs_to :project, inverse_of: :experiments, touch: true, optional: true
belongs_to :project, inverse_of: :experiments, touch: true
belongs_to :created_by,
foreign_key: :created_by_id,
class_name: 'User',
optional: true
class_name: 'User'
belongs_to :last_modified_by,
foreign_key: :last_modified_by_id,
class_name: 'User',
optional: true
class_name: 'User'
belongs_to :archived_by,
foreign_key: :archived_by_id, class_name: 'User', optional: true
belongs_to :restored_by,
@ -225,6 +223,12 @@ class Experiment < ApplicationRecord
workflowimg.service.exist?(workflowimg.blob.key)
end
def workflowimg_file_name
return '' unless workflowimg.attached?
workflowimg.blob&.filename&.sanitized
end
# Get projects where user is either owner or user in the same team
# as this experiment
def projects_with_role_above_user(current_user)

View file

@ -34,7 +34,7 @@ class MyModule < ApplicationRecord
foreign_key: 'restored_by_id',
class_name: 'User',
optional: true
belongs_to :experiment, inverse_of: :my_modules, touch: true, optional: true
belongs_to :experiment, inverse_of: :my_modules, touch: true
belongs_to :my_module_group, inverse_of: :my_modules, optional: true
has_many :results, inverse_of: :my_module, dependent: :destroy
has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy

View file

@ -3,7 +3,7 @@ class MyModuleGroup < ApplicationRecord
validates :experiment, presence: true
belongs_to :experiment, inverse_of: :my_module_groups, optional: true
belongs_to :experiment, inverse_of: :my_module_groups
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',

View file

@ -4,10 +4,8 @@ class MyModuleRepositoryRow < ApplicationRecord
class_name: 'User',
optional: true
belongs_to :repository_row,
optional: true,
inverse_of: :my_module_repository_rows
belongs_to :my_module,
optional: true,
touch: true,
inverse_of: :my_module_repository_rows

View file

@ -2,10 +2,10 @@ class MyModuleTag < ApplicationRecord
validates :my_module, :tag, presence: true
validates :tag_id, uniqueness: { scope: :my_module_id }
belongs_to :my_module, inverse_of: :my_module_tags, optional: true
belongs_to :my_module, inverse_of: :my_module_tags
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',
optional: true
belongs_to :tag, inverse_of: :my_module_tags, optional: true
belongs_to :tag, inverse_of: :my_module_tags
end

View file

@ -29,7 +29,7 @@ class Project < ApplicationRecord
foreign_key: 'restored_by_id',
class_name: 'User',
optional: true
belongs_to :team, inverse_of: :projects, touch: true, optional: true
belongs_to :team, inverse_of: :projects, touch: true
has_many :user_projects, inverse_of: :project
has_many :users, through: :user_projects
has_many :experiments, inverse_of: :project

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Protocol < ApplicationRecord
include SearchableModel
include RenamingUtil
@ -15,6 +17,12 @@ class Protocol < ApplicationRecord
in_repository_archived: 4
}
scope :recent_protocols, lambda { |user, team, amount|
where(team: team, protocol_type: :in_repository_public)
.or(where(team: team, protocol_type: :in_repository_private, added_by: user))
.order(updated_at: :desc).limit(amount)
}
auto_strip_attributes :name, :description, nullify: false
# Name is required when its actually specified (i.e. :in_repository? is true)
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
@ -46,7 +54,7 @@ class Protocol < ApplicationRecord
protocol
.validates_uniqueness_of :name, case_sensitive: false,
scope: :team,
conditions: -> {
conditions: lambda {
where(
protocol_type:
Protocol
@ -59,8 +67,8 @@ class Protocol < ApplicationRecord
# Private protocol must have unique name inside its team & user scope
protocol
.validates_uniqueness_of :name, case_sensitive: false,
scope: [:team, :added_by],
conditions: -> {
scope: %i(team added_by),
conditions: lambda {
where(
protocol_type:
Protocol
@ -72,8 +80,8 @@ class Protocol < ApplicationRecord
# Archived protocol must have unique name inside its team & user scope
protocol
.validates_uniqueness_of :name, case_sensitive: false,
scope: [:team, :added_by],
conditions: -> {
scope: %i(team added_by),
conditions: lambda {
where(
protocol_type:
Protocol
@ -92,7 +100,7 @@ class Protocol < ApplicationRecord
belongs_to :my_module,
inverse_of: :protocols,
optional: true
belongs_to :team, inverse_of: :protocols, optional: true
belongs_to :team, inverse_of: :protocols
belongs_to :parent,
foreign_key: 'parent_id',
class_name: 'Protocol',
@ -244,7 +252,7 @@ class Protocol < ApplicationRecord
src.clone_tinymce_assets(dest, dest.team)
# Update keywords
if clone_keywords then
if clone_keywords
src.protocol_keywords.each do |keyword|
ProtocolProtocolKeyword.create(
protocol: dest,
@ -294,6 +302,7 @@ class Protocol < ApplicationRecord
step.assets.each do |asset|
asset2 = asset.dup
asset2.save!
asset.duplicate_file(asset2)
step2.assets << asset2
assets_to_clone << [asset.id, asset2.id]
end
@ -362,7 +371,7 @@ class Protocol < ApplicationRecord
def space_taken
st = 0
self.steps.find_each do |step|
steps.find_each do |step|
st += step.space_taken
end
st
@ -437,7 +446,7 @@ class Protocol < ApplicationRecord
# Update all module protocols that had
# parent set to this protocol
if result then
if result
Protocol.where(parent: self).find_each do |p|
p.update(
parent: nil,
@ -466,7 +475,7 @@ class Protocol < ApplicationRecord
self.archived_on = nil
self.restored_by = user
self.restored_on = Time.now
if self.published_on.present?
if published_on.present?
self.published_on = Time.now
self.protocol_type = Protocol.protocol_types[:in_repository_public]
else
@ -491,15 +500,15 @@ class Protocol < ApplicationRecord
self.record_timestamps = false
# First, destroy all keywords
self.protocol_protocol_keywords.destroy_all
protocol_protocol_keywords.destroy_all
if keywords.present?
keywords.each do |kw_name|
kw = ProtocolKeyword.find_or_create_by(name: kw_name, team: team)
self.protocol_keywords << kw
protocol_keywords << kw
end
end
end
rescue
rescue StandardError
result = false
end
result
@ -509,7 +518,7 @@ class Protocol < ApplicationRecord
self.parent = nil
self.parent_updated_at = nil
self.protocol_type = Protocol.protocol_types[:unlinked]
self.save!
save!
end
def update_parent(current_user)
@ -518,16 +527,16 @@ class Protocol < ApplicationRecord
parent.reload
# Now, clone step contents
Protocol.clone_contents(self, self.parent, current_user, false)
Protocol.clone_contents(self, parent, current_user, false)
# Lastly, update the metadata
parent.reload
parent.record_timestamps = false
parent.updated_at = self.updated_at
parent.updated_at = updated_at
parent.save!
self.record_timestamps = false
self.parent_updated_at = self.updated_at
self.save!
self.parent_updated_at = updated_at
save!
end
def update_from_parent(current_user)
@ -535,15 +544,15 @@ class Protocol < ApplicationRecord
destroy_contents(current_user)
# Now, clone parent's step contents
Protocol.clone_contents(self.parent, self, current_user, false)
Protocol.clone_contents(parent, self, current_user, false)
# Lastly, update the metadata
self.reload
reload
self.record_timestamps = false
self.updated_at = self.parent.updated_at
self.parent_updated_at = self.parent.updated_at
self.updated_at = parent.updated_at
self.parent_updated_at = parent.updated_at
self.added_by = current_user
self.save!
save!
end
def load_from_repository(source, current_user)
@ -554,14 +563,14 @@ class Protocol < ApplicationRecord
Protocol.clone_contents(source, self, current_user, false)
# Lastly, update the metadata
self.reload
reload
self.record_timestamps = false
self.updated_at = source.updated_at
self.parent = source
self.parent_updated_at = source.updated_at
self.added_by = current_user
self.protocol_type = Protocol.protocol_types[:linked]
self.save!
save!
end
def copy_to_repository(new_name, new_protocol_type, link_protocols, current_user)
@ -570,29 +579,23 @@ class Protocol < ApplicationRecord
description: description,
protocol_type: new_protocol_type,
added_by: current_user,
team: self.team
team: team
)
if clone.in_repository_public?
clone.published_on = Time.now
end
clone.published_on = Time.now if clone.in_repository_public?
# Don't proceed further if clone is invalid
if clone.invalid?
return clone
end
return clone if clone.invalid?
# Okay, clone seems to be valid: let's clone it
clone = deep_clone(clone, current_user)
# If the above operation went well, update published_on
# timestamp
if clone.in_repository_public?
clone.update(published_on: Time.now)
end
clone.update(published_on: Time.now) if clone.in_repository_public?
# Link protocols if neccesary
if link_protocols then
self.reload
if link_protocols
reload
self.record_timestamps = false
self.added_by = current_user
self.parent = clone
@ -600,23 +603,23 @@ class Protocol < ApplicationRecord
self.parent_updated_at = ts
self.updated_at = ts
self.protocol_type = Protocol.protocol_types[:linked]
self.save!
save!
end
return clone
clone
end
def deep_clone_my_module(my_module, current_user)
clone = Protocol.new_blank_for_module(my_module)
clone.name = self.name
clone.authors = self.authors
clone.description = self.description
clone.protocol_type = self.protocol_type
clone.name = name
clone.authors = authors
clone.description = description
clone.protocol_type = protocol_type
if self.linked?
if linked?
clone.added_by = current_user
clone.parent = self.parent
clone.parent_updated_at = self.parent_updated_at
clone.parent = parent
clone.parent_updated_at = parent_updated_at
end
deep_clone(clone, current_user)
@ -624,13 +627,13 @@ class Protocol < ApplicationRecord
def deep_clone_repository(current_user)
clone = Protocol.new(
name: self.name,
authors: self.authors,
description: self.description,
name: name,
authors: authors,
description: description,
added_by: current_user,
team: self.team,
protocol_type: self.protocol_type,
published_on: self.in_repository_public? ? Time.now : nil,
team: team,
protocol_type: protocol_type,
published_on: in_repository_public? ? Time.now : nil
)
cloned = deep_clone(clone, current_user)
@ -653,19 +656,17 @@ class Protocol < ApplicationRecord
def destroy_contents(current_user)
# Calculate total space taken by the protocol
st = self.space_taken
st = space_taken
steps.pluck(:id).each do |id|
unless Step.find(id).destroy(current_user)
raise ActiveRecord::RecordNotDestroyed
end
raise ActiveRecord::RecordNotDestroyed unless Step.find(id).destroy(current_user)
end
# Release space taken by the step
self.team.release_space(st)
self.team.save
team.release_space(st)
team.save
# Reload protocol
self.reload
reload
end
def can_destroy?
@ -700,15 +701,14 @@ class Protocol < ApplicationRecord
p.record_timestamps = false
p.decrement!(:nr_of_linked_children)
end
if self.parent_id != nil
self.parent.record_timestamps = false
self.parent.increment!(:nr_of_linked_children)
unless parent_id.nil?
parent.record_timestamps = false
parent.increment!(:nr_of_linked_children)
end
end
end
def decrement_linked_children
self.parent.decrement!(:nr_of_linked_children) if self.parent.present?
parent.decrement!(:nr_of_linked_children) if parent.present?
end
end

View file

@ -11,8 +11,8 @@ class Report < ApplicationRecord
validates :project, presence: true
validates :user, presence: true
belongs_to :project, inverse_of: :reports, optional: true
belongs_to :user, inverse_of: :reports, optional: true
belongs_to :project, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :team, inverse_of: :reports
belongs_to :last_modified_by,
foreign_key: 'last_modified_by_id',

View file

@ -6,7 +6,7 @@ class Repository < ApplicationRecord
attribute :discarded_by_id, :integer
belongs_to :team, optional: true
belongs_to :team
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User'
has_many :repository_columns, dependent: :destroy
has_many :repository_rows, dependent: :destroy

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class RepositoryAssetValue < ApplicationRecord
belongs_to :created_by,
foreign_key: :created_by_id,
@ -15,6 +17,9 @@ class RepositoryAssetValue < ApplicationRecord
validates :asset, :repository_cell, presence: true
SORTABLE_COLUMN_NAME = 'assets.file_file_name'
SORTABLE_VALUE_INCLUDE = { repository_asset_value: :asset }.freeze
def formatted
asset.file_name
end
@ -28,8 +33,8 @@ class RepositoryAssetValue < ApplicationRecord
end
def update_data!(new_data, user)
file.original_filename = new_data[:file_name]
asset.file.attach(io: new_data[:file_data], filename: new_data[:file_name])
asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data].split(',')[1])),
filename: new_data[:file_name])
asset.last_modified_by = user
self.last_modified_by = user
asset.save! && save!
@ -38,15 +43,15 @@ class RepositoryAssetValue < ApplicationRecord
def self.new_with_payload(payload, attributes)
value = new(attributes)
team = value.repository_cell.repository_column.repository.team
file = Paperclip.io_adapters.for(payload[:file_data])
file.original_filename = payload[:file_name]
value.asset = Asset.create!(
file: file,
created_by: value.created_by,
last_modified_by: value.created_by,
team: team
)
value.asset.post_process_file(team)
value.asset.file.attach(
io: StringIO.new(Base64.decode64(payload[:file_data].split(',')[1])),
filename: payload[:file_name]
)
value
end
end

View file

@ -35,7 +35,7 @@ class RepositoryCell < ActiveRecord::Base
validates_inclusion_of :repository_column,
in: (lambda do |cell|
cell.repository_row.repository.repository_columns
cell.repository_row&.repository&.repository_columns || []
end)
validates :repository_column, presence: true
validate :repository_column_data_type

View file

@ -9,6 +9,9 @@ class RepositoryDateValue < ApplicationRecord
validates :repository_cell, presence: true
validates :data, presence: true
SORTABLE_COLUMN_NAME = 'repository_date_values.data'
SORTABLE_VALUE_INCLUDE = :repository_date_value
def formatted
data
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class RepositoryListValue < ApplicationRecord
belongs_to :repository_list_item
belongs_to :created_by,
@ -12,11 +14,12 @@ class RepositoryListValue < ApplicationRecord
validates :repository_cell, presence: true
validates_inclusion_of :repository_list_item,
in: (lambda do |list_value|
list_value.repository_cell
.repository_column
.repository_list_items
list_value.repository_cell&.repository_column&.repository_list_items || []
end)
SORTABLE_COLUMN_NAME = 'repository_list_items.data'
SORTABLE_VALUE_INCLUDE = { repository_list_value: :repository_list_item }.freeze
def formatted
data.to_s
end

View file

@ -11,6 +11,9 @@ class RepositoryTextValue < ApplicationRecord
presence: true,
length: { maximum: Constants::TEXT_MAX_LENGTH }
SORTABLE_COLUMN_NAME = 'repository_text_values.data'
SORTABLE_VALUE_INCLUDE = :repository_text_value
def formatted
data
end

View file

@ -2,6 +2,7 @@ class Step < ApplicationRecord
include SearchableModel
include SearchableByNameModel
include TinyMceImages
include ViewableModel
auto_strip_attributes :name, :description, nullify: false
validates :name,
@ -65,6 +66,16 @@ class Step < ApplicationRecord
end
end
def default_view_state
{ 'assets' => { 'sort' => 'new' } }
end
def validate_view_state(view_state)
unless %w(new old atoz ztoa).include?(view_state.state.dig('assets', 'sort'))
view_state.errors.add(:state, :wrong_state)
end
end
def destroy(current_user)
@current_user = current_user

View file

@ -41,6 +41,7 @@ class Team < ApplicationRecord
has_many :repositories, dependent: :destroy
has_many :reports, inverse_of: :team, dependent: :destroy
has_many :activities, inverse_of: :team, dependent: :destroy
has_many :assets, inverse_of: :team, dependent: :destroy
attr_accessor :without_templates
attr_accessor :without_intro_demo
@ -57,6 +58,13 @@ class Team < ApplicationRecord
'filter' => 'active' } }
end
def validate_view_state(view_state)
unless %w(new old atoz ztoa).include?(view_state.state.dig('projects', 'cards', 'sort')) &&
%w(active archived).include?(view_state.state.dig('projects', 'filter'))
view_state.errors.add(:state, :wrong_state)
end
end
def search_users(query = nil)
a_query = "%#{query}%"
users.where.not(confirmed_at: nil)

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class TinyMceAsset < ApplicationRecord
include ActiveStorage::Downloading
extend ProtocolsExporter
attr_accessor :reference
before_create :set_reference, optional: true
@ -120,7 +121,8 @@ class TinyMceAsset < ApplicationRecord
new_format = "<img src=\"\" class=\"img-responsive\" data-mce-token=\"#{Base62.encode(token.to_i)}\"/>"
asset = find_by_id(token)
unless asset
# impor flag only for import from file cases, because we don't have image in DB
unless asset || import
# remove tag if asset deleted
description.sub!(old_format, '')
next
@ -138,10 +140,9 @@ class TinyMceAsset < ApplicationRecord
if exists?
order(:id).each do |tiny_mce_asset|
asset_guid = get_guid(tiny_mce_asset.id)
asset_file_name = "rte-#{asset_guid.to_s + tiny_mce_asset.image.blob.filename.extension}"
asset_file_name = "rte-#{asset_guid}.#{tiny_mce_asset.image.blob.filename.extension}"
ostream.put_next_entry("#{dir}/#{asset_file_name}")
ostream.print(tiny_mce_asset.image.download)
input_file.close
end
end
ostream
@ -170,7 +171,7 @@ class TinyMceAsset < ApplicationRecord
tiny_img_clone.transaction do
tiny_img_clone.save!
tiny_img_clone.image.attach(io: image.download, filename: image.filename.sanitized)
duplicate_file(tiny_img_clone)
end
return false unless tiny_img_clone.persisted?
@ -188,6 +189,17 @@ class TinyMceAsset < ApplicationRecord
obj.reassign_tiny_mce_image_references(cloned_img_ids)
end
def blob
image&.blob
end
def duplicate_file(to_asset)
download_blob_to_tempfile do |tmp_file|
to_asset.image.attach(io: tmp_file.open, filename: file_name)
end
TinyMceAsset.update_estimated_size(to_asset.id)
end
private
def self_destruct

View file

@ -8,6 +8,7 @@ class User < ApplicationRecord
include User::ProjectRoles
include TeamBySubjectModel
include InputSanitizeHelper
include ActiveStorage::Downloading
acts_as_token_authenticatable
devise :invitable, :confirmable, :database_authenticatable, :registerable,
@ -248,6 +249,8 @@ class User < ApplicationRecord
end
def avatar_variant(style)
return Constants::DEFAULT_AVATAR_URL.gsub(':style', style) unless avatar.attached?
format = case style.to_sym
when :medium
Constants::MEDIUM_PIC_FORMAT
@ -563,6 +566,28 @@ class User < ApplicationRecord
.map { |i| { name: escape_input(i[:full_name]), id: i[:id] } }
end
def file_name
return '' unless avatar.attached?
avatar.blob&.filename&.sanitized
end
def avatar_base64(style)
unless avatar.present?
missing_link = File.open("#{Rails.root}/app/assets/images/#{style}/missing.png").to_a.join
return "data:image/png;base64,#{Base64.strict_encode64(missing_link)}"
end
avatar_uri = if avatar.options[:storage].to_sym == :s3
URI.parse(avatar.url(style)).open.to_a.join
else
File.open(avatar.path(style)).to_a.join
end
encoded_data = Base64.strict_encode64(avatar_uri)
"data:#{avatar_content_type};base64,#{encoded_data}"
end
protected
def confirmation_required?

View file

@ -8,4 +8,13 @@ class ViewState < ApplicationRecord
scope: %i(viewable_type user_id),
message: :not_unique
}
validate :validate_state_content
private
def validate_state_content
return unless state.present?
viewable.validate_view_state(self)
end
end

View file

@ -49,7 +49,7 @@ class ZipExport < ApplicationRecord
end
def zip_file_name
return '' unless file.attached?
return '' unless zip_file.attached?
zip_file.blob&.filename&.to_s
end

View file

@ -18,10 +18,10 @@ module Api
end
def url
if !object.asset&.file&.exists?
if !object.asset&.file&.attached?
nil
else
rails_blob_path(object.asset.file, disposition: 'attachment')
Rails.application.routes.url_helpers.rails_blob_path(object.asset.file, disposition: 'attachment')
end
end
end

View file

@ -19,10 +19,10 @@ module Api
end
def url
if !object.asset&.file&.exists?
if !object.asset&.file&.attached?
nil
else
rails_blob_path(object.asset.file, disposition: 'attachment')
Rails.application.routes.url_helpers.rails_blob_path(object.asset.file, disposition: 'attachment')
end
end
end

View file

@ -5,20 +5,20 @@ module Api
class UserSerializer < ActiveModel::Serializer
type :users
attributes :full_name, :initials, :email
attribute :avatar_file_name, if: -> { object.avatar.present? }
attribute :avatar_file_size, if: -> { object.avatar.present? }
attribute :avatar_url, if: -> { object.avatar.present? }
attribute :avatar_file_name, if: -> { object.avatar.attached? }
attribute :avatar_file_size, if: -> { object.avatar.attached? }
attribute :avatar_url, if: -> { object.avatar.attached? }
def avatar_file_name
object.avatar_file_name
object.avatar.blob.filename
end
def avatar_file_size
object.avatar.size
object.avatar.blob.byte_size
end
def avatar_url
object.avatar.url(:icon)
object.avatar_url(:icon)
end
end
end

View file

@ -0,0 +1,89 @@
# frozen_string_literal: true
class MarvinJsService
class << self
def url
ENV['MARVINJS_URL']
end
def enabled?
!ENV['MARVINJS_URL'].nil? || !ENV['MARVINJS_API_KEY'].nil?
end
def create_sketch(params, current_user, current_team)
file = generate_image(params)
if params[:object_type] == 'TinyMceAsset'
asset = TinyMceAsset.new(team_id: current_team.id)
attach_file(asset.image, file, params)
asset.save!
return { asset: asset }
end
asset = Asset.new(created_by: current_user,
last_modified_by: current_user,
team_id: current_team.id)
attach_file(asset.file, file, params)
asset.save!
connect_asset(asset, params, current_user)
end
def update_sketch(params, _current_user, current_team)
if params[:object_type] == 'TinyMceAsset'
asset = current_team.tiny_mce_assets.find(Base62.decode(params[:id]))
attachment = asset&.image
else
asset = current_team.assets.find(params[:id])
attachment = asset&.file
end
return unless attachment
file = generate_image(params)
attach_file(attachment, file, params)
asset
end
private
def connect_asset(asset, params, current_user)
if params[:object_type] == 'Step'
object = params[:object_type].constantize.find(params[:object_id])
object.assets << asset
elsif params[:object_type] == 'Result'
my_module = MyModule.find_by_id(params[:object_id])
return unless my_module
object = Result.create(user: current_user,
my_module: my_module,
name: prepare_name(params[:name]),
asset: asset,
last_modified_by: current_user)
end
{ asset: asset, object: object }
end
def generate_image(params)
StringIO.new(Base64.decode64(params[:image].split(',')[1]))
end
def attach_file(asset, file, params)
asset.attach(
io: file,
filename: "#{prepare_name(params[:name])}.jpg",
content_type: 'image/jpeg',
metadata: {
name: prepare_name(params[:name]),
description: params[:description],
asset_type: 'marvinjs'
}
)
end
def prepare_name(sketch_name)
if !sketch_name.empty?
sketch_name
else
I18n.t('marvinjs.new_sketch')
end
end
end
end

View file

@ -67,10 +67,19 @@ module ModelExporters
{
result: result,
result_comments: result.result_comments,
asset: result.asset,
asset: result_assets_data(result.asset),
table: table(result.table),
result_text: result.result_text
}
end
def result_assets_data(asset)
return unless asset&.file&.attached?
{
asset: asset,
asset_blob: asset.file.blob
}
end
end
end

View file

@ -14,27 +14,21 @@ module ModelExporters
def copy_files(assets, attachment_name, dir_name)
assets.flatten.each do |a|
next unless a.public_send(attachment_name).present?
next unless a.public_send(attachment_name).attached?
unless a.public_send(attachment_name).exists?
raise StandardError,
"File id:#{a.id} of type #{attachment_name} is missing"
end
yield if block_given?
dir = FileUtils.mkdir_p(File.join(dir_name, a.id.to_s)).first
if defined?(S3_BUCKET)
s3_asset =
S3_BUCKET.object(a.public_send(attachment_name).path.remove(%r{^/}))
file_name = a.public_send(attachment_name).original_filename
File.open(File.join(dir, file_name), 'wb') do |f|
s3_asset.get(response_target: f)
end
else
FileUtils.cp(
a.public_send(attachment_name).path,
File.join(dir, a.public_send(attachment_name).original_filename)
)
end
tempfile = Tempfile.new
tempfile.binmode
a.public_send(attachment_name).blob.download { |chunk| tempfile.write(chunk) }
tempfile.flush
tempfile.rewind
FileUtils.cp(
tempfile.path,
File.join(dir, a.file_name)
)
tempfile.close!
end
end
@ -57,12 +51,21 @@ module ModelExporters
checklists: step.checklists.map { |c| checklist(c) },
step_comments: step.step_comments,
step_assets: step.step_assets,
assets: step.assets,
assets: step.assets.map { |a| assets_data(a) },
step_tables: step.step_tables,
tables: step.tables.map { |t| table(t) }
}
end
def assets_data(asset)
return unless asset.file.attached?
{
asset: asset,
asset_blob: asset.file.blob
}
end
def checklist(checklist)
{
checklist: checklist,

View file

@ -39,9 +39,7 @@ module ModelExporters
private
def team(team)
if team.tiny_mce_assets.present?
@tiny_mce_assets_to_copy.push(team.tiny_mce_assets)
end
@tiny_mce_assets_to_copy.push(team.tiny_mce_assets) if team.tiny_mce_assets.present?
{
team: team,
default_admin_id: team.user_teams.where(role: 2).first.user.id,
@ -53,7 +51,7 @@ module ModelExporters
.map { |n| notification(n) },
custom_fields: team.custom_fields,
repositories: team.repositories.map { |r| repository(r) },
tiny_mce_assets: team.tiny_mce_assets,
tiny_mce_assets: team.tiny_mce_assets.map { |tma| tiny_mce_asset_data(tma) },
protocols: team.protocols.where(my_module: nil).map do |pr|
protocol(pr)
end,
@ -80,6 +78,7 @@ module ModelExporters
user_json['sign_in_count'] = user.sign_in_count
user_json['last_sign_in_at'] = user.last_sign_in_at
user_json['last_sign_in_ip'] = user.last_sign_in_ip
user_json['avatar'] = user.avatar.blob if user.avatar.attached?
copy_files([user], :avatar, File.join(@dir_to_export, 'avatars'))
{
user: user_json,
@ -152,11 +151,21 @@ module ModelExporters
}
end
def tiny_mce_asset_data(asset)
{
tiny_mce_asset: asset,
tiny_mce_asset_blob: asset.image.blob
}
end
def get_cell_value_asset(cell)
return unless cell.value_type == 'RepositoryAssetValue'
@assets_to_copy.push(cell.value.asset)
cell.value.asset
{
asset: cell.value.asset,
asset_blob: cell.value.asset.blob
}
end
end
end

View file

@ -84,7 +84,9 @@ class ProjectsOverviewService
).joins(
"LEFT OUTER JOIN (#{due_modules.to_sql}) due_modules "\
"ON due_modules.experiment_id = experiments.id"
).left_outer_joins(:user_projects, :project_comments)
).joins(
'LEFT OUTER JOIN user_projects ON user_projects.project_id = projects.id'
).left_outer_joins(:project_comments)
# Only admins see all projects of the team
unless @user.is_admin_of_team?(@team)

View file

@ -10,7 +10,7 @@ module RepositoryActions
end
def call
self.send("duplicate_#{@cell.value_type.underscore}")
__send__("duplicate_#{@cell.value_type.split('::').last.underscore}")
end
private

View file

@ -119,9 +119,18 @@ class RepositoryDatatableService
end
elsif sortable_columns[column_id - 1] == 'repository_cell.value'
id = @mappings.key(column_id.to_s)
type = RepositoryColumn.find_by_id(id)
return records unless type
return select_type(type.data_type, records, id, dir)
sorting_column = RepositoryColumn.find_by_id(id)
return records unless sorting_column
sorting_data_type = sorting_column.data_type.constantize
cells = RepositoryCell.joins(sorting_data_type::SORTABLE_VALUE_INCLUDE)
.where('repository_cells.repository_column_id': sorting_column.id)
.select("repository_cells.repository_row_id,
#{sorting_data_type::SORTABLE_COLUMN_NAME} AS value")
records.joins("LEFT OUTER JOIN (#{cells.to_sql}) AS values ON values.repository_row_id = repository_rows.id")
.order("values.value #{dir}")
elsif sortable_columns[column_id - 1] == 'users.full_name'
# We don't need join user table, because it already joined in fetch_row method
return records.order("users.full_name #{dir}")
@ -151,19 +160,6 @@ class RepositoryDatatableService
records.order(order_by_index)
end
def select_type(type, records, id, dir)
case type
when 'RepositoryTextValue'
filter_by_text_value(records, id, dir)
when 'RepositoryListValue'
filter_by_list_value(records, id, dir)
when 'RepositoryAssetValue'
filter_by_asset_value(records, id, dir)
else
records
end
end
def sort_null_direction(val)
val == 'ASC' ? 'LAST' : 'FIRST'
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class RepositoryTableStateColumnUpdateService
# We're using Constants::REPOSITORY_TABLE_DEFAULT_STATE as a reference for
# default table state; this Ruby Hash makes heavy use of Ruby symbols
@ -56,9 +58,7 @@ class RepositoryTableStateColumnUpdateService
state['order'].reject! { |_, v| v[0] == old_column_index }
state['order'].each do |k, v|
if v[0].to_i > old_column_index.to_i
state['order'][k] = [(v[0].to_i - 1).to_s, v[1]]
end
state['order'][k] = [(v[0].to_i - 1).to_s, v[1]] if v[0].to_i > old_column_index.to_i
end
if state['order'].empty?
# Fallback to default order if user had table ordered by
@ -73,4 +73,4 @@ class RepositoryTableStateColumnUpdateService
table_state.save
end
end
end
end

View file

@ -90,6 +90,7 @@ class TeamImporter
user_notification.notification_id =
@notification_mappings[user_notification.notification_id]
next if user_notification.notification_id.blank?
user_notification.save!
end
@ -241,6 +242,7 @@ class TeamImporter
comment.save! if update_annotation(comment.message)
end
next unless res.result_text
res.save! if update_annotation(res.result_text.text)
end
end
@ -250,6 +252,7 @@ class TeamImporter
# Returns true if text was updated
def update_annotation(text)
return false if text.nil?
updated = false
%w(prj exp tsk rep_item).each do |name|
text.scan(/~#{name}~\w+\]/).each do |text_match|
@ -267,6 +270,7 @@ class TeamImporter
@repository_row_mappings[orig_id]
end
next unless new_id
new_id_encoded = new_id.base62_encode
text.sub!("~#{name}~#{orig_id_encoded}]", "~#{name}~#{new_id_encoded}]")
updated = true
@ -276,6 +280,7 @@ class TeamImporter
orig_id_encoded = user_match.match(/\[@[\w+-@?! ]+~(\w+)\]/)[1]
orig_id = orig_id_encoded.base62_decode
next unless @user_mappings[orig_id]
new_id_encoded = @user_mappings[orig_id].base62_encode
text.sub!("~#{orig_id_encoded}]", "~#{new_id_encoded}]")
updated = true
@ -327,11 +332,12 @@ class TeamImporter
def create_tiny_mce_assets(tmce_assets_json, team)
tmce_assets_json.each do |tiny_mce_asset_json|
tiny_mce_asset = TinyMceAsset.new(tiny_mce_asset_json)
tiny_mce_asset = TinyMceAsset.new(tiny_mce_asset_json['tiny_mce_asset'])
tiny_mce_asset_blob = tiny_mce_asset_json['tiny_mce_asset_blob']
# Try to find and load file
File.open(
File.join(@import_dir, 'tiny_mce_assets', tiny_mce_asset.id.to_s,
tiny_mce_asset.image_file_name)
tiny_mce_asset_blob['filename'])
) do |tiny_mce_file|
orig_tmce_id = tiny_mce_asset.id
tiny_mce_asset.id = nil
@ -341,7 +347,7 @@ class TeamImporter
end
tiny_mce_asset.team = team
tiny_mce_asset.save!
tiny_mce_asset.image.attach(io: tiny_mce_file, filename: tiny_mce_file.basename)
tiny_mce_asset.image.attach(io: tiny_mce_file, filename: File.basename(tiny_mce_file))
@mce_asset_counter += 1
if tiny_mce_asset.object_id.present?
object = tiny_mce_asset.object
@ -368,12 +374,10 @@ class TeamImporter
user.password = user_json['user']['encrypted_password']
user.current_team_id = team.id
user.invited_by_id = @user_mappings[user.invited_by_id]
if user.avatar.present?
if user_json['user']['avatar']
avatar_path = File.join(@import_dir, 'avatars', orig_user_id.to_s,
user.avatar_file_name)
if File.exist?(avatar_path)
File.open(avatar_path) { |f| user.avatar = f }
end
user_json['user']['avatar']['filename'])
File.open(avatar_path) { |f| user.avatar = f } if File.exist?(avatar_path)
end
user.save!
@user_counter += 1
@ -394,6 +398,7 @@ class TeamImporter
notifications_json.each do |notification_json|
notification = Notification.new(notification_json)
next if notification.type_of.blank?
orig_notification_id = notification.id
notification.id = nil
notification.generator_user_id = find_user(notification.generator_user_id)
@ -415,7 +420,6 @@ class TeamImporter
@repository_mappings[orig_repository_id] = repository.id
@repository_counter += 1
repository_json['repository_columns'].each do |repository_column_json|
repository_column = RepositoryColumn.new(
repository_column_json['repository_column']
)
@ -427,6 +431,7 @@ class TeamImporter
repository_column.save!
@repository_column_mappings[orig_rep_col_id] = repository_column.id
next unless repository_column.data_type == 'RepositoryListValue'
repository_column_json['repository_list_items'].each do |list_item|
created_by_id = find_user(repository_column.created_by_id)
repository_list_item = RepositoryListItem.new(data: list_item['data'])
@ -662,9 +667,7 @@ class TeamImporter
protocol.archived_by_id = find_user(protocol.archived_by_id)
protocol.restored_by_id = find_user(protocol.restored_by_id)
protocol.my_module = my_module unless protocol.my_module_id.nil?
unless protocol.parent_id.nil?
protocol.parent_id = @protocol_mappings[protocol.parent_id]
end
protocol.parent_id = @protocol_mappings[protocol.parent_id] unless protocol.parent_id.nil?
protocol.save!
@protocol_counter += 1
@protocol_mappings[orig_protocol_id] = protocol.id
@ -792,9 +795,10 @@ class TeamImporter
# returns asset object
def create_asset(asset_json, team, user_id = nil)
asset = Asset.new(asset_json)
asset = Asset.new(asset_json['asset'])
asset_blob = asset_json['asset_blob']
File.open(
"#{@import_dir}/assets/#{asset.id}/#{asset.file_name}"
"#{@import_dir}/assets/#{asset.id}/#{asset_blob['filename']}"
) do |file|
orig_asset_id = asset.id
asset.id = nil
@ -804,7 +808,7 @@ class TeamImporter
asset.team = team
asset.in_template = true if @is_template
asset.save!
asset.file.attach(io: file, filename: file.basename)
asset.file.attach(io: file, filename: File.basename(file))
asset.post_process_file(team)
@asset_mappings[orig_asset_id] = asset.id
@asset_counter += 1
@ -864,22 +868,14 @@ class TeamImporter
report_element.my_module_id =
@my_module_mappings[report_element.my_module_id]
end
if report_element.step_id
report_element.step_id = @step_mappings[report_element.step_id]
end
if report_element.result_id
report_element.result_id = @result_mappings[report_element.result_id]
end
report_element.step_id = @step_mappings[report_element.step_id] if report_element.step_id
report_element.result_id = @result_mappings[report_element.result_id] if report_element.result_id
if report_element.checklist_id
report_element.checklist_id =
@checklist_mappings[report_element.checklist_id]
end
if report_element.asset_id
report_element.asset_id = @asset_mappings[report_element.asset_id]
end
if report_element.table_id
report_element.table_id = @table_mappings[report_element.table_id]
end
report_element.asset_id = @asset_mappings[report_element.asset_id] if report_element.asset_id
report_element.table_id = @table_mappings[report_element.table_id] if report_element.table_id
if report_element.experiment_id
report_element.experiment_id =
@experiment_mappings[report_element.experiment_id]
@ -902,7 +898,8 @@ class TeamImporter
def find_user(user_id)
return nil if user_id.nil?
@user_mappings[user_id] ? @user_mappings[user_id] : @admin_id
@user_mappings[user_id] || @admin_id
end
def find_list_item_id(list_item_id)

View file

@ -1,14 +1,15 @@
# frozen_string_literal: true
module DelayedUploaderDemo
# Get asset from demo_files folder
def self.get_asset(user, team, file_name)
Asset.new(
file: File.open(
"#{Rails.root}/app/assets/demo_files/#{file_name}", 'r'
),
asset = Asset.create(
created_by: user,
team: team,
last_modified_by: user
)
asset.file.attach(io: File.open("#{Rails.root}/app/assets/demo_files/#{file_name}", 'r'), filename: file_name)
asset
end
# Generates results asset for given module, file_name assumes file is located
@ -32,7 +33,6 @@ module DelayedUploaderDemo
)
temp_result.save
temp_asset.save
# Generate comment if it exists
generate_result_comment(temp_result, current_user, comment) if comment

View file

@ -56,6 +56,7 @@ module ProtocolsExporter
"fileRef=\"#{asset_file_name}\">\n"
asset_xml << "<fileName>#{img.file_name}</fileName>\n"
asset_xml << "<fileType>#{img.content_type}</fileType>\n"
asset_xml << "<fileMetadata><!--[CDATA[ #{img.image.metadata.to_json} ]]--></fileMetadata>\n"
asset_xml << "</tinyMceAsset>\n"
tiny_assets_xml << asset_xml
end
@ -104,6 +105,7 @@ module ProtocolsExporter
"fileRef=\"#{asset_file_name}\">\n"
asset_xml << "<fileName>#{asset.file_name}</fileName>\n"
asset_xml << "<fileType>#{asset.content_type}</fileType>\n"
asset_xml << "<fileMetadata><!--[CDATA[ #{asset.file.metadata.to_json} ]]--></fileMetadata>\n"
asset_xml << "</asset>\n"
step_xml << asset_xml
end

View file

@ -111,7 +111,10 @@ module ProtocolsImporter
)
# Decode the file bytes
asset.file.attach(io: StringIO.new(Base64.decode64(asset_json['bytes'])), filename: asset_json['fileName'])
asset.file.attach(io: StringIO.new(Base64.decode64(asset_json['bytes'])),
filename: asset_json['fileName'],
content_type: asset_json['fileType'],
metadata: JSON.parse(asset_json['fileMetadata'] || '{}'))
asset.save!
asset_ids << asset.id
@ -143,7 +146,7 @@ module ProtocolsImporter
def populate_rte(object_json, object, team)
return populate_rte_legacy(object_json) unless object_json['descriptionAssets']
description = TinyMceAsset.update_old_tinymce(object_json['description'])
description = TinyMceAsset.update_old_tinymce(object_json['description'], nil, true)
object_json['descriptionAssets'].values.each do |tiny_mce_img_json|
tiny_mce_img = TinyMceAsset.new(
object: object,
@ -153,8 +156,11 @@ module ProtocolsImporter
tiny_mce_img.save!
# Decode the file bytes
tiny_mce_img.image.attach(io: StringIO.new(Base64.decode64(tiny_mce_img_json['bytes'])),
filename: tiny_mce_img_json['fileName'])
file = StringIO.new(Base64.decode64(tiny_mce_img_json['bytes']))
tiny_mce_img.image.attach(io: file,
filename: tiny_mce_img_json['fileName'],
content_type: tiny_mce_img_json['fileType'],
metadata: JSON.parse(tiny_mce_img_json['fileMetadata'] || '{}'))
if description.gsub!("data-mce-token=\"#{tiny_mce_img_json['tokenId']}\"",
"data-mce-token=\"#{Base62.encode(tiny_mce_img.id)}\"")
else

View file

@ -14,7 +14,7 @@ module RepositoryImportParser
def get_value(value, record_row)
return unless @column
send("new_#{@column.data_type.underscore}", value, record_row)
__send__("new_#{@column.data_type.split('::').last.underscore}", value, record_row)
end
private

View file

@ -0,0 +1,12 @@
<span
class="btn btn-default new-marvinjs-upload-button"
data-object-id="<%= element_id %>"
data-object-type="<%= element_type %>"
data-marvin-url="<%= marvin_js_assets_path %>"
data-sketch-container="<%= sketch_container %>"
>
<span class="new-marvinjs-upload-icon">
<%= image_tag 'icon_small/marvinjs.svg' %>
</span>
<%= t('marvinjs.new_button') %>
</span>

View file

@ -7,6 +7,11 @@
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
<% if ENV['MARVINJS_API_KEY'] %>
<script src="https://marvinjs.chemicalize.com/v1/<%= ENV['MARVINJS_API_KEY'] %>/client-settings.js"></script>
<script src="https://marvinjs.chemicalize.com/v1/client.js"></script>
<% end %>
<%= favicon_link_tag "favicon.ico" %>
<%= favicon_link_tag "favicon-16.png", type: "image/png", size: "16x16" %>
<%= favicon_link_tag "favicon-32.png", type: "image/png", size: "32x32" %>
@ -43,6 +48,7 @@
<%= render "shared/about_modal" %>
<%= render "shared/file_preview_modal.html.erb" %>
<%= render "shared/file_edit_modal.html.erb" %>
<%= render "shared/marvinjs_modal.html.erb" %>
<%= render "shared/navigation" %>
<% if user_signed_in? && flash[:system_notification_modal] && current_user.show_login_system_notification? %>

View file

@ -0,0 +1,21 @@
<% display_status = protocol.description.blank? && protocol.steps.count.zero? %>
<% if @recent_protcols_positive %>
<div class="my-module-recent-protocols"
style="display: <%= display_status ? '' : 'none' %>"
data-update-url="<%= load_from_repository_protocol_path(protocol) %>"
>
<div class="btn-group">
<div class="title"><%= t('my_modules.module_header.recent_protocols_from_repository') %></div>
<div
class="dropdown-button"
title="Recent protocols"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<span class="caret"></span>
</div>
<ul class="dropdown-menu">
</ul>
</div>
</div>
<% end %>

View file

@ -18,6 +18,9 @@
<%= render partial: "my_modules/protocols/protocol_status_bar.html.erb" %>
</div>
<%= render partial: "my_modules/protocols/protocol_buttons.html.erb" %>
<% if can_manage_protocol_in_module?(@protocol) %>
<%= render partial: "my_modules/recent_protocol_dropdown.html.erb", locals: {protocol: @my_module.protocol}%>
<% end %>
</div>
<div class="row">

View file

@ -41,6 +41,8 @@
<span class="fas fa-paperclip"></span>
<span class="hidden-xs"><%= t("my_modules.results.new_asset_result") %></span>
</a>
<%= render partial: '/assets/marvinjs/create_marvin_sketch_button.html.erb',
locals: { element_id: @my_module.id, element_type: 'Result', sketch_container: "#results[data-module-id=#{@my_module.id}]" } %>
<%= render partial: "assets/wopi/create_wopi_file_button",
locals: { element_id: @my_module.id, element_type: 'Result' } %>
</div>

View file

@ -23,7 +23,7 @@
</div>
<div class="panel-body">
<% if experiment.workflowimg? %>
<% if experiment.workflowimg.attached? %>
<div class="workflowimg-container">
<%= image_tag(
experiment.workflowimg.expiring_url(

View file

@ -18,22 +18,13 @@
</div>
<div class="report-element-body">
<div class="row">
<div class="col-xs-12 comments-container">
<div class="col-xs-12 comments-container simple">
<% if comments.count == 0 %>
<em><%=t "projects.reports.elements.result_comments.no_comments" %></em>
<% else %>
<ul class="no-style comments-list">
<ul class="no-style content-comments">
<% comments.each do |comment| %>
<% comment_ts = comment.created_at %>
<li class="comment" data-ts="<%= comment_ts.to_i %>">
<span class="comment-prefix">
<em><%=t "projects.reports.elements.result_comments.comment_prefix", user: comment.user.full_name, date: l(comment_ts, format: :full_date), time: l(comment_ts, format: :time) %></em>
</span>
<span class="comment-message">
&nbsp;
<%= custom_auto_link(comment.message, team: current_team) %>
</span>
</li>
<%= render partial: 'shared/comments/item.html.erb', locals: { comment: comment, readonly: true, report: true } %>
<% end %>
</ul>
<% end %>

View file

@ -18,22 +18,13 @@
</div>
<div class="report-element-body">
<div class="row">
<div class="col-xs-12 comments-container">
<div class="col-xs-12 comments-container simple">
<% if comments.count == 0 %>
<em><%=t "projects.reports.elements.step_comments.no_comments" %></em>
<% else %>
<ul class="no-style comments-list">
<ul class="no-style content-comments">
<% comments.each do |comment| %>
<% comment_ts = comment.created_at %>
<li class="comment" data-ts="<%= comment_ts.to_i %>">
<span class="comment-prefix">
<em><%=t "projects.reports.elements.step_comments.comment_prefix", user: comment.user.full_name, date: l(comment_ts, format: :full_date), time: l(comment_ts, format: :time) %></em>
</span>
<span class="comment-message">
&nbsp;
<%= custom_auto_link(comment.message, team: current_team) %>
</span>
</li>
<%= render partial: 'shared/comments/item.html.erb', locals: { comment: comment, readonly: true, report: true } %>`
<% end %>
</ul>
<% end %>

View file

@ -25,24 +25,26 @@
<%= f.text_field :name %>
<%= f.form_group :data_type, label: { text: t('libraries.manange_modal_column.colum_type') } do %>
<br />
<%= f.radio_button :data_type,
'RepositoryTextValue'.freeze,
label: t('libraries.manange_modal_column.labels.text'),
inline: true,
checked: checked?(@repository_column, 'RepositoryTextValue'.freeze),
disabled: disabled?(@repository_column, 'RepositoryTextValue'.freeze) %>
<%= f.radio_button :data_type,
'RepositoryAssetValue'.freeze,
label: t('libraries.manange_modal_column.labels.file'),
inline: true,
checked: checked?(@repository_column, 'RepositoryAssetValue'.freeze),
disabled: disabled?(@repository_column, 'RepositoryAssetValue'.freeze) %>
<%= f.radio_button :data_type,
'RepositoryListValue'.freeze,
label: t('libraries.manange_modal_column.labels.dropdown'),
inline: true,
checked: checked?(@repository_column, 'RepositoryListValue'.freeze),
disabled: disabled?(@repository_column, 'RepositoryListValue'.freeze) %>
<span id="repository-column-types-list">
<%= f.radio_button :data_type,
'RepositoryTextValue'.freeze,
label: t('libraries.manange_modal_column.labels.text'),
inline: true,
checked: checked?(@repository_column, 'RepositoryTextValue'.freeze),
disabled: disabled?(@repository_column, 'RepositoryTextValue'.freeze) %>
<%= f.radio_button :data_type,
'RepositoryAssetValue'.freeze,
label: t('libraries.manange_modal_column.labels.file'),
inline: true,
checked: checked?(@repository_column, 'RepositoryAssetValue'.freeze),
disabled: disabled?(@repository_column, 'RepositoryAssetValue'.freeze) %>
<%= f.radio_button :data_type,
'RepositoryListValue'.freeze,
label: t('libraries.manange_modal_column.labels.dropdown'),
inline: true,
checked: checked?(@repository_column, 'RepositoryListValue'.freeze),
disabled: disabled?(@repository_column, 'RepositoryListValue'.freeze) %>
</span>
<% end %>
<input class="form-control"
data-role="tagsinput"

View file

@ -0,0 +1,19 @@
<% if asset.file.processing? && display_image_tag && asset.is_image? %>
<%= image_tag 'medium/processing.gif' %>
<span>
<%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
</span>
<% else %>
<% if asset.is_image? && display_image_tag %>
<%= image_tag asset.url(:medium) %>
<% end %>
<% if display_image_tag %>
<p>
<%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
</p>
<% else %>
<span>
<%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
</span>
<% end %>
<% end %>

View file

@ -0,0 +1,47 @@
<% if MarvinJsService.enabled? %>
<div id="MarvinJsModal"
class="modal modal-marvin-js"
role="dialog"
aria-labelledby="marvinJsModal"
aria-hidden="true"
data-backdrop="static"
data-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="preview-close" data-dismiss="modal"><span class="fas fa-times"></span></button>
<span class="file-name">
<%= t('marvinjs.modal_name_title') %>
<%= text_field_tag :sketch_name, '', placeholder: t('marvinjs.structure_placeholder') %>
</span>
<p class="file-save-link"><span class="fas fa-save"></span> <%= t('SaveClose')%></p>
</div>
<div class="modal-body">
<div id="marvinjs-editor" data-marvinjs-mode="<%= ENV['MARVINJS_API_KEY'] ? 'remote' : 'local' %>">
<% if ENV['MARVINJS_API_KEY'] %>
<div id="marvinjs-sketch" style="width: 600px; height: 480px"></div>
<% elsif ENV['MARVINJS_URL'] %>
<iframe id="marvinjs-sketch" src="<%= MarvinJsService.url %>" frameBorder="0"></iframe>
<% end %>
</div>
</div>
</div>
</div>
</div>
<% else %>
<div class="modal" id="MarvinJsPromoModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">
MarvinJS Promo Title
</h4>
</div>
<div class="modal-body">
MarvinJS Promo Description
</div>
</div>
</div>
</div>
<% end %>

View file

@ -1,6 +1,6 @@
<div class="comments-container" data-object-id = <%= object.id %>>
<% per_page = Constants::COMMENTS_SEARCH_LIMIT %>
<div class="content-comments">
<div class="content-comments inline_scroll_block">
<% if comments.size == per_page %>
<div class="comment-more text-center">
<a class="btn btn-default btn-more-comments-new"

View file

@ -1,6 +1,8 @@
<% user_comment = comment.user == current_user %>
<% report = false unless defined?(report) %>
<% readonly = false unless defined?(readonly) %>
<% edit_mode = (comment.user == current_user && !readonly) %>
<div
class="comment-container <%= user_comment ? 'comment-editable-field' : '' %>"
class="comment-container <%= edit_mode ? 'comment-editable-field' : '' %> <%= report ? 'report' : '' %>"
data-field-to-update="message"
data-params-group="comment"
data-path-to-update="<%= comment_action_url(comment) %>"
@ -10,15 +12,19 @@
error="false"
>
<div class="avatar-placehodler">
<span class='global-avatar-container'>
<%= image_tag avatar_path(comment.user, :icon_small), class: 'avatar' %>
<span class='global-avatar-container'>
<% if report %>
<%= image_tag comment.user.avatar_base64(:icon_small), class: 'avatar' %>
<% else %>
<%= image_tag avatar_path(comment.user, :icon_small), class: 'avatar' %>
<% end %>
</span>
</div>
<div class="content-placeholder">
<div class="comment-name"><%= comment.user.full_name %></div>
<div class="comment-right">
<div class="comment-datetime"><%= l(comment.created_at, format: :full) %></div>
<% if user_comment %>
<% if edit_mode %>
<div class="comment-actions">
<div class="edit-buttons">
<span class="save-button"><i class="fas fa-save"></i><%= t('general.save') %></span>
@ -39,7 +45,7 @@
</div>
<div class="comment-message">
<div class="view-mode"><%= custom_auto_link(comment.message, team: current_team, simple_format: true).html_safe %></div>
<% if user_comment %>
<% if edit_mode %>
<%= text_area_tag 'message', comment.message, disabled: true, class: 'smart-text-area hidden' %>
<% end %>
</div>

View file

@ -87,4 +87,4 @@
<%= t("protocols.steps.new.add_table") %>
<% end %>
</div>
</div>
</div>

View file

@ -1,3 +1,4 @@
<div class="pseudo-attachment-container" style="order: <%= assets_count - i %>">
<%= link_to download_asset_path(asset),
class: 'file-preview-link',
@ -5,8 +6,8 @@
data: { no_turbolink: true,
id: true,
'preview-url': asset_file_preview_path(asset),
'order-atoz': az_ordered_assets_index(step, asset.id),
'order-ztoa': assets_count - az_ordered_assets_index(step, asset.id),
'order-atoz': order_atoz,
'order-ztoa': order_ztoa,
'order-old': i,
'order-new': assets_count - i,
} do %>

View file

@ -1,4 +1,4 @@
<% assets = ordered_assets step %>
<% assets = ordered_assets(step) %>
<div class="col-xs-12">
<hr>
</div>
@ -6,26 +6,32 @@
<div class="title">
<h4>
<%= t('protocols.steps.files', count: assets.count) %>
<%= t('protocols.steps.files', count: assets.length) %>
</h4>
</div>
<div>
<div class="attachemnts-header pull-right">
<% if !(preview) && (can_manage_protocol_in_module?(@protocol) ||
can_manage_protocol_in_repository?(@protocol)) %>
<%= render partial: '/assets/marvinjs/create_marvin_sketch_button.html.erb',
locals: { element_id: step.id, element_type: 'Step', sketch_container: ".attachments#att-#{step.id}" } %>
<%= render partial: '/assets/wopi/create_wopi_file_button.html.erb',
locals: { element_id: step.id, element_type: 'Step' } %>
<% end %>
<div class="dropdown attachments-order" id="dd-att-step-<%= step.id %>">
<button class="btn btn-default dropdown-toggle" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span id="dd-att-step-<%= step.id %>-label"><%= t('protocols.steps.attachments.sort_new').html_safe %></span>
<span id="dd-att-step-<%= step.id %>-label">
<%= t("protocols.steps.attachments.sort.#{step.current_view_state(current_user).state.dig('assets', 'sort')}_html") %>
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="sortMenu">
<ul class="dropdown-menu" aria-labelledby="sortMenu" data-state-save-path="<%= update_view_state_step_path(step.id) %>">
<% ['new', 'old', 'atoz', 'ztoa'].each do |sort| %>
<li>
<a data-order="<%= sort %>" onClick="reorderAttachments('<%= step.id %>', '<%= sort %>')">
<%= t('protocols.steps.attachments.sort_' + sort ).html_safe %></a>
<a data-order="<%= sort %>" onClick="reorderAttachments(this, '<%= step.id %>', '<%= sort %>')">
<%= t("protocols.steps.attachments.sort.#{sort}_html") %>
</a>
</li>
<% end %>
</ul>
@ -36,8 +42,10 @@
<div class="col-xs-12 attachments" id="att-<%= step.id %>">
<% assets.each_with_index do |asset, i| %>
<% order_atoz = az_ordered_assets_index(step, asset.id) %>
<% order_ztoa = assets.length - az_ordered_assets_index(step, asset.id) %>
<%= render partial: 'steps/attachments/item.html.erb',
locals: { asset: asset, i: i, assets_count: assets.count, step: step } %>
locals: { asset: asset, i: i, assets_count: assets.length, step: step, order_atoz: order_atoz, order_ztoa: order_ztoa } %>
<% end %>
</div>
<hr>

View file

@ -64,6 +64,9 @@ class Constants
# Max characters for repository name in Atwho modal
ATWHO_REP_NAME_LIMIT = 16
# Number of protocols in recent protocol dropdown
RECENT_PROTOCOL_LIMIT = 14
#=============================================================================
# File and data memory size
#=============================================================================

View file

@ -218,19 +218,22 @@ class Extends
export_protocol_from_task: 106,
import_inventory_items: 107,
create_tag: 108,
delete_tag: 109
delete_tag: 109,
edit_image_on_result: 110,
edit_image_on_step: 111,
edit_image_on_step_in_repository: 112,
}
ACTIVITY_GROUPS = {
projects: [*0..7, 32, 33, 34, 95, 108, 65, 109],
task_results: [23, 26, 25, 42, 24, 40, 41, 99],
task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110],
task: [8, 58, 9, 59, 10, 11, 12, 13, 14, 35, 36, 37, 53, 54, *60..64, *66..69, 106],
task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 45, 46, 47],
task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 111, 45, 46, 47],
task_inventory: [55, 56],
experiment: [*27..31, 57],
reports: [48, 50, 49],
inventories: [70, 71, 105, 72, 73, 74, 102, 75, 76, 77, 78, 96, 107],
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82, 83, 101],
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82, 83, 101, 112],
team: [92, 94, 93, 97, 104]
}.freeze
end

View file

@ -75,6 +75,8 @@ en:
attributes:
viewable_id:
not_unique: "State already exists for this user and parent object"
state:
wrong_state: "Wrong parameters"
helpers:
label:
@ -606,6 +608,7 @@ en:
no_description: "No task description"
description_label: "Description"
empty_description_edit_label: "Click here to enter Task Description (optional)"
recent_protocols_from_repository: "Recent protocols from the Repository"
protocols:
head_title: "%{project} | %{module} | Protocols"
protocol_status_bar:
@ -1331,6 +1334,9 @@ en:
wupi_file_editing:
started: "editing started"
finished: "editing finished"
file_editing:
started: "editing started"
finished: "editing finished"
protocols:
my_to_team_message: 'My protocols to Team protocols'
team_to_my_message: 'Team protocols to My protocols'
@ -1759,10 +1765,11 @@ en:
complete_title: "Complete Step"
uncomplete_title: "Uncomplete Step"
attachments:
sort_new: "Newest first &#8595;"
sort_old: "Oldest first &#8593;"
sort_atoz: "Name &#8595;"
sort_ztoa: "Name &#8593;"
sort:
new_html: "Newest first &#8595;"
old_html: "Oldest first &#8593;"
atoz_html: "Name &#8595;"
ztoa_html: "Name &#8593;"
new:
add_step_title: "Add new step"
tab_checklists: "Checklists"
@ -2147,3 +2154,9 @@ en:
new: "https://support.scinote.net/hc/en-us/articles/360004627792"
visibility: "https://support.scinote.net/hc/en-us/articles/360004627472"
manage_columns: "https://support.scinote.net/hc/en-us/articles/360004695831"
marvinjs:
new_sketch: "New structure"
new_button: "New structure"
structure_placeholder: "Click here to enter structure name"
modal_name_title: "Structure name:"
checmical_drawing: "Chemical drawings"

View file

@ -131,6 +131,9 @@ en:
edit_tag_html: "%{user} edited tag <strong>%{tag}</strong> in project %{project}."
delete_tag_html: "%{user} deleted tag <strong>%{tag}</strong> in project %{project}."
import_inventory_items_html: "%{user} imported %{num_of_items} inventory item(s) to %{repository}."
edit_image_on_result_html: "%{user} edited image %{asset_name} on result %{result}: %{action}."
edit_image_on_step_html: "%{user} edited image %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}: %{action}."
edit_image_on_step_in_repository_html: "%{user} edited image %{asset_name} on protocol %{protocol}'s step %{step_position} %{step} in Protocol repository: %{action}."
activity_name:
create_project: "Project created"
@ -233,6 +236,9 @@ en:
edit_tag: "Tag edited"
delete_tag: "Tag deleted"
import_inventory_items: "Inventory items imported"
edit_image_on_result: "Image on result edited"
edit_image_on_step: "Image on task step edited"
edit_image_on_step_in_repository: "Image on step edited"
activity_group:
projects: "Projects"

View file

@ -433,6 +433,7 @@ Rails.application.routes.draw do
post 'toggle_step_state'
get 'move_down'
get 'move_up'
post 'update_view_state'
end
end
@ -448,7 +449,16 @@ Rails.application.routes.draw do
end
# tinyMCE image uploader endpoint
post '/tinymce_assets', to: 'tiny_mce_assets#create', as: :tiny_mce_assets
resources :tiny_mce_assets, only: [:create] do
member do
get :download
get :marvinjs, to: 'tiny_mce_assets#marvinjs_show'
put :marvinjs, to: 'tiny_mce_assets#marvinjs_update'
end
collection do
post :marvinjs, to: 'tiny_mce_assets#marvinjs_create'
end
end
resources :results, only: [:update, :destroy] do
resources :result_comments,
@ -516,6 +526,7 @@ Rails.application.routes.draw do
to: 'protocols#protocolsio_import_create'
post 'protocolsio_import_save', to: 'protocols#protocolsio_import_save'
get 'export', to: 'protocols#export'
get 'recent_protocols'
end
end
@ -589,6 +600,7 @@ Rails.application.routes.draw do
post 'files/create_wopi_file',
to: 'assets#create_wopi_file',
as: 'create_wopi_file'
post 'files/:id/start_edit_image', to: 'assets#create_start_edit_image_activity', as: 'start_edit_image'
devise_scope :user do
get 'avatar/:id/:style' => 'users/registrations#avatar', as: 'avatar'
@ -672,6 +684,12 @@ Rails.application.routes.draw do
end
end
resources :marvin_js_assets, only: %i(create update destroy show) do
collection do
get :team_sketches
end
end
post 'global_activities', to: 'global_activities#index'
constraints WopiSubdomain do

View file

@ -3,7 +3,7 @@
module Paperclip
class CustomFilePreview < Processor
def make
libreoffice_path = ENV['LIBREOFFICE_PATH'] || 'libreoffice'
libreoffice_path = ENV['LIBREOFFICE_PATH'] || 'soffice'
directory = File.dirname(@file.path)
basename = File.basename(@file.path, '.*')
original_preview_file = File.join(directory, "#{basename}.png")

View file

@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'rails_helper'
describe AssetsController, type: :controller do
login_user
let(:user) { subject.current_user }
let!(:team) { create :team, created_by: user }
let(:user_team) { create :user_team, :admin, user: user, team: team }
let!(:user_project) { create :user_project, :owner, user: user }
let(:project) do
create :project, team: team, user_projects: [user_project]
end
let(:experiment) { create :experiment, project: project }
let(:my_module) { create :my_module, name: 'test task', experiment: experiment }
let(:protocol) do
create :protocol, my_module: my_module, team: team, added_by: user
end
let(:step) { create :step, protocol: protocol, user: user }
let(:step_asset_task) { create :step_asset, step: step }
let(:result) do
create :result, name: 'test result', my_module: my_module, user: user
end
let(:result_asset) { create :result_asset, result: result }
let(:protocol_in_repository) { create :protocol, :in_public_repository, team: team }
let(:step_in_repository) { create :step, protocol: protocol_in_repository, user: user }
let!(:asset) { create :asset }
let(:step_asset_in_repository) { create :step_asset, step: step_in_repository, asset: asset }
describe 'POST start_edit' do
before do
allow(controller).to receive(:check_edit_permission).and_return(true)
end
let(:action) { post :create_start_edit_image_activity, params: params, format: :json }
let!(:params) do
{ id: nil }
end
it 'calls create activity service (start edit image on step)' do
params[:id] = step_asset_task.asset.id
expect(Activities::CreateActivityService).to receive(:call)
.with(hash_including(activity_type: :edit_image_on_step))
action
end
it 'calls create activity service (start edit image on result)' do
params[:id] = result_asset.asset.id
expect(Activities::CreateActivityService).to receive(:call)
.with(hash_including(activity_type: :edit_image_on_result))
action
end
it 'calls create activity service (start edit image on step in repository)' do
params[:id] = step_asset_in_repository.asset.id
user_team
expect(Activities::CreateActivityService).to receive(:call)
.with(hash_including(activity_type: :edit_image_on_step_in_repository))
action
end
it 'adds activity in DB' do
params[:id] = step_asset_task.asset.id
expect { action }
.to(change { Activity.count })
end
end
end

View file

@ -0,0 +1,108 @@
# frozen_string_literal: true
require 'rails_helper'
describe WopiController, type: :controller do
ENV['WOPI_USER_HOST'] = 'localhost'
login_user
let(:user) { subject.current_user }
let!(:team) { create :team, created_by: user }
let(:user_team) { create :user_team, :admin, user: user, team: team }
let!(:user_project) { create :user_project, :owner, user: user }
let(:project) do
create :project, team: team, user_projects: [user_project]
end
let(:experiment) { create :experiment, project: project }
let(:my_module) { create :my_module, name: 'test task', experiment: experiment }
let(:result) do
create :result, name: 'test result', my_module: my_module, user: user
end
let(:protocol) do
create :protocol, my_module: my_module, team: team, added_by: user
end
let(:step) { create :step, protocol: protocol, user: user }
let(:protocol_in_repository) { create :protocol, :in_public_repository, team: team }
let(:step_in_repository) { create :step, protocol: protocol_in_repository, user: user }
let!(:asset) { create :asset }
let(:step_asset) { create :step_asset, step: step, asset: asset }
let(:step_asset_in_repository) { create :step_asset, step: step_in_repository, asset: asset }
let(:result_asset) { create :result_asset, result: result, asset: asset }
let(:token) { Token.create(token: 'token', ttl: 0, user_id: user.id) }
describe 'POST unlock' do
before do
token
ENV['WOPI_SUBDOMAIN'] = nil
allow(controller).to receive(:verify_proof!).and_return(true)
@request.headers['X-WOPI-Override'] = 'UNLOCK'
@request.headers['X-WOPI-Lock'] = 'lock'
asset.lock_asset('lock')
end
let(:action) { post :post_file_endpoint, params: { id: asset.id, access_token: 'token' } }
describe 'Result asset' do
before do
result_asset
end
it 'calls create activity for finish wopi editing' do
expect(Activities::CreateActivityService)
.to(receive(:call)
.with(hash_including(activity_type:
:edit_wopi_file_on_result)))
action
end
it 'adds activity in DB' do
expect { action }
.to(change { Activity.count })
end
end
describe 'Step asset' do
before do
step_asset
end
it 'calls create activity for finish wopi editing' do
expect(Activities::CreateActivityService)
.to(receive(:call)
.with(hash_including(activity_type:
:edit_wopi_file_on_step)))
action
end
it 'adds activity in DB' do
expect { action }
.to(change { Activity.count })
end
end
describe 'Step asset in repository' do
before do
step_asset_in_repository
user_team
end
it 'calls create activity for finish wopi editing' do
expect(Activities::CreateActivityService)
.to(receive(:call)
.with(hash_including(activity_type:
:edit_wopi_file_on_step_in_repository)))
action
end
it 'adds activity in DB' do
expect { action }
.to(change { Activity.count })
end
end
end
end

View file

@ -2,11 +2,8 @@
FactoryBot.define do
factory :asset do
file_file_name { 'sample_file.txt' }
file_content_type { 'text/plain' }
file_file_size { 69 }
version { 1 }
estimated_size { 232 }
file_processing { false }
file do
fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'test.jpg'), 'image/jpg')
end
end
end

Some files were not shown because too many files have changed in this diff Show more