Merge branch 'master' into features/marvinjs-integration

This commit is contained in:
Anton Ignatov 2019-05-16 09:28:48 +02:00
commit 259b75a6ba
98 changed files with 875 additions and 15314 deletions

View file

@ -82,10 +82,10 @@ integration-tests:
@$(MAKE) rails cmd="bundle exec cucumber"
tests-ci:
@docker-compose run --rm web bash -c "bundle install && npm install"
@docker-compose run --rm web bash -c "bundle install && yarn install"
@docker-compose up -d webpack
@docker-compose ps
@docker-compose run -e ENABLE_EMAIL_CONFIRMATIONS=false -e MAILER_PORT=$MAILER_PORT -e SMTP_DOMAIN=$SMTP_DOMAIN -e SMTP_USERNAME=$SMTP_USERNAME -e SMTP_PASSWORD=$SMTP_PASSWORD -e SMTP_ADDRESS=$SMTP_ADDRESS -e PAPERCLIP_HASH_SECRET=PAPERCLIP_HASH_SECRET -e MAIL_SERVER_URL=localhost -e PAPERCLIP_STORAGE=filesystem -e ENABLE_RECAPTCHA=false -e ENABLE_USER_CONFIRMATION=false -e ENABLE_USER_REGISTRATION=true -e CORE_API_RATE_LIMIT=1000000 --rm web bash -c "rake db:create db:migrate && rake db:migrate RAILS_ENV=test && npm install && bundle exec rspec && bundle exec cucumber"
@docker-compose run -e ENABLE_EMAIL_CONFIRMATIONS=false -e MAILER_PORT=$MAILER_PORT -e SMTP_DOMAIN=$SMTP_DOMAIN -e SMTP_USERNAME=$SMTP_USERNAME -e SMTP_PASSWORD=$SMTP_PASSWORD -e SMTP_ADDRESS=$SMTP_ADDRESS -e PAPERCLIP_HASH_SECRET=PAPERCLIP_HASH_SECRET -e MAIL_SERVER_URL=localhost -e PAPERCLIP_STORAGE=filesystem -e ENABLE_RECAPTCHA=false -e ENABLE_USER_CONFIRMATION=false -e ENABLE_USER_REGISTRATION=true -e CORE_API_RATE_LIMIT=1000000 --rm web bash -c "rake db:create db:migrate && rake db:migrate RAILS_ENV=test && yarn install && bundle exec rspec && bundle exec cucumber"
console:
@$(MAKE) rails cmd="rails console"

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 53.59 64.356" width="53.589577" height="64.356148">
<g transform="translate(-216.07358,-549.28882)">
<g transform="matrix(1.8232952,0,0,1.8232952,-597.71681,-124.12247)">
<g transform="translate(0,-91.137241)">
<g fill="#eb3c00" transform="matrix(0.74069815,0,0,0.74069815,98.5698,-8.2505871)">
<path d="m469.87,671.03,0-28.52,25.229-9.3238,13.711,4.3877,0,38.392-13.711,4.133-25.229-9.0691,25.229,3.0361,0-33.201-16.454,3.8392,0,22.487z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 627 B

View file

@ -1,6 +1,10 @@
/* global animateSpinner FilePreviewModal */
function setupAssetsLoading() {
var DELAY = 2500;
var REPETITIONS = 60;
var cntr = 0;
var intervalId;
function refreshAssets() {
var elements = $("[data-status='asset-loading']");
@ -15,50 +19,55 @@ function setupAssetsLoading() {
// Perform an AJAX call to present URL
// to check if file already exists
$.ajax({
url: $el.data("present-url"),
type: "GET",
dataType: "json",
success: function (data) {
var wopiBtns;
$el.attr("data-status", "asset-loaded");
url: $el.data('present-url'),
type: 'GET',
dataType: 'json',
success: function(data) {
if (data.processing === true) {
return;
}
if (data.processing === false) {
$el.html(data.placeholder_html);
$el.attr('data-status', 'asset-loaded');
return;
}
$el.attr('data-status', 'asset-loaded');
$el.find('img').hide();
$el.next().hide();
$el.html("");
$el.html('');
if (data.type === 'image') {
$el.html(
"<a class='file-preview-link' id='modal_link" +
data['asset-id'] + "' data-status='asset-present' " +
"href='" + data['download-url'] + "' data-preview-url='" +
data['preview-url'] + "'>" +
"<img src='" + data['image-tag-url'] + "'><p>" +
data.filename + '</p></a>'
"<a class='file-preview-link' id='modal_link"
+ data['asset-id'] + "' data-status='asset-present' "
+ "href='" + data['download-url'] + "' data-preview-url='" + data['preview-url'] + "'>"
+ "<img src='" + data['image-tag-url'] + "'><p>" + data.filename + '</p></a>'
);
} else {
$el.html(
"<a class='file-preview-link' id='modal_link" +
data['asset-id'] + "' data-status='asset-present' " +
"href='" + data['download-url'] + "' data-preview-url='" +
data['preview-url'] + "'><p>" +
data.filename + '</p></a>'
"<a class='file-preview-link' id='modal_link"
+ data['asset-id'] + "' data-status='asset-present' "
+ "href='" + data['download-url'] + "' data-preview-url='"
+ data['preview-url'] + "'><p>" + data.filename + '</p></a>'
);
}
animateSpinner(null, false);
FilePreviewModal.init();
},
error: function(data) {
if (data.status == 403) {
if (data.status === 403) {
$el.find('img').hide();
$el.next().hide();
// Image/file exists, but user doesn't have
// rights to download it
if (type === "image") {
if (data.type === 'image') {
$el.html(
"<img src='" + data['image-tag-url'] + "'><p>" +
data.filename + "</p>"
"<img src='" + data['image-tag-url'] + "'><p>" + data.filename + '</p>'
);
} else {
$el.html("<p>" + data.filename + "</p>");
$el.html('<p>' + data.filename + '</p>');
}
} else {
// Do nothing, file is not yet present
@ -76,14 +85,15 @@ function setupAssetsLoading() {
$.each(elements, function(_, el) {
var $el = $(el);
$el.attr("data-status", "asset-failed");
$el.html($el.data("filename"));
$el.attr('data-status', 'asset-failed');
if ($el.data('filename')) {
$el.html($el.data('filename'));
}
});
}
var cntr = 0;
var intervalId = window.setInterval(function() {
cntr++;
intervalId = window.setInterval(function() {
cntr += 1;
if (cntr >= REPETITIONS || !refreshAssets()) {
finalizeAssets();
window.clearInterval(intervalId);

View file

@ -20,9 +20,8 @@ function initCreateWopiFileModal() {
$('#new-office-file-modal form')
.on('ajax:success', function(ev, data) {
window.open(data.edit_url, '_blank');
$('#new-office-file-modal').modal('hide');
// location.reload();
window.focus();
location.reload(); // Reload current page, to display the new element
})
.on('ajax:error', function(ev, response) {
var element;

View file

@ -206,6 +206,24 @@ var Comments = (function() {
$form.submit();
});
$form.find('textarea').on('focus', function(){
$(this).addClass('border');
if (this.value.length > 0) {
$submitBtn.addClass('show');
}
}).on('blur',function(){
if (this.value.length == 0) {
$(this).removeClass('border');
$submitBtn.removeClass('show');
}
}).on('keyup',function(){
if (this.value.length > 0) {
$submitBtn.addClass('show');
} else {
$submitBtn.removeClass('show');
}
})
$('.help-block', $form).addClass('hide');
$form.off().on('ajax:send', function () {
@ -220,6 +238,7 @@ var Comments = (function() {
$('.form-group', $form).removeClass('has-error');
$('.help-block', $form).html('').addClass('hide');
$submitBtn.removeClass('has-error');
$submitBtn.removeClass('show');
var currnetCount = $('#counter-' + stepId).html()
$('#counter-' + stepId).html(parseInt(currnetCount) + 1)

View file

@ -17,7 +17,6 @@ function init() {
initLoadFromRepository();
initRefreshStatusBar();
initImport();
setupAssetsLoading();
}
function initEditMyModuleDescription() {
@ -170,6 +169,10 @@ function initLinkUpdate() {
});
});
}
$('[data-role="protocol-status-bar"] .preview-protocol').click(function(e) {
e.preventDefault();
});
}
function initLoadFromRepository() {

View file

@ -293,17 +293,16 @@ function importProtocolFromFile(
stepGuid,
element.getAttribute('fileref'));
if (description.includes('[~tiny_mce_id')) {
// old format load
imageTag = '<img style="max-width:300px; max-height:300px;" src="data:' + element.children[1].innerHTML + ';base64,' + assetBytes + '" />';
description = description.replace(match, imageTag);
} else {
// new format load
description = $('<div>' + description + '</div>').find('img[data-mce-token="' + element.getAttribute('tokenId') + '"]')
.attr('src', 'data:' + element.children[1].innerHTML + ';base64,' + assetBytes).prop('outerHTML');
}
});
// new format load
description = $('<div>' + description + '</div>');
description.find('img[data-mce-token="' + element.getAttribute('tokenId') + '"]')
.attr('src', 'data:' + element.children[1].innerHTML + ';base64,' + assetBytes);
description = description.prop('outerHTML');
// old format load
imageTag = '<img style="max-width:300px; max-height:300px;" src="data:' + element.children[1].innerHTML + ';base64,' + assetBytes + '" />';
description = description.replace(match, imageTag);
});
// I know is crazy but is the only way I found to pass valid HTML
return $('<div></div>').html(description).html();
}

View file

@ -180,7 +180,7 @@ function initializeNewElement(newEl) {
switch (parent.data("type")) {
case "experiment":
url = dh.data("add-experiment-contents-url"); break;
case 'my_module' || 'protocol':
case 'my_module':
url = dh.data("add-module-contents-url"); break;
case "step":
url = dh.data("add-step-contents-url"); break;

View file

@ -1,7 +1,12 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "initInlineEditing" }]*/
/* global SmartAnnotation */
function initInlineEditing(title) {
var editBlocks = $('.' + title + '-editable-field');
function prepareText(text) {
return text.replace(/(?:\r\n|\r|\n)/g, '<br>');
}
$.each(editBlocks, function(i, element) {
var editBlock = element;
var $editBlock = $(editBlock);
@ -12,19 +17,16 @@ function initInlineEditing(title) {
$editBlock.addClass('inline-edit-active');
if ($inputString.length === 0) {
$inputString = $editBlock.find('textarea');
$inputString.off('keydown').on('keydown', function() {
var el = this;
setTimeout(() => {
el.style.cssText = 'height:0px; padding:0';
el.style.cssText = 'height:' + (el.scrollHeight + 10) + 'px';
}, 0);
});
$inputString.keydown();
}
inputString = $inputString[0]
function cancelAllEditFields() {
$('.inline-edit-active').find('.cancel-button').click();
inputString = $inputString[0];
if (editBlock.dataset.smartAnnotation === 'true') {
SmartAnnotation.init($inputString);
}
function saveAllEditFields() {
$('.inline-edit-active').find('.save-button').click();
}
function updateField() {
@ -33,6 +35,8 @@ function initInlineEditing(title) {
if (inputString.value === editBlock.dataset.originalName) {
inputString.disabled = true;
editBlock.dataset.editMode = 0;
$inputString.addClass('hidden');
$editBlock.find('.view-mode').removeClass('hidden');
return false;
}
params[editBlock.dataset.paramsGroup] = {};
@ -42,9 +46,21 @@ function initInlineEditing(title) {
type: 'PUT',
dataType: 'json',
data: params,
success: function() {
success: function(result) {
var viewData;
if (editBlock.dataset.responseField) {
// If we want to modify preview element on backend
// we can use this data field and we will take string from response
viewData = result[editBlock.dataset.responseField];
} else {
// By default we just copy value from input string
viewData = inputString.value;
}
editBlock.dataset.originalName = inputString.value;
editBlock.dataset.error = false;
$inputString.addClass('hidden');
$editBlock.find('.view-mode').html(prepareText(viewData)).removeClass('hidden');
inputString.disabled = true;
editBlock.dataset.editMode = 0;
},
@ -63,21 +79,28 @@ function initInlineEditing(title) {
return true;
}
$editBlock.click(e => {
cancelAllEditFields();
$editBlock.click((e) => {
// 'A' mean that, if we click on <a></a> element we will not go in edit mode
if (e.target.tagName === 'A') return true;
if (inputString.disabled) {
saveAllEditFields();
editBlock.dataset.editMode = 1;
inputString.disabled = false;
$inputString.removeClass('hidden');
$editBlock.find('.view-mode').addClass('hidden');
$inputString.focus();
}
e.stopPropagation();
return true;
});
$(window).click(() => {
$(window).click((e) => {
if ($(e.target).closest('.atwho-view').length > 0) return false;
if (inputString.disabled === false) {
updateField();
}
editBlock.dataset.editMode = 0;
return true;
});
$($editBlock.find('.save-button')).click(e => {
@ -90,6 +113,8 @@ function initInlineEditing(title) {
editBlock.dataset.editMode = 0;
editBlock.dataset.error = false;
inputString.value = editBlock.dataset.originalName;
$inputString.addClass('hidden');
$editBlock.find('.view-mode').removeClass('hidden');
$inputString.keydown();
e.stopPropagation();
});

View file

@ -190,7 +190,7 @@
$('.attachment-placeholder.new').remove();
_dragNdropAssetsOff();
for(var i = 0; i < droppedFiles.length; i++) {
$('.attacments.edit')
$('.attachments.edit')
.append(_uploadedAssetPreview(droppedFiles[i], i))
.promise()
.done(function() {

View file

@ -308,7 +308,7 @@ var FilePreviewModal = (function() {
};
var mousePosition = {
top: e.clientY - $(imageEditorWindow).offset().top,
top: e.clientY - (imageEditorWindow.offsetTop - scrollContainerInitial.top),
left: e.clientX - $(imageEditorWindow).offset().left
};
@ -452,7 +452,7 @@ var FilePreviewModal = (function() {
link.attr('data-status', 'asset-present');
if (data.type === 'image') {
if (data.processing) {
animateSpinner('.file-preview-container', true);
modal.find('.file-preview-container').append(data['processing-img']);
} else {
animateSpinner('.file-preview-container', false);
modal.find('.file-preview-container')

View file

@ -1,4 +1,8 @@
/* global _ hljs tinyMCE SmartAnnotation MarvinJsEditor FilePreviewModal */
/* global _ I18n */
/* eslint-disable no-unused-vars */
var TinyMCE = (function() {
@ -16,26 +20,6 @@ var TinyMCE = (function() {
});
}
function moveToolbar(editor, editorToolbar, editorToolbaroffset) {
var scrollPosition = $(window).scrollTop();
var containerOffset;
var containerHeight;
var toolbarPosition;
var toolbarPositionLimit;
if (editor.getContainer() === null) return;
containerOffset = $(editor.getContainer()).offset().top;
containerHeight = $(editor.getContainer()).height();
toolbarPosition = scrollPosition - containerOffset + editorToolbaroffset;
toolbarPositionLimit = containerHeight - editorToolbaroffset;
if (toolbarPosition > 0 && toolbarPosition < toolbarPositionLimit) {
editorToolbar.css('top', toolbarPosition + 'px');
} else {
editorToolbar.css(
'top',
toolbarPosition < 0 ? '0px' : toolbarPositionLimit + 'px'
);
}
}
function initImageToolBar(editor) {
var editorForm = $(editor.getContainer()).closest('form');
@ -207,14 +191,17 @@ var TinyMCE = (function() {
var editorContainer = $(editor.getContainer());
var menuBar = editorForm.find('.mce-menubar.mce-toolbar.mce-first .mce-flow-layout');
var editorToolbar = editorForm.find('.mce-top-part');
var editorToolbaroffset = mceConfig.toolbar_offset || 120;
var editorIframe = $('#' + editor.id).prev().find('.mce-edit-area iframe');
var editorToolbaroffset;
$('.tinymce-placeholder').css('height', $(editor.editorContainer).height() + 'px');
setTimeout(() => {
$(editor.editorContainer).addClass('show');
$('.tinymce-placeholder').remove();
moveToolbar(editor, editorToolbar, editorToolbaroffset);
}, 400);
// Init saved status label
@ -222,10 +209,16 @@ var TinyMCE = (function() {
editorForm.find('.tinymce-status-badge').removeClass('hidden');
}
// Init Floating toolbar
$(window).on('scroll', function() {
moveToolbar(editor, editorToolbar, editorToolbaroffset);
});
if ($('.navbar-secondary').length) {
editorToolbaroffset = $('.navbar-secondary').position().top + $('.navbar-secondary').height();
} else if ($('#main-nav').length) {
editorToolbaroffset = $('#main-nav').height();
} else {
editorToolbaroffset = 0;
}
editorToolbar.css('position', 'sticky');
editorToolbar.css('top', editorToolbaroffset + 'px');
// Init image toolbar
initImageToolBar(editor);
@ -288,6 +281,7 @@ var TinyMCE = (function() {
editor.selection.collapse(false);
SmartAnnotation.init($(editor.contentDocument.activeElement));
SmartAnnotation.preventPropagation('.atwho-user-popover');
initHighlightjsIframe($(this.iframeElement).contents());
},
setup: function(editor) {
@ -352,3 +346,16 @@ var TinyMCE = (function() {
highlight: initHighlightjs
});
}());
$(document).on('turbolinks:before-visit', function(e) {
_.each(tinyMCE.editors, function(editor) {
if (editor.isNotDirty === false) {
if (confirm(I18n.t('tiny_mce.leaving_warning'))) {
return false;
}
e.preventDefault();
return false;
}
return false;
});
});

View file

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

View file

@ -61,7 +61,7 @@
img {
margin-right: 5px;
width: 23px;
height: 20px;
}
}

View file

@ -14,6 +14,29 @@
}
}
.protocol-info,
.module-header {
.well {
border: 0;
box-shadow: none;
font-size: 14px;
padding-left: 0;
padding-right: 0;
}
.badge-icon {
background: transparent;
color: $color-silver;
padding-left: 0;
padding-right: 5px;
+ .well-sm {
margin-bottom: 5px;
margin-left: 0;
}
}
}
.module-header {
display: inline-block;
position: relative;
@ -46,25 +69,6 @@
}
}
.well {
border: 0;
box-shadow: none;
padding-left: 0;
padding-right: 0;
}
.badge-icon {
background: transparent;
color: $color-silver;
padding-left: 0;
padding-right: 5px;
+ .well-sm {
margin-bottom: 10px;
margin-left: 0;
}
}
.module-description {
float: left;
width: 100%;
@ -76,7 +80,7 @@
.title {
font-size: 22px;
font-weight: bold;
padding: 10px 0 5px;
padding: 20px 0 5px;
}
.my-module-description-content {
@ -97,14 +101,15 @@
display: inline-block;
line-height: 32px;
padding: 0 5px 0 0;
width: 38px;
width: 28px;
}
.tags-title {
display: inline-block;
font-size: 14px;
line-height: 32px;
padding-right: 3px;
width: 35px;
width: 37px;
}
.select-container {
@ -112,7 +117,7 @@
flex-basis: 100px;
flex-grow: 1;
flex-shrink: 1;
max-width: calc(100% - 73px);
max-width: calc(100% - 65px);
position: relative;
select {

View file

@ -54,7 +54,8 @@
}
}
&:hover input {
&:hover input,
&:hover .view-mode {
border: 1px solid $color-silver;
&:disabled {
@ -62,6 +63,15 @@
}
}
.view-mode {
border: 1px solid transparent;
cursor: pointer;
line-height: 26px;
padding: 8px 5px;
padding-right: 36px;
width: calc(100% - 36px);
}
input {
border: 1px solid $color-silver;
border-radius: $border-radius-small;

View file

@ -0,0 +1,17 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
@import "constants";
@import "mixins";
#protocol-preview-modal .modal-dialog {
.modal-body {
max-height: 75vh;
overflow-y: auto;
width: 100%;
.ql-editor {
min-height: initial;
}
}
}

View file

@ -0,0 +1,12 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
@import "constants";
@import "mixins";
.protocol-info {
.protocol-description {
padding: 0 48px;
}
}

View file

@ -93,7 +93,7 @@
overflow: hidden;
strong {
font-size: 14px;
font-size: 16px;
white-space: nowrap;
}
}
@ -103,17 +103,18 @@
}
.author-block {
display: inline-block;
font-size: 16px;
overflow: hidden;
padding-right: 10px;
margin-right: 20px;
white-space: nowrap;
width: 300px;
}
}
}
}
}
.attacments {
.attachments {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
@ -320,6 +321,17 @@
float: left;
width: 100%;
.view-mode {
border: 1px solid transparent;
border-radius: $border-radius-small;
display: inline-block;
line-height: 16px;
min-height: 20px;
overflow: hidden;
padding: 2px 5px;
width: 100%;
}
textarea {
border: 1px solid $color-silver;
border-radius: $border-radius-small;
@ -327,7 +339,6 @@
line-height: 16px;
overflow: hidden;
padding: 2px 5px;
pointer-events: none;
width: 100%;
&:focus {
@ -337,6 +348,7 @@
&:disabled {
background: transparent;
border: 1px solid transparent;
pointer-events: none;
user-select: none;
}
}
@ -412,7 +424,8 @@
width: 220px;
}
textarea:disabled {
textarea:disabled,
.view-mode {
border: 1px solid $color-gainsboro;
cursor: pointer;
}
@ -444,6 +457,13 @@
.error-block {
display: block;
}
.comment-actions {
.edit-buttons {
display: inline !important;
}
}
}
}
@ -478,7 +498,7 @@
outline: none;
}
&:hover {
&.border {
border: 1px solid $color-silver;
}
}
@ -492,8 +512,8 @@
right: -36px;
transition: $md-transaction;
&.has-error {
top: -64px;
&.show {
right: 0;
}
}
@ -502,9 +522,5 @@
textarea {
border: 1px solid $color-silver;
}
.new-comment-button {
right: 0;
}
}
}

View file

@ -915,12 +915,43 @@ ul.content-activities {
border-radius: 0;
}
.panel-protocol-status {
display: inline-block;
margin-bottom: 5px;
.protocol-status-container {
align-items: center;
display: flex;
flex-wrap: wrap;
& > .panel-body {
padding: 5px 5px 5px 15px;
.protocol-button {
margin-bottom: 5px;
}
.protocol-status-bar {
display: flex;
height: 33px;
margin-bottom: 5px;
margin-right: 15px;
}
.panel-protocol-status {
border-color: $color-silver;
box-shadow: none;
display: inline-block;
height: 33px;
& > .panel-body {
padding: 0 0 0 15px;
}
.link-button {
border-radius: 0;
}
.link-button,
.link-toggle {
height: 33px;
position: relative;
right: -1px;
top: -1px;
}
}
}
@ -940,18 +971,6 @@ ul.content-activities {
}
}
#protocol-preview-modal .modal-dialog {
.modal-body {
max-height: 75vh;
overflow-y: auto;
width: 100%;
.ql-editor {
min-height: initial;
}
}
}
/* Import protocol/s modal */
#import-protocol-modal .modal-dialog {
width: 70%;

View file

@ -10,6 +10,10 @@ class ApplicationController < ActionController::Base
around_action :set_time_zone, if: :current_user
layout 'main'
rescue_from ActionController::InvalidAuthenticityToken do
redirect_to root_path
end
def respond_422(message = t('client_api.permission_error'))
respond_to do |format|
format.json do

View file

@ -5,6 +5,7 @@ class AssetsController < ApplicationController
include ActionView::Helpers::TextHelper
include ActionView::Helpers::UrlHelper
include ActionView::Context
include ApplicationHelper
include InputSanitizeHelper
include FileIconsHelper
@ -31,9 +32,8 @@ class AssetsController < ApplicationController
'asset-id' => @asset.id,
'image-tag-url' => @asset.url(:medium),
'preview-url' => asset_file_preview_path(@asset),
'filename' => truncate(@asset.file_file_name,
length:
Constants::FILENAME_TRUNCATION_LENGTH),
'filename' => truncate(escape_input(@asset.file_file_name),
length: Constants::FILENAME_TRUNCATION_LENGTH),
'download-url' => download_asset_path(@asset),
'type' => asset_data_type(@asset)
}, status: 200
@ -42,12 +42,30 @@ class AssetsController < ApplicationController
end
end
def step_file_present
respond_to do |format|
format.json do
if @asset.file.processing?
render json: { processing: true }
else
render json: {
placeholder_html: render_to_string(
partial: 'steps/attachments/placeholder.html.erb',
locals: { asset: @asset, edit_page: false }
),
processing: false
}
end
end
end
end
def file_preview
response_json = {
'id' => @asset.id,
'type' => (@asset.is_image? ? 'image' : 'file'),
'filename' => truncate(@asset.file_file_name,
'filename' => truncate(escape_input(@asset.file_file_name),
length: Constants::FILENAME_TRUNCATION_LENGTH),
'download-url' => download_asset_path(@asset, timestamp: Time.now.to_i)
}
@ -69,7 +87,7 @@ class AssetsController < ApplicationController
'mime-type' => @asset.file.content_type,
'processing' => @asset.file.processing?,
'large-preview-url' => @asset.url(:large),
'processing-url' => image_tag('medium/processing.gif')
'processing-img' => image_tag('medium/processing.gif')
)
else
response_json.merge!(
@ -81,10 +99,10 @@ class AssetsController < ApplicationController
)
end
if wopi_file?(@asset)
if wopi_enabled? && wopi_file?(@asset)
edit_supported, title = wopi_file_edit_button_status
response_json['wopi-controls'] = render_to_string(
partial: 'shared/file_wopi_controlls.html.erb',
partial: 'assets/wopi/file_wopi_controls.html.erb',
locals: {
asset: @asset,
can_edit: can_edit,
@ -168,15 +186,29 @@ class AssetsController < ApplicationController
# Post process file here
@asset.post_process_file(@asset.team)
render_html = if @asset.step
asset_position = @asset.step.asset_position(@asset)
render_to_string(
partial: 'steps/attachments/item.html.erb',
locals: {
asset: @asset,
i: asset_position[:pos],
assets_count: asset_position[:count],
step: @asset.step
},
formats: :html
)
else
render_to_string(
partial: 'shared/asset_link',
locals: { asset: @asset, display_image_tag: true },
formats: :html
)
end
respond_to do |format|
format.json do
render json: {
html: render_to_string(
partial: 'shared/asset_link',
locals: { asset: @asset, display_image_tag: true },
formats: :html
)
}
render json: { html: render_html }
end
end
end
@ -237,12 +269,9 @@ class AssetsController < ApplicationController
@asset = Asset.find_by_id(params[:id])
return render_404 unless @asset
step_assoc = @asset.step
result_assoc = @asset.result
repository_cell_assoc = @asset.repository_cell
@assoc = step_assoc unless step_assoc.nil?
@assoc = result_assoc unless result_assoc.nil?
@assoc = repository_cell_assoc unless repository_cell_assoc.nil?
@assoc ||= @asset.step
@assoc ||= @asset.result
@assoc ||= @asset.repository_cell
if @assoc.class == Step
@protocol = @asset.step.protocol
@ -293,6 +322,7 @@ class AssetsController < ApplicationController
def asset_data_type(asset)
return 'wopi' if wopi_file?(asset)
return 'image' if asset.is_image?
'file'
end
end

View file

@ -1,4 +1,6 @@
class AtWhoController < ApplicationController
include InputSanitizeHelper
before_action :load_vars
before_action :check_users_permissions
@ -55,7 +57,7 @@ class AtWhoController < ApplicationController
format.json do
render json: {
repositories: repositories.map do |r|
[r.id, r.name.truncate(Constants::ATWHO_REP_NAME_LIMIT)]
[r.id, escape_input(r.name.truncate(Constants::ATWHO_REP_NAME_LIMIT))]
end.to_h,
status: :ok
}
@ -122,9 +124,8 @@ class AtWhoController < ApplicationController
res.each do |obj|
tmp = {}
tmp['id'] = obj[0].base62_encode
tmp['full_name'] =
obj[1].truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN)
tmp['email'] = obj[2]
tmp['full_name'] = escape_input(obj[1].truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN))
tmp['email'] = escape_input(obj[2])
tmp['img_url'] = avatar_path(obj[0], :icon_small)
data << tmp
end

View file

@ -70,13 +70,6 @@ module ReportActions
def generate_module_contents_json(my_module)
res = []
res << generate_new_el(false)
el = generate_el(
'reports/elements/my_module_protocol_element.html.erb',
protocol: my_module.protocol
)
res << el
ReportExtends::MODULE_CONTENTS.each do |contents|
elements = []
contents.values.each do |element|

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class GlobalActivitiesController < ApplicationController
include InputSanitizeHelper
def index
# Preload filter format
# {
@ -109,7 +111,7 @@ class GlobalActivitiesController < ApplicationController
.pluck(:id, :name)
next if matched.length.zero?
results[subject] = matched.map { |pr| { id: pr[0], name: pr[1] } }
results[subject] = matched.map { |pr| { id: pr[0], name: escape_input(pr[1]) } }
end
respond_to do |format|
format.json do

View file

@ -458,6 +458,7 @@ class MyModulesController < ApplicationController
repository: @repository.id,
record_names: dowmstream_records[my_module.id].join(', '))
end
records_names.map! { |n| escape_input(n) }
flash = I18n.t('repositories.assigned_records_flash',
records: records_names.join(', '))
flash = I18n.t('repositories.assigned_records_downstream_flash',
@ -515,7 +516,7 @@ class MyModulesController < ApplicationController
record_names: records.map(&:name).join(', '))
flash = I18n.t('repositories.unassigned_records_flash',
records: records.map(&:name).join(', '))
records: records.map { |r| escape_input(r.name) }.join(', '))
respond_to do |format|
format.json { render json: { flash: flash }, status: :ok }
end

View file

@ -104,7 +104,7 @@ class ProjectsController < ApplicationController
up.save
log_activity(:create_project)
message = t('projects.create.success_flash', name: @project.name)
message = t('projects.create.success_flash', name: escape_input(@project.name))
respond_to do |format|
format.json {
render json: { message: message }, status: :ok
@ -136,7 +136,7 @@ class ProjectsController < ApplicationController
def update
return_error = false
flash_error = t('projects.update.error_flash', name: @project.name)
flash_error = t('projects.update.error_flash', name: escape_input(@project.name))
# Check archive permissions if archiving/restoring
if project_params.include? :archived
@ -147,7 +147,7 @@ class ProjectsController < ApplicationController
return_error = true
is_archive = project_params[:archived] == 'true' ? 'archive' : 'restore'
flash_error =
t("projects.#{is_archive}.error_flash", name: @project.name)
t("projects.#{is_archive}.error_flash", name: escape_input(@project.name))
end
elsif !can_manage_project?(@project)
render_403 && return
@ -177,11 +177,11 @@ class ProjectsController < ApplicationController
log_activity(:archive_project) if project_params[:archived] == 'true'
log_activity(:restore_project) if project_params[:archived] == 'false'
flash_success = t('projects.update.success_flash', name: @project.name)
flash_success = t('projects.update.success_flash', name: escape_input(@project.name))
if project_params[:archived] == 'true'
flash_success = t('projects.archive.success_flash', name: @project.name)
flash_success = t('projects.archive.success_flash', name: escape_input(@project.name))
elsif project_params[:archived] == 'false'
flash_success = t('projects.restore.success_flash', name: @project.name)
flash_success = t('projects.restore.success_flash', name: escape_input(@project.name))
end
respond_to do |format|
format.html do

View file

@ -626,7 +626,7 @@ class ProtocolsController < ApplicationController
format.json do
render json: {
name: p_name, new_name: protocol.name, status: :ok
name: escape_input(p_name), new_name: escape_input(protocol.name), status: :ok
},
status: :ok
end

View file

@ -96,7 +96,7 @@ class RepositoryColumnsController < ApplicationController
id: @repository_column.id,
name: escape_input(@repository_column.name),
message: t('libraries.repository_columns.update.success_flash',
name: @repository_column.name)
name: escape_input(@repository_column.name))
}, status: :ok
else
render json: {
@ -137,7 +137,7 @@ class RepositoryColumnsController < ApplicationController
if @repository_column.destroy
render json: {
message: t('libraries.repository_columns.destroy.success_flash',
name: column_name),
name: escape_input(column_name)),
id: column_id,
status: :ok
}

View file

@ -429,6 +429,7 @@ class RepositoryRowsController < ApplicationController
.where(repository_column: cell.repository_column)
.limit(Constants::SEARCH_LIMIT)
.pluck(:id, :data)
.map { |li| [li[0], escape_input(li[1])] }
end
def fetch_columns_list_items
@ -442,6 +443,7 @@ class RepositoryRowsController < ApplicationController
list_items: column.repository_list_items
.limit(Constants::SEARCH_LIMIT)
.pluck(:id, :data)
.map { |li| [li[0], escape_input(li[1])] }
}
end
collection

View file

@ -101,7 +101,8 @@ class StepCommentsController < ApplicationController
# Generate activity
log_activity(:edit_step_comment)
message = custom_auto_link(@comment.message, team: current_team)
message = custom_auto_link(@comment.message, simple_format: false,
tags: %w(img), team: current_team)
render json: { comment: message }, status: :ok
else
render json: { errors: @comment.errors.to_hash(true) },

View file

@ -1,6 +1,11 @@
class Users::SessionsController < Devise::SessionsController
# before_filter :configure_sign_in_params, only: [:create]
after_action :after_sign_in, only: :create
rescue_from ActionController::InvalidAuthenticityToken do
redirect_to new_user_session_path
end
# GET /resource/sign_in
def new
# If user was redirected here from OAuth's authorize/new page (Doorkeeper

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true
class ZipExportsController < ApplicationController
before_action :load_var, only: :download
before_action :load_var_export_all, only: :download_export_all_zip
before_action :check_edit_permissions, only: :download
before_action :load_var, only: %i(download download_export_all_zip)
before_action :check_download_permissions, except: :file_expired
def download
if @zip_export.stored_on_s3?
@ -22,16 +23,11 @@ class ZipExportsController < ApplicationController
private
def load_var
@zip_export = ZipExport.find_by_id(params[:id])
redirect_to(file_expired_url, status: 301) and return unless @zip_export
@zip_export = current_user.zip_exports.find_by_id(params[:id])
redirect_to(file_expired_url, status: 301) and return unless @zip_export&.zip_file&.exists?
end
def load_var_export_all
@zip_export = TeamZipExport.find_by_id(params[:id])
redirect_to(file_expired_url, status: 301) and return unless @zip_export
end
def check_edit_permissions
def check_download_permissions
render_403 unless @zip_export.user == current_user
end
end

View file

@ -221,4 +221,8 @@ module ApplicationHelper
user.avatar(style) == '/images/icon_small/missing.png' ||
user.avatar(style) == '/images/thumb/missing.png'
end
def wopi_enabled?
ENV['WOPI_ENABLED'] == 'true'
end
end

View file

@ -3,7 +3,7 @@ module ProtocolStatusHelper
def protocol_status_href(protocol)
parent = protocol.parent
res = ''
res << '<a href="#" data-toggle="popover" data-html="true" '
res << '<a href="#" data-toggle="popover" data-html="true" class="preview-protocol"'
res << 'data-trigger="focus" data-placement="bottom" title="'
res << protocol_status_popover_title(parent) +
'" data-content="' + protocol_status_popover_content(parent) +
@ -53,7 +53,7 @@ module ProtocolStatusHelper
else
res = "<p>"
if protocol.description.present?
res << protocol.description
res << protocol.tinymce_render(:description)
else
res << "<em>" + I18n.t("my_modules.protocols.protocol_status_bar.no_description") + "</em>"
end
@ -69,6 +69,6 @@ module ProtocolStatusHelper
end
res << "</p>"
end
sanitize_input(res)
escape_input(res)
end
end

View file

@ -8,7 +8,6 @@ module ReportsHelper
def render_report_element(element, provided_locals = nil)
# Determine partial
file_name = element.type_of
if element.type_of.in? ReportExtends::MY_MODULE_CHILDREN_ELEMENTS
file_name = "my_module_#{element.type_of.singularize}"

View file

@ -49,7 +49,7 @@ class Asset < ApplicationRecord
# This could cause some problems if you create empty asset and want to
# assign it to result
validate :step_or_result_or_repository_asset_value
validate :name_should_not_be_empty_without_extension,
validate :wopi_filename_valid,
on: :wopi_file_creation
belongs_to :created_by,
@ -320,7 +320,7 @@ class Asset < ApplicationRecord
end
def url(style = :original, timeout: Constants::URL_SHORT_EXPIRE_TIME)
if file.is_stored_on_s3?
if file.is_stored_on_s3? && !file.processing?
presigned_url(style, timeout: timeout)
else
file.url(style)
@ -521,13 +521,24 @@ class Asset < ApplicationRecord
end
end
def name_should_not_be_empty_without_extension
def wopi_filename_valid
# Check that filename without extension is not blank
unless file.original_filename[0..-6].present?
errors.add(
:file,
I18n.t('general.text.not_blank')
)
end
# Check maximum filename length
if file.original_filename.length > Constants::FILENAME_MAX_LENGTH
errors.add(
:file,
I18n.t(
'general.file.file_name_too_long',
limit: Constants::FILENAME_MAX_LENGTH
)
)
end
end
def cache

View file

@ -15,7 +15,7 @@ module TinyMceImages
description = self[field]
# Check tinymce for old format
description = TinyMceAsset.update_old_tinymce(description)
description = TinyMceAsset.update_old_tinymce(description, self)
tiny_mce_assets.each do |tm_asset|
tmp_f = Tempfile.open(tm_asset.image_file_name, Rails.root.join('tmp'))
@ -27,6 +27,8 @@ module TinyMceImages
tm_asset_to_update = html_description.css(
"img[data-mce-token=\"#{Base62.encode(tm_asset.id)}\"]"
)[0]
next unless tm_asset_to_update
tm_asset_to_update.attributes['src'].value = new_tm_asset_src
description = html_description.css('body').inner_html.to_s
ensure
@ -38,7 +40,7 @@ module TinyMceImages
end
def tinymce_render(field)
TinyMceAsset.generate_url(self[field])
TinyMceAsset.generate_url(self[field], self)
end
# Takes array of old/new TinyMCE asset ID pairs
@ -48,7 +50,7 @@ module TinyMceImages
description = read_attribute(object_field)
# Check tinymce for old format
description = TinyMceAsset.update_old_tinymce(description)
description = TinyMceAsset.update_old_tinymce(description, self)
parsed_description = Nokogiri::HTML(description)
images.each do |image|
@ -57,7 +59,7 @@ module TinyMceImages
image = parsed_description.at_css("img[data-mce-token=\"#{Base62.encode(old_id)}\"]")
image['data-mce-token'] = Base62.encode(new_id)
end
update(object_field => parsed_description.to_html)
update(object_field => parsed_description.css('body').inner_html.to_s)
end
def clone_tinymce_assets(target, team)

View file

@ -100,6 +100,7 @@ class Report < ApplicationRecord
exp.my_modules.each do |my_module|
module_children = []
module_children += gen_element_content(my_module, nil, 'my_module_protocol', true)
my_module.protocol.steps.each do |step|
step_children =
gen_element_content(step, step.assets, 'step_asset')

View file

@ -132,6 +132,12 @@ class Step < ApplicationRecord
st
end
def asset_position(asset)
assets.order(:file_updated_at).each_with_index do |step_asset, i|
return { count: assets.count, pos: i } if asset.id == step_asset.id
end
end
protected
def cascade_after_destroy

View file

@ -30,7 +30,7 @@ class SystemNotification < ApplicationRecord
user,
query = nil
)
notifications = order(last_time_changed_at: :DESC)
notifications = order(created_at: :DESC)
notifications = notifications.search_notifications(query) if query.present?
notifications.joins(:user_system_notifications)
.where('user_system_notifications.user_id = ?', user.id)
@ -39,6 +39,7 @@ class SystemNotification < ApplicationRecord
:title,
:description,
:last_time_changed_at,
:created_at,
'user_system_notifications.seen_at',
'user_system_notifications.read_at'
)
@ -50,6 +51,6 @@ class SystemNotification < ApplicationRecord
SystemNotification
.order(last_time_changed_at: :desc)
.first&.last_time_changed_at&.to_i ||
User.order(created_at: :desc).first&.created_at&.to_i
User.order(created_at: :asc).first&.created_at&.to_i
end
end

View file

@ -322,7 +322,7 @@ class Team < ApplicationRecord
query = query.where(id: users_team)
end
query = query.where(id: team_by_subject(filters[:subjects])) if filters[:subjects]
query.select(:id, :name)
query.select(:id, :name).map { |i| { id: i[:id], name: ApplicationController.helpers.escape_input(i[:name]) } }
end
private

View file

@ -57,9 +57,9 @@ class TinyMceAsset < ApplicationRecord
Rails.logger.error e.message
end
def self.generate_url(description)
def self.generate_url(description, obj = nil)
# Check tinymce for old format
description = update_old_tinymce(description)
description = update_old_tinymce(description, obj)
description = Nokogiri::HTML(description)
tm_assets = description.css('img')
@ -119,12 +119,23 @@ class TinyMceAsset < ApplicationRecord
asset.destroy if asset && !asset.saved
end
def self.update_old_tinymce(description)
def self.update_old_tinymce(description, obj = nil)
return description unless description
description.scan(/\[~tiny_mce_id:(\w+)\]/).flatten.each do |token|
old_format = /\[~tiny_mce_id:#{token}\]/
new_format = "<img src=\"\" class=\"img-responsive\" data-mce-token=\"#{Base62.encode(token.to_i)}\"/>"
asset = find_by_id(token)
unless asset
# remove tag if asset deleted
description.sub!(old_format, '')
next
end
# If object (step or result text) don't have direct assciation to tinyMCE image, we need copy it.
asset.clone_tinymce_asset(obj) if obj && obj != asset.object
description.sub!(old_format, new_format)
end
description
@ -146,6 +157,42 @@ class TinyMceAsset < ApplicationRecord
ostream
end
def clone_tinymce_asset(obj)
begin
# It will trigger only for Step or ResultText
team_id = if obj.class.name == 'Step'
obj.protocol.team_id
else
obj.result.my_module.protocol.team_id
end
rescue StandardError => e
Rails.logger.error e.message
team_id = nil
end
return false unless team_id
tiny_img_clone = TinyMceAsset.new(
image: image,
estimated_size: estimated_size,
object: obj,
team_id: team_id
)
tiny_img_clone.save!
obj.tiny_mce_assets << tiny_img_clone
# Prepare array of image to update
cloned_img_ids = [[id, tiny_img_clone.id]]
obj_field = Extends::RICH_TEXT_FIELD_MAPPINGS[obj.class.name]
# Update description with new format
obj.update(obj_field => TinyMceAsset.update_old_tinymce(obj[obj_field]))
# reassign images
obj.reassign_tiny_mce_image_references(cloned_img_ids)
end
private
def self_destruct

View file

@ -5,6 +5,7 @@ class User < ApplicationRecord
include User::TeamRoles
include User::ProjectRoles
include TeamBySubjectModel
include InputSanitizeHelper
acts_as_token_authenticatable
devise :invitable, :confirmable, :database_authenticatable, :registerable,
@ -553,7 +554,7 @@ class User < ApplicationRecord
User.where(id: UserTeam.where(team_id: query_teams).select(:user_id))
.search(false, search_query)
.select(:full_name, :id)
.map { |i| { name: i[:full_name], id: i[:id] } }
.map { |i| { name: escape_input(i[:full_name]), id: i[:id] } }
end
protected

View file

@ -4,8 +4,7 @@ class UserSystemNotification < ApplicationRecord
belongs_to :user
belongs_to :system_notification
after_create :send_email,
if: proc { |sn| sn.user.system_message_email_notification }
validates :system_notification, uniqueness: { scope: :user }
scope :unseen, -> { where(seen_at: nil) }
@ -15,23 +14,22 @@ class UserSystemNotification < ApplicationRecord
def self.mark_as_read(notification_id)
notification = find_by_system_notification_id(notification_id)
if notification && notification.read_at.nil?
notification.update(read_at: Time.now)
end
notification.update(read_at: Time.now) if notification && notification.read_at.nil?
end
def self.show_on_login(update_read_time = false)
# for notification check leave update_read_time empty
notification = joins(:system_notification)
.where('system_notifications.show_on_login = true')
.order('system_notifications.last_time_changed_at DESC')
.order('system_notifications.created_at DESC')
.select(
:modal_title,
:modal_body,
'user_system_notifications.id',
:read_at,
:user_id,
:system_notification_id
:system_notification_id,
:created_at
)
.first
if notification && notification.read_at.nil?
@ -44,10 +42,4 @@ class UserSystemNotification < ApplicationRecord
notification
end
end
private
def send_email
AppMailer.delay.system_notification(user, system_notification)
end
end

View file

@ -40,6 +40,7 @@ class UserTeam < ApplicationRecord
end
def destroy(new_owner)
return super() unless new_owner
# If any project of the team has the sole owner and that
# owner is the user to be removed from the team, then we must
# create a new owner of the project (the provided user).

View file

@ -24,6 +24,8 @@ class ZipExport < ApplicationRecord
validates_attachment :zip_file,
content_type: { content_type: 'application/zip' }
after_create :self_destruct
# When using S3 file upload, we can limit file accessibility with url signing
def presigned_url(style = :original,
download: false,
@ -48,6 +50,11 @@ class ZipExport < ApplicationRecord
zip_file.options[:storage].to_sym == :s3
end
def self.delete_expired_export(id)
export = find_by_id(id)
export.destroy if export
end
def generate_exportable_zip(user, data, type, options = {})
I18n.backend.date_format =
user.settings[:date_format] || Constants::DEFAULT_DATE_FORMAT
@ -71,6 +78,11 @@ class ZipExport < ApplicationRecord
private
def self_destruct
ZipExport.delay(run_at: Constants::EXPORTABLE_ZIP_EXPIRATION_DAYS.days.from_now)
.delete_expired_export(id)
end
def method_missing(m, *args, &block)
puts 'Method is missing! To use this zip_export you have to ' \
'define a method: generate_( type )_zip.'

View file

@ -11,7 +11,7 @@ module Api
end
def table_contents
object.table&.contents
object.table&.contents&.force_encoding(Encoding::UTF_8)
end
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Notifications
class HandleSystemNotificationInCommunicationChannelService
extend Service
attr_reader :errors
def initialize(system_notification)
@system_notification = system_notification
@errors = {}
end
def call
@system_notification.user_system_notifications.find_each do |usn|
user = usn.user
AppMailer.delay.system_notification(user, @system_notification) if user.system_message_email_notification
end
self
end
def succeed?
@errors.none?
end
end
end

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
module Notifications
class PushToCommunicationChannelService
extend Service
WHITELISTED_ITEM_TYPES = %w(SystemNotification).freeze
attr_reader :errors
def initialize(item_id:, item_type:)
@item_type = item_type
@item = item_type.constantize.find item_id
@errors = {}
end
def call
return self unless valid?
"Notifications::Handle#{@item_type}InCommunicationChannelService".constantize.call(@item)
self
end
def succeed?
@errors.none?
end
private
def valid?
raise 'Dont know how to handle this type of items' unless WHITELISTED_ITEM_TYPES.include?(@item_type)
if @item.nil?
@errors[:invalid_arguments] = 'Can\'t find item' if @item.nil?
return false
end
true
end
end
end

View file

@ -81,12 +81,27 @@ module Notifications
.where(source_id: attrs[:source_id]).first_or_initialize(attrs)
if n.new_record?
n.users = User.all
n.save!
save_notification n
elsif n.last_time_changed_at < attrs[:last_time_changed_at]
n.update_attributes!(attrs)
end
end
end
def save_notification(notification)
ActiveRecord::Base.transaction do
notification.save!
User.find_in_batches do |user_ids|
user_system_notifications = user_ids.pluck(:id).collect do |item|
Hash[:user_id, item, :system_notification_id, notification.id]
end
UserSystemNotification.import user_system_notifications, validate: false
end
end
Notifications::PushToCommunicationChannelService.delay.call(item_id: notification.id,
item_type: notification.class.name)
end
end
end

View file

@ -28,7 +28,7 @@ module RepositoryActions
def duplicate_row(id)
row = RepositoryRow.find_by_id(id)
new_row = RepositoryRow.new(
row.attributes.merge(new_row_attributes(row.name))
row.attributes.merge(new_row_attributes(row.name, @user.id))
)
if new_row.save
@ -46,10 +46,11 @@ module RepositoryActions
end
end
def new_row_attributes(name)
def new_row_attributes(name, user_id)
timestamp = DateTime.now
{ id: nil,
name: "#{name} (1)",
created_by_id: user_id,
created_at: timestamp,
updated_at: timestamp }
end

View file

@ -158,10 +158,14 @@ module ProtocolsImporter
)
tiny_mce_img.image_content_type = tiny_mce_img_json['fileType']
tiny_mce_img.save!
if description.gsub!("data-mce-token=\"#{tiny_mce_img_json['tokenId']}\"",
"data-mce-token=\"#{Base62.encode(tiny_mce_img.id)}\"")
description.gsub!(' ]]--&gt;', '')
description.gsub!("data-mce-token=\"#{tiny_mce_img_json['tokenId']}\"",
"data-mce-token=\"#{Base62.encode(tiny_mce_img.id)}\"")
.gsub!(' ]]--&gt;', '')
else
description.gsub!("data-mce-token=\"#{Base62.encode(tiny_mce_img_json['tokenId'].to_i)}\"",
"data-mce-token=\"#{Base62.encode(tiny_mce_img.id)}\"").gsub!(' ]]--&gt;', '')
end
end
description
end

View file

@ -1,11 +1,12 @@
<%= link_to create_wopi_file_path,
class: 'btn btn-default create-wopi-file-btn',
target: '_blank',
<% if wopi_enabled? %>
<%= link_to create_wopi_file_path,
class: 'btn btn-default create-wopi-file-btn',
target: '_blank',
title: 'Create_new_file',
data: { 'id': element_id, 'type': element_type, } do %>
<span class="btn btn-default new-asset-upload-button">
<span class="fas fa-file-medical new-asset-upload-icon"></span>
<span class="btn btn-default new-asset-upload-button">
<%= image_tag 'office/office.svg' %>
<%=t 'assets.create_wopi_file.button_text' %>
</span>
</span>
<% end %>
<% end %>

View file

@ -1,9 +1,9 @@
<% provide(:head_title, t("experiments.canvas.head_title", project: h(@project.name)).html_safe) %>
<%= render partial: "shared/sidebar", locals: { current_experiment: @experiment, page: 'canvas' } %>
<%= render partial: "shared/secondary_navigation" , locals: {
<%= render partial: "shared/secondary_navigation" , locals: {
editable: {
name: 'title',
active: true,
active: can_manage_experiment?(@experiment),
width: 'calc(100% - 500px)',
params_group: 'experiment',
field_to_udpate: 'name',

View file

@ -1,25 +1,25 @@
<% if my_module.completed? %>
<%= t("my_modules.states.completed") %>
<span class="due-date-label">
<%= l(my_module.completed_on, format: :full_date) %>
<%= l(my_module.completed_on.utc, format: :full_date) %>
<span class="fas fa-check"></span>
</span>
<% elsif my_module.is_one_day_prior? %>
<%= t("my_modules.states.due_soon") %>
<span class="due-date-label">
<%=l my_module.due_date, format: :full_date %>
<%=l my_module.due_date.utc, format: :full_date %>
<span class="fas fa-exclamation-triangle"></span>
</span>
<% elsif my_module.is_overdue? %>
<%= t("my_modules.states.overdue") %>
<span class="due-date-label">
<%=l my_module.due_date, format: :full_date %>
<%=l my_module.due_date.utc, format: :full_date %>
<span class="fas fa-exclamation-triangle"></span>
</span>
<% elsif my_module.due_date %>
<%= t("experiments.canvas.full_zoom.due_date") %>
<span class="due-date-label">
<%=l my_module.due_date, format: :full_date %>
<%=l my_module.due_date.utc, format: :full_date %>
</span>
<% else %>
<%= t("experiments.canvas.full_zoom.due_date") %>

View file

@ -63,8 +63,8 @@
<div class="badge-icon">
<span class="fas fa-tags"></span>
</div>
<span class="hidden-xs hidden-sm tags-title"><%=t "my_modules.module_header.tags" %></span>
<%= render partial: "my_modules/tags", locals: { my_module: @my_module, editable: can_manage_module?(@my_module) } %>
<span class="hidden-xs hidden-sm tags-title"><%=t "my_modules.module_header.tags" %></span>
<%= render partial: "my_modules/tags", locals: { my_module: @my_module, editable: can_manage_module?(@my_module) } %>
</div>
</div>
@ -77,7 +77,7 @@
<% if can_manage_module?(@my_module) %>
<%= render partial: "description_form" %>
<% elsif @my_module.description.present? %>
<%= custom_auto_link(@my_module.description,
<%= custom_auto_link(@my_module.tinymce_render(:description),
simple_format: false,
tags: %w(img),
team: current_team) %>

View file

@ -4,20 +4,20 @@
<strong>
<% if my_module.completed? %>
<span class="alert-green">
<%= l(my_module.due_date, format: :full) %>
<%= l(my_module.due_date.utc, format: :full) %>
</span>
<% elsif my_module.is_one_day_prior? %>
<span class="alert-yellow">
<%= l(my_module.due_date, format: :full) %>
<%= l(my_module.due_date.utc, format: :full) %>
(<%= t('my_modules.states.due_soon') %>)
</span>
<% elsif my_module.is_overdue? %>
<span class="alert-red">
<%= l(my_module.due_date, format: :full) %>
<%= l(my_module.due_date.utc, format: :full) %>
(<%= t('my_modules.states.overdue') %>)
</span>
<% else %>
<%= l(my_module.due_date, format: :full) %>
<%= l(my_module.due_date.utc, format: :full) %>
<% end %>
</strong>
<% end %>

View file

@ -5,7 +5,7 @@
<%= render partial: "shared/secondary_navigation", locals: {
editable: {
name: 'title',
active: true,
active: can_manage_module?(@my_module),
width: 'calc(100% - 580px)',
params_group: 'my_module',
field_to_udpate: 'name',
@ -22,8 +22,8 @@
<%= t('Protocol') %>
</div>
<div>
<div data-role="protocol-status-bar" style="display: inline;">
<div class="protocol-status-container">
<div class="protocol-status-bar" data-role="protocol-status-bar">
<%= render partial: "my_modules/protocols/protocol_status_bar.html.erb" %>
</div>
<%= render partial: "my_modules/protocols/protocol_buttons.html.erb" %>
@ -40,7 +40,7 @@
}
%>
<% elsif @my_module.protocol.description.present? %>
<%= custom_auto_link(@my_module.protocol.description,
<%= custom_auto_link(@my_module.protocol.tinymce_render(:description),
simple_format: false,
tags: %w(img),
team: current_team) %>

View file

@ -1,4 +1,4 @@
<div class="btn-group" role="group" aria-label="" style="margin-left: 15px;">
<div class="btn-group protocol-button" role="group" aria-label="">
<div class="btn-group">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#">
<span class="fas fa-download"></span>

View file

@ -1,21 +1,21 @@
<div class="btn-group">
<% if @protocol.unlinked? %>
<a type="button" class="btn btn-info" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_unlinked.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_unlinked.text") %>">
<a type="button" class="btn btn-info link-button" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_unlinked.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_unlinked.text") %>">
<span class="fas fa-book"></span>
&nbsp;
<span class="fas fa-times-sign"></span>
</a>
<a type="button" class="btn btn-info dropdown-toggle" disabled="disabled">
<a type="button" class="btn btn-info dropdown-toggle link-toggle" disabled="disabled">
<span class="caret"></span>
</a>
<% else %>
<% if @protocol.linked_no_diff? %>
<a type="button" class="btn btn-info" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_linked_no_diff.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_linked_no_diff.text") %>">
<a type="button" class="btn btn-info link-button" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_linked_no_diff.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_linked_no_diff.text") %>">
<span class="fas fa-book"></span>
&nbsp;
<span class="fas fa-check-circle"></span>
</a>
<a type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a type="button" class="btn btn-info dropdown-toggle link-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
@ -31,12 +31,12 @@
<% end %>
</ul>
<% elsif @protocol.newer_than_parent? %>
<a type="button" class="btn btn-warning" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_newer_than_parent.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_newer_than_parent.text", self_ts: l(@protocol.updated_at, format: :full), parent_ts: l(@protocol.parent.updated_at, format: :full)).html_safe %>">
<a type="button" class="btn btn-warning link-button" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_newer_than_parent.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_newer_than_parent.text", self_ts: l(@protocol.updated_at, format: :full), parent_ts: l(@protocol.parent.updated_at, format: :full)).html_safe %>">
<span class="fas fa-book"></span>
&nbsp;
<span class="fas fa-arrow-circle-up"></span>
</a>
<a type="button" class="btn btn-warning dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a type="button" class="btn btn-warning dropdown-toggle link-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
@ -72,12 +72,12 @@
<% end %>
</ul>
<% elsif @protocol.parent_newer? %>
<a type="button" class="btn btn-warning" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_newer.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_newer.text", self_ts: l(@protocol.updated_at, format: :full), parent_ts: l(@protocol.parent.updated_at, format: :full)).html_safe %>">
<a type="button" class="btn btn-warning link-button" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_newer.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_newer.text", self_ts: l(@protocol.updated_at, format: :full), parent_ts: l(@protocol.parent.updated_at, format: :full)).html_safe %>">
<span class="fas fa-book"></span>
&nbsp;
<span class="fas fa-arrow-circle-up"></span>
</a>
<a type="button" class="btn btn-warning dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a type="button" class="btn btn-warning dropdown-toggle link-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
@ -103,12 +103,12 @@
<% end %>
</ul>
<% elsif @protocol.parent_and_self_newer? %>
<a type="button" class="btn btn-warning" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_and_self_newer.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_and_self_newer.text", self_ts: l(@protocol.updated_at, format: :full), parent_ts: l(@protocol.parent.updated_at, format: :full)).html_safe %>">
<a type="button" class="btn btn-warning link-button" tabindex="0" role="button" data-trigger="focus" data-container="body" data-html="true" data-toggle="popover" data-placement="bottom" title="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_and_self_newer.title") %>" data-content="<%= t("my_modules.protocols.protocol_status_bar.btns_parent_and_self_newer.text", self_ts: l(@protocol.updated_at, format: :full), parent_ts: l(@protocol.parent.updated_at, format: :full)).html_safe %>">
<span class="fas fa-book"></span>
&nbsp;
<span class="fas fa-exclamation-circle"></span>
</a>
<a type="button" class="btn btn-warning dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a type="button" class="btn btn-warning dropdown-toggle link-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</a>
<ul class="dropdown-menu">

View file

@ -59,17 +59,7 @@
<%= javascript_include_tag "handsontable.full.min" %>
<!-- Libraries for formulas -->
<%= javascript_include_tag "lodash" %>
<%= javascript_include_tag "numeral" %>
<%= javascript_include_tag "numeric" %>
<%= javascript_include_tag "md5" %>
<%= javascript_include_tag "jstat" %>
<%= javascript_include_tag "formula" %>
<%= javascript_include_tag "parser" %>
<%= javascript_include_tag "ruleJS" %>
<%= javascript_include_tag "handsontable.formula" %>
<%= javascript_include_tag "big.min" %>
<%= stylesheet_link_tag "handsontable.formula" %>
<%= render partial: "shared/formulas_libraries.html.erb" %>
<%= javascript_include_tag("canvas-to-blob.min") %>
<%= javascript_include_tag "assets/wopi/create_wopi_file" %>

View file

@ -1,9 +1,9 @@
<% provide(:head_title, t("projects.show.head_title", project: h(@project.name)).html_safe) %>
<%= render partial: "shared/sidebar", locals: { current_project: @project, page: 'project' } %>
<%= render partial: "shared/secondary_navigation", locals: {
<%= render partial: "shared/secondary_navigation", locals: {
editable: {
name: 'title',
active: true,
active: can_manage_project?(@project),
width: 'calc(100% - 500px)',
params_group: 'project',
field_to_udpate: 'name',

View file

@ -8,7 +8,7 @@
<% end %>
</div>
<div class="row">
<div class="row protocol-info">
<div class="col-xs-6 col-sm-4 col-md-4">
<div class="badge-icon">
<span class="fas fa-calendar-alt"></span>
@ -31,7 +31,7 @@
<div class="col-xs-12 col-sm-4 col-md-4">
<div class="badge-icon">
<span class="fas fa-user fa-lg"></span>
<span class="fas fa-user"></span>
</div>
<div class="well well-sm">
<span class="hidden-xs hidden-sm hidden-md"><%=t "protocols.header.added_by" %>:</span>
@ -39,7 +39,7 @@
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-6">
<div class="col-xs-12 col-sm-8 col-md-8">
<div class="badge-icon">
<% if can_manage_protocol_in_repository?(@protocol) %>
<a data-action="edit-keywords" data-remote="true" href="<%= edit_keywords_modal_protocol_path(@protocol, format: :json) %>" style="color: inherit;">
@ -61,7 +61,7 @@
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-6">
<div class="col-xs-12 col-sm-4 col-md-4">
<div class="badge-icon">
<% if can_manage_protocol_in_repository?(@protocol) %>
<a data-action="edit-authors" data-remote="true" href="<%= edit_authors_modal_protocol_path(@protocol, format: :json) %>" style="color: inherit;">
@ -83,7 +83,7 @@
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="col-xs-12 col-sm-12 col-md-12 protocol-description">
<%= render partial: "protocols/header/description_label.html.erb", locals: {edit_mode: true} %>
</div>
</div>

View file

@ -31,17 +31,7 @@
<%= javascript_include_tag "handsontable.full.min" %>
<!-- Libraries for formulas -->
<%= javascript_include_tag "lodash" %>
<%= javascript_include_tag "numeral" %>
<%= javascript_include_tag "numeric" %>
<%= javascript_include_tag "md5" %>
<%= javascript_include_tag "jstat" %>
<%= javascript_include_tag "formula" %>
<%= javascript_include_tag "parser" %>
<%= javascript_include_tag "ruleJS" %>
<%= javascript_include_tag "handsontable.formula" %>
<%= javascript_include_tag "big.min" %>
<%= stylesheet_link_tag "handsontable.formula" %>
<%= render partial: "shared/formulas_libraries.html.erb" %>
<%= javascript_include_tag "assets/wopi/create_wopi_file" %>
<%= javascript_include_tag "protocols/steps" %>

View file

@ -10,3 +10,5 @@
</div>
<%= javascript_include_tag "protocols/edit" %>
<!-- Create new office file modal -->
<%= render partial: 'assets/wopi/create_wopi_file_modal.html.erb' %>

View file

@ -150,17 +150,7 @@
<%= javascript_include_tag "handsontable.full.min" %>
<!-- Libraries for formulas -->
<%= javascript_include_tag "lodash" %>
<%= javascript_include_tag "numeral" %>
<%= javascript_include_tag "numeric" %>
<%= javascript_include_tag "md5" %>
<%= javascript_include_tag "jstat" %>
<%= javascript_include_tag "formula" %>
<%= javascript_include_tag "parser" %>
<%= javascript_include_tag "ruleJS" %>
<%= javascript_include_tag "handsontable.formula" %>
<%= javascript_include_tag "big.min" %>
<%= stylesheet_link_tag "handsontable.formula" %>
<%= render partial: "shared/formulas_libraries.html.erb" %>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag "assets/wopi/create_wopi_file" %>

View file

@ -1,4 +1,4 @@
<div class="row">
<div class="row protocol-info">
<div class="col-xs-6 col-sm-4 col-md-4">
<div class="badge-icon">
<span class="fas fa-calendar-alt"></span>
@ -21,7 +21,7 @@
<div class="col-xs-12 col-sm-4 col-md-4">
<div class="badge-icon">
<span class="fas fa-user fa-lg"></span>
<span class="fas fa-user"></span>
</div>
<div class="well well-sm">
<span class="hidden-xs hidden-sm hidden-md"><%=t "protocols.header.added_by" %>:</span>
@ -29,7 +29,7 @@
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-6">
<div class="col-xs-12 col-sm-8 col-md-8">
<div class="badge-icon">
<span class="fas fa-font"></span>
</div>
@ -39,7 +39,7 @@
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-6">
<div class="col-xs-12 col-sm-4 col-md-4">
<div class="badge-icon">
<span class="fas fa-graduation-cap"></span>
</div>
@ -49,12 +49,8 @@
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="badge-icon">
<span class="fas fa-info-circle"></span>
</div>
<div class="col-xs-12 col-sm-12 col-md-12 protocol-description">
<div class="well well-sm">
<span class="hidden-xs hidden-sm hidden-md"><%=t "protocols.header.description" %>:</span>
<%= render partial: "protocols/header/description_label.html.erb", locals: {edit_mode: false} %>
</div>
</div>
@ -68,88 +64,22 @@
</div>
<div id="steps">
<% protocol.steps.order(:position).each do |step| %>
<div class ="step <%= step.completed? ? "completed" : "not-completed" %>">
<div class="badge-num">
<span class="badge size-digit-<%= (step.position_plus_one).to_s.length %>"><%= step.position_plus_one %></span>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong><%= step.name %></strong> |
<span><%= t("protocols.steps.published_on", timestamp: l(step.created_at, format: :full), user: h(step.user.full_name)).html_safe %></span>
</div>
<div class="panel-collapse collapse in" id="step-panel-<%= step.id %>" role="tabpanel" aria-expanded="true">
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<% if sanitize_input(step.description).blank? %>
<em><%= t("protocols.steps.no_description") %></em>
<% else %>
<div class="ql-editor">
<%= sanitize_input(step.tinymce_render(:description), ['img']) %>
</div>
<% end %>
</div>
</div>
<div class="row">
<% unless step.tables.blank? then %>
<hr>
<div class="col-xs-12">
<% step.tables.each do |table| %>
<strong><%= table.name %></strong>
<div data-role="hot-table" class="hot-table">
<%= hidden_field(table, :contents, value: table.contents_utf_8, class: "hot-contents") %>
<div data-role="step-hot-table" class="step-result-hot-table"></div>
</div>
<% end %>
</div>
<% end %>
<% assets = ordered_assets(step) %>
<% unless assets.blank? then %>
<hr>
<div class="col-xs-12">
<strong><%= t("protocols.steps.files") %></strong>
<ul>
<% assets.each do |asset| %>
<li>
<%= render partial: "shared/asset_link", locals: { asset: asset, display_image_tag: true }, formats: :html %>
</li>
<% end %>
</ul>
</div>
<% end %>
<% unless step.checklists.blank? then %>
<div class="col-xs-12">
<% step.checklists.asc.each do |checklist| %>
<strong><%= checklist.name %></strong>
<% if checklist.checklist_items.empty? %>
</br>
<%= t("protocols.steps.empty_checklist") %>
</br>
<% else %>
<% ordered_checklist_items(checklist).each do |checklist_item| %>
<div class="checkbox">
<label>
<% if protocol.in_module? %>
<input type="checkbox" value="" <%= "checked" if checklist_item.checked? %> disabled="disabled"/>
<% else %>
<input type="checkbox" value="" disabled="disabled" />
<% end %>
<%= checklist_item.text %>
</label>
</div>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
<%= render partial: "steps/step.html.erb", locals: { step: step, preview: true } %>
<% end %>
</div>
</div>
<%= javascript_include_tag "handsontable.full.min" %>
<%= javascript_include_tag "lodash" %>
<%= javascript_include_tag "numeral" %>
<%= javascript_include_tag "numeric" %>
<%= javascript_include_tag "md5" %>
<%= javascript_include_tag "jstat" %>
<%= javascript_include_tag "formula" %>
<%= javascript_include_tag "parser" %>
<%= javascript_include_tag "ruleJS" %>
<%= javascript_include_tag "handsontable.formula" %>
<%= javascript_include_tag "big.min" %>
<%= stylesheet_link_tag "handsontable.formula" %>
<%= javascript_include_tag "protocols/steps" %>

View file

@ -1,5 +1,5 @@
<div class="report-element report-module-protocol-element" data-ts="<%= protocol.created_at %>" data-type="protocol" data-id='{ "protocol_id": <%= protocol.id %> }' data-scroll-id="<%= protocol.id %>">
<% protocol ||= my_module.protocol %>
<div class="report-element report-module-protocol-element" data-ts="<%= protocol.created_at %>" data-type="my_module_protocol" data-id='{ "my_module_id": <%= my_module.id %> }' data-scroll-id="<%= protocol.id %>">
<div class="report-element-header">
<div class="row">
<div class="pull-left user-time">
@ -18,4 +18,5 @@
<em><%= t('my_modules.protocols.protocol_status_bar.no_description') %></em>
<% end %>
</div>
</div>
</div>

View file

@ -83,17 +83,7 @@
<%= javascript_include_tag "handsontable.full.min" %>
<!-- Libraries for formulas -->
<%= javascript_include_tag "lodash" %>
<%= javascript_include_tag "numeral" %>
<%= javascript_include_tag "numeric" %>
<%= javascript_include_tag "md5" %>
<%= javascript_include_tag "jstat" %>
<%= javascript_include_tag "formula" %>
<%= javascript_include_tag "parser" %>
<%= javascript_include_tag "ruleJS" %>
<%= javascript_include_tag "handsontable.formula" %>
<%= javascript_include_tag "big.min" %>
<%= stylesheet_link_tag "handsontable.formula" %>
<%= render partial: "shared/formulas_libraries.html.erb" %>
<%= javascript_include_tag("reports/new") %>
<%= javascript_include_tag 'reports/save_pdf_to_inventory' %>

View file

@ -1,5 +1,4 @@
<% my_module_undefined = !defined? my_module or my_module.blank? %>
<div>
<em>
<%= t("projects.reports.elements.modals.module_contents_inner.instructions") %>
@ -12,6 +11,10 @@
<%= form.check_box :module_all, label: t("projects.reports.elements.modals.module_contents_inner.check_all") %>
<ul>
<li>
<%= form.check_box :module_protocol, label: t("projects.reports.elements.modals.module_contents_inner.protocol") %>
</li>
<% if my_module_undefined or my_module.protocol.steps.exists? %>
<li>
<%= form.check_box :module_steps, label: t("projects.reports.elements.modals.module_contents_inner.steps") %>

View file

@ -125,17 +125,7 @@
<%= javascript_include_tag "handsontable.full.min" %>
<!-- Libraries for formulas -->
<%= javascript_include_tag "lodash" %>
<%= javascript_include_tag "numeral" %>
<%= javascript_include_tag "numeric" %>
<%= javascript_include_tag "md5" %>
<%= javascript_include_tag "jstat" %>
<%= javascript_include_tag "formula" %>
<%= javascript_include_tag "parser" %>
<%= javascript_include_tag "ruleJS" %>
<%= javascript_include_tag "handsontable.formula" %>
<%= javascript_include_tag "big.min" %>
<%= stylesheet_link_tag "handsontable.formula" %>
<%= render partial: "shared/formulas_libraries.html.erb" %>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag "protocols/index" %>

View file

@ -0,0 +1,11 @@
<%= javascript_include_tag "lodash" %>
<%= javascript_include_tag "numeral" %>
<%= javascript_include_tag "numeric" %>
<%= javascript_include_tag "md5" %>
<%= javascript_include_tag "jstat" %>
<%= javascript_include_tag "formula" %>
<%= javascript_include_tag "parser" %>
<%= javascript_include_tag "ruleJS" %>
<%= javascript_include_tag "handsontable.formula" %>
<%= javascript_include_tag "big.min" %>
<%= stylesheet_link_tag "handsontable.formula" %>

View file

@ -6,7 +6,8 @@
data-original-name="<%= initial_value %>"
error="false"
>
<input type="text" value="<%= initial_value %>" disabled/>
<div class="view-mode"><%= initial_value %></div>
<input class="hidden" type="text" value="<%= initial_value %>" disabled/>
<div class="button-container">
<span class="save-button"><i class="fas fa-check"></i></span>
<span class="cancel-button"><i class="fas fa-times"></i></span>

View file

@ -152,7 +152,7 @@
<% editable = false if local_assigns[:editable].nil? %>
<h4 class="nav-name <%= (editable && editable[:active]) ? 'editable' : '' %>">
<% if editable && editable[:active] %>
<%= render partial: "shared/inline_editing", locals: {
<%= render partial: "shared/inline_editing", locals: {
initial_value: truncate(title_element.name, length: Constants::MAX_EDGE_LENGTH),
width: editable[:width] || '100%',
name: editable[:name],

View file

@ -75,7 +75,7 @@
</span>
</div>
<div id="new-step-assets-group" class="form-group">
<div class="col-xs-12 attacments edit">
<div class="col-xs-12 attachments edit">
<%= f.nested_fields_for :assets do |ff| %>
<%= render partial: 'steps/attachments/placeholder.html.erb',
locals: { edit_page: true, asset: ff.object, ff: ff } %>

View file

@ -1,8 +1,9 @@
<% preview = (defined?(preview) ? preview : false) %>
<div class ="step <%= step.completed? ? "completed" : "not-completed" %>">
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-options pull-right">
<% if can_complete_or_checkbox_step?(@protocol) %>
<% if can_complete_or_checkbox_step?(@protocol) && !(preview) %>
<% if step.completed? %>
<div data-action="uncomplete-step"
class="complete-step-btn"
@ -24,8 +25,8 @@
<% end %>
<% end %>
<% if can_manage_protocol_in_module?(@protocol) ||
can_manage_protocol_in_repository?(@protocol) %>
<% if (can_manage_protocol_in_module?(@protocol) ||
can_manage_protocol_in_repository?(@protocol)) && !(preview) %>
<a data-action="move-step"
class="btn btn-link"
href="<%= move_up_step_path(step, format: :json) %>"
@ -110,7 +111,7 @@
</div>
<% end %>
<%= render partial: 'steps/attachments/list.html.erb', locals: { step: step } %>
<%= render partial: 'steps/attachments/list.html.erb', locals: { step: step, preview: preview } %>
<% unless step.checklists.blank? then %>
<div class="col-xs-12">

View file

@ -1,8 +1,19 @@
<% if asset.file.processing? && asset.is_image? %>
<% asset_status = 'asset-loading' %>
<% present_url = step_file_present_asset_path(asset.id) %>
<% else %>
<% asset_status = 'asset-present' %>
<% present_url = '' %>
<% end %>
<div class="pseudo-attachment-container" style="order: <%= assets_count - i %>">
<%= link_to download_asset_path(asset),
class: 'file-preview-link',
id: "modal_link#{asset.id}",
data: { no_turbolink: true, id: true, status: 'asset-present',
data: { no_turbolink: true,
id: true,
status: asset_status,
'present-url': present_url,
'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),

View file

@ -16,8 +16,11 @@
<%= render partial: '/assets/marvinjs/create_marvin_sketch_button.html.erb',
locals: { element_id: step.id, element_type: 'Step', sketch_container: ".attacments#att-#{step.id}" } %>
<% end %>
<%= render partial: '/assets/wopi/create_wopi_file_button.html.erb',
locals: { element_id: step.id, element_type: 'Step' } %>
<% if !(preview) %>
<%= 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>
@ -36,7 +39,7 @@
</div>
</div>
<div class="col-xs-12 attacments" id="att-<%= step.id %>">
<div class="col-xs-12 attachments" id="att-<%= step.id %>">
<% assets.each_with_index do |asset, i| %>
<% if asset.class.name == 'Asset' %>
<%= render partial: 'steps/attachments/item.html.erb',

View file

@ -5,6 +5,8 @@
data-params-group="comment"
data-path-to-update="<%= step_step_comment_path(comment.step, comment, format: :json) %>"
data-original-name="<%= comment.message %>"
data-response-field="comment"
data-smart-annotation="true"
error="false"
>
<div class="avatar-placehodler">
@ -34,7 +36,13 @@
<% end %>
</div>
<div class="comment-message">
<%= text_area_tag 'message', comment.message, disabled: true %>
<div class="view-mode"><%= custom_auto_link(comment.message,
simple_format: false,
tags: %w(img br),
team: current_team).gsub(/\n/, '<br/>').html_safe %></div>
<% if user_comment %>
<%= text_area_tag 'message', comment.message, disabled: true, class: 'smart-text-area hidden' %>
<% end %>
</div>
<div class="error-block"></div>
</div>

View file

@ -12,7 +12,7 @@
</div>
<div class="body-block">
<div class="datetime">
<span><%= l(notification.last_time_changed_at, format: :full) %></span>
<span><%= l(notification.created_at, format: :full) %></span>
</div>
<h5 class="title">
<%= notification.title %>

View file

@ -23,6 +23,9 @@ class Constants
COLOR_MAX_LENGTH = 7
# Max characters for text in dropdown list element
DROPDOWN_TEXT_MAX_LENGTH = 15
# Max characters limit for (on most operating systems, it's ~255 characters,
# but this is with a bit more safety margin)
FILENAME_MAX_LENGTH = 100
# Max characters for filenames, after which they get truncated
FILENAME_TRUNCATION_LENGTH = 50
# Max characters for names of exported files and folders, after which they get

View file

@ -38,7 +38,8 @@ class Extends
project_samples: 14, # TODO
experiment: 15,
# Higher number because of addons
my_module_repository: 17 }
my_module_repository: 17,
my_module_protocol: 18 }
# Data type name should match corresponding model's name
REPOSITORY_DATA_TYPES = { RepositoryTextValue: 0,

View file

@ -59,6 +59,10 @@ module ReportExtends
# Module contents element
MODULE_CONTENTS = [
ModuleElement.new([:protocol],
:protocol,
false,
[:my_module]),
ModuleElement.new(%i(completed_steps uncompleted_steps),
:steps,
true,
@ -114,6 +118,7 @@ module ReportExtends
result_comments)
# sets local :my_module to the listed my_module child elements
MY_MODULE_ELEMENTS = %w(my_module
my_module_protocol
my_module_activity
my_module_repository)
@ -153,6 +158,7 @@ module ReportExtends
ElementReference.new(
proc do |report_element|
report_element.my_module? ||
report_element.my_module_protocol? ||
report_element.my_module_activity? ||
report_element.my_module_samples?
end,
@ -198,6 +204,7 @@ module ReportExtends
ElementReference.new(
proc do |report_element|
report_element.my_module? ||
report_element.my_module_protocol? ||
report_element.my_module_activity? ||
report_element.my_module_samples?
end,

View file

@ -410,6 +410,7 @@ en:
module_contents_inner:
instructions: "Select the information from your task that you would like to include to your report."
check_all: "All tasks content"
protocol: "Protocol"
steps: "Steps"
completed_steps: "Completed"
uncompleted_steps: "Uncompleted"
@ -599,7 +600,7 @@ en:
start_date: "Start date:"
due_date: "Due date:"
tags: "Tags:"
no_tags: "click here to add Task Tags (optional)"
no_tags: "Add new Task Tags (optional)"
manage_tags: "Manage tags"
create_new_tag: "create new"
no_description: "No task description"
@ -1954,6 +1955,7 @@ en:
error_message: 'You must choose a file'
server_not_respond: "Didn't get a response from the server"
saved_label: "Saved"
leaving_warning: "You have made some changes, are you sure you want to leave this page?"
general:
save: "Save"
update: "Update"
@ -1981,6 +1983,7 @@ en:
blank: "You didn't select any file"
uploading: "If you leave this page, the file(s) that is/are currently uploading will not be saved! Are you sure you want to continue?"
upload_failure: "Upload connection error. Try again or contact the administrator."
file_name_too_long: 'is too long (maximum is %{limit} characters, with extension)'
text:
not_blank: "can't be blank"
length_too_long_general: "is too long"

View file

@ -340,10 +340,10 @@ Rails.application.routes.draw do
# as well as 'module info' page for single module (HTML)
resources :my_modules, path: '/modules', only: [:show, :update] do
resources :my_module_tags, path: '/tags', only: [:index, :create, :destroy] do
collection do
collection do
get :search_tags
end
member do
member do
post :destroy_by_tag_id
end
end
@ -581,6 +581,7 @@ Rails.application.routes.draw do
# We cannot use 'resources :assets' because assets is a reserved route
# in Rails (assets pipeline) and causes funky behavior
get 'files/:id/present', to: 'assets#file_present', as: 'file_present_asset'
get 'files/:id/present_in_step', to: 'assets#step_file_present', as: 'step_file_present_asset'
get 'files/:id/preview',
to: 'assets#file_preview',
as: 'asset_file_preview'

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
class AddUniqueIndexToSystemNotifications < ActiveRecord::Migration[5.1]
def change
# remove not unique index and add new with uniq
remove_index :system_notifications, :source_id
add_index :system_notifications, :source_id, unique: true
add_index :user_system_notifications, %i(user_id system_notification_id), unique: true,
name: 'index_user_system_notifications_on_user_and_notification_id'
end
end

View file

@ -10,8 +10,10 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190427115413) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
@ -695,7 +697,7 @@ ActiveRecord::Schema.define(version: 20190427115413) do
t.datetime "updated_at", null: false
t.index ["last_time_changed_at"], name: "index_system_notifications_on_last_time_changed_at"
t.index ["source_created_at"], name: "index_system_notifications_on_source_created_at"
t.index ["source_id"], name: "index_system_notifications_on_source_id"
t.index ["source_id"], name: "index_system_notifications_on_source_id", unique: true
end
create_table "tables", force: :cascade do |t|
@ -834,6 +836,7 @@ ActiveRecord::Schema.define(version: 20190427115413) do
t.index ["read_at"], name: "index_user_system_notifications_on_read_at"
t.index ["seen_at"], name: "index_user_system_notifications_on_seen_at"
t.index ["system_notification_id"], name: "index_user_system_notifications_on_system_notification_id"
t.index ["user_id", "system_notification_id"], name: "index_user_system_notifications_on_user_and_notification_id", unique: true
t.index ["user_id"], name: "index_user_system_notifications_on_user_id"
end

View file

@ -5,19 +5,29 @@ namespace :tinymce_assets do
desc 'Migrate old TinyMCE images to new polymorphic format' \
'IT SHOULD BE RUN ONE TIME ONLY'
task migrate_tinymce_assets: :environment do
old_images = TinyMceAsset.where('step_id IS NOT NULL OR result_text_id IS NOT NULL').where(object: nil)
old_images.each do |old_image|
old_format = /\[~tiny_mce_id:#{old_image.id}\]/
new_format = "<img src='' class='img-responsive' data-mce-token='#{Base62.encode(old_image.id)}'/>"
if old_image.step_id
object = old_image.step
object.description.sub!(old_format, new_format)
else
object = old_image.result_text
object.text.sub!(old_format, new_format)
ActiveRecord::Base.no_touching do
old_images = TinyMceAsset.where('step_id IS NOT NULL OR result_text_id IS NOT NULL')
.where(object: nil)
.preload(:step, :result_text)
old_images.find_each do |old_image|
ActiveRecord::Base.transaction do
old_format = /\[~tiny_mce_id:#{old_image.id}\]/
new_format = "<img src='' class='img-responsive' data-mce-token='#{Base62.encode(old_image.id)}'/>"
if old_image.step_id
object = old_image.step
object.description.sub!(old_format, new_format)
else
object = old_image.result_text
object.text.sub!(old_format, new_format)
end
object.save!
old_image.update!(object_id: object.id, object_type: object.class.to_s, step_id: nil, result_text_id: nil)
rescue StandardError => ex
Rails.logger.error "Failed to update TinyMceAsset id: #{old_image.id}"
Rails.logger.error ex.message
raise ActiveRecord::Rollback
end
end
object.save
old_image.update(object: object, step_id: nil, result_text_id: nil)
end
end
end

14824
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ FactoryBot.define do
modal_title { Faker::Name.first_name }
modal_body { Faker::Lorem.paragraphs(4).map { |pr| "<p>#{pr}</p>" }.join }
source_created_at { Faker::Time.between(3.days.ago, Date.today) }
source_id { Faker::Number.between(1, 1000) }
source_id { SystemNotification.order(source_id: :desc).first&.source_id.to_i + 1 }
last_time_changed_at { Time.now }
trait :show_on_login do
show_on_login { true }

View file

@ -56,11 +56,12 @@ describe SystemNotification do
end
context 'when there is no system notifications in db' do
it 'returns last user created_at' do
it 'returns first users created_at' do
create :user
create :user, created_at: Time.now + 5.seconds
expect(described_class.last_sync_timestamp)
.to be == User.last.created_at.to_i
.to be == User.first.created_at.to_i
end
end

View file

@ -15,36 +15,6 @@ describe UserSystemNotification do
it { is_expected.to belong_to(:system_notification) }
end
describe '.create' do
before do
Delayed::Worker.delay_jobs = false
end
after do
Delayed::Worker.delay_jobs = true
end
context 'when user has enabled notifications' do
it 'calls send an email on creation' do
allow(user_system_notification.user)
.to receive(:system_message_email_notification).and_return(true)
expect(user_system_notification).to receive(:send_email)
user_system_notification.save
end
end
context 'when user has disabled notifications' do
it 'doesn\'t call send an email on createion' do
allow(user_system_notification.user)
.to receive(:system_message_email_notification).and_return(false)
expect(user_system_notification).not_to receive(:send_email)
user_system_notification.save
end
end
end
describe 'Methods' do
let(:notifcation_one) { create :system_notification }
let(:notifcation_two) { create :system_notification }

View file

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'rails_helper'
describe Notifications::HandleSystemNotificationInCommunicationChannelService do
let(:system_notification) { create :system_notification }
let!(:user_system_notification) do
create :user_system_notification, user: user, system_notification: system_notification
end
let(:user) { create :user }
let(:service_call) do
Notifications::HandleSystemNotificationInCommunicationChannelService.call(system_notification)
end
before do
Delayed::Worker.delay_jobs = false
end
after do
Delayed::Worker.delay_jobs = true
end
context 'when user has enabled notifications' do
it 'calls AppMailer' do
allow_any_instance_of(User).to receive(:system_message_email_notification).and_return(true)
expect(AppMailer).to receive(:system_notification).and_return(double('Mailer', deliver: true))
service_call
end
end
context 'when user has disabled notifications' do
it 'does not call AppMailer' do
allow_any_instance_of(User).to receive(:system_message_email_notification).and_return(false)
expect(AppMailer).not_to receive(:system_notification)
service_call
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'rails_helper'
describe Notifications::PushToCommunicationChannelService do
let(:system_notification) { create :system_notification }
let(:service_call) do
Notifications::PushToCommunicationChannelService.call(item_id: system_notification.id,
item_type: system_notification.class.name)
end
context 'when call with valid items' do
it 'call service to to handle sending out' do
expect(Notifications::HandleSystemNotificationInCommunicationChannelService)
.to receive(:call).with(system_notification)
service_call
end
end
context 'when call with not valid items' do
it 'returns error with key invalid_arguments when system notification not exists' do
allow(SystemNotification).to receive(:find).and_return(nil)
expect(service_call.errors).to have_key(:invalid_arguments)
end
it 'raise error when have not listed object' do
u = create :user
expect do
Notifications::PushToCommunicationChannelService.call(item_id: u.id, item_type: 'User')
end.to(raise_error('Dont know how to handle this type of items'))
end
end
end

View file

@ -6,6 +6,8 @@ describe Notifications::SyncSystemNotificationsService do
url = 'http://system-notifications-service.test/api/system_notifications'
let!(:user) { create :user }
let(:service_call) do
allow_any_instance_of(Notifications::PushToCommunicationChannelService).to receive(:call).and_return(nil)
Notifications::SyncSystemNotificationsService.call
end
@ -80,6 +82,14 @@ describe Notifications::SyncSystemNotificationsService do
expect { service_call }.to change { UserSystemNotification.count }.by(20)
end
it 'calls service to notify users about notification' do
Delayed::Worker.delay_jobs = false
expect(Notifications::PushToCommunicationChannelService).to receive(:call).exactly(10)
service_call
Delayed::Worker.delay_jobs = true
end
end
context 'when request is unsuccessful' do
@ -101,5 +111,13 @@ describe Notifications::SyncSystemNotificationsService do
expect(service_call.errors).to have_key(:socketerror)
end
it 'does not call service to notify users about notification' do
Delayed::Worker.delay_jobs = false
expect(Notifications::PushToCommunicationChannelService).to_not receive(:call)
service_call
Delayed::Worker.delay_jobs = true
end
end
end

View file

@ -59,11 +59,14 @@ describe RepositoryZipExport, type: :background_job do
end
it 'generates a new zip export object' do
ZipExport.skip_callback(:create, :after, :self_destruct)
RepositoryZipExport.generate_zip(params, repository, user)
expect(ZipExport.count).to eq 1
ZipExport.set_callback(:create, :after, :self_destruct)
end
it 'generates a zip with csv file with exported rows' do
ZipExport.skip_callback(:create, :after, :self_destruct)
RepositoryZipExport.generate_zip(params, repository, user)
csv_zip_file = ZipExport.first.zip_file
parsed_csv_content = Zip::File.open(csv_zip_file.path) do |zip_file|
@ -80,6 +83,7 @@ describe RepositoryZipExport, type: :background_job do
expect(row_hash.fetch('Name')).to eq "row #{index}"
index += 1
end
ZipExport.set_callback(:create, :after, :self_destruct)
end
end
end