Features/protocolsio integration (#1893)

Features/protocolsio integration
This commit is contained in:
Jure Grabnar 2019-07-03 17:32:20 +02:00 committed by GitHub
commit 20134c9f8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 7693 additions and 45 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View 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();

View file

@ -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;
}
}

View file

@ -14,4 +14,8 @@
min-height: initial;
}
}
.general-error {
text-align: center;
}
}

View 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

View file

@ -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?

View file

@ -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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View 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>

View file

@ -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 %>

View file

@ -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>

View 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>

View 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 } %>

View 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 } %>

View file

@ -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 } %>

View 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 } %>

View 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>

View file

@ -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 } %>

View file

@ -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 } %>

View 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 } %>

View 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 } %>

View 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 }
)
%>

View 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 } %>

View file

@ -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 } %>

View file

@ -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 } %>

View 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 } %>

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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">&nbsp;<%= 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>

View 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" %>

View 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>

View file

@ -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>

View 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>

View 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>

View file

@ -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)

View file

@ -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
#=============================================================================

View file

@ -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'

View 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'

View file

@ -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]

View 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

View file

@ -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

View file

@ -0,0 +1,5 @@
{
"description": {
"body": "original desc"
}
}

View 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>"
}
}

View 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."
}
]
}
}

View file

@ -0,0 +1,6 @@
{
"description": {
"body": "original desc",
"image": "www.example.com/images/first.jpg"
}
}

View 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"
}
]
}

View 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
}
]
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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."
}
]
}
}

View 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"
}
]
}

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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