From 786e74e4ded6c91209dfd88762e14e6fd85501e5 Mon Sep 17 00:00:00 2001 From: aignatov-bio <47317017+aignatov-bio@users.noreply.github.com> Date: Wed, 23 Jun 2021 19:48:44 +0200 Subject: [PATCH] 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 --- app/assets/images/icon_small/bio_eddie.png | Bin 0 -> 986 bytes .../images/icon_small/bio_eddie_white.png | Bin 0 -> 1291 bytes app/assets/javascripts/sitewide/bio_eddie.js | 152 ++++++++++++++++++ app/assets/stylesheets/bio_eddie.scss | 75 +++++++++ app/assets/stylesheets/shared/assets.scss | 28 ++++ .../shared_styles/constants/colors.scss | 3 + .../bio_eddie_assets_controller.rb | 99 ++++++++++++ app/services/bio_eddie_service.rb | 78 +++++++++ .../bio_eddie/_create_bio_eddie_li.html.erb | 11 ++ app/views/my_modules/results.html.erb | 5 + app/views/protocols/_steps.html.erb | 2 + app/views/shared/_bio_eddie_modal.html.erb | 33 ++++ .../shared/file_preview/_content.html.erb | 10 ++ app/views/steps/attachments/_list.html.erb | 9 +- config/locales/en.yml | 8 + config/routes.rb | 6 + public/images/icon_small/bio_eddie_white.png | Bin 0 -> 1291 bytes 17 files changed, 515 insertions(+), 4 deletions(-) create mode 100644 app/assets/images/icon_small/bio_eddie.png create mode 100644 app/assets/images/icon_small/bio_eddie_white.png create mode 100644 app/assets/javascripts/sitewide/bio_eddie.js create mode 100644 app/assets/stylesheets/bio_eddie.scss create mode 100644 app/controllers/bio_eddie_assets_controller.rb create mode 100644 app/services/bio_eddie_service.rb create mode 100644 app/views/assets/bio_eddie/_create_bio_eddie_li.html.erb create mode 100644 app/views/shared/_bio_eddie_modal.html.erb create mode 100644 public/images/icon_small/bio_eddie_white.png diff --git a/app/assets/images/icon_small/bio_eddie.png b/app/assets/images/icon_small/bio_eddie.png new file mode 100644 index 0000000000000000000000000000000000000000..6e418c23431197ac120b7d9b8663e4146e9086f7 GIT binary patch literal 986 zcmV<0110>4P)vBk6lFZ=C%&w0){&w0*I<|3P#n#RP$Bs>-ou@k|; zLHM67FWbp?0ydi+QBk{~P;3>iP4dCPpIBU6!h;8oF*`fU>oCxPSl7CXmt5G34hL zVR(2HiHQd=I5>pcw{=KJH~`6NT|{y56BrDI6nqzwBtvua7j$)5HUSAgg+hVj$1^cM zzkr)Jv+?ZN3rtQiTU$FNSb-E3 z6^XR8RQUP%`2el2ufu43%Y2NCjQV^tnVQhk(~I%(3F38*VkJo1w<}Rlkc*O%he%C5 z{9yLSg#Z9Kj2M{I12yES2ni3j=V?d`|Qm#@&?-U)}p zNh4NswbRox>?h7iNe59?^#KC|Kai7i10f+HkTy<6aH7*)M|gM`4j)cIYwK6Ew6vnO zwvM@!$z(Wt_6!;uo8j;8&!ZQ%3CL|O4idi}8yg#^cP^f!!B7glUI(Re8&~geIQ{_% zhq<{qwnT}j8_f{(^ z2?%f}5f$n6d5DbM#WE-@HJIdrd9k!9Dal@ty1GxOuKq~+Izpn$Kv`Kiwf!6W_Qm1W zt!&~(iBqRe;OW!nIDh^e0s{jf>+ZI=jK+7IMXS~DikQvaw4Av-;)aGM3eKYJk+fH3 z)czVLP^na;pS`?V85!v&DKs>cK`vdoz_)-<`svdrNwG7mrGx}6S6E;Fnf9!JDX6HZ zBALYUH9{cVg-qN>9UW#;cQFyU1^f4F_|6f%=jG`UAFn}qxsfNMR4TBvWMiM`=x72t z>Y0g&fy7B{x7cE#AEDl`urRvZRNm=XEZc*S0aHN{YiPJgn_DerTdUSqw$@r|Xw9_Gt=25tDx0}!ZMCNT z!8Vr}5!4_{4P~Tg)Wi^xPeBpvfZ#dr-Ss^0;e6O3`_sL>=i_~z=f3Wb>%Jc4`z%Wk zlL+%%Yk#%lx@N@!ac+J`0V4yz)ysh;pN&;6vv^ZJ)co~O$=Sh!zERjey#Ta+JFaha z<7}wBwLp3?0bIavALMKmq<#zF9dhuIkO3>d407l-NZ(0DanU&)6$3T%VZap^*?W8% zRLRG{lzSZKU56p3_CRH>fK~i~^UeThOP&R_hyeRbVOKojq^cuKyOU2*YsT257GZ#256=b(K;8`C}KP z;eGQ;M*3mhwH2Zx_j&-94=ok`479~1!@6%TRPk1pCpH-Aj9}tT|5;unp*I3_pv(7TC3Ha`zG9 zNMr|YvSr?CK5L<}RvH2XQ*=QUYyeX7Ar~5<7QV!4Ob*c11@AC$_B%*h6&Xl=)Kw?(#y$1DkK+S%Nv$PH>`%%cz&5)kDYXCxk&~*rQ^(r=OTL_4P zB1@1s4JtN?x%-K5)x&{&(V_a>XUVMHkl%NO^k6*GrHdw8Je`mq%fhosU*fEdK_|~- z8Kcegs+9QzkN*z<+Ag;O6H=fSYzXg(KOBAG)#c}nQ1|ZzCS`_X;Ld@yoQ<(8Ht7q~WQdLHne;asag<}*sAaDM z&3l||StjT;13>Mt7EqSsk{sb`crPSsMtsA~4i&}gyweguCSj;2puBRboV`dTO`nId zy^!8tITMwB0E!$aT!tkhm%>V-mb?q4hmXEiBl~%BXF_6X`kaN|r3Rvr+ zcUX>8{;!3)g|gmN$2;Y?2}(|rAVd8G(CLp-|Me6nWW% z5=a#?(%dIZBjrL9tYx2)Az6@L-T= 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 + ); + }); +}()); diff --git a/app/assets/stylesheets/bio_eddie.scss b/app/assets/stylesheets/bio_eddie.scss new file mode 100644 index 000000000..5d25131a6 --- /dev/null +++ b/app/assets/stylesheets/bio_eddie.scss @@ -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%; + } + } +} diff --git a/app/assets/stylesheets/shared/assets.scss b/app/assets/stylesheets/shared/assets.scss index bb9c955c8..2979e3f86 100644 --- a/app/assets/stylesheets/shared/assets.scss +++ b/app/assets/stylesheets/shared/assets.scss @@ -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; diff --git a/app/assets/stylesheets/shared_styles/constants/colors.scss b/app/assets/stylesheets/shared_styles/constants/colors.scss index 3a4012f98..49aebe0ac 100644 --- a/app/assets/stylesheets/shared_styles/constants/colors.scss +++ b/app/assets/stylesheets/shared_styles/constants/colors.scss @@ -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 diff --git a/app/controllers/bio_eddie_assets_controller.rb b/app/controllers/bio_eddie_assets_controller.rb new file mode 100644 index 000000000..d282031c0 --- /dev/null +++ b/app/controllers/bio_eddie_assets_controller.rb @@ -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 diff --git a/app/services/bio_eddie_service.rb b/app/services/bio_eddie_service.rb new file mode 100644 index 000000000..5f1de71de --- /dev/null +++ b/app/services/bio_eddie_service.rb @@ -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 diff --git a/app/views/assets/bio_eddie/_create_bio_eddie_li.html.erb b/app/views/assets/bio_eddie/_create_bio_eddie_li.html.erb new file mode 100644 index 000000000..9cf91bd64 --- /dev/null +++ b/app/views/assets/bio_eddie/_create_bio_eddie_li.html.erb @@ -0,0 +1,11 @@ +
  • + + <%= image_tag 'icon_small/bio_eddie.png' %> + <%= t('bio_eddie.new_button') %> + +
  • diff --git a/app/views/my_modules/results.html.erb b/app/views/my_modules/results.html.erb index 6475dc4cf..6abb42b9c 100644 --- a/app/views/my_modules/results.html.erb +++ b/app/views/my_modules/results.html.erb @@ -29,6 +29,8 @@ <%= 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' } %> @@ -69,6 +71,9 @@ +<%= render partial: "shared/bio_eddie_modal.html.erb" %> + + <%= javascript_include_tag "handsontable.full" %> diff --git a/app/views/protocols/_steps.html.erb b/app/views/protocols/_steps.html.erb index d954e6827..84ba148c7 100644 --- a/app/views/protocols/_steps.html.erb +++ b/app/views/protocols/_steps.html.erb @@ -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" %> diff --git a/app/views/shared/_bio_eddie_modal.html.erb b/app/views/shared/_bio_eddie_modal.html.erb new file mode 100644 index 000000000..4ab9d12e6 --- /dev/null +++ b/app/views/shared/_bio_eddie_modal.html.erb @@ -0,0 +1,33 @@ +<% if BioEddieService.enabled? %> + +<% end %> + diff --git a/app/views/shared/file_preview/_content.html.erb b/app/views/shared/file_preview/_content.html.erb index 933e448ae..dfd3acb5f 100644 --- a/app/views/shared/file_preview/_content.html.erb +++ b/app/views/shared/file_preview/_content.html.erb @@ -21,6 +21,16 @@ <%= t('assets.file_preview.edit_in_marvinjs') %> + <% elsif asset.file.metadata[:asset_type] == 'bio_eddie' %> + <% elsif asset.editable_image? %>