mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-11-17 22:42:22 +08:00
Features/protocolsio integration (#1893)
Features/protocolsio integration
This commit is contained in:
commit
20134c9f8a
73 changed files with 7693 additions and 45 deletions
BIN
app/assets/images/external_protocols/protocolsio_logo.png
Executable file
BIN
app/assets/images/external_protocols/protocolsio_logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
229
app/assets/javascripts/protocols/external_protocols_tab.js
Normal file
229
app/assets/javascripts/protocols/external_protocols_tab.js
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
function applyClickCallbackOnProtocolCards() {
|
||||
$('.protocol-card').off('click').on('click', function(e) {
|
||||
var currProtocolCard = $(this);
|
||||
|
||||
// Check whether this card is already active and deactivate it
|
||||
if ($(currProtocolCard).hasClass('active')) {
|
||||
resetPreviewPanel();
|
||||
$(currProtocolCard).removeClass('active');
|
||||
} else {
|
||||
$('.protocol-card').removeClass('active');
|
||||
currProtocolCard.addClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: $(currProtocolCard).data('show-url'),
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
protocol_source: $(currProtocolCard).data('protocol-source'),
|
||||
protocol_id: $(currProtocolCard).data('show-protocol-id')
|
||||
},
|
||||
beforeSend: animateSpinner($('.protocol-preview-panel'), true),
|
||||
success: function(data) {
|
||||
$('.empty-preview-panel').hide();
|
||||
$('.full-preview-panel').show();
|
||||
$('.btn-holder').html($(currProtocolCard).find('.external-import-btn').clone());
|
||||
$('.preview-iframe').contents().find('body').html(data.html);
|
||||
|
||||
initLoadProtocolModalPreview();
|
||||
animateSpinner($('.protocol-preview-panel'), false);
|
||||
},
|
||||
error: function(_error) {
|
||||
// TODO: we should probably show some alert bubble
|
||||
resetPreviewPanel();
|
||||
animateSpinner($('.protocol-preview-panel'), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Resets preview to the default state
|
||||
function resetPreviewPanel() {
|
||||
$('.empty-preview-panel').show();
|
||||
$('.full-preview-panel').hide();
|
||||
}
|
||||
|
||||
// Reset whole view to the default state
|
||||
function setDefaultViewState() {
|
||||
resetPreviewPanel();
|
||||
$('.empty-text').show();
|
||||
$('.list-wrapper').hide();
|
||||
}
|
||||
|
||||
// Apply AJAX callbacks onto the search box
|
||||
function applySearchCallback() {
|
||||
var timeout;
|
||||
|
||||
// Submit form on every input in the search box
|
||||
$('input[name="key"]').off('input').on('input', function() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(function() {
|
||||
$('form.protocols-search-bar').submit();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Submit form when clicking on sort buttons
|
||||
$('input[name="sort_by"]').off('change').on('change', function() {
|
||||
$('form.protocols-search-bar').submit();
|
||||
});
|
||||
|
||||
// Bind ajax calls on the form
|
||||
$('form.protocols-search-bar').off('ajax:success').off('ajax:error')
|
||||
.bind('ajax:success', function(evt, data, status, xhr) {
|
||||
if (data.html) {
|
||||
resetPreviewPanel();
|
||||
$('.empty-text').hide();
|
||||
$('.list-wrapper').show();
|
||||
|
||||
$('.list-wrapper').html(data.html);
|
||||
applyClickCallbackOnProtocolCards();
|
||||
initLoadProtocolModalPreview();
|
||||
} else {
|
||||
setDefaultViewState();
|
||||
}
|
||||
})
|
||||
.bind("ajax:error", function(evt, xhr, status, error) {
|
||||
setDefaultViewState();
|
||||
|
||||
console.log(xhr.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
function resetFormErrors(modal) {
|
||||
// Reset all errors
|
||||
modal.find('form > .form-group.has-error').removeClass('has-error');
|
||||
modal.find('form > .form-group>span.help-block').html('');
|
||||
modal.find('.general-error > span').html('');
|
||||
}
|
||||
|
||||
function showFormErrors(modal, errors) {
|
||||
resetFormErrors(modal);
|
||||
|
||||
// AR valdiation errors
|
||||
Object.keys(errors.protocol).forEach(function(key) {
|
||||
var input = modal.find('#protocol_' + key);
|
||||
var msg;
|
||||
msg = key.charAt(0).toUpperCase() + key.slice(1) + ': ' + errors.protocol[key].join(', ');
|
||||
if ((input.length > 0) && (errors.protocol[key].length > 0)) {
|
||||
input.parent().next('span.help-block').html(msg);
|
||||
input.parent().parent().addClass('has-error');
|
||||
} else if (errors.protocol[key].length > 0) {
|
||||
modal.find('.general-error > span').append(msg + '<br/>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(table) {
|
||||
$(table).handsontable('render');
|
||||
// Yet another dirty hack to solve HandsOnTable problems
|
||||
if (parseInt($(table).css('height'), 10) < parseInt($(table).css('max-height'), 10) - 30) {
|
||||
$(table).find('.ht_master .wtHolder').css({ height: '100%', width: '100%' });
|
||||
}
|
||||
}
|
||||
|
||||
// Expand all steps
|
||||
function expandAllSteps() {
|
||||
$('.step .panel-collapse').collapse('show');
|
||||
$(document).find("[data-role='step-hot-table']").each(function() {
|
||||
renderTable($(this));
|
||||
});
|
||||
$(document).find('span.collapse-step-icon').each(function() {
|
||||
$(this).addClass('fa-caret-square-up');
|
||||
$(this).removeClass('fa-caret-square-down');
|
||||
});
|
||||
}
|
||||
|
||||
function handleFormSubmit(modal) {
|
||||
var form = modal.find('form');
|
||||
form.on('submit', function(e) {
|
||||
var url = form.attr('action');
|
||||
e.preventDefault(); // avoid to execute the actual submit of the form.
|
||||
e.stopPropagation();
|
||||
animateSpinner(modal, true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: form.serialize(), // serializes the form's elements.
|
||||
success: function(data) {
|
||||
animateSpinner(modal, false);
|
||||
window.location.replace(data.redirect_url);
|
||||
},
|
||||
error: function(data) {
|
||||
showFormErrors(modal, data.responseJSON.errors);
|
||||
},
|
||||
complete: function() {
|
||||
animateSpinner(modal, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initLoadProtocolModalPreview() {
|
||||
$('.external-import-btn').off('click').on('click', function(e) {
|
||||
var link = $(this).parents('.protocol-card');
|
||||
|
||||
// When clicking on the banner button, we have no protocol-card parent
|
||||
if (link.length === 0) {
|
||||
link = $('.protocol-card.active');
|
||||
}
|
||||
|
||||
animateSpinner(null, true);
|
||||
$.ajax({
|
||||
url: link.data('url'),
|
||||
type: 'GET',
|
||||
data: {
|
||||
protocol_source: link.data('protocol-source'),
|
||||
protocol_client_id: link.data('id')
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
var modal = $('#protocol-preview-modal');
|
||||
var modalTitle = modal.find('.modal-title');
|
||||
var modalBody = modal.find('.modal-body');
|
||||
var modalFooter = modal.find('.modal-footer');
|
||||
|
||||
modalTitle.html(data.title);
|
||||
modalBody.html(data.html);
|
||||
modalFooter.html(data.footer);
|
||||
initHandsOnTable(modalBody);
|
||||
modal.modal('show');
|
||||
expandAllSteps();
|
||||
initHandsOnTable(modalBody);
|
||||
|
||||
if (data.validation_errors) {
|
||||
showFormErrors(modal, data.validation_errors);
|
||||
}
|
||||
|
||||
initFormSubmits();
|
||||
handleFormSubmit(modal);
|
||||
},
|
||||
error: function(error) {
|
||||
console.log(error.responseJSON.errors);
|
||||
alert('Server error');
|
||||
},
|
||||
complete: function() {
|
||||
animateSpinner(null, false);
|
||||
}
|
||||
});
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function initFormSubmits() {
|
||||
var modal = $('#protocol-preview-modal');
|
||||
modal.find('button[data-action=import_protocol]').off('click').on('click', function() {
|
||||
var form = modal.find('form');
|
||||
var hiddenField = form.find('#protocol_protocol_type');
|
||||
hiddenField.attr('value', $(this).data('import_type'));
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
applySearchCallback();
|
||||
|
|
@ -57,6 +57,232 @@
|
|||
|
||||
.tab-pane-settings {
|
||||
border: 0;
|
||||
padding: 25px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-pane.external_protocols {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.external-protocols-tab {
|
||||
|
||||
.protocols-search-bar-panel {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding-right: 0;
|
||||
|
||||
.service-provider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.protocolsio-logo {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.protocolsio-title {
|
||||
color: $brand-primary;
|
||||
font-size: 14px;
|
||||
margin-left: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 0;
|
||||
max-width: 50%;
|
||||
|
||||
.form-control {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
background: $color-white;
|
||||
color: $color-silver-chalice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-sort {
|
||||
padding-left: 7px;
|
||||
|
||||
span {
|
||||
color: $color-silver-chalice;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 6px;
|
||||
padding-left: 3px;
|
||||
|
||||
label {
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
|
||||
&.active {
|
||||
background: $color-gainsboro;
|
||||
border-radius: 5px !important;
|
||||
color: $color-emperor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-list-side-panel {
|
||||
background-color: $color-white;
|
||||
border-right: 1px solid $color-silver-chalice;
|
||||
border-top: 1px solid $color-silver-chalice;
|
||||
height: 600px;
|
||||
padding-right: 0;
|
||||
|
||||
.row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
// When no search result is present
|
||||
.empty-text {
|
||||
color: $color-silver-chalice;
|
||||
font-size: 13px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.protocol-card {
|
||||
border-bottom: 1px solid $color-gainsboro;
|
||||
margin-right: 20px;
|
||||
padding: 12px 11px;
|
||||
|
||||
&.active {
|
||||
border: 2px solid $brand-primary;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 4px 0 $color-black;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64,161,215,0.1);
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.external-import-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .external-import-btn, &.active .external-import-btn{
|
||||
display: block;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.protocol-title {
|
||||
color: $brand-primary;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-line {
|
||||
color: $color-dove-gray;
|
||||
font-size: 13px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.protocol-preview-panel {
|
||||
background-color: $color-concrete;
|
||||
border-top: 1px solid $color-silver-chalice;
|
||||
height: 600px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
.row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
// Empty texts on the right
|
||||
.empty-preview-panel {
|
||||
padding-top: 79px;
|
||||
|
||||
.protocol-preview-text {
|
||||
color: $color-silver-chalice;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.protocol-preview-subtext {
|
||||
color: $color-silver-chalice;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-rows {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-separator hr {
|
||||
border: 1px solid $color-alto;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.full-preview-panel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-banner {
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
border-bottom: 1px solid $color-alto;
|
||||
color: $color-dove-gray;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
padding-left: 21px;
|
||||
|
||||
.txt-holder {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.btn-holder {
|
||||
padding-right: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.preview-holder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 40px;
|
||||
padding-left: 21px;
|
||||
padding-right: 21px;
|
||||
width: 100%;
|
||||
|
||||
.preview-iframe {
|
||||
border: 0;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.external-import-btn {
|
||||
background-color: $brand-primary;
|
||||
border: none;
|
||||
color: $color-white;
|
||||
font-size: 12px;
|
||||
padding-bottom: 3px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,8 @@
|
|||
min-height: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.general-error {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
127
app/controllers/external_protocols_controller.rb
Normal file
127
app/controllers/external_protocols_controller.rb
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ExternalProtocolsController < ApplicationController
|
||||
before_action :load_vars
|
||||
before_action :check_import_permissions, only: [:create]
|
||||
|
||||
# GET list_external_protocols
|
||||
def index
|
||||
service_call = ProtocolImporters::SearchProtocolsService
|
||||
.call(protocol_source: index_params[:protocol_source],
|
||||
query_params: index_params)
|
||||
if service_call.succeed?
|
||||
show_import_button = can_create_protocols_in_repository?(@team)
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'protocol_importers/list_of_protocol_cards.html.erb',
|
||||
locals: { protocols: service_call.protocols_list, show_import_button: show_import_button }
|
||||
)
|
||||
}
|
||||
else
|
||||
render json: { errors: service_call.errors }, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
# GET show_external_protocol
|
||||
def show
|
||||
# TODO: this should be refactored, it's only for placeholding
|
||||
endpoint_name = Constants::PROTOCOLS_ENDPOINTS.dig(*show_params[:protocol_source]
|
||||
.split('/').map(&:to_sym))
|
||||
api_client = "ProtocolImporters::#{endpoint_name}::ApiClient".constantize.new
|
||||
|
||||
html_preview = api_client.protocol_html_preview(show_params[:protocol_id])
|
||||
|
||||
render json: {
|
||||
protocol_source: show_params[:protocol_source],
|
||||
protocol_id: show_params[:protocol_id],
|
||||
html: html_preview
|
||||
}
|
||||
rescue StandardError => e
|
||||
render json: {
|
||||
errors: [protocol_html_preview: e.message]
|
||||
}, status: 400
|
||||
end
|
||||
|
||||
# GET build_online_sources_protocol
|
||||
def new
|
||||
service_call = ProtocolImporters::BuildProtocolFromClientService.call(
|
||||
protocol_source: new_params[:protocol_source],
|
||||
protocol_client_id: new_params[:protocol_client_id],
|
||||
user_id: current_user.id,
|
||||
team_id: @team.id,
|
||||
build_with_assets: false
|
||||
)
|
||||
|
||||
if service_call.succeed?
|
||||
@protocol = service_call.built_protocol
|
||||
@protocol&.valid? # Get validations errors here
|
||||
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'protocol_importers/import_form.html.erb',
|
||||
locals: { protocol: @protocol,
|
||||
steps_json: service_call.serialized_steps,
|
||||
steps_assets: service_call.steps_assets }
|
||||
),
|
||||
title: t('protocol_importers.new.modal_title', protocol_name: @protocol.name),
|
||||
footer: render_to_string(
|
||||
partial: 'protocol_importers/preview_modal_footer.html.erb'
|
||||
),
|
||||
validation_errors: { protocol: @protocol.errors.messages }
|
||||
}
|
||||
else
|
||||
render json: { errors: service_call.errors }, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
# POST import_external_protocol
|
||||
def create
|
||||
service_call = ProtocolImporters::ImportProtocolService.call(
|
||||
protocol_params: create_protocol_params,
|
||||
steps_params_json: create_steps_params[:steps],
|
||||
user_id: current_user.id,
|
||||
team_id: @team.id
|
||||
)
|
||||
|
||||
if service_call.succeed?
|
||||
protocol_type = service_call.protocol.in_repository_public? ? 'public' : 'private'
|
||||
|
||||
render json: { protocol: service_call.protocol,
|
||||
redirect_url: protocols_path(type: protocol_type) }
|
||||
else
|
||||
render json: { errors: service_call.errors }, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_vars
|
||||
@team = Team.find_by_id(params[:team_id])
|
||||
|
||||
render_404 unless @team
|
||||
end
|
||||
|
||||
def index_params
|
||||
params.permit(:protocol_source, :key, :page_id, :page_size, :sort_by)
|
||||
end
|
||||
|
||||
def show_params
|
||||
params.permit(:protocol_source, :protocol_id)
|
||||
end
|
||||
|
||||
def new_params
|
||||
params.permit(:protocol_source, :protocol_client_id)
|
||||
end
|
||||
|
||||
def create_protocol_params
|
||||
params.require(:protocol).permit(:name, :authors, :published_on, :protocol_type, :description).except(:steps)
|
||||
end
|
||||
|
||||
def create_steps_params
|
||||
params.require(:protocol).permit(:steps)
|
||||
end
|
||||
|
||||
def check_import_permissions
|
||||
render_403 unless can_create_protocols_in_repository?(@team)
|
||||
end
|
||||
end
|
||||
|
|
@ -142,7 +142,7 @@ class Step < ApplicationRecord
|
|||
end
|
||||
|
||||
def set_last_modified_by
|
||||
if @current_user
|
||||
if @current_user&.is_a?(User)
|
||||
self.tables.each do |t|
|
||||
t.created_by ||= @current_user
|
||||
t.last_modified_by = @current_user if t.changed?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class BuildProtocolFromClientService
|
||||
require 'protocol_importers/protocols_io/v3/errors'
|
||||
extend Service
|
||||
|
||||
attr_reader :errors, :built_protocol, :steps_assets
|
||||
|
||||
def initialize(protocol_client_id:, protocol_source:, user_id:, team_id:, build_with_assets: true)
|
||||
@id = protocol_client_id
|
||||
@protocol_source = protocol_source
|
||||
@user = User.find_by_id user_id
|
||||
@team = Team.find_by_id team_id
|
||||
@build_with_assets = build_with_assets
|
||||
@steps_assets = {}
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def call
|
||||
return self unless valid?
|
||||
|
||||
# Call api client
|
||||
api_response = api_client.single_protocol(@id)
|
||||
|
||||
# Normalize protocol
|
||||
normalized_hash = normalizer.normalize_protocol(api_response)
|
||||
|
||||
pio = ProtocolImporters::ProtocolIntermediateObject.new(normalized_json: normalized_hash,
|
||||
user: @user,
|
||||
team: @team,
|
||||
build_with_assets: @build_with_assets)
|
||||
|
||||
@built_protocol = pio.build
|
||||
@steps_assets = pio.steps_assets unless @build_with_assets
|
||||
|
||||
self
|
||||
rescue client_errors => e
|
||||
@errors[e.error_type] = e.message
|
||||
self
|
||||
end
|
||||
|
||||
def succeed?
|
||||
@errors.none?
|
||||
end
|
||||
|
||||
def serialized_steps
|
||||
# Serialize steps with nested attributes for Tables and NOT nasted attributes for Assets
|
||||
# We want to avoid creating (downloading) Assets instances on building first time and again on importing/creating,
|
||||
# when both actions are not in a row.
|
||||
# Also serialization does not work properly with paperclip attrs
|
||||
return nil unless built_protocol
|
||||
|
||||
built_protocol.steps.map do |step|
|
||||
step_hash = step.attributes.symbolize_keys.slice(:name, :description, :position)
|
||||
|
||||
if !@build_with_assets && @steps_assets[step.position].any?
|
||||
step_hash[:attachments] = @steps_assets[step.position]
|
||||
end
|
||||
|
||||
if step.tables.any?
|
||||
step_hash[:tables_attributes] = step.tables.map { |t| t.attributes.symbolize_keys.slice(:contents) }
|
||||
end
|
||||
|
||||
step_hash
|
||||
end.to_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid?
|
||||
unless [@id, @protocol_source, @user, @team].all?
|
||||
@errors[:invalid_arguments] = {
|
||||
'@id': @id,
|
||||
'@protocol_source': @protocol_source,
|
||||
'user': @user,
|
||||
'team': @team
|
||||
}.map { |key, value| "Can't find #{key.capitalize}" if value.nil? }.compact
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def endpoint_name
|
||||
Constants::PROTOCOLS_ENDPOINTS.dig(*@protocol_source.split('/').map(&:to_sym))
|
||||
end
|
||||
|
||||
def api_client
|
||||
"ProtocolImporters::#{endpoint_name}::ApiClient".constantize.new
|
||||
end
|
||||
|
||||
def normalizer
|
||||
"ProtocolImporters::#{endpoint_name}::ProtocolNormalizer".constantize.new
|
||||
end
|
||||
|
||||
def client_errors
|
||||
"ProtocolImporters::#{endpoint_name}::Error".constantize
|
||||
end
|
||||
end
|
||||
end
|
||||
57
app/services/protocol_importers/import_protocol_service.rb
Normal file
57
app/services/protocol_importers/import_protocol_service.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class ImportProtocolService
|
||||
extend Service
|
||||
|
||||
attr_reader :errors, :protocol
|
||||
|
||||
def initialize(protocol_params:, steps_params_json:, team_id:, user_id:)
|
||||
@user = User.find_by_id user_id
|
||||
@team = Team.find_by_id team_id
|
||||
@protocol_params = protocol_params
|
||||
@steps_params = JSON.parse(steps_params_json) # catch error here
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def call
|
||||
return self unless valid?
|
||||
|
||||
@protocol = Protocol.new(@protocol_params.merge!(added_by: @user, team: @team))
|
||||
|
||||
@protocol.steps << @steps_params.map do |step_params|
|
||||
# Create step with nested attributes for tables
|
||||
s = Step.new(step_params
|
||||
.symbolize_keys
|
||||
.slice(:name, :position, :description, :tables_attributes)
|
||||
.merge(user: @user, completed: false))
|
||||
|
||||
# 'Manually' create assets here. "Accept nasted attributes won't work assets"
|
||||
s.assets << AttachmentsBuilder.generate(step_params.deep_symbolize_keys, user: @user, team: @team)
|
||||
s
|
||||
end
|
||||
|
||||
@errors[:protocol] = @protocol.errors.messages unless @protocol.save
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def succeed?
|
||||
@errors.none?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid?
|
||||
unless [@protocol_params, @user, @team].all?
|
||||
@errors[:invalid_arguments] = {
|
||||
'user': @user,
|
||||
'team': @team,
|
||||
'@protocol_params': @protocol_params
|
||||
}.map { |key, value| "Can't find #{key.capitalize}" if value.nil? }.compact
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
70
app/services/protocol_importers/search_protocols_service.rb
Normal file
70
app/services/protocol_importers/search_protocols_service.rb
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class SearchProtocolsService
|
||||
extend Service
|
||||
require 'protocol_importers/protocols_io/v3/errors'
|
||||
|
||||
attr_reader :errors, :protocols_list
|
||||
|
||||
CONSTANTS = Constants::PROTOCOLS_IO_V3_API
|
||||
|
||||
def initialize(protocol_source:, query_params: {})
|
||||
@protocol_source = protocol_source
|
||||
@query_params = query_params.except(:protocol_source)
|
||||
@errors = Hash.new { |h, k| h[k] = {} }
|
||||
end
|
||||
|
||||
def call
|
||||
return self unless valid?
|
||||
|
||||
# Call api client
|
||||
api_response = api_client.protocol_list(@query_params)
|
||||
|
||||
# Normalize protocols list
|
||||
@protocols_list = normalizer.normalize_list(api_response)
|
||||
|
||||
self
|
||||
rescue client_errors => e
|
||||
@errors[e.error_type] = e.message
|
||||
self
|
||||
end
|
||||
|
||||
def succeed?
|
||||
@errors.none?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid?
|
||||
# try if key is not empty
|
||||
@errors[:invalid_params][:key] = 'Key cannot be empty' if @query_params[:key].blank?
|
||||
|
||||
# try if page id is ok
|
||||
if @query_params[:page_id] && !@query_params[:page_id].to_i.positive?
|
||||
@errors[:invalid_params][:page_id] = 'Page needs to be positive'
|
||||
end
|
||||
|
||||
# try if endpints exists
|
||||
@errors[:invalid_params][:source_endpoint] = 'Wrong source endpoint' unless endpoint_name&.is_a?(String)
|
||||
|
||||
succeed?
|
||||
end
|
||||
|
||||
def endpoint_name
|
||||
Constants::PROTOCOLS_ENDPOINTS.dig(*@protocol_source.split('/').map(&:to_sym))
|
||||
end
|
||||
|
||||
def api_client
|
||||
"ProtocolImporters::#{endpoint_name}::ApiClient".constantize.new
|
||||
end
|
||||
|
||||
def normalizer
|
||||
"ProtocolImporters::#{endpoint_name}::ProtocolNormalizer".constantize.new
|
||||
end
|
||||
|
||||
def client_errors
|
||||
"ProtocolImporters::#{endpoint_name}::Error".constantize
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/utilities/protocol_importers/attachments_builder.rb
Normal file
28
app/utilities/protocol_importers/attachments_builder.rb
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
module AttachmentsBuilder
|
||||
def self.generate(step_json, user: nil, team: nil)
|
||||
return [] unless step_json[:attachments]&.any?
|
||||
|
||||
step_json[:attachments].map do |f|
|
||||
Asset.new(file: URI.parse(f[:url]),
|
||||
created_by: user,
|
||||
last_modified_by: user,
|
||||
team: team,
|
||||
file_file_name: f[:name])
|
||||
end
|
||||
end
|
||||
|
||||
def self.generate_json(step_json)
|
||||
return [] unless step_json[:attachments]&.any?
|
||||
|
||||
step_json[:attachments].map do |f|
|
||||
{
|
||||
name: f[:name],
|
||||
url: f[:url]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class ProtocolDescriptionBuilder
|
||||
def self.generate(protocol_json)
|
||||
return '' unless protocol_json[:description]
|
||||
|
||||
html_string = ApplicationController
|
||||
.renderer
|
||||
.render(template: 'protocol_importers/templates/protocol_description',
|
||||
layout: false,
|
||||
assigns: { description: protocol_json[:description] })
|
||||
html_string
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class ProtocolIntermediateObject
|
||||
attr_reader :normalized_protocol_data, :user, :team, :protocol, :steps_assets, :build_with_assets
|
||||
|
||||
def initialize(normalized_json: {}, user:, team:, build_with_assets: true)
|
||||
@normalized_protocol_data = normalized_json.with_indifferent_access[:protocol] if normalized_json
|
||||
@user = user
|
||||
@team = team
|
||||
@steps_assets = {}
|
||||
@build_with_assets = build_with_assets
|
||||
end
|
||||
|
||||
def import
|
||||
build unless @protocol
|
||||
@protocol.save
|
||||
@protocol
|
||||
end
|
||||
|
||||
def build
|
||||
@protocol = Protocol.new(protocol_attributes)
|
||||
@protocol.description = ProtocolDescriptionBuilder.generate(@normalized_protocol_data)
|
||||
@protocol.steps << build_steps
|
||||
@protocol
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_steps
|
||||
@normalized_protocol_data[:steps].map do |s|
|
||||
step = Step.new(step_attributes(s))
|
||||
if @build_with_assets
|
||||
step.assets << AttachmentsBuilder.generate(s, user: user, team: team)
|
||||
else
|
||||
@steps_assets[step.position] = AttachmentsBuilder.generate_json(s)
|
||||
end
|
||||
step.tables << TablesBuilder.extract_tables_from_html_string(s[:description][:body], true)
|
||||
step.description = StepDescriptionBuilder.generate(s)
|
||||
step
|
||||
end
|
||||
end
|
||||
|
||||
def protocol_attributes
|
||||
{
|
||||
protocol_type: :in_repository_public,
|
||||
added_by: @user,
|
||||
team: @team,
|
||||
name: @normalized_protocol_data[:name],
|
||||
published_on: Time.at(@normalized_protocol_data[:published_on]),
|
||||
authors: @normalized_protocol_data[:authors]
|
||||
}
|
||||
end
|
||||
|
||||
def step_attributes(step_json)
|
||||
defaults = { user: @user, completed: false }
|
||||
step_json.slice(:name, :position).merge!(defaults)
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/utilities/protocol_importers/protocol_normalizer.rb
Normal file
13
app/utilities/protocol_importers/protocol_normalizer.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class ProtocolNormalizer
|
||||
def normalize_list(_client_data)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def normalize_protocol(_client_data)
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
104
app/utilities/protocol_importers/protocols_io/v3/api_client.rb
Normal file
104
app/utilities/protocol_importers/protocols_io/v3/api_client.rb
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
module ProtocolsIO
|
||||
module V3
|
||||
class ApiClient
|
||||
include HTTParty
|
||||
require 'protocol_importers/protocols_io/v3/errors'
|
||||
|
||||
CONSTANTS = Constants::PROTOCOLS_IO_V3_API
|
||||
|
||||
base_uri CONSTANTS[:base_uri]
|
||||
default_timeout CONSTANTS[:default_timeout]
|
||||
logger Rails.logger, CONSTANTS[:debug_level]
|
||||
|
||||
def initialize(token = nil)
|
||||
# Currently we support public tokens only (no token needed for public data)
|
||||
@auth = { token: token }
|
||||
|
||||
# Set default headers
|
||||
self.class.headers('Authorization': "Bearer #{@auth[:token]}") if @auth[:token].present?
|
||||
end
|
||||
|
||||
# Query params available are:
|
||||
# filter (optional): {public|user_public|user_private|shared_with_user}
|
||||
# Which type of protocols to filter.
|
||||
# default is public and requires no auth token.
|
||||
# user_public requires public token.
|
||||
# user_private|shared_with_user require private auth token.
|
||||
# key (optional): string
|
||||
# Search key to search for in protocol name, description, authors.
|
||||
# default: ''
|
||||
# order_field (optional): {activity|date|name|id}
|
||||
# order by this field.
|
||||
# default is activity.
|
||||
# order_dir (optional): {desc|asc}
|
||||
# Direction of ordering.
|
||||
# default is desc.
|
||||
# page_size (optional): int
|
||||
# Number of items per page.
|
||||
# Default 10.
|
||||
# page_id (optional): int (1..n)
|
||||
# id of page.
|
||||
# Default is 1.
|
||||
def protocol_list(query_params = {})
|
||||
response = with_handle_network_errors do
|
||||
sort_mappings = CONSTANTS[:sort_mappings]
|
||||
query = CONSTANTS.dig(:endpoints, :protocols, :default_query_params)
|
||||
.merge(query_params.except(:sort_by))
|
||||
|
||||
if sort_mappings[query_params[:sort_by]&.to_sym]
|
||||
query = query.merge(sort_mappings[query_params[:sort_by].to_sym])
|
||||
end
|
||||
|
||||
self.class.get('/protocols', query: query)
|
||||
end
|
||||
check_for_response_errors(response)
|
||||
end
|
||||
|
||||
# Returns full representation of given protocol ID
|
||||
def single_protocol(id)
|
||||
response = with_handle_network_errors do
|
||||
self.class.get("/protocols/#{id}")
|
||||
end
|
||||
check_for_response_errors(response)
|
||||
end
|
||||
|
||||
# Returns html preview for given protocol
|
||||
# This endpoint is outside the scope of API but is listed here for the
|
||||
# sake of clarity
|
||||
def protocol_html_preview(uri)
|
||||
with_handle_network_errors do
|
||||
self.class.get("https://www.protocols.io/view/#{uri}.html", headers: {})
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_handle_network_errors
|
||||
yield
|
||||
rescue StandardError => e
|
||||
raise ProtocolImporters::ProtocolsIO::V3::NetworkError.new(e.class), e.message
|
||||
end
|
||||
|
||||
def check_for_response_errors(response)
|
||||
error_message = response.parsed_response['error_message']
|
||||
|
||||
case response.parsed_response['status_code']
|
||||
when 0
|
||||
return response
|
||||
when 1
|
||||
raise ProtocolImporters::ProtocolsIO::V3::ArgumentError.new(:missing_or_empty_parameters), error_message
|
||||
when 1218
|
||||
raise ProtocolImporters::ProtocolsIO::V3::UnauthorizedError.new(:invalid_token), error_message
|
||||
when 1219
|
||||
raise ProtocolImporters::ProtocolsIO::V3::UnauthorizedError.new(:token_expires), error_message
|
||||
else
|
||||
raise ProtocolImporters::ProtocolsIO::V3::Error.new(e.class), error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/utilities/protocol_importers/protocols_io/v3/errors.rb
Normal file
27
app/utilities/protocol_importers/protocols_io/v3/errors.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
module ProtocolsIO
|
||||
module V3
|
||||
class Error < StandardError
|
||||
attr_reader :error_type
|
||||
|
||||
def initialize(error_type)
|
||||
@error_type = error_type
|
||||
end
|
||||
end
|
||||
|
||||
# MissingOrEmptyParameters
|
||||
class ArgumentError < Error; end
|
||||
|
||||
# SocketError, HTTPParty::Error
|
||||
class NetworkError < Error; end
|
||||
|
||||
# InvalidToken, ExpiredToken
|
||||
class UnauthorizedError < Error; end
|
||||
|
||||
# General NormalizerError
|
||||
class NormalizerError < Error; end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
module ProtocolsIO
|
||||
module V3
|
||||
class ProtocolNormalizer < ProtocolImporters::ProtocolNormalizer
|
||||
require 'protocol_importers/protocols_io/v3/errors'
|
||||
|
||||
def normalize_protocol(client_data)
|
||||
# client_data is HttpParty ApiReponse object
|
||||
protocol_hash = client_data.parsed_response.with_indifferent_access[:protocol]
|
||||
|
||||
normalized_data = {
|
||||
uri: client_data.request.last_uri.to_s,
|
||||
source: Constants::PROTOCOLS_IO_V3_API[:source_id],
|
||||
doi: protocol_hash[:doi],
|
||||
published_on: protocol_hash[:published_on],
|
||||
version: protocol_hash[:version_id],
|
||||
source_id: protocol_hash[:id],
|
||||
name: protocol_hash[:title],
|
||||
description: {
|
||||
body: protocol_hash[:description],
|
||||
image: protocol_hash[:image][:source],
|
||||
extra_content: []
|
||||
},
|
||||
authors: protocol_hash[:authors].map { |e| e[:name] }.join(', ')
|
||||
}
|
||||
|
||||
{ before_start: 'Before start', guidelines: 'Guidelines', warning: 'Warnings' }.each do |k, v|
|
||||
normalized_data[:description][:extra_content] << { title: v, body: protocol_hash[k] } if protocol_hash[k]
|
||||
end
|
||||
|
||||
normalized_data[:steps] = protocol_hash[:steps].map do |e|
|
||||
{
|
||||
source_id: e[:id],
|
||||
name: StepComponents.name(e[:components]),
|
||||
attachments: StepComponents.attachments(e[:components]),
|
||||
description: {
|
||||
body: StepComponents.description(e[:components]),
|
||||
components: StepComponents.description_components(e[:components])
|
||||
},
|
||||
position: e[:previous_id].nil? ? 0 : nil
|
||||
}
|
||||
end
|
||||
|
||||
# set positions
|
||||
if protocol_hash[:steps].any?
|
||||
first_step_id = normalized_data[:steps].find { |s| s[:position].zero? }[:source_id]
|
||||
next_step_id = protocol_hash[:steps].find { |s| s[:previous_id] == first_step_id }.try(:[], :id)
|
||||
steps = normalized_data[:steps].map { |s| [s[:source_id], s] }.to_h
|
||||
original_order = protocol_hash[:steps].map { |m| [m[:previous_id], m[:id]] }.to_h
|
||||
|
||||
current_position = 0
|
||||
while next_step_id
|
||||
current_position += 1
|
||||
steps[next_step_id][:position] = current_position
|
||||
next_step_id = original_order[next_step_id]
|
||||
end
|
||||
|
||||
# Check if step name are valid
|
||||
steps.each do |step|
|
||||
step[1][:name] = "Step #{(step[1][:position] + 1)}" if step[1][:name].blank?
|
||||
end
|
||||
else
|
||||
normalized_data[:steps] = []
|
||||
end
|
||||
|
||||
{ protocol: normalized_data }
|
||||
rescue StandardError => e
|
||||
raise ProtocolImporters::ProtocolsIO::V3::NormalizerError.new(e.class.to_s.downcase.to_sym), e.message
|
||||
end
|
||||
|
||||
def normalize_list(client_data)
|
||||
# client_data is HttpParty ApiReponse object
|
||||
protocols_hash = client_data.parsed_response.with_indifferent_access[:items]
|
||||
|
||||
normalized_data = {}
|
||||
normalized_data[:protocols] = protocols_hash.map do |e|
|
||||
{
|
||||
id: e[:id],
|
||||
title: e[:title],
|
||||
source: Constants::PROTOCOLS_IO_V3_API[:source_id],
|
||||
created_on: e[:created_on],
|
||||
authors: e[:authors].map { |a| a[:name] }.join(', '),
|
||||
nr_of_steps: e[:stats][:number_of_steps],
|
||||
nr_of_views: e[:stats][:number_of_views],
|
||||
uri: e[:uri]
|
||||
}
|
||||
end
|
||||
normalized_data
|
||||
rescue StandardError => e
|
||||
raise ProtocolImporters::ProtocolsIO::V3::NormalizerError.new(e.class.to_s.downcase.to_sym), e.message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
module ProtocolsIO
|
||||
module V3
|
||||
class StepComponents
|
||||
AVAILABLE_COMPONENTS = {
|
||||
1 => :description,
|
||||
3 => :amount,
|
||||
4 => :duration,
|
||||
6 => :title,
|
||||
7 => :link,
|
||||
8 => :software,
|
||||
9 => :dataset,
|
||||
15 => :command,
|
||||
17 => :result,
|
||||
19 => :safety,
|
||||
20 => :reagents,
|
||||
22 => :gotostep,
|
||||
23 => :file,
|
||||
24 => :temperature,
|
||||
25 => :concentration,
|
||||
26 => :notes
|
||||
}.freeze
|
||||
|
||||
DESCRIPTION_COMPONENTS = AVAILABLE_COMPONENTS.slice(3, 4, 7, 8, 9, 15, 17, 19, 20, 22, 24, 25, 26).freeze
|
||||
|
||||
def self.get_component(id, components)
|
||||
if AVAILABLE_COMPONENTS.include?(id)
|
||||
components.find { |o| o[:type_id] == id }
|
||||
else
|
||||
raise ArgumentError
|
||||
end
|
||||
end
|
||||
|
||||
def self.name(components)
|
||||
get_component(6, components)[:source][:title]
|
||||
end
|
||||
|
||||
def self.description(components)
|
||||
get_component(1, components)[:source][:description]
|
||||
end
|
||||
|
||||
def self.description_components(components)
|
||||
description_components = components.select { |c| DESCRIPTION_COMPONENTS.include?(c[:type_id]) }
|
||||
|
||||
description_components.map do |dc|
|
||||
build_desc_component dc
|
||||
end.compact
|
||||
end
|
||||
|
||||
def self.attachments(components)
|
||||
components.select { |c| c[:type_id] == 23 }.map do |cc|
|
||||
{
|
||||
url: cc[:source][:source],
|
||||
name: cc[:source][:name]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_desc_component(desc_component)
|
||||
case AVAILABLE_COMPONENTS[desc_component[:type_id]]
|
||||
when :amount
|
||||
{
|
||||
type: 'amount',
|
||||
value: desc_component[:source][:amount],
|
||||
unit: desc_component[:source][:unit],
|
||||
name: desc_component[:source][:title]
|
||||
}
|
||||
when :duration
|
||||
{
|
||||
type: 'duration',
|
||||
value: desc_component[:source][:duration],
|
||||
name: desc_component[:source][:title]
|
||||
}
|
||||
when :link
|
||||
{
|
||||
type: 'link',
|
||||
source: desc_component[:source][:link]
|
||||
}
|
||||
when :software
|
||||
{
|
||||
type: 'software',
|
||||
name: desc_component[:source][:name],
|
||||
source: desc_component[:source][:link],
|
||||
details: {
|
||||
repository_link: desc_component[:source][:repository],
|
||||
developer: desc_component[:source][:developer],
|
||||
os_name: desc_component[:source][:os_name]
|
||||
}
|
||||
}
|
||||
when :command
|
||||
{
|
||||
type: 'command',
|
||||
software_name: desc_component[:source][:name],
|
||||
command: desc_component[:source][:command],
|
||||
details: {
|
||||
os_name: desc_component[:source][:os_name]
|
||||
}
|
||||
}
|
||||
when :result
|
||||
{
|
||||
type: 'result',
|
||||
body: desc_component[:source][:body]
|
||||
}
|
||||
when :safety
|
||||
{
|
||||
type: 'warning',
|
||||
body: desc_component[:source][:body],
|
||||
details: {
|
||||
link: desc_component[:source][:link]
|
||||
}
|
||||
}
|
||||
when :reagents
|
||||
{
|
||||
type: 'reagent',
|
||||
name: desc_component[:source][:name],
|
||||
details: {
|
||||
catalog_number: desc_component[:source][:sku],
|
||||
link: desc_component[:source][:vendor][:link],
|
||||
linear_formula: desc_component[:source][:linfor],
|
||||
mol_weight: desc_component[:source][:mol_weight]
|
||||
}
|
||||
}
|
||||
when :gotostep
|
||||
{
|
||||
type: 'gotostep',
|
||||
value: desc_component[:source][:title],
|
||||
step_id: desc_component[:source][:step_guid]
|
||||
}
|
||||
when :temperature
|
||||
{
|
||||
type: 'temperature',
|
||||
value: desc_component[:source][:temperature],
|
||||
unit: desc_component[:source][:unit],
|
||||
name: desc_component[:source][:title]
|
||||
}
|
||||
when :concentration
|
||||
{
|
||||
type: 'concentration',
|
||||
value: desc_component[:source][:concentration],
|
||||
unit: desc_component[:source][:unit],
|
||||
name: desc_component[:source][:title]
|
||||
}
|
||||
when :notes
|
||||
{
|
||||
type: 'note',
|
||||
author: desc_component[:source][:creator][:name],
|
||||
body: desc_component[:source][:body]
|
||||
}
|
||||
when :dataset
|
||||
{
|
||||
type: 'dataset',
|
||||
name: desc_component[:source][:name],
|
||||
source: desc_component[:source][:link]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/utilities/protocol_importers/step_description_builder.rb
Normal file
17
app/utilities/protocol_importers/step_description_builder.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class StepDescriptionBuilder
|
||||
def self.generate(step_json)
|
||||
return '' unless step_json[:description]
|
||||
|
||||
step_json[:description][:body] = TablesBuilder.remove_tables_from_html(step_json[:description][:body])
|
||||
html_string = ApplicationController
|
||||
.renderer
|
||||
.render(template: 'protocol_importers/templates/step_description',
|
||||
layout: false,
|
||||
assigns: { step_description: step_json[:description] })
|
||||
html_string
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/utilities/protocol_importers/tables_builder.rb
Normal file
37
app/utilities/protocol_importers/tables_builder.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProtocolImporters
|
||||
class TablesBuilder
|
||||
def self.extract_tables_from_html_string(description_string, remove_first_column_row = false)
|
||||
tables = []
|
||||
|
||||
doc = Nokogiri::HTML(description_string)
|
||||
tables_nodeset = doc.css('table')
|
||||
|
||||
tables_nodeset.each do |table_node|
|
||||
rows_nodeset = table_node.css('tr')
|
||||
|
||||
two_d_array = Array.new(rows_nodeset.count) { [] }
|
||||
|
||||
rows_nodeset.each_with_index do |row, i|
|
||||
row.css('td').each_with_index do |cell, j|
|
||||
two_d_array[i][j] = cell.inner_html
|
||||
end
|
||||
two_d_array[i].shift if remove_first_column_row
|
||||
end
|
||||
two_d_array.shift if remove_first_column_row
|
||||
|
||||
tables << Table.new(contents: { data: two_d_array }.to_json)
|
||||
end
|
||||
tables
|
||||
end
|
||||
|
||||
def self.remove_tables_from_html(description_string)
|
||||
doc = Nokogiri::HTML(description_string)
|
||||
doc.search('table').each do |t|
|
||||
t.swap('<br/><p><i>There was a table here, it was moved to tables section.</i></p>')
|
||||
end
|
||||
doc.css('body').first.inner_html
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/views/protocol_importers/_import_form.html.erb
Normal file
49
app/views/protocol_importers/_import_form.html.erb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<%= form_for :protocol, url: team_import_external_protocol_path(team_id: current_team.id),
|
||||
method: :post, remote: true do |f|%>
|
||||
<div class="general-error has-error">
|
||||
<span class="has-error help-block"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :name, t('protocols.import_export.import_modal.name_label') %>
|
||||
<%= f.text_field :name, class: 'form-control', value: protocol.name %>
|
||||
<span class="help-block"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :authors, t('protocols.import_export.import_modal.authors_label') %>
|
||||
<%= f.text_field :authors, class: 'form-control', value: protocol.authors %>
|
||||
</div>
|
||||
|
||||
<div class="import-protocol-preview-description">
|
||||
<%= custom_auto_link(protocol.description, simple_format: false, team: current_team) %>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group">
|
||||
<%= f.label :published_on_label, t('protocols.import_export.import_modal.published_on_label')%>
|
||||
<%= f.text_field :published_on_label, value: I18n.l(protocol.published_on, format: :full), class: 'form-control', disabled: true %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= f.hidden_field(:steps, value: steps_json) %>
|
||||
<%= f.hidden_field(:published_on, value: protocol.published_on) %>
|
||||
<%= f.hidden_field(:description, value: protocol.description) %>
|
||||
<%= f.hidden_field(:protocol_type, value: protocol.protocol_type) %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<div data-role="steps-container">
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<h2><%= t("protocols.steps.subtitle") %></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div id="steps">
|
||||
<% protocol.steps.sort_by{ |s| s.position }.each do |step| %>
|
||||
<%= render partial: "steps/step.html.erb", locals: { step: step, steps_assets: steps_assets, preview: true, import: true } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<% protocols[:protocols].each do |protocol| %>
|
||||
<%= render partial: 'protocol_importers/protocol_card',
|
||||
locals: { protocol: protocol, show_import_button: show_import_button } %>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<button type="button" class="btn btn-default" data-dismiss="modal"><%=t "general.cancel" %></button>
|
||||
<button type="button" class="btn btn-primary" data-action="import_protocol" data-import_type="in_repository_public"><%=t "protocols.import_export.import_modal.import_to_team_protocols_label" %></button>
|
||||
<button type="button" class="btn btn-primary" data-action="import_protocol" data-import_type="in_repository_private"><%=t "protocols.import_export.import_modal.import_to_private_protocols_label" %></button>
|
||||
25
app/views/protocol_importers/_protocol_card.html.erb
Normal file
25
app/views/protocol_importers/_protocol_card.html.erb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<div class='protocol-card'
|
||||
data-id='<%= protocol[:id] %>'
|
||||
data-protocol-source='<%= protocol[:source] %>'
|
||||
data-url='<%= team_build_external_protocol_path(current_team.id,) %>'
|
||||
data-show-url='<%= team_show_external_protocol_path(current_team.id) %>'
|
||||
data-show-protocol-id='<%= protocol[:uri] %>'>
|
||||
<p class='protocol-title'><%= protocol[:title] %></p>
|
||||
|
||||
<div class='row'>
|
||||
<div class='row col-md-12 info-line'>
|
||||
<%= l(Time.at(protocol[:created_on]).to_datetime, format: :full_date) %> • <%= protocol[:authors] %>
|
||||
</div>
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-md-6 info-line'>
|
||||
<%= t('protocol_importers.card.views_and_steps', nr_of_views: protocol[:nr_of_views], nr_of_steps: protocol[:nr_of_steps]) %>
|
||||
</div>
|
||||
<div class='col-md-6 info-line'>
|
||||
<% if show_import_button %>
|
||||
<button type="button" class='external-import-btn btn btn-primary pull-right'><%= t('protocol_importers.card.import_button_text') %></button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
4
app/views/protocol_importers/templates/_amount.html.erb
Normal file
4
app/views/protocol_importers/templates/_amount.html.erb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<p class="step-description-component-amount"><b><%= t('protocol_importers.templates.amount.title') %></b><br>
|
||||
<%= "#{item[:value]} #{item[:unit]} #{item[:name]} " %>
|
||||
</p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
5
app/views/protocol_importers/templates/_command.html.erb
Normal file
5
app/views/protocol_importers/templates/_command.html.erb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<p class="step-description-component-command"><b><%= t('protocol_importers.templates.command.title') %></b><br>
|
||||
<%= "#{t('protocol_importers.templates.command.name')}: #{item[:name]}" %>
|
||||
<%= "#{t('protocol_importers.templates.command.code')}: <code>#{item[:command]}</code>" %>
|
||||
</p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<p class="step-description-component-concentration"><b><%= t('protocol_importers.templates.concentration.title') %></b><br/>
|
||||
<%= "#{item[:value]} #{item[:unit]} #{item[:name]} " %></p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
5
app/views/protocol_importers/templates/_dataset.html.erb
Normal file
5
app/views/protocol_importers/templates/_dataset.html.erb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<p class="step-description-component-dataset"><b><%= t('protocol_importers.templates.dataset.title') %></b><br>
|
||||
<%= "#{t('protocol_importers.templates.dataset.name')}: #{item[:name]}" %>
|
||||
<%= "#{t('protocol_importers.templates.dataset.link')}: <a href='#{item[:source]}'>#{item[:source]}</a>" if item[:source] %>
|
||||
</p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
10
app/views/protocol_importers/templates/_details.html.erb
Normal file
10
app/views/protocol_importers/templates/_details.html.erb
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<p>
|
||||
<% if item[:details]&.any? %>
|
||||
<b> <%= t('protocol_importers.templates.details.title')%>: </b>
|
||||
<br />
|
||||
<% item[:details].reject { |_,v| v.blank? }.each do |k, v| %>
|
||||
<%= "#{k.humanize}: #{v.to_s}" %>
|
||||
<br />
|
||||
<% end %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<p class="step-description-component-duration"><b><%= t('protocol_importers.templates.duration.title') %></b><br>
|
||||
<%= "#{item[:name]} #{item[:value]} #{t('protocol_importers.templates.duration.unit')}" %>
|
||||
</p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<p class="step-description-component-gotostep"><b><%= t('protocol_importers.templates.gotostep.title') %></b><br/>
|
||||
<%= "#{t('protocol_importers.templates.gotostep.number')}: #{item[:step_id]}" %><br/>
|
||||
<%= "#{t('protocol_importers.templates.gotostep.reason')}: #{item[:value]}" %><br/>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
4
app/views/protocol_importers/templates/_link.html.erb
Normal file
4
app/views/protocol_importers/templates/_link.html.erb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<p class="step-description-component-link"><b><%= t('protocol_importers.templates.link.title') %></b><br>
|
||||
<a href="<%= item[:source] %>" target="_blank"><%= item[:source] %></a>
|
||||
</p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
4
app/views/protocol_importers/templates/_note.html.erb
Normal file
4
app/views/protocol_importers/templates/_note.html.erb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<p class="step-description-component-note"><b><%= t('protocol_importers.templates.note.title') %></b><br/>
|
||||
<%= "#{t('protocol_importers.templates.note.author')}: #{item[:author]}" %><br/>
|
||||
<%= item[:body] %></p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
15
app/views/protocol_importers/templates/_reagent.html.erb
Normal file
15
app/views/protocol_importers/templates/_reagent.html.erb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<p class="step-description-component-reagent">
|
||||
<b><%= t('protocol_importers.templates.reagent.title') %></b>
|
||||
<br/>
|
||||
<%= "#{t('protocol_importers.templates.reagent.name')}: #{item[:name]}" %>
|
||||
<% if item[:source] %>
|
||||
<%= t('protocol_importers.templates.reagent.link') %>:
|
||||
<%= "<a href='#{item[:source]}'>#{item[:source]}</a>" %>
|
||||
<% end -%>
|
||||
</p>
|
||||
<%= render(
|
||||
partial: 'protocol_importers/templates/details',
|
||||
locals: { item: item }
|
||||
)
|
||||
%>
|
||||
|
||||
3
app/views/protocol_importers/templates/_result.html.erb
Normal file
3
app/views/protocol_importers/templates/_result.html.erb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<p class="step-description-component-result"><b><%= t('protocol_importers.templates.result.title') %></b><br/>
|
||||
<%= item[:body] %></p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<p class="step-description-component-software"><b><%= t('protocol_importers.templates.software.title') %></b><br>
|
||||
<%= "#{t('protocol_importers.templates.software.name')}: #{item[:name]}" %>
|
||||
<%= "#{t('protocol_importers.templates.software.link')}: #{ActionController::Base.helpers.link_to(item[:source], item[:source])}" %>
|
||||
</p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<p class="step-description-component-temperature"><b><%= t('protocol_importers.templates.temperature.title') %></b><br/>
|
||||
<%= "#{item[:value]} #{item[:unit]} #{item[:name]}" %></p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
3
app/views/protocol_importers/templates/_warning.html.erb
Normal file
3
app/views/protocol_importers/templates/_warning.html.erb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<p class="step-description-component-warning"><b><%= t('protocol_importers.templates.warning.title') %><b><br/>
|
||||
<%= item[:body] %></p>
|
||||
<%= render partial: 'protocol_importers/templates/details', locals: { item: item } %>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<% if @description[:body] %>
|
||||
<p> <%= sanitize(@description[:body], tags: Constants::PROTOCOLS_DESC_TAGS) %> </p>
|
||||
<br/>
|
||||
<% end %>
|
||||
<% if @description[:image] %>
|
||||
<br/>
|
||||
<img src='<%= @description[:image] %>' />
|
||||
<br/>
|
||||
<% end %>
|
||||
|
||||
<% @description[:extra_content]&.each do |i| %>
|
||||
<% if i[:body].present? %>
|
||||
<br/><b><%= strip_tags i[:title] %>:</b> <br/>
|
||||
<%= sanitize(i[:body], tags: Constants::PROTOCOLS_DESC_TAGS) %><br/>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<% if @step_description[:body] %>
|
||||
<p> <%= sanitize(@step_description[:body], tags: Constants::PROTOCOLS_DESC_TAGS) %> </p>
|
||||
<% end %>
|
||||
|
||||
<% @step_description[:components]&.each do |component| %>
|
||||
<% sanitized_component = component.except('type') %>
|
||||
<% sanitized_component[:body] = sanitize(component[:body], tags: Constants::PROTOCOLS_DESC_TAGS) if component[:body] %>
|
||||
<%= render partial: "protocol_importers/templates/#{component[:type]}", locals: { item: sanitized_component } %>
|
||||
<% end %>
|
||||
|
||||
<% @step_description[:extra_content]&.each do |i| %>
|
||||
<b><%= strip_tags i[:title] %>:</b> <br/>
|
||||
<%= sanitize(i[:body], tags: Constants::PROTOCOLS_DESC_TAGS) %><br/>
|
||||
<% end %>
|
||||
|
||||
|
||||
|
|
@ -13,12 +13,16 @@
|
|||
<li role="presentation" class="<%= "active" if @type == :private %>">
|
||||
<%= link_to t("protocols.index.navigation.private"), protocols_path(team: @current_team, type: :private) %>
|
||||
</li>
|
||||
<li role="presentation" class="<%= "active" if @type == :external_protocols %>">
|
||||
<%= link_to t("protocols.index.navigation.external_protocols"),
|
||||
protocols_path(team: @current_team, type: :external_protocols) %>
|
||||
</li>
|
||||
<li role="presentation" class="<%= "active" if @type == :archive %>">
|
||||
<%= link_to t("protocols.index.navigation.archive"), protocols_path(team: @current_team, type: :archive) %>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane tab-pane-settings tab-pane-protocols active" role="tabpanel">
|
||||
<div class="tab-pane tab-pane-settings tab-pane-protocols active <%= @type %>" role="tabpanel">
|
||||
<!-- Buttons container -->
|
||||
<% if @type.in? [:public, :private] %>
|
||||
<div class="protocols-description">
|
||||
|
|
@ -89,7 +93,7 @@
|
|||
<span class="hidden-xs"> <%= t("protocols.index.archive_action") %></span>
|
||||
</a>
|
||||
</div>
|
||||
<% else %>
|
||||
<% elsif @type == :archive %>
|
||||
<div class="protocols-description">
|
||||
<%= t("protocols.index.archive.description") %>
|
||||
</div>
|
||||
|
|
@ -101,32 +105,13 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<div id="protocols-datatable-spinner">
|
||||
<div class="protocols-datatable">
|
||||
<table id="protocols-table" class="table" data-team-id="<%= @current_team.id %>" data-type="<%= @type %>" data-source="<%= datatable_protocols_path(team: @current_team, type: @type) %>">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select-all"><input name="select_all" value="1" type="checkbox"></th>
|
||||
<th id="protocol-name"><%= t("protocols.index.thead_name") %></th>
|
||||
<th id="protocol-keywords"><%= t("protocols.index.thead_keywords") %></th>
|
||||
<th id="protocol-nr-of-linked-children"><%= t("protocols.index.thead_nr_of_linked_children") %></th>
|
||||
<% if @type == :public %>
|
||||
<th id="protocol-published-by"><%= t("protocols.index.thead_published_by") %></th>
|
||||
<th id="protocol-published-on"><%= t("protocols.index.thead_published_on") %></th>
|
||||
<% elsif @type == :private %>
|
||||
<th id="protocol-added-by"><%= t("protocols.index.thead_added_by") %></th>
|
||||
<th id="protocol-created-at"><%= t("protocols.index.thead_created_at") %></th>
|
||||
<% else %>
|
||||
<th id="protocol-archived-by"><%= t("protocols.index.thead_archived_by") %></th>
|
||||
<th id="protocol-archived-on"><%= t("protocols.index.thead_archived_on") %></th>
|
||||
<% end %>
|
||||
<th id="protocol-updated-at"><%= t("protocols.index.thead_updated_at") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<%# Main tab content %>
|
||||
<% if @type == :external_protocols %>
|
||||
<%= render partial: "protocols/index/external_protocols_tab.html.erb" %>
|
||||
<% else %>
|
||||
<%= render partial: "protocols/index/protocols_datatable.html.erb" %>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
103
app/views/protocols/index/_external_protocols_tab.html.erb
Normal file
103
app/views/protocols/index/_external_protocols_tab.html.erb
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<div class='external-protocols-tab'>
|
||||
<div class='row'>
|
||||
<%= form_tag team_list_external_protocol_path(@current_team.id),
|
||||
method: :get,
|
||||
class: 'protocols-search-bar',
|
||||
remote: true do %>
|
||||
<div class='col-md-5 protocols-search-bar-panel'>
|
||||
<div class='service-provider'>
|
||||
<%= image_tag 'external_protocols/protocolsio_logo.png',
|
||||
class: 'protocolsio-logo' %>
|
||||
<span class='protocolsio-title'><%= t('protocols.index.external_protocols.protocolsio_title') %></span>
|
||||
<%= hidden_field_tag 'protocol_source', 'protocolsio/v3' %>
|
||||
</div>
|
||||
|
||||
<div class='input-group'>
|
||||
<input class='form-control'
|
||||
type='text'
|
||||
name='key'
|
||||
placeholder="<%= t('protocols.index.external_protocols.search_bar_placeholder') %>" >
|
||||
</input>
|
||||
|
||||
<span class='input-group-addon'><i class='fas fa-search '></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='col-md-7'>
|
||||
<div class='protocol-sort'>
|
||||
<span><%= t('protocols.index.external_protocols.sort_by.title') %></span>
|
||||
<div class='btn-group' data-toggle='buttons'>
|
||||
<label class='btn btn-link active'>
|
||||
<input type='radio' name='sort_by' id='alphabetically-asc' value='alpha_asc' checked>
|
||||
<%= t('protocols.index.external_protocols.sort_by.alphabetically-asc') %>
|
||||
</label>
|
||||
<label class='btn btn-link'>
|
||||
<input type='radio' name='sort_by' id='alphabetically-desc' value='alpha_desc'>
|
||||
<%= t('protocols.index.external_protocols.sort_by.alphabetically-desc') %>
|
||||
</label>
|
||||
<label class='btn btn-link'>
|
||||
<input type='radio' name='sort_by' id='newest' value='newest'>
|
||||
<%= t('protocols.index.external_protocols.sort_by.newest') %>
|
||||
</label>
|
||||
<label class='btn btn-link'>
|
||||
<input type='radio' name='sort_by' id='oldest' value='oldest'>
|
||||
<%= t('protocols.index.external_protocols.sort_by.oldest') %>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class='row main-protocol-panel'>
|
||||
<div class='col-md-5 protocol-list-side-panel'>
|
||||
|
||||
<div class='row empty-text'>
|
||||
<%= t('protocols.index.external_protocols.list_panel.empty_text') %>
|
||||
</div>
|
||||
|
||||
<div class='list-wrapper'></div>
|
||||
</div>
|
||||
|
||||
<div class='col-md-7 protocol-preview-panel'>
|
||||
<div class='empty-preview-panel'>
|
||||
|
||||
<div class='row'>
|
||||
<div class='text-rows protocol-preview-text'>
|
||||
<%= t('protocols.index.external_protocols.preview_panel.empty_title') %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='row'>
|
||||
<div class='text-separator'> <hr> </div>
|
||||
</div>
|
||||
|
||||
<div class='row'>
|
||||
<div class='text-rows protocol-preview-subtext'>
|
||||
<%= t('protocols.index.external_protocols.preview_panel.empty_subtext') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='full-preview-panel' style='display: none;'>
|
||||
<div class='row preview-banner'>
|
||||
<div class='col-md-6 txt-holder'>
|
||||
<span>
|
||||
<%= t('protocols.index.external_protocols.preview_panel.banner_text') %>
|
||||
</span>
|
||||
</div>
|
||||
<div class='col-md-6 btn-holder'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='preview-holder'>
|
||||
<iframe class='preview-iframe'></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag "protocols/external_protocols_tab.js" %>
|
||||
26
app/views/protocols/index/_protocols_datatable.html.erb
Normal file
26
app/views/protocols/index/_protocols_datatable.html.erb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<div id="protocols-datatable-spinner">
|
||||
<div class="protocols-datatable">
|
||||
<table id="protocols-table" class="table" data-team-id="<%= @current_team.id %>" data-type="<%= @type %>" data-source="<%= datatable_protocols_path(team: @current_team, type: @type) %>">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select-all"><input name="select_all" value="1" type="checkbox"></th>
|
||||
<th id="protocol-name"><%= t("protocols.index.thead_name") %></th>
|
||||
<th id="protocol-keywords"><%= t("protocols.index.thead_keywords") %></th>
|
||||
<th id="protocol-nr-of-linked-children"><%= t("protocols.index.thead_nr_of_linked_children") %></th>
|
||||
<% if @type == :public %>
|
||||
<th id="protocol-published-by"><%= t("protocols.index.thead_published_by") %></th>
|
||||
<th id="protocol-published-on"><%= t("protocols.index.thead_published_on") %></th>
|
||||
<% elsif @type == :private %>
|
||||
<th id="protocol-added-by"><%= t("protocols.index.thead_added_by") %></th>
|
||||
<th id="protocol-created-at"><%= t("protocols.index.thead_created_at") %></th>
|
||||
<% else %>
|
||||
<th id="protocol-archived-by"><%= t("protocols.index.thead_archived_by") %></th>
|
||||
<th id="protocol-archived-on"><%= t("protocols.index.thead_archived_on") %></th>
|
||||
<% end %>
|
||||
<th id="protocol-updated-at"><%= t("protocols.index.thead_updated_at") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<% preview = (defined?(preview) ? preview : false) %>
|
||||
<% import = (defined?(import) ? import : false) %>
|
||||
<div class ="step <%= step.completed? ? "completed" : "not-completed" %>">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
|
@ -56,7 +57,7 @@
|
|||
</div>
|
||||
<div class="left-floats">
|
||||
<a class="step-panel-collapse-link"
|
||||
href="#step-panel-<%= step.id %>"
|
||||
href="#step-panel-<%= step.id || step.position %>"
|
||||
data-toggle="collapse"
|
||||
data-remote="true">
|
||||
<span class="fas fa-caret-square-down collapse-step-icon"></span>
|
||||
|
|
@ -65,19 +66,21 @@
|
|||
<span class="step-number"><%= step.position + 1 %></span>
|
||||
|
||||
<a class="step-panel-collapse-link step-name-link"
|
||||
href="#step-panel-<%= step.id %>"
|
||||
href="#step-panel-<%= step.id || step.position %>"
|
||||
data-toggle="collapse"
|
||||
data-remote="true">
|
||||
<span class="name-block"><strong><%= step.name %></strong></span>
|
||||
<span class="delimiter">|</span>
|
||||
<span class="author-block"><%= sanitize_input t('protocols.steps.published_on',
|
||||
timestamp: l(step.created_at, format: :full),
|
||||
user: h(step.user.full_name)) %></span>
|
||||
<span class="name-block"><strong><%= step.name %></strong></span>
|
||||
<% unless step.new_record? %>
|
||||
<span class="delimiter">|</span>
|
||||
<span class="author-block"><%= sanitize_input t('protocols.steps.published_on',
|
||||
timestamp: l(step.created_at, format: :full),
|
||||
user: h(step.user.full_name)) %></span>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="panel-collapse collapse" id="step-panel-<%= step.id %>" role="tabpanel">
|
||||
<div class="panel-collapse collapse" id="step-panel-<%= step.id || step.position %>" role="tabpanel">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
|
|
@ -94,7 +97,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<% unless step.tables.blank? then %>
|
||||
<% if step.tables.any? %>
|
||||
<div class="col-xs-12">
|
||||
<hr>
|
||||
<% step.tables.each do |table| %>
|
||||
|
|
@ -111,7 +114,12 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render partial: 'steps/attachments/list.html.erb', locals: { step: step, preview: preview } %>
|
||||
<% if import %>
|
||||
<%= render partial: 'steps/attachments/preview_list.html.erb',
|
||||
locals: { attachments: steps_assets[step.position]} %>
|
||||
<% else %>
|
||||
<%= render partial: 'steps/attachments/list.html.erb', locals: { step: step, preview: preview } %>
|
||||
<% end %>
|
||||
|
||||
<% unless step.checklists.blank? then %>
|
||||
<div class="col-xs-12">
|
||||
|
|
@ -145,10 +153,12 @@
|
|||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<hr>
|
||||
<%= render partial: 'steps/comments.html.erb', locals: { comments: step.last_comments,
|
||||
comments_count: step.step_comments.count,
|
||||
step: step } %>
|
||||
<% unless import %>
|
||||
<hr>
|
||||
<%= render partial: 'steps/comments.html.erb', locals: { comments: step.last_comments,
|
||||
comments_count: step.step_comments.count,
|
||||
step: step } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
9
app/views/steps/attachments/_new_attachment.html.erb
Normal file
9
app/views/steps/attachments/_new_attachment.html.erb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<div class="attachment-placeholder pull-left new">
|
||||
<div class="attachment-thumbnail no-shadow">
|
||||
<i class="fas fa-image"></i>
|
||||
</div>
|
||||
<div class="attachment-label"><%= truncate(file_name || file_url, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
|
||||
</div>
|
||||
<div class="spencer-bonnet-modif">
|
||||
</div>
|
||||
</div>
|
||||
23
app/views/steps/attachments/_preview_list.html.erb
Normal file
23
app/views/steps/attachments/_preview_list.html.erb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<div class="col-xs-12">
|
||||
<hr>
|
||||
</div>
|
||||
<div class="col-xs-12 attachments-actions">
|
||||
|
||||
<div class="title">
|
||||
<h4>
|
||||
<%= t('protocols.steps.files', count: attachments.size) %>
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div class="attachemnts-header pull-right">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 attachments">
|
||||
<% attachments.each do |a| %>
|
||||
<%= render partial: 'steps/attachments/new_attachment.html.erb',
|
||||
locals: { file_url: a[:url], file_name: a[:name] } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<hr>
|
||||
|
|
@ -56,6 +56,7 @@ Rails.application.config.assets.precompile +=
|
|||
%w(experiments/dropdown_actions.js)
|
||||
Rails.application.config.assets.precompile += %w(reports/new.js)
|
||||
Rails.application.config.assets.precompile += %w(protocols/index.js)
|
||||
Rails.application.config.assets.precompile += %w(protocols/external_protocols_tab.js)
|
||||
Rails.application.config.assets.precompile += %w(protocols/header.js)
|
||||
Rails.application.config.assets.precompile += %w(protocols/steps.js)
|
||||
Rails.application.config.assets.precompile += %w(protocols/edit.js)
|
||||
|
|
|
|||
|
|
@ -199,6 +199,44 @@ class Constants
|
|||
# Default user picture avatar
|
||||
DEFAULT_AVATAR_URL = '/images/:style/missing.png'.freeze
|
||||
|
||||
#=============================================================================
|
||||
# Protocol importers
|
||||
#=============================================================================
|
||||
|
||||
PROTOCOLS_ENDPOINTS = {
|
||||
protocolsio: {
|
||||
v3: 'ProtocolsIO::V3'
|
||||
}
|
||||
}.freeze
|
||||
|
||||
PROTOCOLS_IO_V3_API = {
|
||||
base_uri: 'https://www.protocols.io/api/v3/',
|
||||
default_timeout: 10,
|
||||
debug_level: :debug,
|
||||
sort_mappings: {
|
||||
alpha_asc: { order_field: :name, order_dir: :asc },
|
||||
alpha_desc: { order_field: :name, order_dir: :desc },
|
||||
newest: { order_field: :date, order_dir: :desc },
|
||||
oldest: { order_field: :date, order_dir: :asc }
|
||||
},
|
||||
endpoints: {
|
||||
protocols: {
|
||||
default_query_params: {
|
||||
filter: :public,
|
||||
key: '',
|
||||
order_field: :activity,
|
||||
order_dir: :desc,
|
||||
page_size: 10,
|
||||
page_id: 1,
|
||||
fields: 'id,title,authors,created_on,uri,stats'
|
||||
}
|
||||
}
|
||||
},
|
||||
source_id: 'protocolsio/v3'
|
||||
}.freeze
|
||||
|
||||
PROTOCOLS_DESC_TAGS = %w(a img i br).freeze
|
||||
|
||||
#=============================================================================
|
||||
# Other
|
||||
#=============================================================================
|
||||
|
|
|
|||
|
|
@ -1598,6 +1598,7 @@ en:
|
|||
name_label: "Protocol name"
|
||||
authors_label: "Authors"
|
||||
description_label: "Description"
|
||||
published_on_label: "Published on"
|
||||
created_at_label: "Created at"
|
||||
updated_at_label: "Last modified at"
|
||||
preview_title: "Protocol preview"
|
||||
|
|
@ -1606,6 +1607,8 @@ en:
|
|||
import_current: "Load Current"
|
||||
import_all: "Load All"
|
||||
import: "Load"
|
||||
import_to_team_protocols_label: "Import to Team Protocols"
|
||||
import_to_private_protocols_label: "Import to Private Protocols"
|
||||
export:
|
||||
export_results:
|
||||
title: "Export results"
|
||||
|
|
@ -1640,6 +1643,7 @@ en:
|
|||
navigation:
|
||||
public: "Team protocols"
|
||||
private: "My protocols"
|
||||
external_protocols: "External protocols"
|
||||
archive: "Archive"
|
||||
public_description: "Team protocols are visible and can be used by everyone from the team."
|
||||
private_description: "My protocols are only visible to you."
|
||||
|
|
@ -1740,6 +1744,22 @@ en:
|
|||
row_failed: "Failed"
|
||||
row_in_repository_public: "%{protocol} - <i>into Team protocols</i>"
|
||||
row_in_repository_private: "%{protocol} - <i>into My protocols</i>"
|
||||
external_protocols:
|
||||
search_bar_placeholder: 'Search for protocols'
|
||||
protocolsio_title: 'Protocols.io'
|
||||
sort_by:
|
||||
title: 'Show first:'
|
||||
alphabetically-asc: 'A-Z'
|
||||
alphabetically-desc: 'Z-A'
|
||||
newest: 'newest'
|
||||
oldest: 'oldest'
|
||||
list_panel:
|
||||
empty_text: 'Search for protocols above to list them here'
|
||||
preview_panel:
|
||||
empty_title: 'PROTOCOL PREVIEW'
|
||||
empty_subtext: 'Click on the protocol in the list to preview it here'
|
||||
banner_text: 'Protocol Preview'
|
||||
|
||||
steps:
|
||||
completed: 'Completed'
|
||||
uncompleted: 'Uncompleted'
|
||||
|
|
|
|||
49
config/locales/protocols/en.yml
Normal file
49
config/locales/protocols/en.yml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
en:
|
||||
protocol_importers:
|
||||
new:
|
||||
modal_title: 'Import protocol - %{protocol_name}'
|
||||
card:
|
||||
views_and_steps: '%{nr_of_views} views • %{nr_of_steps} steps'
|
||||
import_button_text: 'Import...'
|
||||
templates:
|
||||
amount:
|
||||
title: 'Amount'
|
||||
command:
|
||||
title: 'Command'
|
||||
name: 'Name'
|
||||
code: 'Command'
|
||||
concentration:
|
||||
title: 'Concentration'
|
||||
dataset:
|
||||
title: 'Dataset'
|
||||
name: 'Name'
|
||||
link: 'Link'
|
||||
details:
|
||||
title: 'Details'
|
||||
duration:
|
||||
title: 'Duration'
|
||||
unit: 'seconds'
|
||||
gotostep:
|
||||
title: 'Go to step'
|
||||
number: 'Step nr'
|
||||
reason: 'When'
|
||||
link:
|
||||
title: 'Link'
|
||||
note:
|
||||
title: 'Note'
|
||||
author: 'Author'
|
||||
reagent:
|
||||
title: 'Reagent'
|
||||
name: 'Name'
|
||||
mol_weight: 'Molecular weight'
|
||||
link: 'Link'
|
||||
result:
|
||||
title: 'Expected result'
|
||||
software:
|
||||
title: 'Software'
|
||||
name: 'Name'
|
||||
link: 'Link'
|
||||
temperature:
|
||||
title: 'Temperature'
|
||||
warning:
|
||||
title: 'Warning'
|
||||
|
|
@ -206,6 +206,13 @@ Rails.application.routes.draw do
|
|||
get 'atwho_my_modules', to: 'at_who#my_modules'
|
||||
get 'atwho_menu_items', to: 'at_who#menu_items'
|
||||
end
|
||||
|
||||
# External protocols routes
|
||||
get 'list_external_protocol', to: 'external_protocols#index'
|
||||
get 'show_external_protocol', to: 'external_protocols#show'
|
||||
get 'build_external_protocol', to: 'external_protocols#new'
|
||||
post 'import_external_protocol', to: 'external_protocols#create'
|
||||
|
||||
match '*path',
|
||||
to: 'teams#routing_error',
|
||||
via: [:get, :post, :put, :patch]
|
||||
|
|
|
|||
209
spec/controllers/external_protocols_controller_spec.rb
Normal file
209
spec/controllers/external_protocols_controller_spec.rb
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExternalProtocolsController, type: :controller do
|
||||
login_user
|
||||
|
||||
let(:user) { subject.current_user }
|
||||
let(:team) { create :team, created_by: user }
|
||||
let!(:user_team) { create :user_team, :admin, user: user, team: team }
|
||||
|
||||
describe 'GET index' do
|
||||
let(:params) do
|
||||
{
|
||||
team_id: team.id,
|
||||
key: 'search_string',
|
||||
protocol_source: 'protocolsio/v3',
|
||||
page_id: 1,
|
||||
page_size: 10,
|
||||
order_field: 'activity',
|
||||
order_dir: 'desc'
|
||||
}
|
||||
end
|
||||
|
||||
let(:action) { get :index, params: params }
|
||||
|
||||
before do
|
||||
service = double('success_service')
|
||||
allow(service).to(receive(:succeed?)).and_return(true)
|
||||
allow(service).to(receive(:protocols_list)).and_return({})
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::SearchProtocolsService).to(receive(:call)).and_return(service)
|
||||
end
|
||||
|
||||
it 'returns JSON, 200 response when protocol parsing was valid' do
|
||||
action
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
it 'contains html key in the response' do
|
||||
action
|
||||
expect(JSON.parse(response.body)).to have_key('html')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET show' do
|
||||
let(:params) do
|
||||
{
|
||||
team_id: team.id,
|
||||
protocol_source: 'protocolsio/v3',
|
||||
protocol_id: 'protocolsio_uri'
|
||||
}
|
||||
end
|
||||
|
||||
let(:action) { get :show, params: params }
|
||||
|
||||
it 'returns JSON, 200 response when preview was successfully returned' do
|
||||
html_preview = '<html></html>'
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:protocol_html_preview)).and_return(html_preview)
|
||||
|
||||
# Call action
|
||||
action
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
it 'should return html preview in the JSON' do
|
||||
html_preview = '<html></html>'
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:protocol_html_preview)).and_return(html_preview)
|
||||
|
||||
# Call action
|
||||
action
|
||||
expect(JSON.parse(response.body)['html']).to eq(html_preview)
|
||||
end
|
||||
|
||||
it 'returns error JSON and 400 response when something went wrong' do
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:protocol_html_preview)).and_raise(StandardError)
|
||||
|
||||
# Call action
|
||||
action
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(JSON.parse(response.body)).to have_key('errors')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET new' do
|
||||
let(:params) do
|
||||
{
|
||||
team_id: team.id,
|
||||
protocol_source: 'protocolsio/v3',
|
||||
page_id: 1,
|
||||
page_size: 10,
|
||||
order_field: 'activity',
|
||||
order_dir: 'desc'
|
||||
}
|
||||
end
|
||||
|
||||
let(:action) { get :new, params: params }
|
||||
|
||||
context 'successful response' do
|
||||
let(:protocol) { create :protocol }
|
||||
|
||||
before do
|
||||
service = double('success_service')
|
||||
allow(service).to(receive(:succeed?)).and_return(true)
|
||||
allow(service).to(receive(:built_protocol)).and_return(protocol)
|
||||
allow(service).to(receive(:serialized_steps)).and_return({}.to_s)
|
||||
allow(service).to(receive(:steps_assets)).and_return([])
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::BuildProtocolFromClientService)
|
||||
.to(receive(:call)).and_return(service)
|
||||
end
|
||||
|
||||
it 'returns JSON, 200 response when protocol parsing was valid' do
|
||||
action
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
it 'should return html form in the JSON' do
|
||||
action
|
||||
expect(JSON.parse(response.body)).to have_key('html')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns JSON, 400 response when protocol parsing was invalid' do
|
||||
# Setup double
|
||||
service = double('failed_service')
|
||||
allow(service).to(receive(:succeed?)).and_return(false)
|
||||
allow(service).to(receive(:errors)).and_return({})
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::BuildProtocolFromClientService)
|
||||
.to(receive(:call)).and_return(service)
|
||||
|
||||
# Call action
|
||||
action
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST create' do
|
||||
context 'when user has import permissions for the team' do
|
||||
let(:params) do
|
||||
{
|
||||
team_id: team.id,
|
||||
protocol: {
|
||||
name: 'name',
|
||||
steps: {}.to_s
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:action) { post :create, params: params }
|
||||
|
||||
it 'returns JSON, 200 response when protocol parsing was valid' do
|
||||
# Setup double
|
||||
service = double('success_service')
|
||||
allow(service).to(receive(:succeed?)).and_return(true)
|
||||
allow(service).to(receive(:protocol)).and_return(create(:protocol))
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ImportProtocolService).to(receive(:call)).and_return(service)
|
||||
|
||||
# Call action
|
||||
action
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
it 'returns JSON, 400 response when protocol parsing was invalid' do
|
||||
# Setup double
|
||||
service = double('success_service')
|
||||
allow(service).to(receive(:succeed?)).and_return(false)
|
||||
allow(service).to(receive(:errors)).and_return({})
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ImportProtocolService).to(receive(:call)).and_return(service)
|
||||
|
||||
# Call action
|
||||
action
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has no import permissions for the team' do
|
||||
let(:user_two) { create :user }
|
||||
let(:team_two) { create :team, created_by: user_two }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
team_id: team_two.id,
|
||||
protocol_params: {},
|
||||
steps_params: {}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns 403 when trying to import to forbidden team' do
|
||||
post :create, params: params
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
FactoryBot.define do
|
||||
factory :table do
|
||||
name { Faker::Name.unique.name }
|
||||
contents { { some_data: 'needs to be here' } }
|
||||
contents { { data: [%w(A B C), %w(D E F), %w(G H I)] } }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
5
spec/fixtures/files/protocol_importers/description_with_body.json
vendored
Normal file
5
spec/fixtures/files/protocol_importers/description_with_body.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"description": {
|
||||
"body": "original desc"
|
||||
}
|
||||
}
|
||||
5
spec/fixtures/files/protocol_importers/description_with_body_html.json
vendored
Normal file
5
spec/fixtures/files/protocol_importers/description_with_body_html.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"description": {
|
||||
"body": "<script>alert('boom')</script><div class='col-5'><h5>Text only</h5> <img src='nekaj.com'/> <i></i> <b></b> <a href='nekaj.com'>Link tukaj</a> WTF <div class='asd'></div>"
|
||||
}
|
||||
}
|
||||
19
spec/fixtures/files/protocol_importers/description_with_extra_content.json
vendored
Normal file
19
spec/fixtures/files/protocol_importers/description_with_extra_content.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"description": {
|
||||
"body": "original desc",
|
||||
"extra_content": [
|
||||
{
|
||||
"title": "First extra",
|
||||
"body": "Body of first extra."
|
||||
},
|
||||
{
|
||||
"title": "Second extra",
|
||||
"body": "Body of second extra."
|
||||
},
|
||||
{
|
||||
"title": "Third extra",
|
||||
"body": "Body of third extra."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
6
spec/fixtures/files/protocol_importers/description_with_image.json
vendored
Normal file
6
spec/fixtures/files/protocol_importers/description_with_image.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"description": {
|
||||
"body": "original desc",
|
||||
"image": "www.example.com/images/first.jpg"
|
||||
}
|
||||
}
|
||||
104
spec/fixtures/files/protocol_importers/normalized_list.json
vendored
Normal file
104
spec/fixtures/files/protocol_importers/normalized_list.json
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"protocols": [
|
||||
{
|
||||
"id": 22532,
|
||||
"title": "Producing rooted cassava plantlets for use in pot experiments",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1556012701,
|
||||
"authors": "Matema Imakumbili",
|
||||
"nr_of_steps": 13,
|
||||
"nr_of_views": 37,
|
||||
"uri": "producing-rooted-cassava-plantlets-for-use-in-pot-z9cf92w"
|
||||
},
|
||||
{
|
||||
"id": 832,
|
||||
"title": "Preparation of Virus DNA from Seawater for Metagenomics",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1434495014,
|
||||
"authors": "Matthew Sullivan Lab",
|
||||
"nr_of_steps": 11,
|
||||
"nr_of_views": 457,
|
||||
"uri": "Preparation-of-Virus-DNA-from-Seawater-for-Metagen-c28yhv"
|
||||
},
|
||||
{
|
||||
"id": 14506,
|
||||
"title": "MALE CIRCUMCISION FOR PREVENTION OF HETEROSEXUAL TRANSMISSION OF HIV IN ADULT MALES IN SOWETO, what do indicators and incidence rates show?untitled protocol",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1533553359,
|
||||
"authors": "Hillary Mukudu, Neil Martinson, Benn Sartorius",
|
||||
"nr_of_steps": 0,
|
||||
"nr_of_views": 5,
|
||||
"uri": "male-circumcision-for-prevention-of-heterosexual-t-seiebce"
|
||||
},
|
||||
{
|
||||
"id": 10927,
|
||||
"title": "physiological and biochemical parameters",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1521454618,
|
||||
"authors": "Amor Slama, Elhem Mallek-Maalej, Hatem Ben Mohamed, Thouraya Rhim, Leila Radhouane, Amor SLAMA",
|
||||
"nr_of_steps": 0,
|
||||
"nr_of_views": 34,
|
||||
"uri": "physiological-and-biochemical-parameters-nwpdfdn"
|
||||
},
|
||||
{
|
||||
"id": 822,
|
||||
"title": "Iron Chloride Precipitation of Viruses from Seawater",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1434402745,
|
||||
"authors": "Seth John, Bonnie Poulos, Christine Schirmer",
|
||||
"nr_of_steps": 17,
|
||||
"nr_of_views": 1805,
|
||||
"uri": "Iron-Chloride-Precipitation-of-Viruses-from-Seawat-c2wyfd"
|
||||
},
|
||||
{
|
||||
"id": 12115,
|
||||
"title": "Measuring specific leaf area and water content",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1526074093,
|
||||
"authors": "Etienne Laliberté",
|
||||
"nr_of_steps": 33,
|
||||
"nr_of_views": 259,
|
||||
"uri": "measuring-specific-leaf-area-and-water-content-p3tdqnn"
|
||||
},
|
||||
{
|
||||
"id": 1842,
|
||||
"title": "Purification of viral assemblages from seawater in CsCl gradients",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1445287995,
|
||||
"authors": "Janice E. Lawrence and Grieg F. Steward",
|
||||
"nr_of_steps": 23,
|
||||
"nr_of_views": 116,
|
||||
"uri": "Purification-of-viral-assemblages-from-seawater-in-d2s8ed"
|
||||
},
|
||||
{
|
||||
"id": 12540,
|
||||
"title": "An improved primer set and PCR amplification protocol with increased specificity and sensitivity targeting the Symbiodinium ITS2 region using the SymVar primer pair",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1527418313,
|
||||
"authors": "Benjamin Hume, Maren Ziegler, Christian Voolstra, Julie Poulain, Xavier Pochon, Sarah Romac, Emilie Boissin, Colomban de Vargas, Serge Planes, Patrick Wincker",
|
||||
"nr_of_steps": 1,
|
||||
"nr_of_views": 234,
|
||||
"uri": "an-improved-primer-set-and-pcr-amplification-proto-qg4dtyw"
|
||||
},
|
||||
{
|
||||
"id": 985,
|
||||
"title": "NATURAL SEAWATER-BASED PRO99 MEDIUM",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1435347035,
|
||||
"authors": "Chisholm Lab",
|
||||
"nr_of_steps": 10,
|
||||
"nr_of_views": 85,
|
||||
"uri": "NATURAL-SEAWATER-BASED-PRO99-MEDIUM-c7zzp5"
|
||||
},
|
||||
{
|
||||
"id": 1033,
|
||||
"title": "SN Maintenance Medium for Synechococcus",
|
||||
"source": "protocolsio/v3",
|
||||
"created_on": 1435778857,
|
||||
"authors": "JB Waterbury \u0026 JM Willey",
|
||||
"nr_of_steps": 6,
|
||||
"nr_of_views": 33,
|
||||
"uri": "SN-Maintenance-Medium-for-Synechococcus-c9hz35"
|
||||
}
|
||||
]
|
||||
}
|
||||
130
spec/fixtures/files/protocol_importers/normalized_single_protocol.json
vendored
Normal file
130
spec/fixtures/files/protocol_importers/normalized_single_protocol.json
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"protocol": {
|
||||
"uri": "https://www.protocols.io/api/v3/protocols/9451",
|
||||
"source": "protocolsio/v3",
|
||||
"doi": "dx.doi.org/10.17504/protocols.io.mgjc3un",
|
||||
"published_on": 1516132805,
|
||||
"version": 0,
|
||||
"source_id": 9451,
|
||||
"name": "CUT\u0026RUN: Targeted in situ genome-wide profiling with high efficiency for low cell numbers",
|
||||
"description": {
|
||||
"body": "\u003cdiv class = \"text-blocks\"\u003e\u003cdiv class = \"text-block\"\u003eCleavage Under Targets and Release Using Nuclease (CUT\u0026RUN) is an epigenomic profiling strategy in which antibody-targeted controlled cleavage by micrococcal nuclease releases specific protein-DNA complexes into the supernatant for paired-end DNA sequencing. As only the targeted fragments enter into solution, and the vast majority of DNA is left behind, CUT\u0026RUN has exceptionally low background levels. CUT\u0026RUN outperforms the most widely used Chromatin Immunoprecipitation (ChIP) protocols in resolution, signal-to-noise, and depth of sequencing required. In contrast to ChIP, CUT\u0026RUN is free of solubility and DNA accessibility artifacts and can be used to profile insoluble chromatin and to detect long-range 3D contacts without cross-linking. Here we present an improved CUT\u0026RUN protocol that does not require isolation of nuclei and provides high-quality data starting with only 100 cells for a histone modification and 1000 cells for a transcription factor. From cells to purified DNA CUT\u0026RUN requires less than a day at the lab bench.\u003c/div\u003e\u003cdiv class = \"text-block\"\u003eIn summary, CUT\u0026RUN has several advantages over ChIP-seq: (1) The method is performed in situ in non-crosslinked cells and does not require chromatin fragmentation or solubilization; (2) The intrinsically low background allows low sequence depth and identification of low signal genomic features invisible to ChIP; (3) The simple procedure can be completed within a day and is suitable for robotic automation; (4) The method can be used with low cell numbers compared to existing methodologies; (5) A simple spike-in strategy can be used for accurate quantitation of protein-DNA interactions. As such, CUT\u0026RUN represents an attractive replacement for ChIPseq, which is one of the most popular methods in biological research.\u003c/div\u003e\u003c/div\u003e",
|
||||
"image": "https://s3.amazonaws.com/pr-journal/rzxfw26.png",
|
||||
"extra_content": []
|
||||
},
|
||||
"authors": "Peter J. Skene, Steven Henikoff",
|
||||
"steps": [
|
||||
{
|
||||
"source_id": 601564,
|
||||
"name": "Binding cells to beads",
|
||||
"description": {
|
||||
"body": "\u003cdiv class = \"text-blocks\"\u003e\u003cdiv class = \"text-block\"\u003e\u003cspan style = \":justify;\"\u003eHarvest fresh culture(s) at room temperature and count cells. The same protocol can be used for 100 to 250,000 mammalian cells per sample and/or digestion time point.\u003c/span\u003e\u003c/div\u003e\u003c/div\u003e",
|
||||
"components": [
|
||||
{
|
||||
"type": "amount",
|
||||
"value": 123,
|
||||
"unit": "µl",
|
||||
"name": "of MGH"
|
||||
},
|
||||
{
|
||||
"type": "duration",
|
||||
"value": 11,
|
||||
"name": "boil"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"source": "www.google.com/protocols/123"
|
||||
},
|
||||
{
|
||||
"type": "software",
|
||||
"name": "RubyMine",
|
||||
"source": "www.rubymine.com",
|
||||
"details": {
|
||||
"repository_link": "www.github.com/rubymine/",
|
||||
"developer": "JozaHans",
|
||||
"os_name": "OSX and Linux."
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "dataset",
|
||||
"name": "WiFi passwords dictionary",
|
||||
"source": "www.passwords.com/wifi"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"software_name": "Terminal",
|
||||
"command": "curl",
|
||||
"details": {
|
||||
"os_name": "OSX and Linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "result",
|
||||
"body": "\u003cdiv class = \"text-blocks\"\u003e\u003cdiv class = \"text-block\"\u003eCentrifuge 3 min... This is expected result with HTML.\u003c/div\u003e\u003c/div\u003e"
|
||||
},
|
||||
{
|
||||
"type": "warning",
|
||||
"body": "\u003cdiv class = \"text-blocks\"\u003e\u003cdiv class = \"text-block\"\u003eCentrifuge 3 min... This is WARNING with HTML.\u003c/div\u003e\u003c/div\u003e",
|
||||
"details": {
|
||||
"link": "www.warnings.com/be_careful"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "reagent",
|
||||
"name": "2 mg Gastrin I, human",
|
||||
"details": {
|
||||
"catalog_number": "",
|
||||
"link": "https://www.biorbyt.com/gastrin-i-human-orb321073",
|
||||
"linear_formula": "C130H204N38O31S",
|
||||
"mol_weight": 0.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "gotostep",
|
||||
"value": "In case of someting, go to step",
|
||||
"step_id": 601565
|
||||
},
|
||||
{
|
||||
"type": "temperature",
|
||||
"value": 55,
|
||||
"unit": "°C",
|
||||
"name": "boil"
|
||||
},
|
||||
{
|
||||
"type": "concentration",
|
||||
"value": 12,
|
||||
"unit": "g/L",
|
||||
"name": "Name of concentration"
|
||||
},
|
||||
{
|
||||
"type": "note",
|
||||
"author": "Frank Jones",
|
||||
"body": "\u003cdiv class = \"text-blocks\"\u003e\u003cdiv class = \"text-block\"\u003eCentrifuge 3 min... This is expected result with HTML.\u003c/div\u003e\u003c/div\u003e"
|
||||
}
|
||||
]
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"url": "https://pbs.twimg.com/media/Cwu3zrZWQAA7axs.jpg",
|
||||
"name": "First file"
|
||||
},
|
||||
{
|
||||
"url": "http://something.com/wp-content/uploads/2014/11/14506718045_5b3e71dacd_o.jpg",
|
||||
"name": "Second file"
|
||||
}
|
||||
],
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"source_id": 601565,
|
||||
"name": "Binding cells to beads 2",
|
||||
"description": {
|
||||
"body": "\u003cdiv class = \"text-blocks\"\u003e\u003cdiv class = \"text-block\"\u003eCentrifuge 3 min 600 x g at room temperature and withdraw liquid.\u003c/div\u003e\u003c/div\u003e",
|
||||
"components": []
|
||||
},
|
||||
"attachments": [],
|
||||
"position": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
3072
spec/fixtures/files/protocol_importers/protocols_io/v3/protocols.json
vendored
Normal file
3072
spec/fixtures/files/protocol_importers/protocols_io/v3/protocols.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
1486
spec/fixtures/files/protocol_importers/protocols_io/v3/single_protocol.json
vendored
Normal file
1486
spec/fixtures/files/protocol_importers/protocols_io/v3/single_protocol.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
22
spec/fixtures/files/protocol_importers/step_description_with_components.json
vendored
Normal file
22
spec/fixtures/files/protocol_importers/step_description_with_components.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"description": {
|
||||
"body": "original desc",
|
||||
"components": [
|
||||
{
|
||||
"type": "note",
|
||||
"author": "First extra",
|
||||
"body": "Body of first extra."
|
||||
},
|
||||
{
|
||||
"type": "note",
|
||||
"author": "Second extra",
|
||||
"body": "<script>alert('boom')</script><div class='col-5'><h5>Text only</h5>"
|
||||
},
|
||||
{
|
||||
"type": "note",
|
||||
"author": "Third extra",
|
||||
"body": "Body of third extra."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
18
spec/fixtures/files/protocol_importers/step_with_attachments.json
vendored
Normal file
18
spec/fixtures/files/protocol_importers/step_with_attachments.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"source_id": 601564,
|
||||
"name": "Binding cells to beads",
|
||||
"position": 0,
|
||||
"description": {
|
||||
"body": "\u003cdiv class = \"text-blocks\"\u003e\u003cdiv class = \"text-block\"\u003eCentrifuge 3 min 600 x g at room temperature and withdraw liquid.\u003c/div\u003e\u003c/div\u003e"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"url": "https://pbs.twimg.com/media/Cwu3zrZWQAA7axs.jpg",
|
||||
"name": "First file"
|
||||
},
|
||||
{
|
||||
"url": "http://something.com/wp-content/uploads/2014/11/14506718045_5b3e71dacd_o.jpg",
|
||||
"name": "Second file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProtocolImporters::BuildProtocolFromClientService do
|
||||
let(:user) { create :user }
|
||||
let(:team) { create :team }
|
||||
let(:service_call) do
|
||||
ProtocolImporters::BuildProtocolFromClientService
|
||||
.call(protocol_client_id: 'id', protocol_source: 'protocolsio/v3', user_id: user.id, team_id: team.id)
|
||||
end
|
||||
let(:service_call_without_assets) do
|
||||
ProtocolImporters::BuildProtocolFromClientService
|
||||
.call(protocol_client_id: 'id',
|
||||
protocol_source: 'protocolsio/v3',
|
||||
user_id: user.id,
|
||||
team_id: team.id,
|
||||
build_with_assets: false)
|
||||
end
|
||||
let(:normalized_response) do
|
||||
JSON.parse(file_fixture('protocol_importers/normalized_single_protocol.json').read)
|
||||
.to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'when have invalid arguments' do
|
||||
it 'returns an error when can\'t find user' do
|
||||
allow(User).to receive(:find_by_id).and_return(nil)
|
||||
|
||||
expect(service_call.errors).to have_key(:invalid_arguments)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when raise api client error' do
|
||||
it 'return api errors' do
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:single_protocol)
|
||||
.and_raise(ProtocolImporters::ProtocolsIO::V3::ArgumentError
|
||||
.new(:missing_or_empty_parameters), 'Missing Or Empty Parameters Error'))
|
||||
|
||||
expect(service_call.errors).to have_key(:missing_or_empty_parameters)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when normalize protocol fails' do
|
||||
it 'return normalizer errors' do
|
||||
client_data = double('api_response')
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:single_protocol)
|
||||
.and_return(client_data))
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ProtocolNormalizer)
|
||||
.to(receive(:normalize_protocol).with(client_data)
|
||||
.and_raise(ProtocolImporters::ProtocolsIO::V3::NormalizerError.new(:nil_protocol), 'Nil Protocol'))
|
||||
|
||||
expect(service_call.errors).to have_key(:nil_protocol)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have valid arguments' do
|
||||
before do
|
||||
client_data = double('api_response')
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:single_protocol)
|
||||
.and_return(client_data))
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ProtocolNormalizer)
|
||||
.to(receive(:normalize_protocol).with(client_data)
|
||||
.and_return(normalized_response))
|
||||
|
||||
# Do not generate and request real images
|
||||
allow(ProtocolImporters::AttachmentsBuilder).to(receive(:generate).and_return([]))
|
||||
end
|
||||
|
||||
it 'returns ProtocolIntermediateObject' do
|
||||
expect(service_call.built_protocol).to be_instance_of(Protocol)
|
||||
end
|
||||
# more tests will be implemented when add error handling to service
|
||||
|
||||
describe 'serialized_steps' do
|
||||
context 'when build without assets' do
|
||||
it 'returns JSON with attachments' do
|
||||
expect(JSON.parse(service_call_without_assets.serialized_steps).first).to have_key('attachments')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProtocolImporters::ImportProtocolService do
|
||||
let(:user) { create :user }
|
||||
let(:team) { create :team }
|
||||
let(:protocol_params) { attributes_for :protocol, :in_public_repository }
|
||||
let(:steps_params) do
|
||||
[
|
||||
attributes_for(:step).merge!(attachments: [{ name: 'random.jpg', url: 'http://www.example.com/random.jpg' }]),
|
||||
attributes_for(:step).merge!(tables_attributes: [attributes_for(:table), attributes_for(:table)])
|
||||
].to_json
|
||||
end
|
||||
|
||||
let(:service_call) do
|
||||
ProtocolImporters::ImportProtocolService
|
||||
.call(protocol_params: protocol_params, steps_params_json: steps_params, user_id: user.id, team_id: team.id)
|
||||
end
|
||||
|
||||
context 'when have invalid arguments' do
|
||||
it 'returns an error when can\'t find user' do
|
||||
allow(User).to receive(:find_by_id).and_return(nil)
|
||||
|
||||
expect(service_call.errors).to have_key(:invalid_arguments)
|
||||
end
|
||||
|
||||
it 'returns invalid protocol when can\'t save it' do
|
||||
# step with file without name
|
||||
steps_invalid_params = [
|
||||
attributes_for(:step).except(:name).merge!(tables_attributes: [attributes_for(:table)])
|
||||
].to_json
|
||||
|
||||
s = ProtocolImporters::ImportProtocolService.call(protocol_params: protocol_params,
|
||||
steps_params_json: steps_invalid_params,
|
||||
user_id: user.id, team_id: team.id)
|
||||
expect(s.protocol).to be_invalid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have valid arguments' do
|
||||
before do
|
||||
stub_request(:get, 'http://www.example.com/random.jpg').to_return(status: 200, body: '', headers: {})
|
||||
|
||||
@protocol = Protocol.new
|
||||
end
|
||||
|
||||
# rubocop:disable MultilineMethodCallIndentation
|
||||
it do
|
||||
expect do
|
||||
service_result = service_call
|
||||
@protocol = service_result.protocol
|
||||
end.to change { Protocol.all.count }.by(1)
|
||||
.and change { @protocol.steps.count }.by(2)
|
||||
.and change { Table.joins(:step_table).where('step_tables.step_id': @protocol.steps.pluck(:id)).count }.by(2)
|
||||
.and change { Asset.joins(:step_asset).where('step_assets.step_id': @protocol.steps.pluck(:id)).count }.by(1)
|
||||
end
|
||||
# rubocop:enable MultilineMethodCallIndentation
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProtocolImporters::SearchProtocolsService do
|
||||
let(:service_call) do
|
||||
ProtocolImporters::SearchProtocolsService.call(protocol_source: 'protocolsio/v3',
|
||||
query_params: {
|
||||
key: 'someting',
|
||||
page_id: 5,
|
||||
order_field: 'title',
|
||||
order_dir: 'asc'
|
||||
})
|
||||
end
|
||||
|
||||
let(:service_call_with_wrong_params) do
|
||||
ProtocolImporters::SearchProtocolsService.call(protocol_source: 'protocolsio3',
|
||||
query_params: {
|
||||
key: '',
|
||||
page_id: -1,
|
||||
order_field: 'gender',
|
||||
order_dir: 'descc'
|
||||
})
|
||||
end
|
||||
let(:normalized_list) do
|
||||
JSON.parse(file_fixture('protocol_importers/normalized_list.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'when have invalid attributes' do
|
||||
it 'returns an error when params are invalid' do
|
||||
expect(service_call_with_wrong_params.errors).to have_key(:invalid_params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when raise api client error' do
|
||||
it 'return api errors' do
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:protocol_list)
|
||||
.and_raise(ProtocolImporters::ProtocolsIO::V3::ArgumentError
|
||||
.new(:missing_or_empty_parameters), 'Missing Or Empty Parameters Error'))
|
||||
|
||||
expect(service_call.errors).to have_key(:missing_or_empty_parameters)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when normalize protocol fails' do
|
||||
it 'return normalizer errors' do
|
||||
client_data = double('api_response')
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:protocol_list)
|
||||
.and_return(client_data))
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ProtocolNormalizer)
|
||||
.to(receive(:normalize_list).with(client_data)
|
||||
.and_raise(ProtocolImporters::ProtocolsIO::V3::NormalizerError.new(:nil_protocol), 'Nil Protocol'))
|
||||
|
||||
expect(service_call.errors).to have_key(:nil_protocol)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have valid attributes' do
|
||||
before do
|
||||
client_data = double('api_response')
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ApiClient)
|
||||
.to(receive(:protocol_list)
|
||||
.and_return(client_data))
|
||||
|
||||
allow_any_instance_of(ProtocolImporters::ProtocolsIO::V3::ProtocolNormalizer)
|
||||
.to(receive(:normalize_list).with(client_data)
|
||||
.and_return(normalized_list))
|
||||
end
|
||||
|
||||
it 'returns an error when params are invalid' do
|
||||
expect(service_call.protocols_list).to be == normalized_list
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProtocolImporters::AttachmentsBuilder do
|
||||
let(:step) do
|
||||
JSON.parse(file_fixture('protocol_importers/step_with_attachments.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
let(:generate_files_from_step) { described_class.generate(step) }
|
||||
let(:first_file_in_result) { generate_files_from_step.first }
|
||||
let(:generate_json_files_from_step) { described_class.generate_json(step) }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://pbs.twimg.com/media/Cwu3zrZWQAA7axs.jpg').to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:get, 'http://something.com/wp-content/uploads/2014/11/14506718045_5b3e71dacd_o.jpg')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
end
|
||||
|
||||
describe 'self.generate' do
|
||||
it 'returns array of Asset instances' do
|
||||
expect(first_file_in_result).to be_instance_of(Asset)
|
||||
end
|
||||
|
||||
it 'returns valid table' do
|
||||
expect(first_file_in_result).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe 'self.generate_json' do
|
||||
it 'returns JSON with 2 items (files)' do
|
||||
expect(generate_json_files_from_step.size).to be == 2
|
||||
end
|
||||
|
||||
it 'returns JSON item with name and url' do
|
||||
expect(generate_json_files_from_step.first.keys).to contain_exactly(:name, :url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProtocolImporters::ProtocolDescriptionBuilder do
|
||||
let(:description_only) do
|
||||
JSON.parse(file_fixture('protocol_importers/description_with_body.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
let(:description_with_image) do
|
||||
JSON.parse(file_fixture('protocol_importers/description_with_image.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
let(:description_with_extra_content) do
|
||||
JSON.parse(file_fixture('protocol_importers/description_with_extra_content.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
let(:description_with_html) do
|
||||
JSON.parse(file_fixture('protocol_importers/description_with_body_html.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
describe 'self.generate' do
|
||||
context 'when description field not exists' do
|
||||
it 'returns empty string' do
|
||||
expect(described_class.generate({})).to be == ''
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have only description' do
|
||||
it 'includes paragraph description' do
|
||||
expect(described_class.generate(description_only)).to include('<p> original desc </p>')
|
||||
end
|
||||
|
||||
it 'strips HTML tags' do
|
||||
expect(described_class.generate(description_with_html).scan('script').count).to be == 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when includes image' do
|
||||
it 'includes image tag' do
|
||||
expect(described_class.generate(description_with_image)).to include('<img src=')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have extra content' do
|
||||
it 'add extra fields as paragraphs' do
|
||||
expect(described_class.generate(description_with_extra_content).scan('<br/>').size).to be == 10
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProtocolImporters::ProtocolIntermediateObject do
|
||||
subject(:pio) { described_class.new(normalized_json: normalized_result, user: user, team: team) }
|
||||
let(:invalid_pio) { described_class.new(normalized_json: normalized_result, user: nil, team: team) }
|
||||
let(:pio_without_assets) do
|
||||
described_class.new(normalized_json: normalized_result, user: user, team: team, build_with_assets: false)
|
||||
end
|
||||
let(:user) { create :user }
|
||||
let(:team) { create :team }
|
||||
let(:normalized_result) do
|
||||
JSON.parse(file_fixture('protocol_importers/normalized_single_protocol.json').read)
|
||||
.to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://pbs.twimg.com/media/Cwu3zrZWQAA7axs.jpg').to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:get, 'http://something.com/wp-content/uploads/2014/11/14506718045_5b3e71dacd_o.jpg')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
end
|
||||
|
||||
describe '.build' do
|
||||
it { expect(subject.build).to be_instance_of(Protocol) }
|
||||
end
|
||||
|
||||
describe '.import' do
|
||||
context 'when have valid object' do
|
||||
it { expect { pio.import }.to change { Protocol.all.count }.by(1) }
|
||||
it { expect { pio.import }.to change { Step.all.count }.by(2) }
|
||||
it { expect { pio.import }.to change { Asset.all.count }.by(2) }
|
||||
it { expect(pio.import).to be_valid }
|
||||
end
|
||||
|
||||
context 'when have invalid object' do
|
||||
it { expect(invalid_pio.import).to be_invalid }
|
||||
it { expect { invalid_pio.import }.not_to(change { Protocol.all.count }) }
|
||||
end
|
||||
|
||||
context 'when build wihout assets' do
|
||||
it { expect { pio_without_assets.import }.to change { Protocol.all.count }.by(1) }
|
||||
it { expect { invalid_pio.import }.not_to(change { Asset.all.count }) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.steps_assets' do
|
||||
context 'when have default pio' do
|
||||
it 'retuns empty hash for steps_assets' do
|
||||
pio.build
|
||||
expect(pio.steps_assets).to be == {}
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have pio built without assets' do
|
||||
it 'returns hash with two assets on steps by position' do
|
||||
pio_without_assets.build
|
||||
expect(pio_without_assets.steps_assets.size).to be == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProtocolImporters::ProtocolNormalizer do
|
||||
describe '.normalize_list' do
|
||||
it { expect { subject.normalize_list({}) }.to raise_error(NotImplementedError) }
|
||||
end
|
||||
|
||||
describe '.normalize_protocol' do
|
||||
it { expect { subject.normalize_protocol({}) }.to raise_error(NotImplementedError) }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProtocolImporters::ProtocolsIO::V3::ApiClient do
|
||||
CONSTANTS = Constants::PROTOCOLS_IO_V3_API
|
||||
TOKEN = 'test_token'
|
||||
|
||||
describe '#protocol_list' do
|
||||
URL = "#{CONSTANTS[:base_uri]}protocols"
|
||||
|
||||
let(:stub_protocols) do
|
||||
stub_request(:get, URL).with(query: hash_including({}))
|
||||
.to_return(status: 200,
|
||||
body: JSON.generate(status_code: 0),
|
||||
headers: { 'Content-Type': 'application/json' })
|
||||
end
|
||||
|
||||
let(:default_query_params) do
|
||||
CONSTANTS.dig(:endpoints, :protocols, :default_query_params)
|
||||
end
|
||||
|
||||
it 'returns 200 on successfull call' do
|
||||
stub_protocols
|
||||
expect(subject.protocol_list.code).to eq 200
|
||||
expect(stub_protocols).to have_been_requested
|
||||
end
|
||||
|
||||
it 'raises NetworkError on timeout' do
|
||||
stub_request(:get, URL).with(query: hash_including({})).to_timeout
|
||||
|
||||
expect { subject.protocol_list }.to raise_error(ProtocolImporters::ProtocolsIO::V3::NetworkError)
|
||||
end
|
||||
|
||||
it 'raises ArgumentError when status_code = 1' do
|
||||
stub_request(:get, URL).with(query: hash_including({}))
|
||||
.to_return(status: 200,
|
||||
body: JSON.generate(status_code: 1, error_message: 'Argument error'),
|
||||
headers: { 'Content-Type': 'application/json' })
|
||||
|
||||
expect { subject.protocol_list }.to raise_error(ProtocolImporters::ProtocolsIO::V3::ArgumentError)
|
||||
end
|
||||
|
||||
it 'raises UnauthorizedError when status_code = 1218' do
|
||||
stub_request(:get, URL).with(query: hash_including({}))
|
||||
.to_return(status: 200,
|
||||
body: JSON.generate(status_code: 1218, error_message: 'Argument error'),
|
||||
headers: { 'Content-Type': 'application/json' })
|
||||
|
||||
expect { subject.protocol_list }.to raise_error(ProtocolImporters::ProtocolsIO::V3::UnauthorizedError)
|
||||
end
|
||||
|
||||
it 'raises UnauthorizedError when status_code = 1219' do
|
||||
stub_request(:get, URL).with(query: hash_including({}))
|
||||
.to_return(status: 200,
|
||||
body: JSON.generate(status_code: 1219, error_message: 'Argument error'),
|
||||
headers: { 'Content-Type': 'application/json' })
|
||||
|
||||
expect { subject.protocol_list }.to raise_error(ProtocolImporters::ProtocolsIO::V3::UnauthorizedError)
|
||||
end
|
||||
|
||||
it 'requests server with default query parameters if none are given' do
|
||||
stub_protocols.with(query: default_query_params)
|
||||
|
||||
subject.protocol_list
|
||||
expect(WebMock).to have_requested(:get, URL).with(query: default_query_params)
|
||||
end
|
||||
|
||||
it 'requests server with given query parameters' do
|
||||
query = {
|
||||
filter: :user_public,
|
||||
key: 'banana',
|
||||
order_dir: :asc,
|
||||
order_field: :date,
|
||||
page_id: 2,
|
||||
page_size: 15,
|
||||
fields: 'somefields'
|
||||
}
|
||||
stub_protocols.with(query: query)
|
||||
|
||||
subject.protocol_list(query)
|
||||
expect(WebMock).to have_requested(:get, URL).with(query: query)
|
||||
end
|
||||
|
||||
it 'should send authorization token if provided on initialization' do
|
||||
headers = { 'Authorization': "Bearer #{TOKEN}" }
|
||||
stub_request(:get, URL).with(headers: headers, query: default_query_params)
|
||||
.to_return(status: 200,
|
||||
body: JSON.generate(status_code: 0),
|
||||
headers: { 'Content-Type': 'application/json' })
|
||||
|
||||
ProtocolImporters::ProtocolsIO::V3::ApiClient.new(TOKEN).protocol_list
|
||||
expect(WebMock).to have_requested(:get, URL).with(headers: headers, query: default_query_params)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#single_protocol' do
|
||||
PROTOCOL_ID = 15
|
||||
SINGLE_PROTOCOL_URL = "#{CONSTANTS[:base_uri]}protocols/#{PROTOCOL_ID}"
|
||||
|
||||
let(:stub_single_protocol) do
|
||||
stub_request(:get, SINGLE_PROTOCOL_URL).to_return(
|
||||
status: 200,
|
||||
body: JSON.generate(status_code: 0),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns 200 on successfull call' do
|
||||
stub_single_protocol
|
||||
|
||||
expect(subject.single_protocol(PROTOCOL_ID).code).to eq 200
|
||||
expect(stub_single_protocol).to have_been_requested
|
||||
end
|
||||
|
||||
it 'raises NetworkError on timeout' do
|
||||
stub_request(:get, SINGLE_PROTOCOL_URL).to_timeout
|
||||
|
||||
expect { subject.single_protocol(PROTOCOL_ID) }.to raise_error(ProtocolImporters::ProtocolsIO::V3::NetworkError)
|
||||
end
|
||||
|
||||
it 'should send authorization token if provided on initialization' do
|
||||
headers = { 'Authorization': "Bearer #{TOKEN}" }
|
||||
stub_single_protocol.with(headers: headers)
|
||||
|
||||
ProtocolImporters::ProtocolsIO::V3::ApiClient.new(TOKEN).single_protocol(PROTOCOL_ID)
|
||||
expect(WebMock).to have_requested(:get, SINGLE_PROTOCOL_URL).with(headers: headers)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#protocol_html_preview' do
|
||||
PROTOCOL_URI = 'Extracting-DNA-from-bananas-esvbee6'
|
||||
|
||||
let(:stub_html_preview) do
|
||||
stub_request(:get, "https://www.protocols.io/view/#{PROTOCOL_URI}.html")
|
||||
end
|
||||
|
||||
it 'returns 200 on successfull call' do
|
||||
stub_html_preview.to_return(status: 200, body: '[]', headers: {})
|
||||
|
||||
expect(subject.protocol_html_preview(PROTOCOL_URI).code).to eq 200
|
||||
expect(stub_html_preview).to have_been_requested
|
||||
end
|
||||
|
||||
it 'raises NetworkErrorr on timeout' do
|
||||
stub_html_preview.to_timeout
|
||||
|
||||
expect { subject.protocol_html_preview(PROTOCOL_URI) }
|
||||
.to raise_error(ProtocolImporters::ProtocolsIO::V3::NetworkError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProtocolImporters::ProtocolsIO::V3::ProtocolNormalizer do
|
||||
let(:client_data) { double('api_response') }
|
||||
|
||||
let(:protocols_io_single_protocol) do
|
||||
JSON.parse(file_fixture('protocol_importers/protocols_io/v3/single_protocol.json').read)
|
||||
.to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:response_without_title) do
|
||||
res_without_title = protocols_io_single_protocol
|
||||
res_without_title[:protocol].reject! { |a| a == 'title' }
|
||||
res_without_title
|
||||
end
|
||||
|
||||
let(:protocols_io_list) do
|
||||
JSON.parse(file_fixture('protocol_importers/protocols_io/v3/protocols.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:normalized_protocol) do
|
||||
JSON.parse(file_fixture('protocol_importers/normalized_single_protocol.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:normalized_list) do
|
||||
JSON.parse(file_fixture('protocol_importers/normalized_list.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#normalize_protocol' do
|
||||
before do
|
||||
allow(client_data).to(receive_message_chain(:request, :last_uri, :to_s)
|
||||
.and_return('https://www.protocols.io/api/v3/protocols/9451'))
|
||||
end
|
||||
|
||||
context 'when have all data' do
|
||||
it 'should normalize data correctly' do
|
||||
allow(client_data).to receive_message_chain(:parsed_response)
|
||||
.and_return(protocols_io_single_protocol)
|
||||
|
||||
expect(subject.normalize_protocol(client_data).deep_stringify_keys).to be == normalized_protocol
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises NormalizerError if response is empty' do
|
||||
allow(client_data).to receive_message_chain(:parsed_response)
|
||||
.and_return({})
|
||||
|
||||
expect { subject.normalize_protocol(client_data) }
|
||||
.to raise_error(ProtocolImporters::ProtocolsIO::V3::NormalizerError)
|
||||
end
|
||||
|
||||
context 'when do not have name' do
|
||||
it 'sets nil for name' do
|
||||
allow(client_data).to receive_message_chain(:parsed_response)
|
||||
.and_return(response_without_title)
|
||||
|
||||
expect(subject.normalize_protocol(client_data)[:protocol][:name]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#normalize_list' do
|
||||
it 'should normalize data correctly' do
|
||||
allow(client_data).to receive_message_chain(:parsed_response).and_return(protocols_io_list)
|
||||
|
||||
expect(subject.normalize_list(client_data).deep_stringify_keys).to be == normalized_list
|
||||
end
|
||||
|
||||
it 'raises NormalizerError if response is empty' do
|
||||
allow(client_data).to receive_message_chain(:parsed_response)
|
||||
.and_return({})
|
||||
|
||||
expect { subject.normalize_list(client_data) }
|
||||
.to raise_error(ProtocolImporters::ProtocolsIO::V3::NormalizerError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProtocolImporters::StepDescriptionBuilder do
|
||||
let(:description_only) do
|
||||
JSON.parse(file_fixture('protocol_importers/description_with_body.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:description_with_html) do
|
||||
JSON.parse(file_fixture('protocol_importers/description_with_body_html.json').read).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:description_with_components) do
|
||||
JSON.parse(file_fixture('protocol_importers/step_description_with_components.json').read)
|
||||
.to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:description_with_extra_content) do
|
||||
JSON.parse(file_fixture('protocol_importers/description_with_extra_content.json').read)
|
||||
.to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:normalized_json) do
|
||||
JSON.parse(file_fixture('protocol_importers/normalized_single_protocol.json').read)
|
||||
.to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:step_description_from_normalized_json) { described_class.generate(normalized_json[:protocol][:steps].first) }
|
||||
|
||||
describe 'self.generate' do
|
||||
context 'when description field not exists' do
|
||||
it 'returns empty string' do
|
||||
expect(described_class.generate({})).to be == ''
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have only description body' do
|
||||
it 'includes paragraph description' do
|
||||
expect(described_class.generate(description_only)).to include('<p> original desc')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have components' do
|
||||
it 'retunrs extra content with title and body' do
|
||||
expect(step_description_from_normalized_json.scan('step-description-component-').size).to be == 13
|
||||
end
|
||||
|
||||
it 'strips HTML tags from body values for component' do
|
||||
expect(described_class.generate(description_with_components).scan('<script>').size).to be == 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have extra_fileds' do
|
||||
it 'add extra fields as paragraphs' do
|
||||
expect(described_class.generate(description_with_extra_content).scan('<br/>').size).to be == 6
|
||||
end
|
||||
|
||||
it 'strips HTML tags for values' do
|
||||
expect(described_class.generate(description_with_html).scan('script').count).to be == 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when have allowed html tags' do
|
||||
it 'does not strip img tags' do
|
||||
expect(described_class.generate(description_with_html).scan('img').size).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
51
spec/utilities/protocol_importers/tables_builder_spec.rb
Normal file
51
spec/utilities/protocol_importers/tables_builder_spec.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProtocolImporters::TablesBuilder do
|
||||
# rubocop:disable Metrics/LineLength
|
||||
let(:description_string) { '<table><tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td></tr><tr><td>a</td><td>b</td><td>c</td><td>d</td><td>e</td><td>f</td><td>g</td><td>h</td><td>a</td><td>a</td></tr><tr><td>1</td><td>1</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>1</td><td>1</td><td>1</td></tr><tr><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td></tr><tr><td>asd</td><td>as</td><td>das</td><td>a</td><td>as</td><td>asd</td><td>sad</td><td>sa</td><td>asd</td><td>as124521</td></tr></table><table><tr><td>1</td><td>2</td><td>3</td></tr></table>' }
|
||||
let(:description_string_with_headers) { '<table><tr><td></td><td>1</td><td>2</td><td>3</td></tr><tr><td>A</td><td>d1</td><td>d2</td><td>d3</td></tr><tr><td>B</td><td>c1</td><td>c2</td><td>c3</td></tr></table>' }
|
||||
# rubocop:enable Metrics/LineLength
|
||||
|
||||
let(:extract_tables_from_string_result) { described_class.extract_tables_from_html_string(description_string) }
|
||||
let(:first_table_in_result) { extract_tables_from_string_result.first }
|
||||
|
||||
describe '.extract_tables_from_string' do
|
||||
it 'returns array of Table instances' do
|
||||
expect(first_table_in_result).to be_instance_of(Table)
|
||||
end
|
||||
|
||||
it 'returns 2 tables ' do
|
||||
expect(extract_tables_from_string_result.count).to be == 2
|
||||
end
|
||||
|
||||
it 'returns valid table' do
|
||||
expect(first_table_in_result).to be_valid
|
||||
end
|
||||
|
||||
it 'returns table with 5 rows' do
|
||||
expect(JSON.parse(first_table_in_result.contents)['data'].count).to be == 5
|
||||
end
|
||||
|
||||
it 'returns table with 10 columns' do
|
||||
expect(JSON.parse(first_table_in_result.contents)['data'].first.count).to be == 10
|
||||
end
|
||||
|
||||
context 'when droping headers' do
|
||||
it 'returns table with 2 rows and 3 columns' do
|
||||
table = described_class.extract_tables_from_html_string(description_string_with_headers, true).first
|
||||
|
||||
expect(JSON.parse(table.contents)['data'].count).to be == 2
|
||||
expect(JSON.parse(table.contents)['data'].first.count).to be == 3
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.remove_tables_from_html' do
|
||||
it 'returns description string without tables' do
|
||||
expect(described_class.remove_tables_from_html(description_string)
|
||||
.scan('<p><i>There was a table here, it was moved to tables section.</i></p>').size).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue