Merge branch 'master' of https://github.com/biosistemika/scinote-web into decoupling

This commit is contained in:
zmagod 2018-01-23 11:06:33 +01:00
commit f8a3fca2db
84 changed files with 2546 additions and 485 deletions

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
# Root EditorConfig preference file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
[*.rb]
charset = utf-8
indent_style = space
indent_size = 2
[*.{js,jsx}]
charset = utf-8
indent_style = space
indent_size = 2

18
Gemfile
View file

@ -16,13 +16,17 @@ gem 'yomu'
gem 'font-awesome-rails', '~> 4.7.0.2'
gem 'recaptcha', require: 'recaptcha/rails'
gem 'sanitize', '~> 4.4'
gem 'omniauth'
# Gems for API implementation
gem 'jwt'
# JS datetime library, requirement of datetime picker
gem 'momentjs-rails', '~> 2.17.1'
# JS datetime picker
gem 'bootstrap3-datetimepicker-rails', '~> 4.15.35'
# Select elements for Bootstrap
gem 'bootstrap-select-rails'
gem 'bootstrap-select-rails', '~> 1.6.3'
gem 'uglifier', '>= 1.3.0'
# jQuery & plugins
gem 'jquery-turbolinks'
@ -37,7 +41,7 @@ gem 'spinjs-rails'
gem 'autosize-rails' # jQuery autosize plugin
gem 'underscore-rails'
gem 'turbolinks'
gem 'turbolinks', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'bcrypt', '~> 3.1.10'
gem 'logging', '~> 2.0.0'
@ -47,17 +51,16 @@ gem 'nested_form_fields'
gem 'ajax-datatables-rails', '~> 0.3.1'
gem 'commit_param_routing' # Enables different submit actions in the same form to route to different actions in controller
gem 'kaminari'
gem "i18n-js", ">= 3.0.0.rc11" # Localization in javascript files
gem 'i18n-js', '~> 3.0' # Localization in javascript files
gem 'roo', '~> 2.7.1' # Spreadsheet parser
gem 'creek'
gem 'wicked_pdf'
gem 'wicked_pdf', '~> 1.0.6'
gem 'silencer' # Silence certain Rails logs
gem 'wkhtmltopdf-heroku'
gem 'remotipart', '~> 1.2' # Async file uploads
gem 'faker' # Generate fake data
gem 'auto_strip_attributes', '~> 2.1' # Removes unnecessary whitespaces from ActiveRecord or ActiveModel attributes
gem 'deface', '~> 1.0'
gem 'nokogiri' # HTML/XML parser
gem 'nokogiri', '~> 1.8.1' # HTML/XML parser
gem 'sneaky-save', git: 'https://github.com/einzige/sneaky-save'
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
gem 'delayed_paperclip',
@ -122,7 +125,8 @@ end
group :production do
gem 'puma'
gem 'rails_12factor'
gem 'whacamole'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'tzinfo-data', platforms: %i(mingw mswin x64_mingw jruby)

View file

@ -516,7 +516,7 @@ DEPENDENCIES
better_errors
binding_of_caller
bootstrap-sass (~> 3.3.5)
bootstrap-select-rails
bootstrap-select-rails (~> 1.6.3)
bootstrap3-datetimepicker-rails (~> 4.15.35)
bootstrap_form
bullet
@ -540,13 +540,13 @@ DEPENDENCIES
figaro
font-awesome-rails (~> 4.7.0.2)
hammerjs-rails
i18n-js (>= 3.0.0.rc11)
i18n-js (~> 3.0)
introjs-rails
jbuilder
jquery-rails
jquery-scrollto-rails!
jquery-turbolinks
jquery-ui-rails
jquery-ui-rails (~> 5.0)
js_cookie_rails
json_matchers
kaminari

View file

@ -1,2 +1,3 @@
web: bundle exec puma -C config/puma.rb
worker: bundle exec rake jobs:work
whacamole: bundle exec whacamole -c ./config/whacamole.rb

View file

@ -1 +1 @@
1.12.5
1.12.9

View file

@ -219,6 +219,9 @@ var HelperModule = (function(){
} else if (type === 'info') {
alertType = ' alert-info ';
glyphSign = ' glyphicon-exclamation-sign ';
} else if (type === 'warning') {
alertType = ' alert-warning ';
glyphSign = ' glyphicon-exclamation-sign ';
}
var htmlSnippet = '<div class="alert alert' + alertType +
'alert-dismissable alert-floating">' +

View file

@ -240,6 +240,7 @@ function initializeEdit() {
// Restore draggable position
restoreDraggablePosition($("#diagram"), $("#canvas-container"));
preventCanvasReloadOnSave();
$("#canvas-container").submit(function (){
animateSpinner(
this,
@ -3108,3 +3109,39 @@ function tutorialAfterCb() {
.css({'pointer-events': 'auto'});
});
}
/** prevent reload page */
var preventCanvasReloadOnSave = (function() {
'use strict';
function confirmReload() {
if( confirm(I18n.t('experiments.canvas.reload_on_submit')) ) {
return true;
} else {
return false
}
}
function preventCanvasReload() {
document.onkeydown = function(){
switch (event.keyCode){
case 116:
event.returnValue = false;
return confirmReload();
case 82:
if (event.ctrlKey){
event.returnValue = false;
return confirmReload();
}
}
}
}
function bindToCanvasSave(fun) {
$('#canvas-save').on('click', function() {
fun();
})
}
return function() { bindToCanvasSave(preventCanvasReload) };
})();

View file

@ -4,7 +4,6 @@
function init(){
$("[data-id]").each(function(){
var that = $(this);
that.find(".workflowimg-container").hide();
initProjectExperiment(that);
});
@ -36,7 +35,9 @@
},
error: function (ev) {
if (ev.status == 404) {
setTimeout(checkUpdatedImg(img_url, url, timestamp, container), 500);
setTimeout(function () {
checkUpdatedImg(img_url, url, timestamp, container)
}, 5000);
} else {
animateSpinner(container, false);
}

View file

@ -134,8 +134,11 @@ function importProtocolFromFile(
var stepGuid = node.attr("guid");
var stepPosition = String(Number.parseInt(node.attr("position")) + 1);
var stepName = node.children("name").text();
var stepDescription = $(node.children("description")).html();
var stepDescription = displayTinyMceAssetInDescription(
node,
protocolFolders[position],
stepGuid
);
// Generate step element
var stepEl = newPreviewElement(
"step",
@ -225,6 +228,37 @@ function importProtocolFromFile(
});
}
// display tiny_mce_assets in step description
function displayTinyMceAssetInDescription(node, folder, stepGuid) {
if (node.children('descriptionAssets').length === 0) {
var description = node.children('description').html();
return $('<div></div>').html(
description.replace(/\[~tiny_mce_id:([0-9a-zA-Z]+)\]/,
'<strong>Can\'t import image</strong>')
.replace('<!--[CDATA[ ', '')
.replace(' ]]--&gt;', '')
.replace(' ]]-->','')
.replace(' ]]&gt;', '')
).html();
}
var description = node.children('description').html();
node.find('descriptionAssets > tinyMceAsset').each(function(i, element) {
var match = '[~tiny_mce_id:' + element.getAttribute('tokenId') + ']';
var assetBytes = getAssetBytes(folder,
stepGuid,
element.getAttribute('fileref'));
var image_tag = "<img style='max-width:300px; max-height:300px;' src='data:" + element.children[1].innerHTML + ";base64," + assetBytes + "' />"
description = description.replace(match, image_tag); // replace the token with image
}).bind(this);
// I know is crazy but is the only way I found to pass valid HTML
return $('<div></div>').html(
description.replace('<!--[CDATA[ ', '')
.replace(' ]]--&gt;', '')
.replace(' ]]-->','')
.replace(' ]]&gt;', '')
).html();
}
/* Navigation functions */
function navigationJumpToFirstProtocol() {
@ -435,7 +469,28 @@ function importProtocolFromFile(
stepJson.id = stepId;
stepJson.position = $(this).attr("position");
stepJson.name = $(this).children("name").text();
stepJson.description = $(this).children("description").html();
// I know is crazy but is the only way I found to pass valid HTML
stepJson.description = $('<div></div>').html($(this)
.children("description")
.html()
.replace('<!--[CDATA[', '')
.replace(']]&gt;', ''))
.html();
// Iterage throug tiny_mce_assets
var descriptionAssetsJson = [];
$(this).find("descriptionAssets > tinyMceAsset").each(function() {
var tinyMceAsset = {};
var fileRef = $(this).attr("fileRef");
tinyMceAsset.tokenId = $(this).attr('tokenId');
tinyMceAsset.fileType= $(this).children("fileType").text();
tinyMceAsset.bytes = getAssetBytes(
protocolFolders[index],
stepGuid,
fileRef
)
descriptionAssetsJson.push(tinyMceAsset);
});
stepJson.descriptionAssets = descriptionAssetsJson;
// Iterate through assets
var stepAssetsJson = [];

View file

@ -123,6 +123,7 @@
initPreviewModal();
SmartAnnotation.preventPropagation('.atwho-user-popover');
TinyMCE.destroyAll();
DragNDropSteps.clearFiles();
}, 1000);
})
@ -148,8 +149,8 @@
initializeCheckboxSorting();
animateSpinner(null, false);
initPreviewModal();
TinyMCE.refresh()
DragNDropSteps.clearFiles();
TinyMCE.refresh();
$("#new-step-checklists fieldset.nested_step_checklists ul").each(function () {
enableCheckboxSorting(this);
});
@ -371,6 +372,7 @@
newStepHandler();
});
toggleButtons(true);
DragNDropSteps.clearFiles();
});
}
@ -592,22 +594,17 @@
});
animateSpinner(null, false);
setupAssetsLoading();
DragNDropSteps.clearFiles();
},
error: function(e) {
$form.after(xhr.responseJSON.html);
var $new_form = $form.next();
$form.remove();
$errInput = $new_form.find(".form-group.has-error")
.first()
.find("input");
renderFormError(e, $errInput);
formCallback($form);
applyCancelOnNew();
error: function(xhr) {
if (xhr.responseJSON['assets.file']) {
$('#new-step-assets-group').addClass('has-error');
$('#new-step-assets-tab').addClass('has-error');
$.each(xhr.responseJSON['assets.file'], function(_, value) {
$('#new-step-assets-group').prepend('<span class="help-block">' + value + '</span>');
});
}
animateSpinner(null, false);
TinyMCE.destroyAll();
SmartAnnotation.preventPropagation('.atwho-user-popover');
}
});

View file

@ -118,6 +118,7 @@ function dataTableInit() {
$('#selected_info').html(' ('+rowsSelected.length+' entries selected)');
},
preDrawCallback: function() {
rowsSelected = [];
animateSpinner(this);
$('.sample-info-link').off('click');
},

View file

@ -19,6 +19,10 @@
return filesValid;
}
function clearFiles() {
droppedFiles = [];
}
// loops through a list of files and display each file in a separate panel
function listItems() {
totalSize = 0;
@ -26,7 +30,7 @@
$('.panel-step-attachment-new').remove();
_dragNdropAssetsOff();
for(var i = 0; i < droppedFiles.length; i++) {
$('#new-step-assets')
$('#new-step-assets-group')
.append(_uploadedAssetPreview(droppedFiles[i], i))
.promise()
.done(function() {
@ -49,7 +53,6 @@
var name = 'step[assets_attributes][' + index + '][file]';
fd.append(name, droppedFiles[i]);
}
droppedFiles = [];
filesValid = true;
totalSize = 0;
_dragNdropAssetsOff();
@ -144,7 +147,8 @@
init: init,
appendFilesToForm: appendFilesToForm,
listItems: listItems,
filesStatus: filesStatus
filesStatus: filesStatus,
clearFiles: clearFiles
});
})();

View file

@ -163,7 +163,7 @@ function filesSizeValidator(ev, fileInputs, fileTypeEnum) {
case FileTypeEnum.FILE:
return "<%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %>".strToErrorFormat();
case FileTypeEnum.AVATAR:
return "<%= I18n.t('general.file.total_size', size: Constants::AVATAR_MAX_SIZE_MB) %>".strToErrorFormat();
return "<%= I18n.t('users.registrations.edit.avatar_total_size', size: Constants::AVATAR_MAX_SIZE_MB) %>".strToErrorFormat();
}
}
}

View file

@ -29,9 +29,7 @@
// This cannot be scoped outside this function
// because it is generated via JS
teamSelectorDropdown2 =
teamSelectorDropdown
.next('.btn-group.bootstrap-select.form-control')
.find('button.dropdown-toggle, li');
teamSelectorDropdown.parent().find('button.dropdown-toggle, li');
// Show/hide correct step
stepForm.show();

View file

@ -88,8 +88,10 @@ var TinyMCE = (function() {
},
destroyAll: function() {
_.each(tinymce.editors, function(editor) {
editor.destroy();
initHighlightjs();
if(editor) {
editor.destroy();
initHighlightjs();
}
});
},
refresh: function() {

View file

@ -337,6 +337,19 @@ a {
border-color: darken($color-theme-secondary, 10%);
}
}
.btn-link-alt {
border-radius: 4px;
cursor: pointer;
margin-right: 5px;
padding: 3px;
}
.btn-invis-file {
display: none;
opacity: 0;
position: absolute;
z-index: -1;
}
.btn-open-file {
position: relative;
@ -1259,8 +1272,50 @@ ul.content-module-activities {
position: relative;
top: -10px;
}
}
// Protocolsio Import protocol modal
.import-protocols-modal-preview-container-json {
height: 300px;
overflow-y: scroll;
width: 100%;
.eln-table {
height: 21px;
text-align: center;
}
.badge-preview {
background-color: $color-silver;
border-radius: 2em;
float: left;
font-size: 20px;
padding: 5px 14px;
position: relative;
top: -10px;
}
}
// Protocolsio Preview protocol modal
@media (min-width: 768px) {
#modal-import-json-protocol-preview .modal-dialog {
width: 70%;
}
}
#modal-import-json-protocol-preview .modal-dialog {
.modal-body {
max-height: 75vh;
overflow-y: auto;
width: 100%;
.ql-editor {
min-height: initial;
}
}
}
/* Steps and Results */
#steps {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAABCAYAAACsXeyTAAAAIUlEQVQImWNgoD5gZGBgMILSjKRo/P//vwiSGQwMDAwMAEnaA0jgHoquAAAAAElFTkSuQmCC");
@ -1796,6 +1851,15 @@ textarea.textarea-sm {
-webkit-padding-before: 0;
-webkit-padding-after: 0;
}
.btn.btn-default-link {
border: 0;
height: 100%;
min-height: 34px;
padding: 0;
-webkit-padding-after: 0;
-webkit-padding-before: 0;
}
}
}
@ -2002,6 +2066,7 @@ th.custom-field .modal-tooltiptext {
padding: 3px;
}
.btn:last-child {
margin-right: 20px;
}

View file

@ -0,0 +1,90 @@
module Api
class ApiController < ActionController::Base
attr_reader :iss
attr_reader :token
attr_reader :current_user
before_action :load_token, except: %i(authenticate status)
before_action :load_iss, except: %i(authenticate status)
before_action :authenticate_request!, except: %i(authenticate status)
rescue_from StandardError do |e|
logger.error e.message
render json: {}, status: :bad_request
end
rescue_from JWT::InvalidPayload, JWT::DecodeError do |e|
logger.error e.message
render json: { message: I18n.t('api.core.invalid_token') },
status: :unauthorized
end
def initialize
super
@iss = nil
end
def status
response = {}
response[:message] = I18n.t('api.core.status_ok')
response[:versions] = []
Extends::API_VERSIONS.each do |ver|
response[:versions] << { version: ver, baseUrl: "/api/#{ver}/" }
end
render json: response, status: :ok
end
def authenticate
if auth_params[:grant_type] == 'password'
user = User.find_by_email(auth_params[:email])
unless user && user.valid_password?(auth_params[:password])
raise StandardError, 'Wrong user password'
end
payload = { user_id: user.id }
token = CoreJwt.encode(payload)
render json: { token_type: 'bearer', access_token: token }
else
raise StandardError, 'Wrong grant type in request'
end
end
private
def load_token
if request.headers['Authorization']
@token =
request.headers['Authorization'].scan(/Bearer (.*)$/).flatten.last
end
raise StandardError, 'No token in the header' unless @token
end
def authenticate_request!
Extends::API_PLUGABLE_AUTH_METHODS.each do |auth_method|
method(auth_method).call
return true if current_user
end
# Check request header for proper auth token
payload = CoreJwt.decode(token)
@current_user = User.find_by_id(payload['user_id'])
# Implement sliding sessions, i.e send new token in case of successful
# authorization and when tokens TTL reached specific value (to avoid token
# generation on each request)
if CoreJwt.refresh_needed?(payload)
new_token = CoreJwt.encode(user_id: @current_user.id)
response.headers['X-Access-Token'] = new_token
end
rescue JWT::ExpiredSignature
render json: { message: I18n.t('api.core.expired_token') },
status: :unauthorized
end
def load_iss
@iss = CoreJwt.read_iss(token)
raise JWT::InvalidPayload, 'Wrong ISS in the token' unless @iss
end
def auth_params
params.permit(:grant_type, :email, :password)
end
end
end

View file

@ -0,0 +1,54 @@
module Api
module V20170715
class CoreApiController < ApiController
include PermissionHelper
def tasks_tree
teams_json = []
current_user.teams.find_each do |tm|
team = tm.as_json(only: %i(name description))
team['team_id'] = tm.id.to_s
projects = []
tm.projects.find_each do |pr|
project = pr.as_json(only: %i(name visibility archived))
project['project_id'] = pr.id.to_s
experiments = []
pr.experiments.find_each do |exp|
experiment = exp.as_json(only: %i(name description archived))
experiment['experiment_id'] = exp.id.to_s
tasks = []
exp.my_modules.find_each do |tk|
task = tk.as_json(only: %i(name description archived))
task['task_id'] = tk.id.to_s
task['editable'] = can_edit_module(tk)
tasks << task
end
experiment['tasks'] = tasks
experiments << experiment
end
project['experiments'] = experiments
projects << project
end
team['projects'] = projects
teams_json << team
end
render json: teams_json, status: :ok
end
def task_samples
task = MyModule.find_by_id(params[:task_id])
return render json: {}, status: :not_found unless task
return render json: {}, status: :forbidden unless can_view_module(task)
samples = task.samples
samples_json = []
samples.find_each do |s|
sample = {}
sample['sample_id'] = s.id.to_s
sample['name'] = s.name
samples_json << sample
end
render json: samples_json, status: :ok
end
end
end
end

View file

@ -1,84 +1,85 @@
class ProtocolsController < ApplicationController
include RenamingUtil
include ProtocolsImporter
include ProtocolsExporter
include InputSanitizeHelper
include ProtocolsIoHelper
before_action :check_create_permissions, only: [
:create_new_modal,
:create
]
before_action :check_clone_permissions, only: [ :clone ]
before_action :check_view_permissions, only: [
:protocol_status_bar,
:updated_at_label,
:preview,
:linked_children,
:linked_children_datatable
]
before_action :check_edit_permissions, only: [
:edit,
:update_metadata,
:update_keywords,
:edit_name_modal,
:edit_keywords_modal,
:edit_authors_modal,
:edit_description_modal
]
before_action :check_view_all_permissions, only: [
:index,
:datatable
]
before_action :check_unlink_permissions, only: [
:unlink,
:unlink_modal
]
before_action :check_revert_permissions, only: [
:revert,
:revert_modal
]
before_action :check_update_parent_permissions, only: [
:update_parent,
:update_parent_modal
]
before_action :check_update_from_parent_permissions, only: [
:update_from_parent,
:update_from_parent_modal
]
before_action :check_load_from_repository_views_permissions, only: [
:load_from_repository_modal,
:load_from_repository_datatable
]
before_action :check_create_permissions, only: %i(
create_new_modal
create
)
before_action :check_clone_permissions, only: [:clone]
before_action :check_view_permissions, only: %i(
protocol_status_bar
updated_at_label
preview
linked_children
linked_children_datatable
)
before_action :check_edit_permissions, only: %i(
edit
update_metadata
update_keywords
edit_name_modal
edit_keywords_modal
edit_authors_modal
edit_description_modal
)
before_action :check_view_all_permissions, only: %i(
index
datatable
)
before_action :check_unlink_permissions, only: %i(
unlink
unlink_modal
)
before_action :check_revert_permissions, only: %i(
revert
revert_modal
)
before_action :check_update_parent_permissions, only: %i(
update_parent
update_parent_modal
)
before_action :check_update_from_parent_permissions, only: %i(
update_from_parent
update_from_parent_modal
)
before_action :check_load_from_repository_views_permissions, only: %i(
load_from_repository_modal
load_from_repository_datatable
)
before_action :check_load_from_repository_permissions, only: [
:load_from_repository
]
before_action :check_load_from_file_permissions, only: [
:load_from_file
]
before_action :check_copy_to_repository_permissions, only: [
:copy_to_repository,
:copy_to_repository_modal
]
before_action :check_make_private_permissions, only: [ :make_private ]
before_action :check_publish_permissions, only: [ :publish ]
before_action :check_archive_permissions, only: [ :archive ]
before_action :check_restore_permissions, only: [ :restore ]
before_action :check_import_permissions, only: [ :import ]
before_action :check_export_permissions, only: [ :export ]
before_action :check_copy_to_repository_permissions, only: %i(
copy_to_repository
copy_to_repository_modal
)
before_action :check_make_private_permissions, only: [:make_private]
before_action :check_publish_permissions, only: [:publish]
before_action :check_archive_permissions, only: [:archive]
before_action :check_restore_permissions, only: [:restore]
before_action :check_import_permissions, only: [:import]
before_action :check_export_permissions, only: [:export]
def index
end
def index; end
def datatable
respond_to do |format|
format.json {
format.json do
render json: ::ProtocolsDatatable.new(
view_context,
@current_team,
@type,
current_user
)
}
end
end
end
@ -103,50 +104,47 @@ class ProtocolsController < ApplicationController
def linked_children
respond_to do |format|
format.json {
format.json do
render json: {
title: I18n.t('protocols.index.linked_children.title',
protocol: escape_input(@protocol.name)),
html: render_to_string({
partial: "protocols/index/linked_children_modal_body.html.erb",
locals: { protocol: @protocol }
})
html: render_to_string(partial: 'protocols/index/linked_children_modal_body.html.erb',
locals: { protocol: @protocol })
}
}
end
end
end
def linked_children_datatable
respond_to do |format|
format.json {
format.json do
render json: ::ProtocolLinkedChildrenDatatable.new(
view_context,
@protocol,
current_user,
self
)
}
end
end
end
def make_private
move_protocol("make_private")
move_protocol('make_private')
end
def publish
move_protocol("publish")
move_protocol('publish')
end
def archive
move_protocol("archive")
move_protocol('archive')
end
def restore
move_protocol("restore")
move_protocol('restore')
end
def edit
end
def edit; end
def update_metadata
@protocol.record_timestamps = false
@ -154,27 +152,27 @@ class ProtocolsController < ApplicationController
respond_to do |format|
if @protocol.save
format.json {
format.json do
render json: {
updated_at_label: render_to_string(
partial: "protocols/header/updated_at_label.html.erb"
partial: 'protocols/header/updated_at_label.html.erb'
),
name_label: render_to_string(
partial: "protocols/header/name_label.html.erb"
partial: 'protocols/header/name_label.html.erb'
),
authors_label: render_to_string(
partial: "protocols/header/authors_label.html.erb"
partial: 'protocols/header/authors_label.html.erb'
),
description_label: render_to_string(
partial: "protocols/header/description_label.html.erb"
partial: 'protocols/header/description_label.html.erb'
)
}
}
end
else
format.json {
format.json do
render json: @protocol.errors,
status: :unprocessable_entity
}
end
end
end
end
@ -182,20 +180,22 @@ class ProtocolsController < ApplicationController
def update_keywords
respond_to do |format|
# sanitize user input
params[:keywords].collect! do |keyword|
escape_input(keyword)
if params[:keywords]
params[:keywords].collect! do |keyword|
escape_input(keyword)
end
end
if @protocol.update_keywords(params[:keywords])
format.json {
format.json do
render json: {
updated_at_label: render_to_string(
partial: "protocols/header/updated_at_label.html.erb"
partial: 'protocols/header/updated_at_label.html.erb'
),
keywords_label: render_to_string(
partial: "protocols/header/keywords_label.html.erb"
partial: 'protocols/header/keywords_label.html.erb'
)
}
}
end
else
format.json { render json: {}, status: :unprocessable_entity }
end
@ -214,13 +214,11 @@ class ProtocolsController < ApplicationController
@protocol.record_timestamps = false
@protocol.created_at = ts
@protocol.updated_at = ts
if @type == :public
@protocol.published_on = ts
end
@protocol.published_on = ts if @type == :public
respond_to do |format|
if @protocol.save
format.json {
format.json do
render json: {
url: edit_protocol_path(
@protocol,
@ -228,12 +226,12 @@ class ProtocolsController < ApplicationController
type: @type
)
}
}
end
else
format.json {
format.json do
render json: @protocol.errors,
status: :unprocessable_entity
}
end
end
end
end
@ -248,9 +246,9 @@ class ProtocolsController < ApplicationController
end
end
respond_to do |format|
if cloned != nil
if !cloned.nil?
flash[:success] = t(
"protocols.index.clone.success_flash",
'protocols.index.clone.success_flash',
original: @original.name,
new: cloned.name
)
@ -258,7 +256,7 @@ class ProtocolsController < ApplicationController
format.json { render json: {}, status: :ok }
else
flash[:error] = t(
"protocols.index.clone.error_flash",
'protocols.index.clone.error_flash',
original: @original.name
)
flash.keep(:error)
@ -287,18 +285,18 @@ class ProtocolsController < ApplicationController
if transaction_error
# Bad request error
format.json {
format.json do
render json: {
message: t("my_modules.protocols.copy_to_repository_modal.error_400")
message: t('my_modules.protocols.copy_to_repository_modal.error_400')
},
status: :bad_request
}
end
elsif @new.invalid?
# Render errors
format.json {
format.json do
render json: @new.errors,
status: :unprocessable_entity
}
end
else
# Everything good, render 200
format.json { render json: { refresh: link_protocols }, status: :ok }
@ -320,16 +318,16 @@ class ProtocolsController < ApplicationController
if transaction_error
# Bad request error
format.json {
format.json do
render json: {
message: t("my_modules.protocols.unlink_error")
message: t('my_modules.protocols.unlink_error')
},
status: :bad_request
}
end
else
# Everything good, display flash & render 200
flash[:success] = t(
"my_modules.protocols.unlink_flash",
'my_modules.protocols.unlink_flash'
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
@ -392,12 +390,12 @@ class ProtocolsController < ApplicationController
if transaction_error
# Bad request error
format.json {
format.json do
render json: {
message: t("my_modules.protocols.update_parent_error")
message: t('my_modules.protocols.update_parent_error')
},
status: :bad_request
}
end
else
# Everything good, record activity, display flash & render 200
Activity.create(
@ -413,7 +411,7 @@ class ProtocolsController < ApplicationController
)
)
flash[:success] = t(
"my_modules.protocols.update_parent_flash",
'my_modules.protocols.update_parent_flash'
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
@ -443,16 +441,16 @@ class ProtocolsController < ApplicationController
if transaction_error
# Bad request error
format.json {
format.json do
render json: {
message: t("my_modules.protocols.update_from_parent_error")
message: t('my_modules.protocols.update_from_parent_error')
},
status: :bad_request
}
end
else
# Everything good, display flash & render 200
flash[:success] = t(
"my_modules.protocols.update_from_parent_flash",
'my_modules.protocols.update_from_parent_flash'
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
@ -533,9 +531,9 @@ class ProtocolsController < ApplicationController
end
if transaction_error
format.json {
format.json do
render json: { status: :error }, status: :bad_request
}
end
else
# Everything good, record activity, display flash & render 200
Activity.create(
@ -551,12 +549,12 @@ class ProtocolsController < ApplicationController
)
)
flash[:success] = t(
'my_modules.protocols.load_from_file_flash',
'my_modules.protocols.load_from_file_flash'
)
flash.keep(:success)
format.json {
format.json do
render json: { status: :ok }, status: :ok
}
end
end
else
format.json do
@ -586,20 +584,123 @@ class ProtocolsController < ApplicationController
t('protocols.index.no_protocol_name')
end
if transaction_error
format.json {
format.json do
render json: { name: p_name, status: :bad_request }, status: :bad_request
}
end
else
format.json {
format.json do
render json: {
name: p_name, new_name: protocol.name, status: :ok
},
status: :ok
}
end
end
end
end
def protocolsio_import_create
@protocolsio_too_big = false
@protocolsio_invalid_file = false
extension = File.extname(params[:json_file].path)
file_size = File.size(params[:json_file].path)
if extension != '.txt' && extension != '.json'
@protocolsio_invalid_file = true
respond_to do |format|
format.js {}
end
return 0 # return 0 stops the rest of the controller code from executing
end
if file_size > Constants::FILE_MAX_SIZE_MB.megabytes
@protocolsio_too_big = true
respond_to do |format|
format.js {}
# if file is too big, default to the js.erb file,
# named the same as this controller
# where a javascript alert is called
end
return 0 # return 0 stops the rest of the controller code from executing
end
json_file_contents = File.read(params[:json_file].path)
json_file_contents.gsub! '\"', "'"
# escaped double quotes too stressfull, html works with single quotes too
# json double quotes dont get escaped since they dont match \"
unless valid_protocol_json(json_file_contents)
@protocolsio_invalid_file = true
respond_to do |format|
format.js {}
end
return 0 # return 0 stops the rest of the controller code from executing
end
@json_object = JSON.parse(json_file_contents)
@protocol = Protocol.new
respond_to do |format|
format.js {} # go to the js.erb file named the same as this controller,
# where a preview modal is rendered,
# and some modals get closed and opened
end
end
def protocolsio_import_save
@json_object = JSON.parse(params['json_object'])
@db_json = {}
@toolong = false
@db_json['name'] = pio_eval_title_len(
sanitize_input(params['protocol']['name'])
)
# since scinote only has description field, and protocols.io has many others
# ,here i am putting everything important from protocols.io into description
@db_json['authors'] = pio_eval_title_len(
sanitize_input(params['protocol']['authors'])
)
@db_json['created_at'] = pio_eval_title_len(
sanitize_input(params['protocol']['created_at'])
)
@db_json['updated_at'] = pio_eval_title_len(
sanitize_input(params['protocol']['last_modified'])
)
@db_json['steps'] = {}
@db_json['steps'] = protocols_io_fill_step(
@json_object, @db_json['steps']
)
protocol = nil
respond_to do |format|
transaction_error = false
@protocolsio_general_error = false
Protocol.transaction do
begin
protocol = import_new_protocol(
@db_json, current_team, params[:type].to_sym, current_user
)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
end
p_name =
if @db_json['name'].present?
escape_input(@db_json['name'])
else
t('protocols.index.no_protocol_name')
end
if transaction_error
@protocolsio_general_error = true
# General something went wrong, upload to db failed error
# format.json {
# render json: { name: p_name, status: :bad_request },
# status: :bad_request
# }
else
@protocolsio_general_error = false
format.json do
render json:
{ name: @db_json['name'], new_name: @db_json['name'], status: :ok },
status: :ok
end
end
format.js {}
end
end
def export
# Make a zip output stream and send it to the client
respond_to do |format|
@ -622,15 +723,29 @@ class ProtocolsController < ApplicationController
protocol.steps.order(:id).each do |step|
step_guid = get_guid(step.id)
step_dir = "#{protocol_dir}/#{step_guid}"
next if step.assets.count <= 0
step.assets.order(:id).each do |asset|
asset_guid = get_guid(asset.id)
asset_file_name = asset_guid.to_s +
File.extname(asset.file_file_name).to_s
ostream.put_next_entry("#{step_dir}/#{asset_file_name}")
input_file = asset.open
ostream.print(input_file.read)
input_file.close
if step.assets.exists?
step.assets.order(:id).each do |asset|
asset_guid = get_guid(asset.id)
asset_file_name = asset_guid.to_s +
File.extname(asset.file_file_name).to_s
ostream.put_next_entry("#{step_dir}/#{asset_file_name}")
input_file = asset.open
ostream.print(input_file.read)
input_file.close
end
end
if step.tiny_mce_assets.exists?
step.tiny_mce_assets.order(:id).each do |tiny_mce_asset|
asset_guid = get_guid(tiny_mce_asset.id)
asset_file_name =
"rte-#{asset_guid.to_s +
File.extname(tiny_mce_asset.image_file_name).to_s}"
ostream.put_next_entry("#{step_dir}/#{asset_file_name}")
input_file = tiny_mce_asset.open
ostream.print(input_file.read)
input_file.close
end
end
end
end
@ -658,81 +773,81 @@ class ProtocolsController < ApplicationController
def unlink_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: t("my_modules.protocols.confirm_link_update_modal.unlink_title"),
message: t("my_modules.protocols.confirm_link_update_modal.unlink_message"),
btn_text: t("my_modules.protocols.confirm_link_update_modal.unlink_btn_text"),
title: t('my_modules.protocols.confirm_link_update_modal.unlink_title'),
message: t('my_modules.protocols.confirm_link_update_modal.unlink_message'),
btn_text: t('my_modules.protocols.confirm_link_update_modal.unlink_btn_text'),
url: unlink_protocol_path(@protocol)
}
}
end
end
end
def revert_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: t("my_modules.protocols.confirm_link_update_modal.revert_title"),
message: t("my_modules.protocols.confirm_link_update_modal.revert_message"),
btn_text: t("my_modules.protocols.confirm_link_update_modal.revert_btn_text"),
title: t('my_modules.protocols.confirm_link_update_modal.revert_title'),
message: t('my_modules.protocols.confirm_link_update_modal.revert_message'),
btn_text: t('my_modules.protocols.confirm_link_update_modal.revert_btn_text'),
url: revert_protocol_path(@protocol)
}
}
end
end
end
def update_parent_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: t("my_modules.protocols.confirm_link_update_modal.update_parent_title"),
message: t("my_modules.protocols.confirm_link_update_modal.update_parent_message"),
btn_text: t("general.update"),
title: t('my_modules.protocols.confirm_link_update_modal.update_parent_title'),
message: t('my_modules.protocols.confirm_link_update_modal.update_parent_message'),
btn_text: t('general.update'),
url: update_parent_protocol_path(@protocol)
}
}
end
end
end
def update_from_parent_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: t("my_modules.protocols.confirm_link_update_modal.update_self_title"),
message: t("my_modules.protocols.confirm_link_update_modal.update_self_message"),
btn_text: t("general.update"),
title: t('my_modules.protocols.confirm_link_update_modal.update_self_title'),
message: t('my_modules.protocols.confirm_link_update_modal.update_self_message'),
btn_text: t('general.update'),
url: update_from_parent_protocol_path(@protocol)
}
}
end
end
end
def load_from_repository_datatable
@protocol = Protocol.find_by_id(params[:id])
@type = (params[:type] || "public").to_sym
@type = (params[:type] || 'public').to_sym
respond_to do |format|
format.json {
format.json do
render json: ::LoadFromRepositoryProtocolsDatatable.new(
view_context,
@protocol.team,
@type,
current_user
)
}
end
end
end
def load_from_repository_modal
@protocol = Protocol.find_by_id(params[:id])
respond_to do |format|
format.json {
format.json do
render json: {
html: render_to_string({
partial: "my_modules/protocols/load_from_repository_modal_body.html.erb"
})
}
}
end
end
end
@ -740,99 +855,99 @@ class ProtocolsController < ApplicationController
@new = Protocol.new
@original = Protocol.find(params[:id])
respond_to do |format|
format.json {
format.json do
render json: {
html: render_to_string({
partial: "my_modules/protocols/copy_to_repository_modal_body.html.erb"
})
}
}
end
end
end
def protocol_status_bar
respond_to do |format|
format.json {
format.json do
render json: {
html: render_to_string({
partial: "my_modules/protocols/protocol_status_bar.html.erb"
})
}
}
end
end
end
def updated_at_label
respond_to do |format|
format.json {
format.json do
render json: {
html: render_to_string({
partial: "protocols/header/updated_at_label.html.erb"
})
}
}
end
end
end
def create_new_modal
@new_protocol = Protocol.new
respond_to do |format|
format.json {
format.json do
render json: {
html: render_to_string({
partial: "protocols/index/create_new_modal_body.html.erb"
})
}
}
end
end
end
def edit_name_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: I18n.t('protocols.header.edit_name_modal.title',
protocol: escape_input(@protocol.name)),
html: render_to_string({
partial: "protocols/header/edit_name_modal_body.html.erb"
})
html: render_to_string({
partial: "protocols/header/edit_name_modal_body.html.erb"
})
}
}
end
end
end
def edit_keywords_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: I18n.t('protocols.header.edit_keywords_modal.title',
protocol: escape_input(@protocol.name)),
html: render_to_string({
partial: "protocols/header/edit_keywords_modal_body.html.erb"
}),
html: render_to_string({
partial: "protocols/header/edit_keywords_modal_body.html.erb"
}),
keywords: @protocol.team.protocol_keywords_list
}
}
end
end
end
def edit_authors_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: I18n.t('protocols.header.edit_authors_modal.title',
protocol: escape_input(@protocol.name)),
html: render_to_string({
partial: "protocols/header/edit_authors_modal_body.html.erb"
})
html: render_to_string({
partial: "protocols/header/edit_authors_modal_body.html.erb"
})
}
}
end
end
end
def edit_description_modal
respond_to do |format|
format.json {
format.json do
render json: {
title: I18n.t('protocols.header.edit_description_modal.title',
protocol: escape_input(@protocol.name)),
@ -840,12 +955,19 @@ class ProtocolsController < ApplicationController
partial: "protocols/header/edit_description_modal_body.html.erb"
})
}
}
end
end
end
private
def valid_protocol_json(json)
JSON.parse(json)
return true
rescue JSON::ParserError => e
return false
end
def move_protocol(action)
rollbacked = false
results = []
@ -875,19 +997,19 @@ class ProtocolsController < ApplicationController
end
respond_to do |format|
unless rollbacked
format.json {
if rollbacked
format.json do
render json: {}, status: :bad_request
end
else
format.json do
render json: {
html: render_to_string({
partial: "protocols/index/results_modal_body.html.erb",
locals: { results: results, en_action: "#{action}_results" }
})
}
}
else
format.json {
render json: {}, status: :bad_request
}
end
end
end
end
@ -895,7 +1017,7 @@ class ProtocolsController < ApplicationController
def load_team_and_type
@current_team = current_team
# :public, :private or :archive
@type = (params[:type] || "public").to_sym
@type = (params[:type] || 'public').to_sym
end
def check_view_all_permissions
@ -943,25 +1065,19 @@ class ProtocolsController < ApplicationController
def check_unlink_permissions
@protocol = Protocol.find_by_id(params[:id])
if @protocol.blank? || !can_unlink_protocol(@protocol)
render_403
end
render_403 if @protocol.blank? || !can_unlink_protocol(@protocol)
end
def check_revert_permissions
@protocol = Protocol.find_by_id(params[:id])
if @protocol.blank? || !can_revert_protocol(@protocol)
render_403
end
render_403 if @protocol.blank? || !can_revert_protocol(@protocol)
end
def check_update_parent_permissions
@protocol = Protocol.find_by_id(params[:id])
if @protocol.blank? || !can_update_parent_protocol(@protocol)
render_403
end
render_403 if @protocol.blank? || !can_update_parent_protocol(@protocol)
end
def check_update_from_parent_permissions
@ -975,9 +1091,7 @@ class ProtocolsController < ApplicationController
def check_load_from_repository_views_permissions
@protocol = Protocol.find_by_id(params[:id])
if @protocol.blank? || !can_view_protocol(@protocol)
render_403
end
render_403 if @protocol.blank? || !can_view_protocol(@protocol)
end
def check_load_from_repository_permissions
@ -995,9 +1109,9 @@ class ProtocolsController < ApplicationController
@my_module = @protocol.my_module
if @protocol_json.blank? ||
@protocol.blank? ||
@my_module.blank? ||
!can_load_protocol_into_module(@my_module)
@protocol.blank? ||
@my_module.blank? ||
!can_load_protocol_into_module(@my_module)
render_403
end
end
@ -1006,7 +1120,7 @@ class ProtocolsController < ApplicationController
@protocol = Protocol.find_by_id(params[:id])
@my_module = @protocol.my_module
if @my_module.blank? or !can_copy_protocol_to_repository(@my_module)
if @my_module.blank? || !can_copy_protocol_to_repository(@my_module)
render_403
end
end
@ -1059,12 +1173,9 @@ class ProtocolsController < ApplicationController
@protocol_json = params[:protocol]
@team = Team.find(params[:team_id])
@type = params[:type] ? params[:type].to_sym : nil
if !(
@protocol_json.present? &&
@team.present? &&
(@type == :public || @type == :private) &&
can_create_protocols_in_repository?(@team)
)
unless @protocol_json.present? && @team.present? &&
(@type == :public || @type == :private) &&
can_create_protocols_in_repository?(@team)
render_403
end
end
@ -1091,5 +1202,4 @@ class ProtocolsController < ApplicationController
def metadata_params
params.require(:protocol).permit(:name, :authors, :description)
end
end

View file

@ -34,7 +34,8 @@ class ResultTextsController < ApplicationController
def create
@result_text = ResultText.new(result_params[:result_text_attributes])
# gerate a tag that replaces img tag in database
@result_text.text = parse_tiny_mce_asset_to_token(@result_text.text)
@result_text.text = parse_tiny_mce_asset_to_token(@result_text.text,
@result_text)
@result = Result.new(
user: current_user,
my_module: @my_module,
@ -88,7 +89,8 @@ class ResultTextsController < ApplicationController
end
def edit
@result_text.text = generate_image_tag_from_token(@result_text.text)
@result_text.text = generate_image_tag_from_token(@result_text.text,
@result_text)
respond_to do |format|
format.json {
render json: {

View file

@ -30,7 +30,7 @@ class StepsController < ApplicationController
def create
@step = Step.new(step_params)
# gerate a tag that replaces img tag in database
@step.description = parse_tiny_mce_asset_to_token(@step.description)
@step.description = parse_tiny_mce_asset_to_token(@step.description, @step)
@step.completed = false
@step.position = @protocol.number_of_steps
@step.protocol = @protocol
@ -96,11 +96,9 @@ class StepsController < ApplicationController
status: :ok
end
else
format.json {
render json: {
html: render_to_string(partial: 'new.html.erb')
}, status: :bad_request
}
format.json do
render json: @step.errors.to_json, status: :bad_request
end
end
end
end
@ -120,7 +118,7 @@ class StepsController < ApplicationController
end
def edit
@step.description = generate_image_tag_from_token(@step.description)
@step.description = generate_image_tag_from_token(@step.description, @step)
respond_to do |format|
format.json do
render json: {

View file

@ -10,6 +10,7 @@ module Users
before_action :update_sanitized_params, only: :update
def update
return super unless Rails.configuration.x.new_team_on_signup
# Instantialize a new team with the provided name
@team = Team.new
@team.name = params[:team][:name]
@ -29,6 +30,8 @@ module Users
end
def accept_resource
return super unless Rails.configuration.x.new_team_on_signup
unless @team.valid?
# Find the user being invited
resource = User.find_by_invitation_token(

View file

@ -1,28 +1,32 @@
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# You should configure your model like this:
# devise :omniauthable, omniauth_providers: [:twitter]
module Users
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :verify_authenticity_token
# You should also create an action method in this controller like this:
# def twitter
# end
# You should configure your model like this:
# devise :omniauthable, omniauth_providers: [:twitter]
# More info at:
# https://github.com/plataformatec/devise#omniauth
# You should also create an action method in this controller like this:
# def twitter
# end
# GET|POST /resource/auth/twitter
# def passthru
# super
# end
# More info at:
# https://github.com/plataformatec/devise#omniauth
# GET|POST /users/auth/twitter/callback
# def failure
# super
# end
# GET|POST /resource/auth/twitter
# def passthru
# super
# end
# protected
# GET|POST /users/auth/twitter/callback
# def failure
# super
# end
# The path used when OmniAuth fails
# def after_omniauth_failure_path_for(scope)
# super(scope)
# end
# protected
# The path used when OmniAuth fails
# def after_omniauth_failure_path_for(scope)
# super(scope)
# end
end
end

View file

@ -128,38 +128,44 @@ class Users::RegistrationsController < Devise::RegistrationsController
def create
render_403 && return unless Rails.configuration.x.enable_user_registration
build_resource(sign_up_params)
valid_resource = resource.valid?
# ugly checking if new team on sign up is enabled :(
if Rails.configuration.x.new_team_on_signup
# Create new team for the new user
@team = Team.new
@team.name = params[:team][:name]
valid_team = @team.valid?
# Create new team for the new user
@team = Team.new
@team.name = params[:team][:name]
valid_team = @team.valid?
if valid_team && valid_resource
# this must be called after @team variable is defined. Otherwise this
# variable won't be accessable in view.
super do |resource|
# Set the confirmed_at == created_at IF not using email confirmations
unless Rails.configuration.x.enable_email_confirmations
resource.update(confirmed_at: resource.created_at)
end
if valid_team && valid_resource
if resource.valid? && resource.persisted?
@team.created_by = resource # set created_by for oraganization
@team.save
# this must be called after @team variable is defined. Otherwise this
# variable won't be accessable in view.
# Add this user to the team as owner
UserTeam.create(user: resource, team: @team, role: :admin)
# set current team to new user
resource.current_team_id = @team.id
resource.save
end
end
else
render :new
end
elsif valid_resource
super do |resource|
# Set the confirmed_at == created_at IF not using email confirmations
unless Rails.configuration.x.enable_email_confirmations
resource.update(confirmed_at: resource.created_at)
end
if resource.valid? && resource.persisted?
@team.created_by = resource # set created_by for oraganization
@team.save
# Add this user to the team as owner
UserTeam.create(
user: resource,
team: @team,
role: :admin
)
# set current team to new user
resource.current_team_id = @team.id
resource.save
end
end

View file

@ -0,0 +1,8 @@
module Users
module Settings
module Account
class AddonsController < ApplicationController
end
end
end
end

View file

@ -0,0 +1,375 @@
module ProtocolsIoHelper
#=============================================================================
# Protocols.io limits
#=============================================================================
TEXT_MAX_LENGTH = Constants::TEXT_MAX_LENGTH
PIO_ELEMENT_RESERVED_LENGTH_BIG = TEXT_MAX_LENGTH * 0.015
PIO_ELEMENT_RESERVED_LENGTH_MEDIUM = TEXT_MAX_LENGTH * 0.01
PIO_ELEMENT_RESERVED_LENGTH_SMALL = TEXT_MAX_LENGTH * 0.005
# PROTOCOLS.IO PROTOCOL ATTRIBUTES
PIO_P_AVAILABLE_LENGTH =
TEXT_MAX_LENGTH -
(PIO_ELEMENT_RESERVED_LENGTH_SMALL * 2 +
PIO_ELEMENT_RESERVED_LENGTH_MEDIUM * 8 +
PIO_ELEMENT_RESERVED_LENGTH_BIG * 2)
# -- 2 small = created at , publish date PROTOCOL ATTRIBUTES
# -- 8 medium = description,tags,before_start,warning,guidelines,
# manuscript_citation,keywords,vendor_name PROTOCOL ATTRIBUTES
# -- 2 big = vendor_link, link PROTOCOL ATTRIBUTES
# PROTOCOLS.IO STEP ATTRIBUTES
PIO_S_AVAILABLE_LENGTH =
TEXT_MAX_LENGTH -
(PIO_ELEMENT_RESERVED_LENGTH_SMALL * 20)
# -- 20 small = description,expected_result,safety_information
# software_package version, software_package os_name,
# software_package os_version,software_package link,
# software_package repository,software_package developer,software_package name
# commands os_version,commands os_name, commands name,commands description,
# sub protocol full name (author), sub protocol name, sub protocol link,
# dataset link,dataset name, safety_information link,
# -- 0 medium =
# -- 0 big =
PIO_TITLE_TOOLONG_LEN =
I18n.t('protocols.protocols_io_import.title_too_long').length + 2
PIO_STEP_TOOLONG_LEN =
I18n.t('protocols.protocols_io_import.too_long').length
# The + 2 above (in title) is there because if the length was at the limit,
# the cutter method had issues, this gives it some space
def protocolsio_string_to_table_element(description_string)
string_without_tables = string_html_table_remove(description_string)
table_regex = %r{<table\b[^>]*>(.*?)<\/table>}m
tr_regex = %r{<tr\b[^>]*>(.*?)<\/tr>}m
td_regex = %r{<td\b[^>]*>(.*?)<\/td>}m
tables = {}
table_strings = description_string.scan(table_regex)
table_strings.each_with_index do |table, table_counter|
tables[table_counter.to_s] = {}
tr_strings = table[0].scan(tr_regex)
contents = {}
contents['data'] = []
tr_strings.each_with_index do |tr, tr_counter|
td_strings = tr[0].scan(td_regex)
contents['data'][tr_counter] = []
td_strings.each do |td|
td_stripped = ActionController::Base.helpers.strip_tags(td[0])
contents['data'][tr_counter].push(td_stripped)
end
end
tables[table_counter.to_s]['contents'] = Base64.encode64(
contents.to_s.sub('=>', ':')
)
tables[table_counter.to_s]['name'] = nil
end
# return string_without_tables, tables
return tables, string_without_tables
end
def string_html_table_remove(description_string)
description_string.remove!("\n", "\t", "\r", "\f")
table_whole_regex = %r{(<table\b[^>]*>.*?<\/table>)}m
table_pattern_array = description_string.scan(table_whole_regex)
string_without_tables = description_string
table_pattern_array.each do |table_pattern|
string_without_tables = string_without_tables.gsub(
table_pattern[0],
t('protocols.protocols_io_import.comp_append.table_moved').html_safe
)
end
string_without_tables
end
def pio_eval_prot_desc(text, attribute_name)
case attribute_name
when 'publish_date'
pio_eval_len(text, ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL)
when 'vendor_link', 'link'
pio_eval_len(text, ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_BIG)
else
pio_eval_len(text, ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM)
end
end
def pio_eval_title_len(text)
if text
text += ' ' if text.length < Constants::NAME_MIN_LENGTH
if text.length > Constants::NAME_MAX_LENGTH
text =
text[0..(Constants::NAME_MAX_LENGTH - PIO_TITLE_TOOLONG_LEN)] +
t('protocols.protocols_io_import.title_too_long')
@toolong = true
end
text
end
end
def pio_eval_len(text, reserved)
if text
text_end = reserved + @remaining - PIO_STEP_TOOLONG_LEN
text_end = 2 if text_end < 2
# Since steps have very low reserved values now (below 100),
# the above sets their index to 1 if its negative
# (length of toolong text is about 90 chars, and if remaining is 0,
# then the negative index just gets set to 1. this is a workaround
# it would also be possible to not count the length of the "too long" text
# or setting the import reserved value to 95,but then available characters
# will be like before (around 7600)
if text.length - reserved > @remaining
text =
close_open_html_tags(
text[0..text_end] + t('protocols.protocols_io_import.too_long')
)
@toolong = true
@remaining = 0
elsif (text.length - reserved) > 0
@remaining -= text.length - reserved
end
text
end
end
# Checks so that null values are returned as zero length strings
# Did this so views arent as ugly (i avoid using if present statements)
def not_null(attribute)
if attribute
attribute
else
''
end
end
def close_open_html_tags(text)
Nokogiri::HTML::DocumentFragment.parse(text).to_html
end
def prepare_for_view(attribute_text1, size, table = 'no_table')
if table == 'no_table'
attribute_text = sanitize_input(not_null(attribute_text1))
elsif table == 'table'
attribute_text = sanitize_input(
string_html_table_remove(not_null(attribute_text1))
)
end
pio_eval_len(
attribute_text,
size
)
end
def fill_attributes(attribute_name, attribute_text, step_component)
output_string = ''
trans_string = step_component
trans_string +=
if attribute_name != 'os_name' && attribute_name != 'os_version'
attribute_name
else
'os'
end
output_string +=
if attribute_name != 'os_version'
t(trans_string)
else
' , '
end
output_string += prepare_for_view(
attribute_text, ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL
)
output_string
end
# pio_stp_x means protocols io step (id of component) parser
def pio_stp_1(iterating_key) # protocols io description parser
br = '<br>'
append =
if iterating_key.present?
br +
prepare_for_view(
iterating_key,
ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL
) +
br
else
t('protocols.protocols_io_import.comp_append.missing_desc')
end
append
end
def pio_stp_6(iterating_key) # protocols io section(title) parser
return pio_eval_title_len(sanitize_input(iterating_key)) if iterating_key.present?
t('protocols.protocols_io_import.comp_append.missing_step')
end
def pio_stp_17(iterating_key) # protocols io expected result parser
if iterating_key.present?
append =
t('protocols.protocols_io_import.comp_append.expected_result') +
prepare_for_view(
iterating_key, ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL
) +
'<br>'
return append
end
''
end
# protocols io software package,dataset,commands,sub_protocol and safety_information parser
def pio_stp(iterating_key, parse_elements_array, en_local_text)
append = ''
parse_elements_array.each do |element|
return '' unless iterating_key[element]
append += fill_attributes(
element,
iterating_key[element],
en_local_text
)
end
append
end
def protocols_io_fill_desc(json_hash)
description_array = %w[
( before_start warning guidelines manuscript_citation publish_date
vendor_name vendor_link keywords tags link created_on )
]
description_string =
if json_hash['description'].present?
'<strong>' + t('protocols.protocols_io_import.preview.description') +
'</strong>' +
prepare_for_view(
json_hash['description'],
ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM
).html_safe
else
'<strong>' + t('protocols.protocols_io_import.preview.description') +
'</strong>' + t('protocols.protocols_io_import.comp_append.missing_desc')
end
description_string += '<br>'
description_array.each do |e|
if e == 'created_on' && json_hash[e].present?
new_e = '<strong>' + e.humanize + '</strong>'
description_string +=
new_e.to_s + ': ' +
prepare_for_view(
params['protocol']['created_at'].to_s,
ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL
) +
+ '<br>'
elsif e == 'tags' && json_hash[e].any? && json_hash[e] != ''
new_e = '<strong>' + e.humanize + '</strong>'
description_string +=
new_e.to_s + ': '
tags_length_checker = ''
json_hash[e].each do |tag|
tags_length_checker +=
sanitize_input(tag['tag_name']) + ' , '
end
description_string += prepare_for_view(
tags_length_checker,
ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM
)
description_string += '<br>'
elsif json_hash[e].present?
new_e = '<strong>' + e.humanize + '</strong>'
description_string +=
new_e.to_s + ': ' +
pio_eval_prot_desc(
sanitize_input(json_hash[e]),
e
).html_safe + '<br>'
end
end
description_string
end
def protocols_io_fill_step(original_json, newj)
# newj = new json
# (simple to map) id 1= step description, id 6= section (title),
# id 17= expected result
# (complex mapping with nested hashes) id 8 = software package,
# id 9 = dataset, id 15 = command, id 18 = attached sub protocol
# id 19= safety information ,
# id 20= regents (materials, like scinote samples kind of)
newj['0'] = {}
newj['0']['position'] = 0
newj['0']['name'] = 'Protocol info'
@remaining = ProtocolsIoHelper::PIO_P_AVAILABLE_LENGTH
newj['0']['tables'], table_str = protocolsio_string_to_table_element(
sanitize_input(protocols_io_fill_desc(original_json).html_safe)
)
newj['0']['description'] = table_str
original_json['steps'].each_with_index do |step, pos_orig| # loop over steps
i = pos_orig + 1
@remaining = ProtocolsIoHelper::PIO_S_AVAILABLE_LENGTH
# position of step (first, second.... etc),
newj[i.to_s] = {} # the json we will insert into db
newj[i.to_s]['position'] = i
newj[i.to_s]['description'] = '' unless newj[i.to_s].key?('description')
newj[i.to_s]['name'] = '' unless newj[i.to_s].key?('name')
step['components'].each do |key, value|
# sometimes there are random index values as keys
# instead of hashes, this is a workaround to that buggy json format
key = value if value.class == Hash
# append is the string that we append values into for description
# pio_stp_x means protocols io step (id of component) parser
case key['component_type_id']
when '1'
newj[i.to_s]['description'] += pio_stp_1(key['data'])
when '6'
newj[i.to_s]['name'] = pio_stp_6(key['data'])
when '17'
newj[i.to_s]['description'] += pio_stp_17(key['data'])
when '8'
pe_array = %w(
name developer version link repository os_name os_version
)
trans_text = 'protocols.protocols_io_import.comp_append.soft_packg.'
newj[i.to_s]['description'] += pio_stp(
key['source_data'], pe_array, trans_text
)
when '9'
pe_array = %w(
name link
)
trans_text = 'protocols.protocols_io_import.comp_append.dataset.'
newj[i.to_s]['description'] += pio_stp(
key['source_data'], pe_array, trans_text
)
when '15'
pe_array = %w(
name description os_name os_version
)
trans_text = 'protocols.protocols_io_import.comp_append.command.'
newj[i.to_s]['description'] += pio_stp(
key['source_data'], pe_array, trans_text
)
when '18'
pe_array = %w(
protocol_name full_name link
)
trans_text = 'protocols.protocols_io_import.comp_append.sub_protocol.'
newj[i.to_s]['description'] += pio_stp(
key['source_data'], pe_array, trans_text
)
when '19'
pe_array = %w(
body link
)
trans_text = 'protocols.protocols_io_import.comp_append.safety_infor.'
newj[i.to_s]['description'] += pio_stp(
key['source_data'], pe_array, trans_text
)
end # case end
end # finished looping over step components
newj[i.to_s]['tables'], table_str = protocolsio_string_to_table_element(
newj[i.to_s]['description']
)
newj[i.to_s]['description'] = table_str
end # steps
newj
end
end

View file

@ -1,29 +1,30 @@
module TinyMceHelper
def parse_tiny_mce_asset_to_token(text, ref = nil)
def parse_tiny_mce_asset_to_token(text, obj)
ids = []
html = Nokogiri::HTML(text)
html = Nokogiri::HTML(remove_pasted_tokens(text))
html.search('img').each do |img|
next unless img['data-token']
img_id = Base62.decode(img['data-token'])
ids << img_id
token = "[~tiny_mce_id:#{img_id}]"
img.replace(token)
next unless ref
next unless obj
tiny_img = TinyMceAsset.find_by_id(img_id)
tiny_img.reference = ref unless tiny_img.step || tiny_img.result_text
tiny_img.reference = obj unless tiny_img.step || tiny_img.result_text
tiny_img.save
end
destroy_removed_tiny_mce_assets(ids, ref) if ref
destroy_removed_tiny_mce_assets(ids, obj) if obj
html
end
def generate_image_tag_from_token(text)
def generate_image_tag_from_token(text, obj)
return unless text
regex = /\[~tiny_mce_id:([0-9a-zA-Z]+)\]/
regex = Constants::TINY_MCE_ASSET_REGEX
text.gsub(regex) do |el|
match = el.match(regex)
img = TinyMceAsset.find_by_id(match[1])
next unless img
next unless img && img.team == current_team
next unless check_image_permissions(obj, img)
image_tag img.url,
class: 'img-responsive',
data: { token: Base62.encode(img.id) }
@ -32,7 +33,7 @@ module TinyMceHelper
def link_tiny_mce_assets(text, ref)
ids = []
regex = /\[~tiny_mce_id:([0-9a-zA-Z]+)\]/
regex = Constants::TINY_MCE_ASSET_REGEX
text.gsub(regex) do |img|
match = img.match(regex)
tiny_img = TinyMceAsset.find_by_id(match[1])
@ -44,7 +45,35 @@ module TinyMceHelper
destroy_removed_tiny_mce_assets(ids, ref)
end
def replace_tiny_mce_assets(text, img_ids)
img_ids.each do |src_id, dest_id|
regex = /\[~tiny_mce_id:#{src_id}\]/
new_token = "[~tiny_mce_id:#{dest_id}]"
text.sub!(regex, new_token)
end
text
end
def destroy_removed_tiny_mce_assets(ids, ref)
ref.tiny_mce_assets.where.not('id IN (?)', ids).destroy_all
# need to check if the array is empty because if we pass the empty array
# in the SQL query it will not work properly
if ids.empty?
ref.tiny_mce_assets.destroy_all
else
ref.tiny_mce_assets.where.not('id IN (?)', ids).destroy_all
end
end
def check_image_permissions(obj, img)
if obj.class == Step
img.step == obj
elsif obj.class == ResultText
img.result_text == obj
end
end
def remove_pasted_tokens(text)
regex = Constants::TINY_MCE_ASSET_REGEX
text.gsub(regex, ' ')
end
end

View file

@ -1,7 +1,8 @@
module UserSettingsHelper
def on_settings_account_page?
controller_name == 'registrations' && action_name == 'edit' ||
controller_name == 'preferences' && action_name == 'index'
controller_name == 'preferences' && action_name == 'index' ||
controller_name == 'addons' && action_name == 'index'
end
def on_settings_account_profile_page?
@ -12,6 +13,10 @@ module UserSettingsHelper
controller_name == 'preferences'
end
def on_settings_account_addons_page?
controller_name == 'addons'
end
def on_settings_team_page?
controller_name.in?(%w(teams audits)) &&
action_name.in?(%w(index new create show audits_index))

View file

@ -12,8 +12,10 @@ class Asset < ApplicationRecord
has_attached_file :file,
styles: { large: [Constants::LARGE_PIC_FORMAT, :jpg],
medium: [Constants::MEDIUM_PIC_FORMAT, :jpg] },
convert_options: { medium: '-quality 70 -strip' }
convert_options: {
medium: '-quality 70 -strip',
all: '-background "#d2d2d2" -flatten +matte'
}
validates_attachment :file,
presence: true,
size: {

View file

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

View file

@ -135,8 +135,7 @@ class Repository < ApplicationRecord
end
total_nr += 1
# Creek XLSX parser returns Hash of the row, Roo - Array
row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s)
row = SpreadsheetParser.parse_row(row, sheet)
record_row = RepositoryRow.new(name: row[name_index],
repository: self,

View file

@ -86,8 +86,7 @@ class Team < ApplicationRecord
next
end
total_nr += 1
# Creek XLSX parser returns Hash of the row, Roo - Array
row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s)
row = SpreadsheetParser.parse_row(row, sheet)
sample = Sample.new(name: row[sname_index],
team: self,
@ -100,6 +99,7 @@ class Team < ApplicationRecord
end
row.each.with_index do |value, index|
next unless value.present?
if index == stype_index
stype = SampleType.where(team: self)
.where('name ILIKE ?', value.strip)

View file

@ -54,6 +54,14 @@ class TinyMceAsset < ApplicationRecord
end
end
def open
if image.is_stored_on_s3?
Kernel.open(presigned_url, 'rb')
else
File.open(image.path, 'rb')
end
end
private
def update_estimated_size

View file

@ -5,6 +5,7 @@ class User < ApplicationRecord
acts_as_token_authenticatable
devise :invitable, :confirmable, :database_authenticatable, :registerable,
:async, :recoverable, :rememberable, :trackable, :validatable,
:omniauthable, omniauth_providers: Extends::OMNIAUTH_PROVIDERS,
stretches: Constants::PASSWORD_STRETCH_FACTOR
has_attached_file :avatar,
styles: {
@ -51,6 +52,7 @@ class User < ApplicationRecord
)
# Relations
has_many :user_identities, inverse_of: :user
has_many :user_teams, inverse_of: :user
has_many :teams, through: :user_teams
has_many :user_projects, inverse_of: :user
@ -219,6 +221,17 @@ class User < ApplicationRecord
Team.find_by_id(self.current_team_id)
end
def self.from_omniauth(auth)
includes(:user_identities)
.where(
'user_identities.provider=? AND user_identities.uid=?',
auth.provider,
auth.uid
)
.references(:user_identities)
.take
end
# Search all active users for username & email. Can
# also specify which team to ignore.
def self.search(

View file

@ -0,0 +1,5 @@
class UserIdentity < ActiveRecord::Base
belongs_to :user
validates :provider, uniqueness: { scope: :user_id }
validates :uid, uniqueness: { scope: :provider }
end

25
app/services/api.rb Normal file
View file

@ -0,0 +1,25 @@
module Api
class << self
attr_accessor :configuration
end
def self.configuration
@configuration ||= Configuration.new
end
def self.configure
yield(configuration)
end
class Configuration
attr_accessor :core_api_sign_alg
attr_accessor :core_api_token_ttl
attr_accessor :core_api_token_iss
def initialize
@core_api_sign_alg = 'HS256'
@core_api_token_ttl = 30.minutes
@core_api_token_iss = 'SciNote'
end
end
end

View file

@ -0,0 +1,34 @@
module Api
class CoreJwt
require 'jwt'
KEY_SECRET = Rails.application.secrets.secret_key_base
def self.encode(payload, expires_at = nil)
if expires_at
payload[:exp] = expires_at
else
payload[:exp] = Api.configuration.core_api_token_ttl.from_now.to_i
end
payload[:iss] = Api.configuration.core_api_token_iss
JWT.encode(payload, KEY_SECRET, Api.configuration.core_api_sign_alg)
end
def self.decode(token)
HashWithIndifferentAccess.new(
JWT.decode(token, KEY_SECRET, Api.configuration.core_api_sign_alg)[0]
)
end
def self.read_iss(token)
HashWithIndifferentAccess.new(
JWT.decode(token, nil, false)[0]
)[:iss].to_s
end
def self.refresh_needed?(payload)
time_left = payload[:exp].to_i - Time.now.to_i
return true if time_left < (Api.configuration.core_api_token_ttl.to_i / 2)
false
end
end
end

View file

@ -20,8 +20,8 @@ class SpreadsheetParser
when '.xlsx'
# Roo Excel parcel was replaced with Creek, but it can be enabled back,
# just swap lines below. But only one can be enabled at the same time.
# Roo::Excelx.new(file_path)
Creek::Book.new(file_path).sheets[0]
Roo::Excelx.new(file_path)
# Creek::Book.new(file_path).sheets[0]
else
raise TypeError
end
@ -52,4 +52,15 @@ class SpreadsheetParser
end
return header, columns
end
def self.parse_row(row, sheet)
# Creek XLSX parser returns Hash of the row, Roo - Array
if row.is_a?(Hash)
row.values.map(&:to_s)
elsif sheet.is_a?(Roo::Excelx)
row.map { |cell| cell.value.to_s }
else
row.map(&:to_s)
end
end
end

View file

@ -35,6 +35,32 @@ module ProtocolsExporter
envelope_xml
end
def tiny_mce_asset_present?(step)
step.tiny_mce_assets.exists?
end
def get_tiny_mce_assets(text)
return unless text
regex = Constants::TINY_MCE_ASSET_REGEX
tiny_assets_xml = "<descriptionAssets>\n"
text.gsub(regex) do |el|
match = el.match(regex)
img = TinyMceAsset.find_by_id(match[1])
next unless img
img_guid = get_guid(img.id)
asset_file_name = "rte-#{img_guid}" \
"#{File.extname(img.image_file_name)}"
asset_xml = "<tinyMceAsset tokenId=\"#{match[1]}\" id=\"#{img.id}\" guid=\"#{img_guid}\" " \
"fileRef=\"#{asset_file_name}\">\n"
asset_xml << "<fileName>#{img.image_file_name}</fileName>\n"
asset_xml << "<fileType>#{img.image_content_type}</fileType>\n"
asset_xml << "</tinyMceAsset>\n"
tiny_assets_xml << asset_xml
end
tiny_assets_xml << "</descriptionAssets>\n"
tiny_assets_xml
end
def generate_protocol_xml(protocol)
protocol_name = get_protocol_name(protocol)
protocol_xml = "<eln xmlns=\"http://www.scinote.net\" version=\"1.0\">\n"
@ -54,8 +80,14 @@ module ProtocolsExporter
step_xml = "<step id=\"#{step.id}\" guid=\"#{step_guid}\" " \
"position=\"#{step.position}\">\n"
step_xml << "<name>#{step.name}</name>\n"
step_xml << "<description>#{step.description}</description>\n"
# uses 2 spaces to make more difficult to remove user data on import
step_xml << "<description><!--[CDATA[ #{
Nokogiri::HTML::DocumentFragment.parse(step.description).to_s
} ]]--></description>\n"
if tiny_mce_asset_present?(step)
step_xml << get_tiny_mce_assets(step.description)
end
# Assets
if step.assets.count > 0
step_xml << "<assets>\n"
@ -217,6 +249,30 @@ module ProtocolsExporter
eln_xsd << "</xs:sequence>\n"
eln_xsd << "</xs:complexType>\n"
eln_xsd << "</xs:element>\n"
eln_xsd << "<xs:element name=\"descriptionAssets\" minOccurs=\"0\">\n"
eln_xsd << "<xs:complexType>\n"
eln_xsd << "<xs:sequence>\n"
eln_xsd << "<xs:element name=\"tinyMceAsset\" maxOccurs=\"unbounded\">\n"
eln_xsd << "<xs:complexType>\n"
eln_xsd << "<xs:all>\n"
eln_xsd << "<xs:element name=\"fileName\" " \
"type=\"xs:string\"></xs:element>\n"
eln_xsd << "<xs:element name=\"fileType\" " \
"type=\"xs:string\"></xs:element>\n"
eln_xsd << "</xs:all>\n"
eln_xsd << "<xs:attribute name=\"id\" type=\"xs:int\" " \
"use=\"required\"></xs:attribute>\n"
eln_xsd << "<xs:attribute name=\"tokenId\" type=\"xs:string\" " \
"use=\"required\"></xs:attribute>\n"
eln_xsd << "<xs:attribute name=\"guid\" type=\"xs:string\" " \
"use=\"required\"></xs:attribute>\n"
eln_xsd << "<xs:attribute name=\"fileRef\" type=\"xs:string\" " \
"use=\"required\"></xs:attribute>\n"
eln_xsd << "</xs:complexType>\n"
eln_xsd << "</xs:element>\n"
eln_xsd << "</xs:sequence>\n"
eln_xsd << "</xs:complexType>\n"
eln_xsd << "</xs:element>\n"
eln_xsd << "<xs:element name=\"assets\" minOccurs=\"0\">\n"
eln_xsd << "<xs:complexType>\n"
eln_xsd << "<xs:sequence>\n"

View file

@ -45,86 +45,87 @@ module ProtocolsImporter
def populate_protocol(protocol, protocol_json, user, team)
protocol.reload
asset_ids = []
step_pos = 0
# Check if protocol has steps
if protocol_json['steps']
protocol_json['steps'].values.each do |step_json|
step = Step.create!(
name: step_json['name'],
description: step_json['description'],
position: step_pos,
completed: false,
user: user,
last_modified_by: user,
protocol: protocol
)
step_pos += 1
step = Step.create!(
name: step_json['name'],
position: step_pos,
completed: false,
user: user,
last_modified_by: user,
protocol: protocol
)
# need step id to link image to step
step.description = populate_rte(step_json, step, team)
step.save!
step_pos += 1
if step_json["checklists"]
step_json["checklists"].values.each do |checklist_json|
checklist = Checklist.create!(
name: checklist_json["name"],
step: step,
created_by: user,
last_modified_by: user
)
if checklist_json["items"]
item_pos = 0
checklist_json["items"].values.each do |item_json|
item = ChecklistItem.create!(
text: item_json["text"],
checked: false,
position: item_pos,
created_by: user,
last_modified_by: user,
checklist: checklist
)
item_pos += 1
if step_json["checklists"]
step_json["checklists"].values.each do |checklist_json|
checklist = Checklist.create!(
name: checklist_json["name"],
step: step,
created_by: user,
last_modified_by: user
)
if checklist_json["items"]
item_pos = 0
checklist_json["items"].values.each do |item_json|
item = ChecklistItem.create!(
text: item_json["text"],
checked: false,
position: item_pos,
created_by: user,
last_modified_by: user,
checklist: checklist
)
item_pos += 1
end
end
end
end
end
if step_json['tables']
step_json['tables'].values.each do |table_json|
table = Table.create!(
name: table_json['name'],
contents: Base64.decode64(table_json['contents']),
created_by: user,
last_modified_by: user,
team: team
)
StepTable.create!(
step: step,
table: table
)
if step_json['tables']
step_json['tables'].values.each do |table_json|
table = Table.create!(
name: table_json['name'],
contents: Base64.decode64(table_json['contents']),
created_by: user,
last_modified_by: user,
team: team
)
StepTable.create!(
step: step,
table: table
)
end
end
if step_json["assets"]
step_json["assets"].values.each do |asset_json|
asset = Asset.new(
created_by: user,
last_modified_by: user,
team: team
)
# Decode the file bytes
asset.file = StringIO.new(Base64.decode64(asset_json["bytes"]))
asset.file_file_name = asset_json["fileName"]
asset.file_content_type = asset_json["fileType"]
asset.save!
asset_ids << asset.id
StepAsset.create!(
step: step,
asset: asset
)
end
end
end
if step_json["assets"]
step_json["assets"].values.each do |asset_json|
asset = Asset.new(
created_by: user,
last_modified_by: user,
team: team
)
# Decode the file bytes
asset.file = StringIO.new(Base64.decode64(asset_json["bytes"]))
asset.file_file_name = asset_json["fileName"]
asset.file_content_type = asset_json["fileType"]
asset.save!
asset_ids << asset.id
StepAsset.create!(
step: step,
asset: asset
)
end
end
end
end
# Post process assets
@ -144,4 +145,33 @@ module ProtocolsImporter
end
end
# create tiny_mce assets and change the inport tokens
def populate_rte(step_json, step, team)
return populate_rte_legacy(step_json) unless step_json['descriptionAssets']
description = step_json['description']
step_json['descriptionAssets'].values.each do |tiny_mce_img_json|
tiny_mce_img = TinyMceAsset.new(
reference: step,
team_id: team.id
)
# Decode the file bytes
tiny_mce_img.image = StringIO.new(
Base64.decode64(tiny_mce_img_json['bytes'])
)
tiny_mce_img.image_content_type = tiny_mce_img_json['fileType']
tiny_mce_img.save!
description.gsub!("[~tiny_mce_id:#{tiny_mce_img_json['tokenId']}]",
"[~tiny_mce_id:#{tiny_mce_img.id}]")
.gsub!(' ]]--&gt;', '')
end
description
end
# handle import from legacy exports
def populate_rte_legacy(step_json)
return unless step_json['description'] && step_json['description'].present?
step_json['description'].gsub(Constants::TINY_MCE_ASSET_REGEX, '')
.gsub(' ]]--&gt;', '')
end
end

View file

@ -22,7 +22,7 @@
</div>
<div class="panel-body">
<% if experiment.workflowimg? && experiment.active_modules.length > 0 %>
<% if experiment.active_modules.length > 0 %>
<div class="workflowimg-container"
data-check-img="<%= updated_img_experiment_url(experiment) %>"
data-updated-img="<%= fetch_workflow_img_experiment_url(experiment) %>"

View file

@ -0,0 +1,32 @@
<div
class="modal fade"
id="modal-import-json-protocol"
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"><%= t('protocols.index.modal_import_json_title') %></h4>
<%= t("protocols.index.modal_import_json_notice") %>
</div>
<%= form_tag({ action: "protocolsio_import_create"}, id:"protocols_io_form",
format: :json, multipart: true,remote: true,:method => "post") do %>
<div class="modal-body">
<%= file_field_tag 'json_file', accept: '.txt,.json' %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><%= t('general.cancel')%></button>
<%= submit_tag t('protocols.index.modal_import_json_upload'), class: "btn
btn-primary" %>
</div>
<% end %>
</div>
</div>
</div>

View file

@ -0,0 +1,91 @@
<div style="display: block;">
<hr>
<td>
<div class="badge-num">
<span class="badge-preview bg-primary size-digit-1">
<b data-val="position"><%= 1 %></b>
</span>
&nbsp; &nbsp;
<span class="step-panel-collapse-link" data-toggle="collapse">
<span class="glyphicon collapse-step-icon glyphicon-collapse-up"></span>
<strong data-val="name"><%= 'Protocol info' %></strong>
</span>
</div>
<br>
<div class="tab-content">
<div class="tab-pane active" role="tabpanel">
<div data-val="description" class="ql-editor">
<br>
<% prot_info_string = '' %>
<% @remaining = ProtocolsIoHelper::PIO_P_AVAILABLE_LENGTH %>
<% protocol_table_elements_array = [] %>
<% add_to_info_string_elements = ['description','before_start','warning','guidelines','manuscript_citation']%>
<% protocol_attr_len_hash = {
description: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
before_start: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
warning: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
guidelines: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
manuscript_citation: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
vendor_name: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
keywords: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
tags: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,
publish_date: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,
link: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_BIG,
vendor_link: ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_BIG } %>
<% protocol_attr_len_hash.each do |element, value|%>
<% element = element.to_s %>
<% if(json_object[element].present?) %>
<% if element == 'tags' %>
<% translation_string = 'protocols.protocols_io_import.preview.' + element %>
<strong><%= t(translation_string) %></strong>
<% string_of_tags='' %>
<% json_object['tags'].each do |tag| %>
<% string_of_tags += tag['tag_name']+' , ' %>
<% end %>
<%= prepare_for_view(string_of_tags, ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_MEDIUM,'table').html_safe %>
<% else %>
<% prot_info_string += not_null(json_object[element]) if add_to_info_string_elements.include? element %>
<% translation_string = 'protocols.protocols_io_import.preview.' + element %>
<strong><%= t(translation_string) %></strong>
<%= prepare_for_view(json_object[element],protocol_attr_len_hash[element.to_sym],'table').html_safe %>
<% end %>
<br>
<% end %>
<% end %>
<% tables, garbage = protocolsio_string_to_table_element(prot_info_string) %>
<% if tables.present? %>
<br><hr><br>
<% end %>
<% table_count = 0 %>
<% tables.each do |index, table| %>
<% table_hash = JSON.parse((Base64.decode64(table['contents']))) %>
<% pio_table_id = "pio_table_prot_info_"+table_count.to_s %>
<% protocol_table_elements_array.push([pio_table_id,table_hash['data']]) %>
<div id="<%=pio_table_id%>" ></div>
<% table_count += 1 %>
<% end %>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$('#modal-import-json-protocol-preview').on('shown.bs.modal', function (e) {
var javascript_table_elements_p = <%=raw sanitize_input(protocol_table_elements_array.to_json) %>
for(var j=0;j<javascript_table_elements_p.length;j++)
{
var target = document.getElementById(javascript_table_elements_p[j][0]);
var hot = new Handsontable(target, {
data: javascript_table_elements_p[j][1],
startRows: <%= Constants::HANDSONTABLE_INIT_ROWS_CNT %>,
startCols: <%= Constants::HANDSONTABLE_INIT_COLS_CNT %>,
rowHeaders: true,
colHeaders: true,
fillHandle: false,
formulas: true,
readOnly: true
});
}
})
</script>

View file

@ -0,0 +1,96 @@
<%= form_for(@protocol, :url => url_for(:controller => 'protocols',
:action=>'protocolsio_import_save'),method: :post,format: :javascript,
remote: true, :html=> { :id => "protocolsio-import-form" }) do |f| %>
<%= hidden_field_tag :json_object, JSON.generate(@json_object) %>
<% if URI.parse(request.referrer).query %>
<%= hidden_field_tag :type,CGI.parse(URI.parse(request.referrer).query).fetch('type') %>
<% else %>
<%= hidden_field_tag :type,'public' %>
<% end %>
<!--Get the type of protocol to import (private, public) from the url -->
<div
id="modal-import-json-protocol-preview"
class="modal fade"
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">&times;</button>
<h4 class="modal-title" data-role="header-import-into-protocol_protocols_io"><%=
t('protocols.import_export.import_modal.title_import_into_protocol_protocols_io')
%></h4>
</div>
<div class="modal-body">
<!-- General protocol info -->
<div class="form-group">
<label><%= t('protocols.import_export.import_modal.name_label') %></label>
<%= f.text_field :name, :value => pio_eval_title_len(sanitize_input(@json_object['protocol_name'])), class:
"form-control" %>
</div>
<div class="form-group">
<label>
<span class="glyphicon glyphicon-user">
</span>&nbsp;<%= t('protocols.import_export.import_modal.authors_label') %>
</label>
<%= f.text_field :authors, :value => pio_eval_title_len(sanitize_input(@json_object['full_name'])), class:
"form-control" %>
</div>
<div class="form-group">
<label><%= t('protocols.import_export.import_modal.description_label') %></label>
<%= f.text_area :description_notice, :value => t('protocols.protocols_io_import.import_description_notice'), class:
"form-control",readonly: true %>
</div>
<div class="form-group">
<div class="row">
<div class="col-xs-4">
<label><%= t('protocols.import_export.import_modal.created_at_label') %></label>
<% display_created_at=Time.at(@json_object['created_on'].to_i) %>
<%= f.text_field :created_at, :value => display_created_at.to_s,
readonly: true, class: "form-control" %>
</div>
<div class="col-xs-4">
<label><%= t('protocols.import_export.import_modal.updated_at_label') %></label>
<% display_last_modified=Time.at(@json_object['last_modified'].to_i) %>
<%= f.text_field :last_modified, :value =>
display_last_modified.to_s,readonly: true, class:
"form-control" %>
</div>
</div>
</div>
<!-- Preview title -->
<div>
<h2 style="display: inline;"><%= t('protocols.import_export.import_modal.preview_title') %></h2>
<h3 style="display: none;" data-role="title-position"></h3>
</div>
<!-- Preview scroller -->
<div
class="import-protocols-modal-preview-container-json"
data-role="preview-container">
<!-- Partial -->
<%= render partial: "protocols/import_export/import_json_protocol_p_desc.html.erb", locals: {json_object: @json_object} %>
<%= render partial: "protocols/import_export/import_json_protocol_s_desc.html.erb", locals: {json_object: @json_object} %>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><%= t('general.cancel') %></button>
<%= f.submit t('protocols.import_export.import_modal.import'),class: "btn
btn-primary" %>
<% end %>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,161 @@
<% whitelist_simple=['1','6','17'] %>
<% whitelist_complex=['8','9','15','18','19','20']%>
<% step_table_elements_array = [] %>
<% json_object['steps'].each_with_index do |step,counter_orig| %>
<% @remaining = ProtocolsIoHelper::PIO_S_AVAILABLE_LENGTH %>
<% counter = counter_orig + 2 %>
<div style="display: block;">
<hr>
<td>
<div class="badge-num">
<span class="badge-preview bg-primary size-digit-1">
<b data-val="position"><%= (counter) %></b>
</span>
&nbsp; &nbsp;
<span class="step-panel-collapse-link" data-toggle="collapse">
<span class="glyphicon collapse-step-icon glyphicon-collapse-up"></span>
<% title = nil %>
<% step['components'].each do |key1,value1| #finding section (title of step) %>
<% key1 = value1 if value1.class == Hash %>
<% if(key1['component_type_id']=='6') %>
<% title = pio_eval_title_len(sanitize_input(key1['data'])) if key1['data'].present? %>
<% end %>
<% end %>
<% title ||=t('protocols.protocols_io_import.comp_append.missing_step') %>
<strong data-val="name"><%= title %></strong>
</span>
</div>
<br>
<div class="tab-content">
<div class="tab-pane active" role="tabpanel">
<div data-val="description" class="ql-editor">
<% step_info_string = '' %>
<% step['components'].each do |key,value| %>
<%#here i made an if to distinguish the first step from the
others, because the first step #sometimes has index values as keys instead of
hashes, you might see this elsewhere %>
<% key = value if value.class == Hash %>
<% if whitelist_simple.include?(key['component_type_id']) &&
key['data'].present? %>
<br>
<% case key['component_type_id']
when '1' %>
<% step_info_string += (key['data']) %>
<br>
<strong><%= t('protocols.protocols_io_import.preview.strng_s_desc') %></strong>
<%= prepare_for_view(key['data'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<% when '17' %>
<% step_info_string += (key['data']) %>
<br>
<strong><%= t('protocols.protocols_io_import.preview.s_exp_res') %></strong>
<%= prepare_for_view(key['data'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<% end %>
<% elsif key && whitelist_complex.include?(key['component_type_id']) %>
<% case key['component_type_id']
when '8'%>
<% step_info_string += not_null(key['source_data']['name']) %>
<br>
<strong><%= not_null(key['name'])+': ' %></strong>
<%= prepare_for_view(key['source_data']['name'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.dev') %>
<%= prepare_for_view(key['source_data']['developer'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.vers') %>
<%= prepare_for_view(key['source_data']['version'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.s_link') %>
<%= prepare_for_view(key['source_data']['link'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.repo') %>
<%= prepare_for_view(key['source_data']['repository'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.os') %>
<%= prepare_for_view(key['source_data']['os_name'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe + ' , ' +
prepare_for_view(key['source_data']['os_version'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<% when '9'%>
<% step_info_string += not_null(key['source_data']['name']) %>
<br>
<strong><%= not_null(key['name'])+': ' %></strong>
<%= prepare_for_view(key['source_data']['name'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.s_link') %>
<%= prepare_for_view(key['source_data']['link'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<% when '15'%>
<% step_info_string += not_null(key['source_data']['description']) %>
<br>
<strong><%= key['name']+': ' %></strong>
<%= prepare_for_view(key['source_data']['name'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.s_desc') %>
<%= prepare_for_view(key['source_data']['description'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.os') %>
<%= prepare_for_view(key['source_data']['os_name'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe + ' , ' +
prepare_for_view(key['source_data']['os_version'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<% when '18'%>
<br>
<strong><%= t('protocols.protocols_io_import.preview.sub_prot') %></strong>
<%= prepare_for_view(key['source_data']['protocol_name'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.auth') %>
<%= prepare_for_view(key['source_data']['full_name'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<% if key['source_data']['link'].present? %>
<%= t('protocols.protocols_io_import.preview.s_nobr_link') %>
<%= prepare_for_view(key['source_data']['link'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<% end %>
<% when '19'%>
<% step_info_string += not_null(key['source_data']['body']) %>
<br>
<strong><%= not_null(key['name'])+': ' %></strong>
<%= prepare_for_view(key['source_data']['body'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<br>
<%= t('protocols.protocols_io_import.preview.s_link') %>
<%= prepare_for_view(key['source_data']['link'],ProtocolsIoHelper::PIO_ELEMENT_RESERVED_LENGTH_SMALL,'table').html_safe %>
<% else %>
<% end #case if%>
<% end #inner if%>
<% end #step component loop %>
<% tables, garbage = protocolsio_string_to_table_element(step_info_string) %>
<% if tables.present? %>
<br><hr><br>
<% end %>
<% table_count = 0 %>
<% tables.each do |index, table| %>
<% table_hash = JSON.parse((Base64.decode64(table['contents']))) %>
<% pio_table_id = "pio_table_step_"+counter.to_s+"_info_"+table_count.to_s %>
<% step_table_elements_array.push([pio_table_id,table_hash['data']]) %>
<div id="<%=pio_table_id%>" ></div>
<% table_count += 1 %>
<% end %>
</div>
</div>
</div>
</div>
<% end #step loop%>
<br>
<script type="text/javascript">
$('#modal-import-json-protocol-preview').on('shown.bs.modal', function (e) {
var javascript_table_elements_s = <%=raw sanitize_input(step_table_elements_array.to_json) %>
for(var j=0;j<javascript_table_elements_s.length;j++)
{
var target = document.getElementById(javascript_table_elements_s[j][0]);
var hot = new Handsontable(target, {
data: javascript_table_elements_s[j][1],
startRows: <%= Constants::HANDSONTABLE_INIT_ROWS_CNT %>,
startCols: <%= Constants::HANDSONTABLE_INIT_COLS_CNT %>,
rowHeaders: true,
colHeaders: true,
fillHandle: false,
formulas: true,
readOnly: true
});
}
})
</script>

View file

@ -1,8 +1,8 @@
<% provide(:head_title, t("protocols.index.head_title")) %>
<% if current_team %>
<%= render partial: "protocols/breadcrumbs.html.erb", locals: { teams: @teams, current_team: @current_team, type: @type } %>
<ul class="nav nav-tabs nav-settings">
<li role="presentation" class="<%= "active" if @type == :public %>">
<%= link_to t("protocols.index.navigation.public"), protocols_path(team: @current_team, type: :public) %>
@ -48,16 +48,29 @@
</a>
<% end %>
</div>
<div id="import-export-protocols" class="btn-group" role="group">
<a class="btn btn-default btn-open-file" <%= can_create_protocols_in_repository?(@current_team) ? 'data-action="import"' : 'disabled="disabled"' %>>
<span class="glyphicon glyphicon-import"></span>
<span class="hidden-xs">&nbsp;<%= t("protocols.index.import") %></span>
<input type="file" value="" accept=".eln" data-role="import-file-input"
data-team-id="<%= @current_team.id %>"
data-type="<%= @type %>" data-import-url="<%= import_protocols_path %>"
<%= 'disabled="disabled"' unless can_create_protocols_in_repository?(@current_team) %>>
</a>
<a class="btn btn-default btn-open-file" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#">
<span class="glyphicon glyphicon-import"></span><span class="hidden-xs">&nbsp;<%= "Import protocol" %></span></a>
<ul class="dropdown-menu">
<li>
<a class="btn btn-default btn-open-file" <%= can_create_protocols_in_repository?(@current_team) ? 'data-action="import"' : 'disabled="disabled"' %>>
<span class="glyphicon glyphicon-import"></span>
<span class="hidden-xs">&nbsp;<%= t("protocols.index.import") %></span>
<input type="file" value="" accept=".eln" data-role="import-file-input"
data-team-id="<%= @current_team.id %>"
data-type="<%= @type %>" data-import-url="<%= import_protocols_path %>"
<%= 'disabled="disabled"' unless can_create_protocols_in_repository?(@current_team) %>>
</a>
</li>
<li>
<%= link_to "#modal-import-json-protocol", "data-toggle" => "modal" do %>
<span class="glyphicon glyphicon-file"></span>
<span class="hidden-xs"><%= t("protocols.index.import_json") %></span>
<% end %>
</li>
</ul>
<a class="btn btn-default" data-action="export" data-export-url="<%= export_protocols_path() %>">
<span class="glyphicon glyphicon-export"></span>
<span class="hidden-xs">&nbsp;<%= t("protocols.index.export") %></span>
@ -121,6 +134,8 @@
</p>
</div>
<% end %>
<div id="protocolsio-preview-modal-target"></div>
<%= render partial: "protocols/import_export/import_json_protocol_modal.html.erb" %>
<%= render partial: "protocols/index/create_new_modal.html.erb" %>
<%= render partial: "protocols/index/make_private_results_modal.html.erb" %>

View file

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

View file

@ -0,0 +1,28 @@
<% if @protocolsio_too_big %>
$('#modal-import-json-protocol').modal('hide');
HelperModule.flashAlertMsg(' <%= t('my_modules.protocols.load_from_file_size_error',
size: Constants::FILE_MAX_SIZE_MB ) %>','danger');
<% elsif @protocolsio_invalid_file %>
$('#modal-import-json-protocol').modal('hide');
HelperModule.flashAlertMsg(' <%= t('my_modules.protocols.load_from_file_invalid_error') %>','danger');
<% else %>
$('#modal-import-json-protocol').modal('hide');
<% if remotipart_submitted? %> <%# a workaround to a bug with remotipart, that caused alot of headache, courtesy of github.com/dhampik %>
$('#protocolsio-preview-modal-target').html(
"<%= j "#{render(:partial => 'protocols/import_export/import_json_protocol_preview_modal')}" %>"
);
<% else %>
$('#protocolsio-preview-modal-target').html(
"<%= j render(:partial => 'protocols/import_export/import_json_protocol_preview_modal') %>"
);
<% end %>
$('#modal-import-json-protocol-preview').modal('show');
$('.modal').on('hidden.bs.modal', function (e) {
if($('.modal').hasClass('in')) {
$('body').addClass('modal-open');
}
});
<% end %>

View file

@ -0,0 +1,15 @@
<% if @protocolsio_general_error %>
$('#modal-import-json-protocol-preview').modal('hide');
alert(' <%= t('my_modules.protocols.load_from_file_protocol_general_error',
max: Constants::NAME_MAX_LENGTH, min: Constants::NAME_MIN_LENGTH) %>');
<% elsif @toolong%>
$('#modal-import-json-protocol-preview').modal('hide');
$('#protocols_io_form').trigger("reset");
protocolsDatatable.ajax.reload();
HelperModule.flashAlertMsg(' <%= t('protocols.index.import_results.message_warn_truncated')%>', 'warning');
<% else %>
$('#modal-import-json-protocol-preview').modal('hide');
$('#protocols_io_form').trigger("reset");
protocolsDatatable.ajax.reload();
HelperModule.flashAlertMsg(' <%= t('protocols.index.import_results.message_ok_pio')%>', 'success');
<% end %>

View file

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

View file

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

View file

@ -16,6 +16,7 @@
<div class="form-group">
<%= f.text_field :name,
label: t("repositories.index.modal_copy.name"),
help: t("repositories.index.modal_copy.description"),
autofocus: true,
placeholder: t("repositories.index.modal_copy.name_placeholder") %>
</div>

View file

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

View file

@ -1,11 +1,11 @@
<ul class="nav nav-tabs">
<li role="presentation" id="new-step-main-tab" class="active">
<a href="#new-step-main" data-toggle="tab">
<a href="#new-step-main" data-toggle="tab" data-no-turbolink="true">
<span class="glyphicon glyphicon-pencil"></span>
</a>
</li>
<li role="presentation" id="new-step-checklists-tab">
<a href="#new-step-checklists" data-toggle="tab">
<a href="#new-step-checklists" data-toggle="tab" data-no-turbolink="true">
<span class="glyphicon glyphicon-list"></span>
<%= t("protocols.steps.new.tab_checklists") %>
</a>
@ -13,13 +13,13 @@
<li role="presentation"
id="new-step-assets-tab"
onClick="dragNdropAssetsInit('steps')">
<a href="#new-step-assets" data-toggle="tab">
<a href="#new-step-assets" data-toggle="tab" data-no-turbolink="true">
<span class="glyphicon glyphicon-file"></span>
<%= t("protocols.steps.new.tab_assets") %>
</a>
</li>
<li role="presentation" id="new-step-tables-tab">
<a href="#new-step-tables" data-toggle="tab">
<a href="#new-step-tables" data-toggle="tab" data-no-turbolink="true">
<span class="glyphicon glyphicon-th"></span>
<%= t("protocols.steps.new.tab_tables") %>
</a>
@ -50,9 +50,11 @@
style="display: none" multiple>
</label>
</div>
<%= f.nested_fields_for :assets do |ff| %>
<%= render "form_assets.html.erb", ff: ff, step: step %>
<% end %>
<div id="new-step-assets-group" class="form-group">
<%= f.nested_fields_for :assets do |ff| %>
<%= render "form_assets.html.erb", ff: ff, step: step %>
<% end %>
</div>
</div>
<div class="tab-pane" role="tabpanel" id="new-step-tables">
<%= f.nested_fields_for :tables do |ff| %>

View file

@ -57,7 +57,7 @@
<em><%= t('protocols.steps.no_description') %></em>
<% else %>
<div class="ql-editor">
<%= custom_auto_link(generate_image_tag_from_token(step.description),
<%= custom_auto_link(generate_image_tag_from_token(step.description, step),
simple_format: false,
tags: %w(img)) %>
</div>

View file

@ -3,26 +3,9 @@
<div class="row">
<div class="col-xs-3 col-md-1">
<span style="display: none;" data-hook="user-notification-list-item"></span>
<% if notification.type_of == 'recent_changes' %>
<div class="text-center">
<%= image_tag avatar_path(notification.generator_user, :icon_small), class: 'avatar img-circle' %>
</div>
<% end %>
<% if notification.type_of == 'assignment' %>
<div class="text-center">
<span class="assignment"><%= fa_icon 'newspaper-o' %></span>
</div>
<% end %>
<% if notification.type_of == 'system_message' %>
<div class="text-center">
<span class="system-message"><i class="glyphicon glyphicon-tower" aria-hidden="true"></i></span>
</div>
<% end %>
<% if notification.type_of == 'deliver' %>
<div class="text-center">
<span class="deliver"><%= fa_icon 'truck' %></span>
</div>
<% end %>
<%= render partial: "user_notifications/icons/#{notification.type_of}",
locals: {notification: notification},
formats: [:html] %>
</div>
<div class="col-xs-9 col-md-11">
<strong><%= sanitize_input(notification.title) %></strong> <br>

View file

@ -2,27 +2,9 @@
<li class="notification <%= 'unseen' unless notification.already_seen(current_user) %>">
<div class="row">
<div class="col-xs-2">
<span style="display: none;" data-hook="user-notification-recent-list-item"></span>
<% if notification.type_of == 'recent_changes' %>
<div class="text-center">
<%= image_tag avatar_path(notification.generator_user, :icon_small), class: 'avatar' %>
</div>
<% end %>
<% if notification.type_of == 'assignment' %>
<div class="text-center">
<span class="assignment"><%= fa_icon 'newspaper-o' %></span>
</div>
<% end %>
<% if notification.type_of == 'system_message' %>
<div class="text-center">
<span class="system-message"><i class="glyphicon glyphicon-tower" aria-hidden="true"></i></span>
</div>
<% end %>
<% if notification.type_of == 'deliver' %>
<div class="text-center">
<span class="deliver"><%= fa_icon 'truck' %></span>
</div>
<% end %>
<%= render partial: "user_notifications/icons/#{notification.type_of}",
locals: {notification: notification},
formats: [:html] %>
</div>
<div class="col-xs-10">

View file

@ -0,0 +1,3 @@
<div class="text-center">
<span class="assignment"><%= fa_icon 'newspaper-o' %></span>
</div>

View file

@ -0,0 +1,3 @@
<div class="text-center">
<span class="deliver"><%= fa_icon 'truck' %></span>
</div>

View file

@ -0,0 +1,3 @@
<div class="text-center">
<%= image_tag avatar_path(notification.generator_user, :icon_small), class: 'avatar img-circle' %>
</div>

View file

@ -0,0 +1,5 @@
<div class="text-center">
<span class="system-message">
<i class="glyphicon glyphicon-tower" aria-hidden="true"></i>
</span>
</div>

View file

@ -29,6 +29,7 @@
<%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
</div>
<% if Rails.configuration.x.new_team_on_signup %>
<div class="form-group"
data-hook="confirm-invitation-team-name">
<%= label :team, :name, t('users.registrations.new.team_name_label') %>
@ -42,7 +43,7 @@
<% end %>
<span><small><%= t 'users.registrations.new.team_name_help' %></small></span>
</div>
<% end %>
<%= recaptcha_input_tag %>
<div class="form-group" data-hook="confirm-invitation-form-submit">

View file

@ -27,7 +27,7 @@
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'form-control' %>
</div>
<% if Rails.configuration.x.new_team_on_signup %>
<div class="form-group" id="team_name_form">
<%= label :team, :name, t('users.registrations.new.team_name_label') %>
<% if @team %>
@ -37,7 +37,7 @@
<% end %>
<span><small><%= t 'users.registrations.new.team_name_help' %></small></span>
</div>
<% end %>
<%= recaptcha_input_tag %>
<div class="form-group" data-hook="sign-up-form-submit">

View file

@ -1,8 +1,11 @@
<ul class="nav nav-pills nav-stacked nav-stacked-arrow">
<ul class="nav nav-pills nav-stacked nav-stacked-arrow" data-hook="user-settings-account-navigation-html">
<li role="presentation" class="<%= 'active' if on_settings_account_profile_page? %>">
<%= link_to t('users.settings.account.navigation.profile'), edit_user_registration_path %>
</li>
<li role="presentation" class="<%= 'active' if on_settings_account_preferences_page? %>">
<%= link_to t('users.settings.account.navigation.preferences'), preferences_path %>
</li>
</ul>
<li role="presentation" class="<%= 'active' if on_settings_account_addons_page? %>">
<%= link_to t('users.settings.account.navigation.addons'), addons_path %>
</li>
</ul>

View file

@ -0,0 +1,22 @@
<% provide(:head_title, t('users.settings.account.addons.head_title')) %>
<%= render partial: 'users/settings/navigation.html.erb' %>
<div class="tab-content">
<div class="tab-pane tab-pane-settings active" role="tabpanel">
<div class="row">
<div class="col-xs-12 col-sm-3">
<%= render partial: 'users/settings/account/navigation.html.erb' %>
</div>
<div class="col-xs-12 col-sm-9">
<h4><%= t('users.settings.account.addons.title') %></h4>
<div data-hook="settings-addons-container">
<em data-hook="settings-addons-no-addons">
<%= t('users.settings.account.addons.no_addons') %>
</em>
</div>
</div>
</div>
</div>
<div class="tab-pane tab-pane-settings" role="tabpanel"></div>
</div>

View file

@ -18,8 +18,6 @@
<%= link_to t("devise.links.not_receive_unlock"), new_unlock_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= link_to t("devise.links.sign_in_provider", provider: "#{provider.to_s.titleize}"), omniauth_authorize_path(resource_name, provider) %><br />
<% end -%>
<%- if devise_mapping.omniauthable? && resource_class.omniauth_providers.any? %>
<div data-hook="omniauth-sign-in-links"></div>
<% end -%>

View file

@ -123,4 +123,12 @@ Rails.application.configure do
config.cache_store = :null_store
end
# Enable new team on sign up
new_team_on_signup = ENV['NEW_TEAM_ON_SIGNUP'] || 'true'
if new_team_on_signup == 'true'
config.x.new_team_on_signup = true
else
config.x.new_team_on_signup = false
config.x.enable_tutorial = false
end
end

View file

@ -156,4 +156,13 @@ Rails.application.configure do
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
# `config/secrets.yml.key`.
config.read_encrypted_secrets = true
# Enable new team on sign up
new_team_on_signup = ENV['NEW_TEAM_ON_SIGNUP'] || 'true'
if new_team_on_signup == 'true'
config.x.new_team_on_signup = true
else
config.x.new_team_on_signup = false
config.x.enable_tutorial = false
end
end

View file

@ -864,6 +864,8 @@ class Constants
# Very basic regex to check for validity of emails
BASIC_EMAIL_REGEX = URI::MailTo::EMAIL_REGEXP
TINY_MCE_ASSET_REGEX = /\[~tiny_mce_id:([0-9a-zA-Z]+)\]/
# Team name for default admin user
DEFAULT_PRIVATE_TEAM_NAME = 'My projects'.freeze

View file

@ -0,0 +1,11 @@
Api.configure do |config|
if ENV['CORE_API_SIGN_ALG']
config.core_api_sign_alg = ENV['CORE_API_SIGN_ALG']
end
if ENV['CORE_API_TOKEN_TTL']
config.core_api_token_ttl = ENV['CORE_API_TOKEN_TTL']
end
if ENV['CORE_API_TOKEN_ISS']
config.core_api_token_iss = ENV['CORE_API_TOKEN_ISS']
end
end

View file

@ -0,0 +1,65 @@
# DelayedWorkerConfig is a configuration object that parses and sanitize
# environmental variables. @note Used only for Delayed::Worker settings
module DelayedWorkerConfig
module_function
# If you want to keep failed jobs, set DELAYED_WORKER_DESTROY_FAILED_JOBS to
# false. The failed jobs will be marked with non-null failed_at.
def destroy_failed_jobs
value = ENV['DELAYED_WORKER_DESTROY_FAILED_JOBS']
return false unless value
return false unless value.in? %w(true 1)
true
end
# If no jobs are found, the worker sleeps for the amount of time specified by
# the sleep delay option. Default to 60 second.
def sleep_delay
value = ENV['DELAYED_WORKER_SLEEP_DELAY'].to_i
return 60 if value.zero?
value
end
# The default is 6 attempts. After this, the job either deleted
# or left in the database with "failed_at" set dempends on the
# DESTROY_FAILED_JOBS value
def max_attempts
value = ENV['DELAYED_WORKER_MAX_ATTEMPTS'].to_i
return 6 if value.zero?
value
end
# The default DELAYED_WORKER_MAX_RUN_TIME is 30.minutes.
# If your job takes longer than that, another computer could pick it up.
# It's up to you to make sure your job doesn't exceed this time.
# You should set this to the longest time you think the job could take.
def max_run_time
value = ENV['DELAYED_WORKER_MAX_RUN_TIME'].to_i
return 30.minutes if value.zero?
value.minutes
end
# The default behavior is to read 10 jobs from the queue when finding an
# available job. You can configure this by setting.
def read_ahead
value = ENV['DELAYED_WORKER_READ_AHEAD'].to_i
return 10 if value.zero?
value
end
# By default all jobs will be queued without a named queue.
# A default named queue can be specified by using
def default_queue_name
value = ENV['DELAYED_WORKER_DEFAULT_QUEUE_NAME']
return 'default' unless value
value
end
end
# Delayed::Worker configuration
Delayed::Worker.destroy_failed_jobs = DelayedWorkerConfig.destroy_failed_jobs
Delayed::Worker.sleep_delay = DelayedWorkerConfig.sleep_delay
Delayed::Worker.max_attempts = DelayedWorkerConfig.max_attempts
Delayed::Worker.max_run_time = DelayedWorkerConfig.max_run_time
Delayed::Worker.read_ahead = DelayedWorkerConfig.read_ahead
Delayed::Worker.default_queue_name = DelayedWorkerConfig.default_queue_name

View file

@ -42,4 +42,12 @@ class Extends
# Data type name should match corresponding model's name
REPOSITORY_DATA_TYPES = { RepositoryTextValue: 0,
RepositoryDateValue: 1 }
# List of implemented core API versions
API_VERSIONS = ['20170715']
# Array used for injecting names of additional authentication methods for API
API_PLUGABLE_AUTH_METHODS = []
OMNIAUTH_PROVIDERS = []
end

View file

@ -4,12 +4,12 @@ if ENV['PAPERCLIP_HASH_SECRET'].nil?
exit 1
end
Paperclip::Attachment.default_options.merge!({
Paperclip::Attachment.default_options.merge!(
hash_data: ':class/:attachment/:id/:style',
hash_secret: ENV['PAPERCLIP_HASH_SECRET'],
preserve_files: false,
preserve_files: true,
url: '/system/:class/:attachment/:id_partition/:hash/:style/:filename'
})
)
if ENV['PAPERCLIP_STORAGE'] == "s3"
@ -183,6 +183,7 @@ module Paperclip
)) ||
# Word processor application
(Set[content_type, content_types_from_name].subset? Set.new %w(
application/zip
application/vnd.ms-office
application/msword
application/msword-template
@ -218,9 +219,11 @@ module Paperclip
application/vnd.sun.xml.calc.template
application/vnd.stardivision.calc
application/x-starcalc
application/CDFV2-encrypted
)) ||
# Presentation application
(Set[content_type, content_types_from_name].subset? Set.new %w(
application/zip
application/vnd.ms-office
application/vnd.ms-powerpoint
application/vnd.openxmlformats-officedocument.presentationml.presentation

View file

@ -605,6 +605,8 @@ en:
load_from_file_error: "Failed to load the protocol from file."
load_from_file_error_locked: "Failed to load the protocol from file. One or more files are currently being edited."
load_from_file_size_error: "Failed to load the protocol from file. Limit is %{size}Mb."
load_from_file_invalid_error: "The file you provided is invalid or has an invalid extension."
load_from_file_protocol_general_error: "Failed to load the protocol from file. It is likely that certain fields (protocol and individual step titles and names) contain too many or too few characters.(max is %{max} and min is %{min})"
results:
head_title: "%{project} | %{module} | Results"
add_label: "Add new result:"
@ -684,6 +686,7 @@ en:
head_title: "%{project} | Overview"
canvas_edit: "Edit experiment"
zoom: "Zoom: "
reload_on_submit: "Save action is running. Reloading this page may cause unexpected behavior."
modal_manage_tags:
head_title: "Manage tags for"
subtitle: "Showing tags of task %{module}"
@ -777,11 +780,6 @@ en:
archived_on: "Archived on"
archived_on_title: "Task archived on %{date} at %{time}."
my_module_groups:
new:
name: "Workflow %{index}"
suffix: "..."
tags:
new:
head_title: "Create tag"
@ -889,6 +887,7 @@ en:
modal_copy:
title_html: "Copy repository <em>%{name}</em>"
name: "New repository name"
description: "Only the structure of the repository is going to be copied."
name_placeholder: "My repository"
copy: "Copy repository"
modal_create:
@ -1254,6 +1253,7 @@ en:
avatar_title: "Change avatar"
avatar_edit_label: "Upload new avatar file"
avatar_submit: "Upload"
avatar_total_size: "Your avatar file cannot be larger than 0.2 MB. (Please try again with a smaller file.)"
name_label: "Full name"
name_title: "Change name"
initials_label: "Initials"
@ -1289,6 +1289,7 @@ en:
navigation:
profile: "Profile"
preferences: "Preferences"
addons: "Add-ons"
preferences:
head_title: "Settings | My preferences"
edit:
@ -1304,6 +1305,10 @@ en:
select_team: "Select team"
tutorial_reset_flash: "Tutorial can now be repeated."
tutorial_reset_error: "Tutorial could not be repeated."
addons:
head_title: "Settings | Add-ons"
title: "Add-ons"
no_addons: "You have no sciNote Add-ons."
teams:
head_title: "Settings | Teams"
breadcrumbs:
@ -1381,11 +1386,66 @@ en:
breadcrumbs:
manager: "Protocol management"
edit: "Edit protocol"
protocols_io_import:
title_too_long: "... Text is too long so we had to cut it off."
too_long: "... <span class='label label-warning'>Text is too long so we had to cut it off.</span>"
import_description_notice: "The protocols description is listed below under \"Protocol info\"."
preview:
description: "Protocol Description: "
before_start: "Before starting protocol information:"
warning: "Protocol warning:"
guidelines: "Guidelines:"
link: "Link:"
s_nobr_link: "Link:"
s_link: "Link:"
s_desc: "Description:"
strng_s_desc: "Description:"
s_exp_res: "Expected result:"
dev: "Developer:"
vers: "Version:"
repo: "Repository:"
os: "OS name , OS version:"
sub_prot: "This protocol also contains an attached sub-protocol:"
auth: "Author:"
manuscript_citation: "Manuscript citation:"
publish_date: "Publish date:"
vendor_name: "Vendor name:"
vendor_link: "Vendor link:"
keywords: "Keywords:"
tags: "Tags:"
comp_append:
table_moved: "<br><strong><i>There was a table here, it was moved to the end of this step. </i></strong>"
missing_step: "Step"
missing_desc: "Description missing"
general_link: "<br>Link: "
expected_result: "<br><strong>Expected result: </strong>"
soft_packg:
name: "<br><strong>Software package: </strong>"
developer: "<br>Developer: "
version: "<br>Version: "
link: "<br>Link: "
repository: "<br>Repository: "
os: "<br>OS name , OS version: "
dataset:
name: "<br><strong>Dataset: </strong>"
link: "<br>Link: "
command:
name: "<br><strong>Command: </strong>"
description: "<br>Description: "
os: "<br>OS name , OS version: "
sub_protocol:
protocol_name: "<br><strong>This step also contains an attached sub-protocol: </strong>"
full_name: "<br>Author: "
link: "<br>Link: "
safety_infor:
body: "<br><strong>Safety information: </strong>"
link: "<br>Link: "
import_export:
load_file_error: "Failed to load the protocol file."
import_modal:
title_import: "Import protocol/s"
title_import_into_protocol: "Load protocol from file"
title_import_into_protocol_protocols_io: "Load protocol from protocols.io file"
import_into_protocol_message: "This will overwrite the current protocol!"
import_into_protocol_confirm: "Are you sure you wish to load protocol from file? This action will overwrite the current protocol."
import_to_linked_task_file: "Are you sure you wish to load protocol from file? This action will overwrite the current protocol in the task and unlink it from repository. The current protocol will remain unchanged in repository."
@ -1441,6 +1501,11 @@ en:
edit: "Edit"
clone_btn: "Copy"
import: "Import"
import_alt: "from SciNote protocol file(.eln)"
import_json: "from protocols.io file"
modal_import_json_upload: "Upload file"
modal_import_json_title: "Import protocols.io file"
modal_import_json_notice: "Upload your protocols.io protocol file"
export: "Export"
make_private: "Make private"
publish: "Publish"
@ -1506,6 +1571,8 @@ en:
title: "Import results"
message_failed: "Failed to import %{nr} protocol/s."
message_ok: "Successfully imported %{nr} protocol/s."
message_ok_pio: "Successfully imported protocol from protocols.io file."
message_warn_truncated: "Successfully imported protocol from protocols.io file. However, text in some fields was too long so we had to cut it off."
row_success: "Imported"
row_renamed: "Imported & renamed"
row_failed: "Failed"
@ -1735,8 +1802,8 @@ en:
zip_export:
modal_label: 'Export repository'
notification_title: 'Your package is ready to be exported!'
expired_title: 'The required file was expired!'
expired_description: 'The downloadable file expires in 7 days after its creation.'
expired_title: 'Looks like your link has expired.'
expired_description: 'Please export the data again in order to receive a new link.'
modal_label: 'Export request received'
modal_html: "<p>Your export request is being processed.</p><p>When completed we will <strong>send an email to %{email}</strong> inbox with a link to your exported samples. Note that the link will expire in 7 days.</p>"
repository_html: '<p>You are about to export selected items in repository %{repository}</p> <br> Repository will be exported in a .csv file format. You will receive <strong>email with a link</strong> where you can download it.'
@ -1784,6 +1851,12 @@ en:
busy: "The server is still processing your request. If you leave this page, the changes will be lost! Are you sure you want to continue?"
no_name: "(no name)"
api:
core:
status_ok: "Ok"
expired_token: "Token is expired"
invalid_token: "Token is invalid"
Add: "Add"
Asset: "File"
Assets: "Files"

View file

@ -6,10 +6,12 @@ Rails.application.routes.draw do
end
constraints UserSubdomain do
devise_for :users, controllers: { registrations: 'users/registrations',
sessions: 'users/sessions',
invitations: 'users/invitations',
confirmations: 'users/confirmations' }
devise_for :users,
controllers: { registrations: 'users/registrations',
sessions: 'users/sessions',
invitations: 'users/invitations',
confirmations: 'users/confirmations',
omniauth_callbacks: 'users/omniauth_callbacks' }
root 'projects#index'
@ -48,6 +50,9 @@ Rails.application.routes.draw do
get 'users/settings/account/preferences',
to: 'users/settings/account/preferences#index',
as: 'preferences'
get 'users/settings/account/addons',
to: 'users/settings/account/addons#index',
as: 'addons'
put 'users/settings/account/preferences',
to: 'users/settings/account/preferences#update',
as: 'update_preferences'
@ -418,6 +423,7 @@ Rails.application.routes.draw do
to: 'protocols#load_from_repository_modal'
post 'load_from_repository', to: 'protocols#load_from_repository'
post 'load_from_file', to: 'protocols#load_from_file'
get 'copy_to_repository_modal', to: 'protocols#copy_to_repository_modal'
post 'copy_to_repository', to: 'protocols#copy_to_repository'
get 'protocol_status_bar', to: 'protocols#protocol_status_bar'
@ -435,6 +441,8 @@ Rails.application.routes.draw do
post 'archive', to: 'protocols#archive'
post 'restore', to: 'protocols#restore'
post 'import', to: 'protocols#import'
post 'protocolsio_import_create', to: 'protocols#protocolsio_import_create'
post 'protocolsio_import_save', to: 'protocols#protocolsio_import_save'
get 'export', to: 'protocols#export'
end
end
@ -491,6 +499,15 @@ Rails.application.routes.draw do
post 'avatar_signature' => 'users/registrations#signature'
get 'users/auth_token_sign_in' => 'users/sessions#auth_token_create'
end
namespace :api, defaults: { format: 'json' } do
get 'status', to: 'api#status'
post 'auth/token', to: 'api#authenticate'
scope '20170715', module: 'v20170715' do
get 'tasks/tree', to: 'core_api#tasks_tree'
get 'tasks/:task_id/samples', to: 'core_api#task_samples'
end
end
end
constraints WopiSubdomain do

14
config/whacamole.rb Normal file
View file

@ -0,0 +1,14 @@
# https://github.com/arches/whacamole
#
# Steps to setup this on your heroku instance, this will affect only the worker
#
# 1) set the env variables (api token: > heroku auth:token)
# 2) > heroku labs:enable log-runtime-metrics --app YOUR_APP_NAME
# 3) > heroku ps:scale whacamole=1 --app YOUR_APP_NAME
Whacamole.configure(ENV['HEROKU_APP_NAME']) do |config|
config.api_token = ENV['HEROKU_API_TOKEN']
config.dynos = %w{worker}
config.restart_threshold = ENV['WHACAMOLE_DYNO_RESTART_THRESHOLD'].to_i
config.restart_window = ENV['WHACAMOLE_DYNO_RESTART_TIME_IN_SEC'].to_i
end

View file

@ -0,0 +1,13 @@
class AddUserIdentityTable < ActiveRecord::Migration
def change
create_table :user_identities do |t|
t.belongs_to :user, index: true
t.string :provider, null: false
t.string :uid, null: false
t.timestamps null: true
end
add_index :user_identities, %i(provider uid), unique: true
add_index :user_identities, %i(user_id provider), unique: true
end
end

View file

@ -1,9 +1,117 @@
namespace :tiny_mce_asset do
REGEX = /\[~tiny_mce_id:([0-9a-zA-Z]+)\]/
desc 'Remove obsolete images that were created on new steps or '\
'results and the step/result didn\'t get saved.'
task remove_obsolete_images: :environment do
TinyMceAsset.where('step_id IS ? AND ' \
'result_text_id IS ? AND created_at < ?',
nil, nil, 7.days.ago)
nil, nil, 7.days.ago).destroy_all
end
desc 'Generate new tiny_mce_assets and replace old assets in RTE for ' \
'steps. Assign the last printed id if the script crashes or ' \
'id + 1 if there is a problematic asset'
task :regenerate_step_images, [:last_id] => :environment do |_, args|
replaced_images = 0
failed_attempts = 0
all_images = TinyMceAsset.where.not(step: nil).count
failed_attempts_ids = []
puts 'Start processing steps...'
params = { batch_size: 100 }
if args.present? && args[:last_id].present?
# fetch all steps and sort them asc
params[:start] = args[:last_id].to_i
end
Step.find_each(params) do |step|
next unless step.description && step.description.match(REGEX)
team = step.protocol.team
puts "******************************* \n\n\n\n"
puts "Processing step id => [#{step.id}] \n\n\n\n"
puts '*******************************'
step.description.gsub!(REGEX) do |el|
match = el.match(REGEX)
old_img = TinyMceAsset.find_by_id(match[1])
# skip other processing and deletes tiny_mce tag
# if image is not in database
next unless old_img
new_img = TinyMceAsset.create(image: old_img.image,
team: team,
reference: step)
if new_img
# This image will be removed by `remove_obsolete_images` rake task
# until all the steps are not updated we still need this image
# in case it appears on some other step
old_img.update_attributes(result_text_id: nil, step_id: nil)
replaced_images += 1
"[~tiny_mce_id:#{new_img.id}]"
else
failed_attempts += 1
failed_attempts_ids << old_img.id
"[~tiny_mce_id:#{old_img.id}]" # return the old img
end
end
step.save
end
puts 'Completed processing steps...'
puts '----------- TASK REPORT -----------------'
puts "All images: #{all_images}"
puts "Recreated images: #{replaced_images}"
puts "Failed attempts: #{failed_attempts}"
puts "TinyMceAsset ids of failed attempts: #{failed_attempts_ids}"
puts '-----------------------------------------'
end
desc 'Generate new tiny_mce_assets and replace old assets in RTE ' \
'for results. Assign the last printed id if the script crashes or ' \
'id + 1 if there is a problematic asset'
task :regenerate_results_images, [:last_id] => :environment do |_, args|
replaced_images = 0
failed_attempts = 0
all_images = TinyMceAsset.where.not(result_text: nil).count
failed_attempts_ids = []
params = { batch_size: 100 }
if args.present? && args[:last_id].present?
params[:start] = args[:last_id].to_i
end
puts 'Start processing result_texts...'
ResultText.find_each(params) do |result_text|
next unless result_text.text && result_text.text.match(REGEX)
team = result_text.result.my_module.protocol.team
puts "******************************************* \n\n\n\n"
puts "Processing result_text id => [#{result_text.id}] \n\n\n\n"
puts '*******************************************'
result_text.text.gsub!(REGEX) do |el|
match = el.match(REGEX)
old_img = TinyMceAsset.find_by_id(match[1])
# skip other processing and deletes tiny_mce tag
# if image is not in database
next unless old_img
new_img = TinyMceAsset.create(image: old_img.image,
team: team,
reference: result_text)
if new_img
# This image will be removed by `remove_obsolete_images` rake task
# until all the steps are not updated we still need this image
# in case it appears on some other step
old_img.update_attributes(result_text_id: nil, step_id: nil)
replaced_images += 1
"[~tiny_mce_id:#{new_img.id}]"
else
failed_attempts += 1
failed_attempts_ids << old_img.id
"[~tiny_mce_id:#{old_img.id}]" # return the old img
end
end
result_text.save
end
puts 'Completed processing result_texts...'
puts '----------- TASK REPORT -----------------'
puts "All images: #{all_images}"
puts "Recreated images: #{replaced_images}"
puts "Failed attempts: #{failed_attempts}"
puts "TinyMceAsset ids of failed attempts: #{failed_attempts_ids}"
puts '-----------------------------------------'
end
end

View file

@ -0,0 +1,65 @@
require 'rails_helper'
describe Api::ApiController, type: :controller do
describe 'GET #status' do
before do
get :status
end
it 'Returns HTTP success' do
expect(response).to be_success
expect(response).to have_http_status(200)
end
it 'Response with correct JSON status structure' do
hash_body = nil
expect { hash_body = json }.not_to raise_exception
expect(hash_body).to match(
'message' => I18n.t('api.core.status_ok'),
'versions' => [{ 'version' => '20170715',
'baseUrl' => '/api/20170715/' }]
)
end
end
describe 'Post #authenticate' do
let(:user) { create(:user) }
context 'When valid request' do
before do
post :authenticate, email: user.email,
password: user.password,
grant_type: 'password'
end
it 'Returns HTTP success' do
expect(response).to have_http_status(200)
end
it 'Returns valid JWT token' do
token = nil
expect { token = json['access_token'] }.not_to raise_exception
user_id = nil
expect { user_id = decode_token(token) }.not_to raise_exception
expect(user_id).to eq(user.id)
end
end
context 'When invalid password in request' do
it 'Returns HTTP error' do
post :authenticate, email: user.email,
password: 'wrong_password',
grant_type: 'password'
expect(response).to have_http_status(400)
end
end
context 'When no grant_type in request' do
it 'Returns HTTP error' do
post :authenticate, email: user.email,
password: user.password
expect(response).to have_http_status(400)
end
end
end
end

View file

@ -0,0 +1,129 @@
require 'rails_helper'
describe Api::V20170715::CoreApiController, type: :controller do
let(:user) { create(:user) }
let(:task) { create(:my_module) }
let(:sample1) { create(:sample) }
let(:sample2) { create(:sample) }
let(:sample3) { create(:sample) }
before do
task.samples << [sample1, sample2, sample3]
UserProject.create!(user: user, project: task.experiment.project, role: 0)
end
describe 'GET #task_samples' do
context 'When valid request' do
before do
request.headers['HTTP_ACCEPT'] = 'application/json'
request.headers['Authorization'] = 'Bearer ' + generate_token(user.id)
get :task_samples, task_id: task.id
end
it 'Returns HTTP success' do
expect(response).to have_http_status(200)
end
it 'Returns JSON body containing expected Samples' do
hash_body = nil
expect { hash_body = json }.not_to raise_exception
expect(hash_body).to match(
[{ 'sample_id' => sample1.id.to_s, 'name' => sample1.name },
{ 'sample_id' => sample2.id.to_s, 'name' => sample2.name },
{ 'sample_id' => sample3.id.to_s, 'name' => sample3.name }]
)
end
end
context 'When invalid request' do
context 'When invalid token' do
before do
request.headers['HTTP_ACCEPT'] = 'application/json'
request.headers['Authorization'] = 'Bearer WroNgToken'
get :task_samples, task_id: task.id
end
it 'Returns HTTP unauthorized' do
expect(response).to have_http_status(401)
end
end
context 'When valid token, but invalid request' do
before do
request.headers['HTTP_ACCEPT'] = 'application/json'
request.headers['Authorization'] = 'Bearer ' + generate_token(user.id)
end
it 'Returns HTTP not found' do
get :task_samples, task_id: 1000
expect(response).to have_http_status(404)
expect(json).to match({})
end
it 'Returns HTTP access denied' do
UserProject.where(user: user, project: task.experiment.project)
.first
.destroy!
get :task_samples, task_id: task.id
expect(response).to have_http_status(403)
expect(json).to match({})
end
end
end
end
describe 'GET #tasks_tree' do
context 'When valid request' do
before do
request.headers['HTTP_ACCEPT'] = 'application/json'
request.headers['Authorization'] = 'Bearer ' + generate_token(user.id)
get :tasks_tree
end
it 'Returns HTTP success' do
expect(response).to have_http_status(200)
end
it 'Returns JSON body containing expected Task tree' do
team = user.teams.first
experiment = task.experiment
project = experiment.project
hash_body = nil
expect { hash_body = json }.not_to raise_exception
expect(hash_body).to match(
['team_id' => team.id.to_s, 'name' => team.name,
'description' => team.description,
'projects' => [{
'project_id' => project.id.to_s, 'name' => project.name,
'visibility' => project.visibility,
'archived' => project.archived,
'experiments' => [{
'experiment_id' => experiment.id.to_s,
'name' => experiment.name,
'description' => experiment.description,
'archived' => experiment.archived,
'tasks' => [{
'task_id' => task.id.to_s, 'name' => task.name,
'description' => task.description,
'archived' => task.archived
}]
}]
}]]
)
end
end
context 'When invalid request' do
context 'When invalid token' do
before do
request.headers['HTTP_ACCEPT'] = 'application/json'
request.headers['Authorization'] = 'Bearer WroNgToken'
get :tasks_tree
end
it 'Returns HTTP unauthorized' do
expect(response).to have_http_status(401)
end
end
end
end
end

View file

@ -5,5 +5,8 @@ FactoryGirl.define do
email 'admin_test@scinote.net'
password 'asdf1243'
password_confirmation 'asdf1243'
after(:create) do |user|
user.teams << (Team.first || FactoryGirl.create(:team))
end
end
end

View file

@ -16,7 +16,7 @@ describe MyModuleGroup, type: :model do
describe 'Relations' do
it { should belong_to :experiment }
it { should belong_to(:created_by).class_name('User') }
it { should have_many(:my_modules)}
it { should have_many(:my_modules) }
end
describe 'Should be a valid object' do

View file

@ -10,7 +10,7 @@ require File.expand_path('../../config/environment', __FILE__)
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
@ -82,6 +82,7 @@ RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
# Devise
config.include Devise::Test::ControllerHelpers, type: :controller
config.include ApiHelper, type: :controller
config.extend ControllerMacros, type: :controller
end

View file

@ -0,0 +1,17 @@
module ApiHelper
def generate_token(user_id)
Api::CoreJwt.encode(user_id: user_id)
end
def generate_expired_token(user_id)
Api::CoreJwt.encode({ user_id: user_id }, (Time.now.to_i - 300))
end
def decode_token(token)
Api::CoreJwt.decode(token)['user_id'].to_i
end
def json
JSON.parse(response.body)
end
end