Add BioEddie integration [SCI-5654][SCI-5657][SCI-5658][SCI-5670] (#3391)

* Add backend for bioeddie [SCI-5654]

* Add modal for bioeddie [SCI-5654]

* Add bio eddie editor to steps and results [SCI-5654]

* Fix markup and code styling
This commit is contained in:
aignatov-bio 2021-06-23 19:48:44 +02:00 committed by GitHub
parent b7513c6d5f
commit 786e74e4de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 515 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,152 @@
/* global HelperModule I18n */
var bioEddieEditor = (function() {
var BIO_EDDIE;
var CHEMAXON;
var bioEddieIframe;
var bioEddieModal;
function importMolecule() {
var monomerModel = BIO_EDDIE.getMonomerModel();
var monomerImporter = new CHEMAXON.HelmImportModule();
var molecule = bioEddieModal.data('molecule') || '';
monomerImporter.import(molecule, monomerModel)
.then(builder => BIO_EDDIE.setModel(builder.graphStoreData));
}
function loadBioEddie() {
BIO_EDDIE = bioEddieIframe.contentWindow.bioEddieEditor;
CHEMAXON = bioEddieIframe.contentWindow.chemaxon;
if (typeof BIO_EDDIE === 'undefined' || typeof CHEMAXON === 'undefined') {
setTimeout(function() {
loadBioEddie();
}, 2000);
} else {
importMolecule();
}
}
function initIframe() {
if (typeof BIO_EDDIE === 'undefined' || typeof CHEMAXON === 'undefined') {
bioEddieIframe.src = bioEddieIframe.dataset.src;
loadBioEddie();
} else {
importMolecule();
}
}
function saveMolecule(svg, structure) {
var moleculeName = bioEddieModal.find('.file-name input').val();
$.post(bioEddieModal.data('create-url'), {
description: structure,
object_id: bioEddieModal.data('object_id'),
object_type: bioEddieModal.data('object_type'),
name: moleculeName,
image: svg
}, function(result) {
var newAsset = $(result.html);
if (bioEddieModal.data('object_type') === 'Step') {
newAsset.find('.file-preview-link').css('top', '-300px');
newAsset.addClass('new').prependTo($(bioEddieModal.data('assets_container')));
setTimeout(function() {
newAsset.find('.file-preview-link').css('top', '0px');
}, 200);
bioEddieModal.modal('hide');
} else if (bioEddieModal.data('object_type') === 'Result') {
window.location.reload();
}
});
}
function updateMolecule(svg, structure) {
var moleculeName = bioEddieModal.find('.file-name input').val();
$.ajax({
url: bioEddieModal.data('update-url'),
data: {
description: structure,
name: moleculeName,
image: svg
},
dataType: 'json',
type: 'PUT',
success: function(json) {
$('#modal_link' + json.id + ' img').attr('src', json.url);
$('#modal_link' + json.id + ' .attachment-label').html(json.file_name);
bioEddieModal.modal('hide');
},
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
}
});
}
function generateImage(structure) {
var imageGenerator = new CHEMAXON.ImageGenerator();
var emptySVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
imageGenerator.generateSVGFromHelm(emptySVG, structure)
.then(svg => {
if (bioEddieModal.data('edit-mode')) {
updateMolecule(svg, structure);
} else {
saveMolecule(svg, structure);
}
});
}
$(document).on('turbolinks:load', function() {
bioEddieIframe = document.getElementById('bioEddieIframe');
bioEddieModal = $('#bioEddieModal');
bioEddieModal.on('shown.bs.modal', function() {
initIframe();
});
bioEddieModal.on('click', '.file-save-link', function() {
var model = BIO_EDDIE.getModel();
var monomerModel = BIO_EDDIE.getMonomerModel();
var monomerExporter = new CHEMAXON.Helm2ExportModule();
monomerExporter.export(model, monomerModel)
.then(structure => generateImage(structure));
});
});
return {
open_new: (objectId, objectType, container) => {
bioEddieModal.data('object_id', objectId);
bioEddieModal.data('object_type', objectType);
bioEddieModal.data('assets_container', container);
bioEddieModal.find('.file-name input').val('');
bioEddieModal.modal('show');
},
open_edit: (name, molecule, updateUrl) => {
bioEddieModal.data('edit-mode', true);
bioEddieModal.data('molecule', molecule);
bioEddieModal.data('update-url', updateUrl);
bioEddieModal.find('.file-name input').val(name);
bioEddieModal.modal('show');
}
};
}());
(function() {
$(document).on('click', '.new-bio-eddie-upload-button', function() {
bioEddieEditor.open_new(
this.dataset.objectId,
this.dataset.objectType,
this.dataset.assetsContainer
);
});
$(document).on('click', '.bio-eddie-edit-button', function() {
$('#filePreviewModal').modal('hide');
bioEddieEditor.open_edit(
this.dataset.moleculeName,
this.dataset.moleculeDescription,
this.dataset.updateUrl
);
});
}());

View file

@ -0,0 +1,75 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
// scss-lint:disable SelectorFormat
// scss-lint:disable ImportantRule
// scss-lint:disable IdSelector
@import "constants";
@import "mixins";
// MarvinJs modal
.modal-bio-eddie {
background: transparent;
font-size: $font-size-base;
padding: 0 !important;
.modal-dialog {
height: 100%;
margin: 0;
padding: 0;
width: auto;
}
.modal-content {
background: transparent;
border: 0;
box-shadow: none;
height: 100%;
width: auto;
}
.modal-header {
background: $color-white;
display: flex;
height: 60px;
line-height: 40px;
padding: 10px 15px;
text-align: center;
.file-save-link {
flex-shrink: 0;
margin: 0 20px 0 0;
}
.file-name {
align-items: center;
display: flex;
flex-shrink: 0;
float: left;
margin-right: auto;
input {
border-radius: 5px;
box-shadow: none;
color: $color-black;
height: 40px;
margin-left: 5px;
outline: 0;
padding: 5px 10px;
position: relative;
width: 350px;
}
}
}
.modal-body {
height: calc(100% - 60px);
padding: 0;
iframe {
height: 100%;
position: relative;
width: 100%;
}
}
}

View file

@ -61,6 +61,34 @@
}
}
&.bio_eddie {
&::before,
&::after {
border-radius: 1em 0 0 1em;
bottom: 1em;
content: "";
display: block;
height: 2em;
line-height: 2em;
position: absolute;
right: -1em;
width: 2.25em;
}
&::before {
background: $bio-eddie-color;
}
&::after {
background-image: url("/images/icon_small/bio_eddie_white.png");
background-repeat: no-repeat;
height: 1.85em;
right: -1.15em;
width: 2em;
}
}
.fas {
font-size: 100px;
line-height: 160px;

View file

@ -41,6 +41,9 @@ $office-ms-powerpoint: #d24726;
// MarvinJS color:
$marvinjs-color: #29999c;
// BioEddie color:
$bio-eddie-color: #ffa000;
$pdf-color: #f40f02;
// Don't use them

View file

@ -0,0 +1,99 @@
# frozen_string_literal: true
class BioEddieAssetsController < ApplicationController
include ActiveStorage::SetCurrent
before_action :load_vars, except: :create
before_action :load_create_vars, only: :create
before_action :check_read_permission
before_action :check_edit_permission, only: %i(update create start_editing)
def create
asset = BioEddieService.create_molecule(bio_eddie_params, current_user, current_team)
if asset && bio_eddie_params[:object_type] == 'Step'
render json: {
html: render_to_string(partial: 'assets/asset.html.erb', locals: {
asset: asset,
gallery_view_id: bio_eddie_params[:object_id]
})
}
elsif asset && bio_eddie_params[:object_type] == 'Result'
render json: { status: 'created' }, status: :ok
else
render json: asset.errors, status: :unprocessable_entity
end
end
def update
asset = BioEddieService.update_molecule(bio_eddie_params, current_user, current_team)
if asset
render json: { url: rails_representation_url(asset.medium_preview),
id: asset.id,
file_name: asset.blob.metadata['name'] }
else
render json: { error: t('bio_eddie.no_molecules_found') }, status: :unprocessable_entity
end
end
def start_editing
# Activity here
end
private
def load_vars
@asset = current_team.assets.find_by(id: params[:id])
return render_404 unless @asset
@assoc = @asset.step || @asset.result
case @assoc
when Step
@protocol = @assoc.protocol
when Result
@my_module = @assoc.my_module
end
end
def load_create_vars
case bio_eddie_params[:object_type]
when 'Step'
@assoc = Step.find_by(id: bio_eddie_params[:object_id])
@protocol = @assoc.protocol
when 'Result'
@assoc = MyModule.find_by(id: bio_eddie_params[:object_id])
@my_module = @assoc
end
end
def check_read_permission
case @assoc
when Step
return render_403 unless can_read_protocol_in_module?(@protocol) ||
can_read_protocol_in_repository?(@protocol)
when Result, MyModule
return render_403 unless can_read_experiment?(@my_module.experiment)
else
render_403
end
end
def check_edit_permission
case @assoc
when Step
return render_403 unless can_manage_protocol_in_module?(@protocol) ||
can_manage_protocol_in_repository?(@protocol)
when Result, MyModule
return render_403 unless can_manage_module?(@my_module)
else
render_403
end
end
def bio_eddie_params
params.permit(:id, :description, :object_id, :object_type, :name, :image)
end
end

View file

@ -0,0 +1,78 @@
# frozen_string_literal: true
class BioEddieService
class << self
def url
ApplicationSettings.instance.values['bio_eddie_url']
end
def enabled?
url.present?
end
def create_molecule(params, current_user, current_team)
file = image_io(params)
asset = Asset.new(created_by: current_user,
last_modified_by: current_user,
team_id: current_team.id)
attach_file(asset.file, file, params)
asset.save!
asset.post_process_file(current_team)
connect_asset(asset, params, current_user)
end
def update_molecule(params, _current_user, current_team)
asset = current_team.assets.find(params[:id])
attachment = asset&.file
return unless attachment
file = image_io(params)
attach_file(attachment, file, params)
asset
end
private
def connect_asset(asset, params, current_user)
case params[:object_type]
when 'Step'
object = params[:object_type].constantize.find(params[:object_id])
asset.update!(view_mode: object.assets_view_mode)
object.assets << asset
when 'Result'
my_module = MyModule.find_by(id: params[:object_id])
return unless my_module
Result.create(user: current_user,
my_module: my_module,
name: prepare_name(params[:name]),
asset: asset,
last_modified_by: current_user)
end
asset
end
def image_io(params)
StringIO.new(params[:image])
end
def attach_file(attachment, file, params)
attachment.attach(
io: file,
filename: "#{prepare_name(params[:name])}.svg",
content_type: 'image/svg+xml',
metadata: {
name: prepare_name(params[:name]),
description: params[:description],
asset_type: 'bio_eddie'
}
)
end
def prepare_name(sketch_name)
sketch_name.presence || I18n.t('bio_eddie.new_molecule')
end
end
end

View file

@ -0,0 +1,11 @@
<li>
<a
class="new-bio-eddie-upload-button"
data-object-id="<%= element_id %>"
data-object-type="<%= element_type %>"
data-assets-container="<%= assets_container %>"
>
<%= image_tag 'icon_small/bio_eddie.png' %>
<%= t('bio_eddie.new_button') %>
</a>
</li>

View file

@ -29,6 +29,8 @@
</li>
<%= render partial: '/assets/marvinjs/create_marvin_sketch_li',
locals: { element_id: @my_module.id, element_type: 'Result', sketch_container: "#results[data-module-id=#{@my_module.id}]" } %>
<%= render partial: '/assets/bio_eddie/create_bio_eddie_li.html.erb',
locals: { element_id: @my_module.id, element_type: 'Result', assets_container:"#results[data-module-id=#{@my_module.id}]" } %>
<%= render partial: "assets/wopi/create_wopi_file_li",
locals: { element_id: @my_module.id, element_type: 'Result' } %>
</ul>
@ -69,6 +71,9 @@
</div>
</div>
<%= render partial: "shared/bio_eddie_modal.html.erb" %>
<%= javascript_include_tag "handsontable.full" %>
<!-- Libraries for formulas -->

View file

@ -34,3 +34,5 @@
<%= javascript_include_tag "assets/wopi/create_wopi_file" %>
<%= javascript_include_tag "protocols/steps" %>
<%= render partial: "shared/bio_eddie_modal.html.erb" %>

View file

@ -0,0 +1,33 @@
<% if BioEddieService.enabled? %>
<div id="bioEddieModal"
class="modal modal-bio-eddie"
role="dialog"
aria-labelledby="bioEddieModal"
aria-hidden="true"
data-create-url="<%= bio_eddie_assets_path %>"
data-backdrop="static"
data-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<span class="file-name">
<div class="sci-input-container ">
<%= text_field_tag :molecule_name, '', placeholder: t('bio_eddie.molecule_name_placeholder'), class: 'sci-input-field' %>
</div>
</span>
<button class="file-save-link btn btn-light">
<i class="fas fa-save"></i>
<%= t('SaveClose')%>
</button>
<button class="preview-close btn btn-light icon-btn" data-dismiss="modal">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<iframe id="bioEddieIframe" data-src="<%= BioEddieService.url %>/index.html" frameBorder="0" ></iframe>
</div>
</div>
</div>
</div>
<% end %>

View file

@ -21,6 +21,16 @@
<span class="fas fa-pencil-alt"></span>
<%= t('assets.file_preview.edit_in_marvinjs') %>
</button>
<% elsif asset.file.metadata[:asset_type] == 'bio_eddie' %>
<button class="btn btn-light bio-eddie-edit-button"
data-molecule-id="<%= asset.id %>"
data-update-url="<%= bio_eddie_asset_path(asset) %>"
data-molecule-name="<%= asset.file.metadata[:name] %>"
data-molecule-description="<%= asset.file.metadata[:description] %>"
>
<span class="fas fa-pencil-alt"></span>
<%= t('assets.file_preview.edit_in_bio_eddie') %>
</button>
<% elsif asset.editable_image? %>
<button class="btn btn-light image-edit-button"
data-image-id="<%= asset.id %>"

View file

@ -24,10 +24,11 @@
data-step-id="<%= step.id %>"
data-state-save-url="<%= update_view_state_step_path(step.id) %>">
<li class="divider-label"><%= t("protocols.steps.attachments.add") %></li>
<li>
<%= render partial: '/assets/marvinjs/create_marvin_sketch_li.html.erb',
locals: { element_id: step.id, element_type: 'Step', sketch_container: ".attachments[data-step-id=#{step.id}]" } %>
</li>
<%= render partial: '/assets/marvinjs/create_marvin_sketch_li.html.erb',
locals: { element_id: step.id, element_type: 'Step', sketch_container: ".attachments[data-step-id=#{step.id}]" } %>
<%= render partial: '/assets/bio_eddie/create_bio_eddie_li.html.erb',
locals: { element_id: step.id, element_type: 'Step', assets_container: ".attachments[data-step-id=#{step.id}]" } %>
<li>
<%= render partial: '/assets/wopi/create_wopi_file_li.html.erb',
locals: { element_id: step.id, element_type: 'Step' } %>

View file

@ -2396,6 +2396,7 @@ en:
file_preview:
edit_in_scinote: "Edit in SciNote"
edit_in_marvinjs: "Edit in Marvin JS"
edit_in_bio_eddie: "Edit in BioEddie"
context_menu:
set_view_size: "SET VIEW SIZE"
delete: "Delete file"
@ -2635,6 +2636,12 @@ en:
invite_users:
permission_error: "You don't have permission to invite additional users to this team. Contact its administrator/s."
bio_eddie:
new_molecule: "New Molecule"
new_button: "Create Biomolecule"
molecule_name_placeholder: "Click here to enter Molecule name"
no_molecules_found: "No Molecules Found"
marvinjs:
new_sketch: "New Chemical Drawing"
new_button: "Create Chemical Drawing"
@ -2642,6 +2649,7 @@ en:
structure_placeholder: "Click here to enter Chemical Drawing name"
modal_name_title: "Chemical Drawing name:"
checmical_drawing: "Chemical drawings"
no_sketches_found: "No Sketches Found"
pdf_preview:
fit_to_screen: 'Fit to screen'

View file

@ -773,6 +773,12 @@ Rails.application.routes.draw do
end
end
resources :bio_eddie_assets, only: %i(create update) do
member do
post :start_editing
end
end
post 'global_activities', to: 'global_activities#index'
constraints WopiSubdomain do

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB