diff --git a/Gemfile b/Gemfile index d2605325a..85ecc1e71 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,7 @@ gem 'commit_param_routing' # Enables different submit actions in the same form t gem 'kaminari' gem "i18n-js", ">= 3.0.0.rc11" # Localization in javascript files gem 'roo', '~> 2.7.1' # Spreadsheet parser +gem 'creek' gem 'wicked_pdf' gem 'silencer' # Silence certain Rails logs gem 'wkhtmltopdf-heroku' @@ -68,6 +69,7 @@ gem 'activerecord-import' gem 'paperclip', '~> 5.1' # File attachment, image attachment library gem 'aws-sdk', '~> 2' +gem 'aws-sdk-v1' gem 'delayed_job_active_record' gem 'devise-async', diff --git a/Gemfile.lock b/Gemfile.lock index de910acd1..dcd6bd846 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,7 +73,7 @@ GEM activemodel (= 5.1.1) activesupport (= 5.1.1) arel (~> 8.0) - activerecord-import (0.19.1) + activerecord-import (0.20.2) activerecord (>= 3.2) activesupport (5.1.1) concurrent-ruby (~> 1.0, >= 1.0.2) @@ -87,32 +87,36 @@ GEM arel (8.0.0) aspector (0.14.0) ast (2.3.0) - auto_strip_attributes (2.1.0) + auto_strip_attributes (2.2.0) activerecord (>= 3.0) - autoprefixer-rails (7.1.2.4) + autoprefixer-rails (7.1.6) execjs autosize-rails (1.18.17) rails (>= 3.1) awesome_print (1.8.0) - aws-sdk (2.10.21) - aws-sdk-resources (= 2.10.21) - aws-sdk-core (2.10.21) + aws-sdk (2.10.69) + aws-sdk-resources (= 2.10.69) + aws-sdk-core (2.10.69) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.10.21) - aws-sdk-core (= 2.10.21) - aws-sigv4 (1.0.1) + aws-sdk-resources (2.10.69) + aws-sdk-core (= 2.10.69) + aws-sdk-v1 (1.67.0) + json (~> 1.4) + nokogiri (~> 1) + aws-sigv4 (1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) + backports (3.10.3) base62 (1.0.0) bcrypt (3.1.11) - better_errors (2.3.0) + better_errors (2.4.0) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) - binding_of_caller (0.7.2) + binding_of_caller (0.7.3) debug_inspector (>= 0.0.1) bootstrap-sass (3.3.7) autoprefixer-rails (>= 5.2.1) @@ -125,7 +129,7 @@ GEM bullet (5.6.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) - byebug (9.0.6) + byebug (9.1.0) capybara (2.13.0) addressable mime-types (>= 1.16) @@ -141,7 +145,7 @@ GEM climate_control (0.2.0) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - coderay (1.1.1) + coderay (1.1.2) coffee-rails (4.2.2) coffee-script (>= 2.2.0) railties (>= 4.0.0) @@ -152,22 +156,31 @@ GEM commit_param_routing (0.0.1) concurrent-ruby (1.0.5) crass (1.0.2) - cucumber (2.4.0) + creek (2.0) + httparty (~> 0.15.5) + nokogiri (~> 1.7.0) + rubyzip (>= 1.0.0) + cucumber (3.0.1) builder (>= 2.1.2) - cucumber-core (~> 1.5.0) + cucumber-core (~> 3.0.0) + cucumber-expressions (~> 4.0.3) cucumber-wire (~> 0.0.1) - diff-lcs (>= 1.1.3) + diff-lcs (~> 1.3) gherkin (~> 4.0) multi_json (>= 1.7.5, < 2.0) multi_test (>= 0.1.2) - cucumber-core (1.5.0) - gherkin (~> 4.0) + cucumber-core (3.0.0) + backports (>= 3.8.0) + cucumber-tag_expressions (>= 1.0.1) + gherkin (>= 4.1.3) + cucumber-expressions (4.0.4) cucumber-rails (1.5.0) capybara (>= 1.1.2, < 3) cucumber (>= 1.3.8, < 4) mime-types (>= 1.17, < 4) nokogiri (~> 1.5) railties (>= 4, < 5.2) + cucumber-tag_expressions (1.0.1) cucumber-wire (0.0.1) database_cleaner (1.6.1) debug_inspector (0.0.3) @@ -192,9 +205,9 @@ GEM devise (>= 4.0.0) diff-lcs (1.3) docile (1.1.5) - erubi (1.6.1) + erubi (1.7.0) execjs (2.7.0) - factory_girl (4.8.0) + factory_girl (4.8.1) activesupport (>= 3.0.0) factory_girl_rails (4.8.0) factory_girl (~> 4.8.0) @@ -209,9 +222,12 @@ GEM gherkin (4.1.3) globalid (0.4.0) activesupport (>= 4.2.0) - hammerjs-rails (2.0.4) + hammerjs-rails (2.0.8) headless (2.3.1) - i18n (0.8.6) + httparty (0.15.6) + multi_xml (>= 0.5.2) + i18n (0.9.0) + concurrent-ruby (~> 1.0) i18n-js (3.0.1) i18n (~> 0.6, >= 0.6.6) introjs-rails (1.0.0) @@ -233,18 +249,18 @@ GEM js_cookie_rails (2.1.4) railties (>= 3.1) json (1.8.6) - kaminari (1.0.1) + kaminari (1.1.1) activesupport (>= 4.1.0) - kaminari-actionview (= 1.0.1) - kaminari-activerecord (= 1.0.1) - kaminari-core (= 1.0.1) - kaminari-actionview (1.0.1) + kaminari-actionview (= 1.1.1) + kaminari-activerecord (= 1.1.1) + kaminari-core (= 1.1.1) + kaminari-actionview (1.1.1) actionview - kaminari-core (= 1.0.1) - kaminari-activerecord (1.0.1) + kaminari-core (= 1.1.1) + kaminari-activerecord (1.1.1) activerecord - kaminari-core (= 1.0.1) - kaminari-core (1.0.1) + kaminari-core (= 1.1.1) + kaminari-core (1.1.1) lazy_priority_queue (0.1.1) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) @@ -254,27 +270,29 @@ GEM logging (2.0.0) little-plugger (~> 1.1) multi_json (~> 1.10) - loofah (2.0.3) + loofah (2.1.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.6.6) mime-types (>= 1.16, < 4) - method_source (0.8.2) + method_source (0.9.0) mime-types (1.25.1) mimemagic (0.3.2) - mini_portile2 (2.3.0) + mini_portile2 (2.1.0) minitest (5.10.3) momentjs-rails (2.17.1) railties (>= 3.1) - multi_json (1.12.1) + multi_json (1.12.2) multi_test (0.1.2) - nested_form_fields (0.8.1) + multi_xml (0.6.0) + nested_form_fields (0.8.2) coffee-rails (>= 3.2.1) jquery-rails rails (>= 3.2.0) - newrelic_rpm (4.3.0.335) + newrelic_rpm (4.5.0.337) nio4r (2.1.0) - nokogiri (1.8.1) - mini_portile2 (~> 2.3.0) + nokogiri (1.7.2) + mini_portile2 (~> 2.1.0) nokogumbo (1.4.13) nokogiri oj (2.18.5) @@ -291,17 +309,16 @@ GEM pg (0.21.0) polyglot (0.3.5) powerpack (0.1.1) - pry (0.10.4) + pry (0.11.2) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) + method_source (~> 0.9.0) + pry-byebug (3.5.0) + byebug (~> 9.1) pry (~> 0.10) pry-rails (0.3.6) pry (>= 0.10.4) public_suffix (3.0.0) - puma (3.9.1) + puma (3.10.0) rack (2.0.3) rack-test (0.6.3) rack (>= 1.0) @@ -341,12 +358,12 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - rake (12.0.0) + rake (12.1.0) rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) rdoc (4.3.0) - recaptcha (4.3.1) + recaptcha (4.6.2) json remotipart (1.3.1) responders (2.4.0) @@ -359,32 +376,32 @@ GEM roo (2.7.1) nokogiri (~> 1) rubyzip (~> 1.1, < 2.0.0) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec-core (3.7.0) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-mocks (3.6.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-rails (3.6.0) + rspec-support (~> 3.7.0) + rspec-rails (3.7.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) - rspec-support (3.6.0) - rubocop (0.49.1) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) + rubocop (0.51.0) parallel (~> 1.10) parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rainbow (>= 2.2.2, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-graphviz (1.2.3) - ruby-progressbar (1.8.1) + ruby-progressbar (1.9.0) ruby_dep (1.5.0) rubyzip (1.2.1) sanitize (4.5.0) @@ -398,7 +415,7 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - scss_lint (0.54.0) + scss_lint (0.55.0) rake (>= 0.9, < 13) sass (~> 3.4.20) sdoc (0.4.2) @@ -414,19 +431,18 @@ GEM actionmailer (>= 3.2.6, < 6) actionpack (>= 3.2.6, < 6) devise (>= 3.2, < 6) - simplecov (0.14.1) + simplecov (0.15.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) - simplecov-html (0.10.1) - slop (3.6.0) + simplecov-html (0.10.2) sourcemap (0.1.1) spinjs-rails (1.4) rails (>= 3.1) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.0) + sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -438,10 +454,10 @@ GEM ruby-progressbar (~> 1.8) sourcemap (~> 0.1) stream (0.5) - thor (0.19.4) + thor (0.20.0) thread_safe (0.3.6) tilt (2.0.8) - tinymce-rails (4.6.5) + tinymce-rails (4.6.7) railties (>= 3.1.1) turbolinks (5.0.1) turbolinks-source (~> 5) @@ -481,6 +497,7 @@ DEPENDENCIES autosize-rails awesome_print aws-sdk (~> 2) + aws-sdk-v1 base62 bcrypt (~> 3.1.10) better_errors @@ -494,6 +511,7 @@ DEPENDENCIES capybara capybara-webkit (~> 1.14) commit_param_routing + creek cucumber-rails (~> 1.5) database_cleaner deface (~> 1.0) diff --git a/VERSION b/VERSION index 81f363239..e0a6b34fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.3 +1.12.5 diff --git a/app/assets/javascripts/my_modules/results.js.erb b/app/assets/javascripts/my_modules/results.js.erb index 079815e61..b56218e41 100644 --- a/app/assets/javascripts/my_modules/results.js.erb +++ b/app/assets/javascripts/my_modules/results.js.erb @@ -166,8 +166,9 @@ var $nameInput = $form.find('#result_name'); var nameValid = textValidator(ev, $nameInput, 0, <%= Constants::NAME_MAX_LENGTH %>); - var $textInput = TinyMCE.getContent(); - textValidator(ev, $textInput, 1, <%= Constants::TEXT_MAX_LENGTH %>, false, true); + var $descrTextarea = $form.find("#result_result_text_attributes_text"); + var $tinyMCEInput = TinyMCE.getContent(); + textValidator(ev, $descrTextarea, 1, <%= Constants::TEXT_MAX_LENGTH %>, false, $tinyMCEInput); break; case ResultTypeEnum.COMMENT: var $commentInput = $form.find('#comment_message'); diff --git a/app/assets/javascripts/projects/canvas.js.erb b/app/assets/javascripts/projects/canvas.js.erb index 65375ca2a..bd070ad7a 100644 --- a/app/assets/javascripts/projects/canvas.js.erb +++ b/app/assets/javascripts/projects/canvas.js.erb @@ -163,7 +163,6 @@ function initializeEdit() { // Read permissions from the data attributes of the form var canEditModules = _.isEqual($("#update-canvas").data("can-edit-modules"), "yes"); - var canEditModuleGroups = _.isEqual($("#update-canvas").data("can-edit-module-groups"), "yes"); var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes"); var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes"); var canMoveModules = _.isEqual($("#update-canvas").data("can-move-modules"), "yes"); @@ -211,11 +210,6 @@ function initializeEdit() { $(".edit-module").on("click touchstart", editModuleHandler); } - if (canEditModuleGroups) { - initEditModuleGroups(); - $(".edit-module-group").on("click touchstart", editModuleGroupHandler); - } - if (canCloneModules) { bindCloneModuleAction( $(".module-options a.clone-module"), @@ -1220,22 +1214,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) { // Add click handler for the edit module $(editModuleLink).on("click touchstart", editModuleHandler); } - if (_.isEqual($("#update-canvas").data("can-edit-module-groups"), "yes")) { - var editModuleGroupItem = document.createElement("li"); - $(editModuleGroupItem).appendTo(dropdownMenu); - $(editModuleGroupItem).hide(); - - var editModuleGroupLink = document.createElement("a"); - $(editModuleGroupLink) - .attr("href", "") - .attr("data-module-id", id) - .addClass("edit-module-group") - .html($("#edit-group-link-placeholder").text().trim()) - .appendTo(editModuleGroupItem); - - // Add click handler for the edit module group - $(editModuleGroupLink).on("click touchstart", editModuleGroupHandler); - } // Add clone links if neccesary if (_.isEqual($("#update-canvas").data("can-clone-modules"), "yes")) { @@ -1636,95 +1614,11 @@ editModuleHandler = function(ev) { /** * Initialize editing of module groups. */ -function initEditModuleGroups() { - function handleRenameConfirm(modal, ev) { - var input = modal.find("#edit-module-group-name-input"); - // Validate module group name - var moduleNameValid = textValidator(ev, input, 1, - <%= Constants::NAME_MAX_LENGTH %>, true); - if (moduleNameValid) { - var newModuleGroupName = input.val(); - var moduleId = modal.attr("data-module-id"); - var moduleEl = $("#" + moduleId); - // Update the module group name for all modules - // currently in the module group - var ids = connectedComponents(graph, moduleEl.attr("data-module-id")); - _.each(ids, function(id) { - $("#" + id).attr("data-module-group-name", newModuleGroupName); - }); - - // Hide modal - modal.modal("hide"); - } - } - - $("#modal-edit-module-group") - .on("show.bs.modal", function (event) { - var modal = $(this); - var moduleId = modal.attr("data-module-id"); - var moduleEl = $("#" + moduleId); - var input = modal.find("#edit-module-group-name-input"); - - // Set the input to the current module's name - input - .attr("value", moduleEl.attr("data-module-group-name")); - input.val(moduleEl.attr("data-module-group-name")); - - // Bind on enter button - input.keydown(function(ev) { - if (ev.keyCode == 13) { - // "Submit" modal - handleRenameConfirm(modal, ev); - - // In any case, prevent form submission - ev.preventDefault(); - ev.stopPropagation(); - return false; - } - }); - }) - .on("shown.bs.modal", function (event) { - $(this).find("#edit-module-group-name-input").focus(); - }) - .on("hide.bs.modal", function (event) { - // Remove potential error classes - $(this).find("#edit-module-group-name-input").parent().removeClass("has-error"); - $(this).find("span.help-block").remove(); - - $(this).find("#edit-module-group-name-input").off("keydown"); - - // When hiding modal, re-enable events - toggleCanvasEvents(true); - }); - - // Bind the confirm button on modal - $("#modal-edit-module-group").find("button[data-action='confirm']").on("click", function(ev) { - var modal = $(this).closest(".modal"); - handleRenameConfirm(modal, ev); - }); -} /** * Handler when editing a module group. */ -editModuleGroupHandler = function(ev) { - var modal = $("#modal-edit-module-group"); - var moduleEl = $(this).closest(".module"); - - // Set modal's module id - modal.attr("data-module-id", moduleEl.attr("data-module-id")); - - // Disable dragging & zooming events on canvas temporarily - toggleCanvasEvents(false); - - // Show modal - modal.modal("show"); - - ev.preventDefault(); - ev.stopPropagation(); - return false; -}; function initMoveModules() { function handleMoveConfirm(modal) { @@ -1978,7 +1872,6 @@ function deleteModule(id, linkConnections) { _.each (ins, function(inEdge) { if (graph.degree(inEdge[0]) === 0) { tempModuleEl = $("#" + inEdge[0]); - tempModuleEl.find(".edit-module-group").parents("li").hide(); tempModuleEl.find(".clone-module-group").parents("li").hide(); tempModuleEl.find(".move-module-group").parents("li").hide(); tempModuleEl.find(".delete-module-group").parents("li").hide(); @@ -1989,7 +1882,6 @@ function deleteModule(id, linkConnections) { _.each (outs, function(outEdge) { if (graph.degree(outEdge[1]) === 0) { tempModuleEl = $("#" + outEdge[1]); - tempModuleEl.find(".edit-module-group").parents("li").hide(); tempModuleEl.find(".clone-module-group").parents("li").hide(); tempModuleEl.find(".move-module-group").parents("li").hide(); tempModuleEl.find(".delete-module-group").parents("li").hide(); @@ -2361,7 +2253,6 @@ cloneModuleGroupHandler = function(moduleId, modulesSel, gridDistX, gridDistY) { var nm = cloneModule(m, gridDistX, gridDistY, elLeft(m), elTop(m) + height + offset - gridDistY); //Show module group options - nm.find(".edit-module-group").parents("li").show(); nm.find(".clone-module-group").parents("li").show(); nm.find(".move-module-group").parents("li").show(); nm.find(".delete-module-group").parents("li").show(); @@ -2963,12 +2854,10 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) { //Modules should belong to module group now //Show module group options for target and source - srcModuleEl.find(".edit-module-group").parents("li").show(); srcModuleEl.find(".clone-module-group").parents("li").show(); srcModuleEl.find(".move-module-group").parents("li").show(); srcModuleEl.find(".delete-module-group").parents("li").show(); - targetModuleEl.find(".edit-module-group").parents("li").show(); targetModuleEl.find(".clone-module-group").parents("li").show(); targetModuleEl.find(".move-module-group").parents("li").show(); targetModuleEl.find(".delete-module-group").parents("li").show(); @@ -3001,13 +2890,11 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) { var targetModuleEl = $("#" + c.targetId); //First source if (graph.degree(c.sourceId) === 0) { - srcModuleEl.find(".edit-module-group").parents("li").hide(); srcModuleEl.find(".clone-module-group").parents("li").hide(); srcModuleEl.find(".move-module-group").parents("li").hide(); srcModuleEl.find(".delete-module-group").parents("li").hide(); } if (graph.degree(c.targetId) === 0) { - targetModuleEl.find(".edit-module-group").parents("li").hide(); targetModuleEl.find(".clone-module-group").parents("li").hide(); targetModuleEl.find(".move-module-group").parents("li").hide(); targetModuleEl.find(".delete-module-group").parents("li").hide(); diff --git a/app/assets/javascripts/protocols/steps.js.erb b/app/assets/javascripts/protocols/steps.js.erb index 3458fecb2..93ca6d6b2 100644 --- a/app/assets/javascripts/protocols/steps.js.erb +++ b/app/assets/javascripts/protocols/steps.js.erb @@ -540,8 +540,9 @@ var nameValid = textValidator(ev, $nameInput, 1, <%= Constants::NAME_MAX_LENGTH %>); var $descrTextarea = $form.find("#step_description"); + var $tinyMCEInput = TinyMCE.getContent(); var descriptionValid = textValidator(ev, $descrTextarea, 0, - <%= Constants::TEXT_MAX_LENGTH %>); + <%= Constants::TEXT_MAX_LENGTH %>, false, $tinyMCEInput); if (DragNDropSteps.filesStatus() && checklistsValid && diff --git a/app/assets/javascripts/sitewide/drag_n_drop.js.erb b/app/assets/javascripts/sitewide/drag_n_drop.js.erb index 0f1c62068..f585c3aaa 100644 --- a/app/assets/javascripts/sitewide/drag_n_drop.js.erb +++ b/app/assets/javascripts/sitewide/drag_n_drop.js.erb @@ -21,12 +21,13 @@ // loops through a list of files and display each file in a separate panel function listItems() { - droppedFiles = droppedFiles.filter(Boolean); + totalSize = 0; + _enableSubmitButton(); $('.panel-step-attachment-new').remove(); _dragNdropAssetsOff(); for(var i = 0; i < droppedFiles.length; i++) { $('#new-step-assets') - .append(_uploadedAseetPreview(droppedFiles[i], i)) + .append(_uploadedAssetPreview(droppedFiles[i], i)) .promise() .done(function() { _removeItemHandler(i); @@ -42,7 +43,6 @@ var prevEls = $('input').filter(function() { return this.name.match(regex); }); - droppedFiles = droppedFiles.filter(Boolean); var fd = new FormData($(ev.target).closest('form').get(0)); for(var i = 0; i < droppedFiles.length; i++) { var index = i + prevEls.length; @@ -56,30 +56,55 @@ return fd; } - function _validateFilesSize(file) { - var maxSize = file.size/1048576; - if(maxSize > <%= Constants::FILE_MAX_SIZE_MB %> && filesValid) { - return "

<%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %>

"; + function _disableSubmitButton() { + $('.step-save').prop('disabled', true); + } + + function _enableSubmitButton() { + $('.step-save').prop('disabled', false); + } + + function _filerAndCheckFiles() { + for(var i = 0; i < droppedFiles.length; i++) { + if(droppedFiles[i].isValid == false) { + return false; + } + } + return (droppedFiles.length > 0); + } + + function _validateFilesSize(file) { + var fileSize = file.size; + totalSize += parseInt(fileSize); + if(fileSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) { + file.isValid = false; + _disableSubmitButton(); + return "

<%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %>

"; } - totalSize += parseInt(maxSize); return ''; } function _validateTotalSize() { - if(totalSize > <%= Constants::FILE_MAX_SIZE_MB %>) { + if(totalSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) { filesValid = false; + _disableSubmitButton(); $.each($('.panel-step-attachment-new'), function() { - $(this) - .find('.panel-body') - .append("

<%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %>

"); + if(!$(this).find('p').hasClass('dnd-total-error')) { + $(this) + .find('.panel-body') + .append("

<%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %>

"); + } }); } else { - $('.dnd-error').remove(); - filesValid = true; + $('.dnd-total-error').remove(); + if(_filerAndCheckFiles()) { + filesValid = true; + _enableSubmitButton(); + } } } - function _uploadedAseetPreview(asset, i) { + function _uploadedAssetPreview(asset, i) { var html = '
'; html += '
'; html += ''; @@ -104,10 +129,9 @@ e.stopPropagation(); var $el = $(this); var index = $el.data('item-id'); - totalSize -= parseInt(droppedFiles[index]/1048576); - droppedFiles[index] = null; - $el.closest('.panel-step-attachment-new').remove(); - _validateTotalSize(); + totalSize -= parseInt(droppedFiles[index].size); + droppedFiles.splice(index, 1); + listItems(); }); } @@ -131,11 +155,10 @@ var totalSize = 0; function init(files) { - var filesPresent = droppedFiles.length; for(var i = 0; i < files.length; i++) { droppedFiles.push(files[i]); } - listItems(filesPresent); + listItems(); } // return the status of files if they are ready to submit @@ -144,18 +167,24 @@ } // loops through a list of files and display each file in a separate panel - function listItems(index) { - _dragNdropAssetsOff(); - for(var i = index; i < droppedFiles.length; i++) { - $('#new-result-assets-select') - .after(_uploadedAseetPreview(droppedFiles[i], i)) - .promise() - .done(function() { - _removeItemHandler(i); - }); + function listItems() { + totalSize = 0; + $('.panel-result-attachment-new').remove(); + if(droppedFiles.length < 1) { + _disableSubmitButton(); + } else { + _dragNdropAssetsOff(); + for(var i = 0; i < droppedFiles.length; i++) { + $('#new-result-assets-select') + .after(_uploadedAssetPreview(droppedFiles[i], i)) + .promise() + .done(function() { + _removeItemHandler(i); + }); + } + _validateTotalSize(); + dragNdropAssetsInit('results'); } - _validateTotalSize(); - dragNdropAssetsInit('results'); } // appent the files to the form before submit @@ -181,31 +210,51 @@ return fd; } + function _disableSubmitButton() { + $('.save-result').prop('disabled', true); + } + + function _enableSubmitButton() { + $('.save-result').prop('disabled', false); + } + function _filerAndCheckFiles() { - droppedFiles = droppedFiles.filter(Boolean); + for(var i = 0; i < droppedFiles.length; i++) { + if(droppedFiles[i].isValid == false) { + return false; + } + } return (droppedFiles.length > 0); } function _validateFilesSize(file) { - var maxSize = file.size/1048576; - if(maxSize > <%= Constants::FILE_MAX_SIZE_MB %> && isValid) { - return "

<%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %>

"; + var fileSize = file.size; + totalSize += parseInt(fileSize); + if(fileSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) { + file.isValid = false; + _disableSubmitButton(); + return "

<%= I18n.t 'general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB %>

"; } - totalSize += parseInt(maxSize); return ''; } function _validateTotalSize() { - if(totalSize > <%= Constants::FILE_MAX_SIZE_MB %>) { + if(totalSize > <%= Constants::FILE_MAX_SIZE_MB.megabyte %>) { isValid = false; + _disableSubmitButton(); $.each($('.panel-result-attachment-new'), function() { - $(this) - .find('.panel-body') - .append("

<%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %>

"); + if(!$(this).find('p').hasClass('dnd-total-error')) { + $(this) + .find('.panel-body') + .append("

<%= I18n.t('general.file.total_size', size: Constants::FILE_MAX_SIZE_MB) %>

"); + } }); } else { - $('.dnd-error').remove(); - isValid = true; + $('.dnd-total-error').remove(); + if(_filerAndCheckFiles()) { + isValid = true; + _enableSubmitButton(); + } } } @@ -220,7 +269,7 @@ } } - function _uploadedAseetPreview(asset, i) { + function _uploadedAssetPreview(asset, i) { var html = '
'; html += '
'; html += ''; @@ -249,10 +298,9 @@ e.stopPropagation(); var $el = $(this); var index = $el.data('item-id'); - totalSize -= parseInt(droppedFiles[index]/1048576); - droppedFiles[index] = null; - $el.closest('.panel-result-attachment-new').remove(); - _validateTotalSize(); + totalSize -= parseInt(droppedFiles[index].size); + droppedFiles.splice(index, 1); + listItems(); }); } diff --git a/app/assets/javascripts/sitewide/form_validators.js.erb b/app/assets/javascripts/sitewide/form_validators.js.erb index 323457e5d..aca5ef0f3 100644 --- a/app/assets/javascripts/sitewide/form_validators.js.erb +++ b/app/assets/javascripts/sitewide/form_validators.js.erb @@ -22,11 +22,11 @@ $.fn.onSubmitValidator = function(validatorCb) { * @param {boolean} clearErr Set clearErr to true if this is the only * error that can happen/show. */ -function textValidator(ev, textInput, textLimitMin, textLimitMax, clearErr, tinyMCE) { +function textValidator(ev, textInput, textLimitMin, textLimitMax, clearErr, tinyMCEInput) { clearErr = _.isUndefined(clearErr) ? false : clearErr; - if(tinyMCE){ - var text = textInput.length; + if(tinyMCEInput){ + var text = tinyMCEInput; } else { var text = $(textInput).val().trim(); $(textInput).val(text); diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index e5a41a988..91a296e07 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -1288,7 +1288,8 @@ ul.content-module-activities { padding-top: 30px; } -.dnd-error { +.dnd-error, +.dnd-total-error { color: $color-milano-red; } diff --git a/app/controllers/canvas_controller.rb b/app/controllers/canvas_controller.rb index 4f0291a64..14728cc92 100644 --- a/app/controllers/canvas_controller.rb +++ b/app/controllers/canvas_controller.rb @@ -29,58 +29,41 @@ class CanvasController < ApplicationController end def update - error = false - # Make sure that remove parameter is valid to_archive = [] - if can_archive_modules(@experiment) and - update_params[:remove].present? then - to_archive = update_params[:remove].split(",") - unless to_archive.all? { |id| is_int? id } - error = true + if can_archive_modules(@experiment) && update_params[:remove].present? + to_archive = update_params[:remove].split(',') + if to_archive.all? { |id| is_int? id } + to_archive.collect!(&:to_i) else - to_archive.collect! { |id| id.to_i } + return render_403 end end - if error then - render_403 and return - end - # Make sure connections parameter is valid connections = [] - if can_edit_connections(@experiment) and - update_params[:connections].present? then - conns = update_params[:connections].split(",") - unless conns.length % 2 == 0 and - conns.all? { |c| c.is_a? String } then - error = true - else + if can_edit_connections(@experiment) && update_params[:connections].present? + conns = update_params[:connections].split(',') + if conns.length.even? && conns.all? { |c| c.is_a? String } conns.each_slice(2).each do |c| connections << [c[0], c[1]] end + else + return render_403 end end - if error then - render_403 and return - end - # Make sure positions parameter is valid - positions = Hash.new - if can_reposition_modules(@experiment) and - update_params[:positions].present? then - poss = update_params[:positions].split(";") - center = "" - (poss.collect { |pos| pos.split(",") }).each_with_index do |pos, index| - unless pos.length == 3 && - pos[0].is_a?(String) && - float?(pos[1]) && - float?(pos[2]) - error = true - break + positions = {} + if can_reposition_modules(@experiment) && update_params[:positions].present? + poss = update_params[:positions].split(';') + center = '' + (poss.collect { |pos| pos.split(',') }).each_with_index do |pos, index| + unless pos.length == 3 && pos[0].is_a?(String) && + float?(pos[1]) && float?(pos[2]) + return render_403 end - if index == 0 + if index.zero? center = pos x = 0 y = 0 @@ -89,86 +72,59 @@ class CanvasController < ApplicationController y = pos[2].to_i - center[2].to_i end # Multiple modules cannot have same position - if positions.any? { |k,v| v[:x] == x and v[:y] == y} then - error = true - break - end + return render_403 if positions.any? { |_, v| v[:x] == x && v[:y] == y } positions[pos[0]] = { x: x, y: y } end end - if error then - render_403 and return - end - # Make sure that to_add is an array of strings, # as well as that positions for newly added modules exist to_add = [] - if can_create_modules(@experiment) and - update_params[:add].present? and - update_params["add-names"].present? then - ids = update_params[:add].split(",") - names = update_params["add-names"].split("|") - unless ids.length == names.length and - ids.all? { |id| id.is_a? String and positions.include? id } and - names.all? { |name| name.is_a? String } - error = true - else + if can_create_modules(@experiment) && update_params[:add].present? && + update_params['add-names'].present? + ids = update_params[:add].split(',') + names = update_params['add-names'].split('|') + if ids.length == names.length && + ids.all? { |id| id.is_a?(String) && positions.include?(id) } && + names.all? { |name| name.is_a? String } ids.each_with_index do |id, i| - to_add << { - id: id, - name: names[i], - x: positions[id][:x], - y: positions[id][:y] - } + to_add << { id: id, name: names[i], + x: positions[id][:x], y: positions[id][:y] } end + else + return render_403 end end - if error then - render_403 and return - end - # Make sure rename parameter is valid - to_rename = Hash.new - if can_edit_modules(@experiment) and - update_params[:rename].present? then + to_rename = {} + if can_edit_modules(@experiment) && update_params[:rename].present? begin to_rename = JSON.parse(update_params[:rename]) - # Okay, JSON parsed! - unless ( - to_rename.is_a? Hash and - to_rename.keys.all? { |k| k.is_a? String } and - to_rename.values.all? { |k| k.is_a? String } - ) - error = true + unless to_rename.is_a?(Hash) && + to_rename.keys.all? { |k| k.is_a? String } && + to_rename.values.all? { |k| k.is_a? String } + return render_403 end rescue - error = true + return render_403 end end - if error then - render_403 and return - end - # Make sure move parameter is valid to_move = {} if can_move_modules(@experiment) && update_params[:move].present? begin to_move = JSON.parse(update_params[:move]) - # Okay, JSON parsed! - unless ( - to_move.is_a? Hash and - to_move.keys.all? { |k| k.is_a? String } && - to_move.values.all? { |k| k.is_a? String } - ) - error = true + unless to_move.is_a?(Hash) && + to_move.keys.all? { |k| k.is_a? String } && + to_move.values.all? { |k| k.is_a? String } + return render_403 end rescue - error = true + return render_403 end end @@ -181,54 +137,21 @@ class CanvasController < ApplicationController end end - render_403 and return if error - # Make sure that to_clone is an array of pairs, # as well as that all IDs exist - to_clone = Hash.new - if can_clone_modules(@experiment) and - update_params[:cloned].present? then - clones = update_params[:cloned].split(";") - (clones.collect { |v| v.split(",") }).each do |val| - unless (val.length == 2 and - is_int? val[0] and - val[1].is_a? String and - to_add.any? { |m| m[:id] == val[1] }) - error = true - break - else + to_clone = {} + if can_clone_modules(@experiment) && update_params[:cloned].present? + clones = update_params[:cloned].split(';') + (clones.collect { |v| v.split(',') }).each do |val| + if val.length == 2 && is_int?(val[0]) && val[1].is_a?(String) && + to_add.any? { |m| m[:id] == val[1] } to_clone[val[1]] = val[0] + else + return render_403 end end end - if error then - render_403 and return - end - - module_groups = Hash.new - if can_edit_module_groups(@experiment) and - update_params["module-groups"].present? then - begin - module_groups = JSON.parse(update_params["module-groups"]) - - # Okay, JSON parsed! - unless ( - module_groups.is_a? Hash and - module_groups.keys.all? { |k| k.is_a? String } and - module_groups.values.all? { |k| k.is_a? String } - ) - error = true - end - rescue - error = true - end - end - - if error then - render_403 and return - end - # Call the "master" function to do all the updating for us unless @experiment.update_canvas( to_archive, @@ -239,36 +162,29 @@ class CanvasController < ApplicationController to_clone, connections, positions, - current_user, - module_groups + current_user ) - render_403 and return + return render_403 end - #Save activities that modules were archived + # Save activities that modules were archived to_archive.each do |module_id| my_module = MyModule.find_by_id(module_id) - unless my_module.blank? - Activity.create( - type_of: :archive_module, - project: my_module.experiment.project, - experiment: my_module.experiment, - my_module: my_module, - user: current_user, - message: t( - 'activities.archive_module', - user: current_user.full_name, - module: my_module.name - ) - ) - end + next if my_module.blank? + Activity.create(type_of: :archive_module, + project: my_module.experiment.project, + experiment: my_module.experiment, + my_module: my_module, + user: current_user, + message: t('activities.archive_module', + user: current_user.full_name, + module: my_module.name)) end # Create workflow image @experiment.delay.generate_workflow_img - flash[:success] = t( - "experiments.canvas.update.success_flash") + flash[:success] = t('experiments.canvas.update.success_flash') redirect_to canvas_experiment_path(@experiment) end diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index 513229aee..af1ae1e8b 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -77,6 +77,8 @@ class ExperimentsController < ApplicationController def canvas @project = @experiment.project + @active_modules = @experiment.active_modules + .includes(:tags, :inputs, :outputs) current_team_switch(@project.team) end diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index be9eec021..c0457c635 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -291,7 +291,7 @@ class MyModulesController < ApplicationController task_names = [] new_samples = [] - @my_module.get_downstream_modules.each do |my_module| + @my_module.downstream_modules.each do |my_module| new_samples = samples.select { |el| my_module.samples.exclude?(el) } my_module.samples.push(*new_samples) task_names << my_module.name @@ -330,7 +330,7 @@ class MyModulesController < ApplicationController end task_names = [] - @my_module.get_downstream_modules.each do |my_module| + @my_module.downstream_modules.each do |my_module| task_names << my_module.name my_module.samples.destroy(samples & my_module.samples) end diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 524123e90..7a544e972 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -182,7 +182,8 @@ class ReportsController < ApplicationController @html = '

No content

' if @html.blank? render pdf: 'report', header: { right: '[page] of [topage]' }, - template: 'reports/report.pdf.erb' + template: 'reports/report.pdf.erb', + disable_javascript: true end end end diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index bb56e7938..e356471e8 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -198,12 +198,14 @@ class RepositoriesController < ApplicationController if parsed_file.too_large? repository_response(t('general.file.size_exceeded', file_size: Constants::FILE_MAX_SIZE_MB)) - elsif parsed_file.empty? - flash[:notice] = t('teams.parse_sheet.errors.empty_file') - redirect_to back and return else @import_data = parsed_file.data - if parsed_file.generated_temp_file? + + if @import_data.header.empty? || @import_data.columns.empty? + return repository_response(t('teams.parse_sheet.errors.empty_file')) + end + + if (@temp_file = parsed_file.generate_temp_file) respond_to do |format| format.json do render json: { diff --git a/app/controllers/result_assets_controller.rb b/app/controllers/result_assets_controller.rb index faeb069e2..e966bd731 100644 --- a/app/controllers/result_assets_controller.rb +++ b/app/controllers/result_assets_controller.rb @@ -76,23 +76,23 @@ class ResultAssetsController < ApplicationController @result.last_modified_by = current_user @result.assign_attributes(update_params) - success_flash = t("result_assets.update.success_flash", - module: @my_module.name) + success_flash = t('result_assets.update.success_flash', + module: @my_module.name) if @result.archived_changed?(from: false, to: true) if previous_asset.locked? respond_to do |format| - format.html { + format.html do flash[:error] = t('result_assets.archive.error_flash') redirect_to results_my_module_path(@my_module) return - } + end end end saved = @result.archive(current_user) - success_flash = t("result_assets.archive.success_flash", - module: @my_module.name) + success_flash = t('result_assets.archive.success_flash', + module: @my_module.name) if saved Activity.create( type_of: :archive_result, @@ -126,7 +126,7 @@ class ResultAssetsController < ApplicationController # Asset (file) and/or name has been changed saved = @result.save - if saved then + if saved # Release team's space taken due to # previous asset being removed team = @result.my_module.experiment.project.team @@ -134,9 +134,7 @@ class ResultAssetsController < ApplicationController team.save # Post process new file if neccesary - if @result.asset.present? - @result.asset.post_process_file(team) - end + @result.asset.post_process_file(team) if @result.asset.present? Activity.create( type_of: :edit_result, @@ -144,32 +142,33 @@ class ResultAssetsController < ApplicationController project: @my_module.experiment.project, experiment: @my_module.experiment, my_module: @my_module, - message: t( - "activities.edit_asset_result", - user: current_user.full_name, - result: @result.name - ) + message: t('activities.edit_asset_result', + user: current_user.full_name, + result: @result.name) ) end end respond_to do |format| if saved - format.html { + format.html do flash[:success] = success_flash redirect_to results_my_module_path(@my_module) - } - format.json { + end + format.json do render json: { - html: render_to_string({ - partial: "my_modules/result.html.erb", locals: {result: @result} - }) + html: render_to_string( + partial: 'my_modules/result.html.erb', locals: { result: @result } + ) }, status: :ok - } + end else - format.json { - render json: @result.errors, status: :bad_request - } + format.json do + render json: { + status: :error, + errors: @result.errors + }, status: :bad_request + end end end end @@ -189,22 +188,15 @@ class ResultAssetsController < ApplicationController def load_vars_nested @my_module = MyModule.find_by_id(params[:my_module_id]) - - unless @my_module - render_404 - end + render_404 unless @my_module end def check_create_permissions - unless can_create_result_asset_in_module(@my_module) - render_403 - end + render_403 unless can_create_result_asset_in_module(@my_module) end def check_edit_permissions - unless can_edit_result_asset_in_module(@my_module) - render_403 - end + render_403 unless can_edit_result_asset_in_module(@my_module) end def check_archive_permissions diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index de98b6347..5cfe0bcfe 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -8,7 +8,6 @@ class SearchController < ApplicationController search_projects if @search_category == :projects search_experiments if @search_category == :experiments - search_workflows if @search_category == :workflows search_modules if @search_category == :modules search_results if @search_category == :results search_tags if @search_category == :tags @@ -145,7 +144,6 @@ class SearchController < ApplicationController def count_search_results @project_search_count = count_by_name Project @experiment_search_count = count_by_name Experiment - @workflow_search_count = count_by_name MyModuleGroup @module_search_count = count_by_name MyModule @result_search_count = count_by_name Result @tag_search_count = count_by_name Tag @@ -161,7 +159,6 @@ class SearchController < ApplicationController @search_results_count = @project_search_count @search_results_count += @experiment_search_count - @search_results_count += @workflow_search_count @search_results_count += @module_search_count @search_results_count += @result_search_count @search_results_count += @tag_search_count @@ -190,14 +187,6 @@ class SearchController < ApplicationController @search_count = @experiment_search_count end - def search_workflows - @workflow_results = [] - if @workflow_search_count > 0 - @workflow_results = search_by_name(MyModuleGroup) - end - @search_count = @workflow_search_count - end - def search_modules @module_results = [] @module_results = search_by_name(MyModule) if @module_search_count > 0 diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 20ee1b81b..9b12b30b6 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -290,10 +290,12 @@ class StepsController < ApplicationController "activities.uncheck_step_checklist_item" completed_items = chkItem.checklist.checklist_items.where(checked: true).count all_items = chkItem.checklist.checklist_items.count + text_activity = smart_annotation_parser(chkItem.text) + .gsub(/\s+/, ' ') message = t( str, user: current_user.full_name, - checkbox: smart_annotation_parser(simple_format(chkItem.text)), + checkbox: text_activity, step: chkItem.checklist.step.position + 1, step_name: chkItem.checklist.step.name, completed: completed_items, diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 622cff038..982b34d11 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -7,106 +7,57 @@ class TeamsController < ApplicationController def parse_sheet session[:return_to] ||= request.referer - respond_to do |format| - if params[:file] - begin + unless params[:file] + return parse_sheet_error(t('teams.parse_sheet.errors.no_file_selected')) + end + if params[:file].size > Constants::FILE_MAX_SIZE_MB.megabytes + error = t('general.file.size_exceeded', + file_size: Constants::FILE_MAX_SIZE_MB) + return parse_sheet_error(error) + end - if params[:file].size > Constants::FILE_MAX_SIZE_MB.megabytes - error = t 'general.file.size_exceeded', - file_size: Constants::FILE_MAX_SIZE_MB + begin + sheet = SpreadsheetParser.open_spreadsheet(params[:file]) + @header, @columns = SpreadsheetParser.first_two_rows(sheet) - format.html { - flash[:alert] = error - redirect_to session.delete(:return_to) + if @header.empty? || @columns.empty? + return parse_sheet_error(t('teams.parse_sheet.errors.empty_file')) + end + + # Fill in fields for dropdown + @available_fields = @team.get_available_sample_fields + # Truncate long fields + @available_fields.update(@available_fields) do |_k, v| + v.truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) + end + + # Save file for next step (importing) + @temp_file = TempFile.new( + session_id: session.id, + file: params[:file] + ) + + if @temp_file.save + @temp_file.destroy_obsolete + respond_to do |format| + format.json do + render json: { + html: render_to_string( + partial: 'samples/parse_samples_modal.html.erb' + ) } - format.json { - render json: {message: error}, - status: :unprocessable_entity - } - - else - sheet = Team.open_spreadsheet(params[:file]) - - # Check if we actually have any rows (last_row > 1) - if sheet.last_row.between?(0, 1) - flash[:notice] = t( - "teams.parse_sheet.errors.empty_file") - redirect_to session.delete(:return_to) and return - end - - # Get data (it will trigger any errors as well) - @header = sheet.row(1) - @columns = sheet.row(2) - - # Fill in fields for dropdown - @available_fields = @team.get_available_sample_fields - # Truncate long fields - @available_fields.update(@available_fields) do |_k, v| - v.truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) - end - - # Save file for next step (importing) - @temp_file = TempFile.new( - session_id: session.id, - file: params[:file] - ) - - if @temp_file.save - @temp_file.destroy_obsolete - # format.html - format.json { - render :json => { - :html => render_to_string({ - :partial => "samples/parse_samples_modal.html.erb" - }) - } - } - else - error = t("teams.parse_sheet.errors.temp_file_failure") - format.html { - flash[:alert] = error - redirect_to session.delete(:return_to) - } - format.json { - render json: {message: error}, - status: :unprocessable_entity - } - end end - rescue ArgumentError, CSV::MalformedCSVError - error = t('teams.parse_sheet.errors.invalid_file', - encoding: ''.encoding) - format.html { - flash[:alert] = error - redirect_to session.delete(:return_to) - } - format.json { - render json: {message: error}, - status: :unprocessable_entity - } - rescue TypeError - error = t("teams.parse_sheet.errors.invalid_extension") - format.html { - flash[:alert] = error - redirect_to session.delete(:return_to) - } - format.json { - render json: {message: error}, - status: :unprocessable_entity - } end else - error = t("teams.parse_sheet.errors.no_file_selected") - format.html { - flash[:alert] = error - session[:return_to] ||= request.referer - redirect_to session.delete(:return_to) - } - format.json { - render json: {message: error}, - status: :unprocessable_entity - } + return parse_sheet_error( + t('teams.parse_sheet.errors.temp_file_failure') + ) end + rescue ArgumentError, CSV::MalformedCSVError + return parse_sheet_error(t('teams.parse_sheet.errors.invalid_file', + encoding: ''.encoding)) + rescue TypeError + return parse_sheet_error(t('teams.parse_sheet.errors.invalid_extension')) end end @@ -122,7 +73,7 @@ class TeamsController < ApplicationController if @temp_file.session_id == session.id # Check if mappings exists or else we don't have anything to parse if params[:mappings] - @sheet = Team.open_spreadsheet(@temp_file.file) + @sheet = SpreadsheetParser.open_spreadsheet(@temp_file.file) # Check for duplicated values h1 = params[:mappings].clone.delete_if { |k, v| v.empty? } @@ -275,6 +226,20 @@ class TeamsController < ApplicationController private + def parse_sheet_error(error) + respond_to do |format| + format.html do + flash[:alert] = error + session[:return_to] ||= request.referer + redirect_to session.delete(:return_to) + end + format.json do + render json: { message: error }, + status: :unprocessable_entity + end + end + end + def load_vars @team = Team.find_by_id(params[:id]) diff --git a/app/helpers/activity_helper.rb b/app/helpers/activity_helper.rb index 83cf57023..feb13115a 100644 --- a/app/helpers/activity_helper.rb +++ b/app/helpers/activity_helper.rb @@ -1,17 +1,52 @@ module ActivityHelper + # constants for correct truncation length + TAGS_LENGTH = 4 + TRUNCATE_OFFSET = 3 def activity_truncate(message, len = Constants::NAME_TRUNCATION_LENGTH) activity_titles = message.scan(/(.*?)<\/strong>/) activity_titles.each do |activity_title| activity_title = activity_title[0] - if activity_title.length > Constants::NAME_TRUNCATION_LENGTH + # find first closing tag of smart annotation + closing_tag_sa = activity_title.index('') + unless closing_tag_sa.nil? + opening_tag_sa = activity_title.index(' opening_temp + end + len_temp = len + # check until we run out of smart annotations in message + while !opening_tag_sa.nil? && !closing_tag_sa.nil? && + opening_tag_sa < len_temp + stripped = strip_tags(activity_title[opening_tag_sa...closing_tag_sa]) + .length + len_temp += (activity_title[opening_tag_sa...closing_tag_sa + + TAGS_LENGTH]).length - stripped + len = len_temp + TRUNCATE_OFFSET + TAGS_LENGTH if len <= closing_tag_sa + closing_temp = closing_tag_sa + 1 + closing_tag_sa = activity_title.index('', closing_temp) + unless closing_tag_sa.nil? + # find next smart annotation + opening_tag_sa = activity_title.index(' opening_temp + end + end + # adjust truncation length according to smart annotations length + len = activity_title.length if len > activity_title.length && + len != Constants::NAME_TRUNCATION_LENGTH + if activity_title.length > len title = "" else - title = truncate(activity_title, length: len) + title = truncate(activity_title, length: len, escape: false) end message = message.gsub(/#{Regexp.escape(activity_title)}/, title) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1713b706c..7a5222ee0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -126,12 +126,12 @@ module ApplicationHelper next unless project if project.archived? "" \ - "#{sanitize_input(match[2])} " \ + "#{sanitize_input(match[2])}" \ "#{link_to project.name, projects_archive_path} #{I18n.t('atwho.res.archived')}" else "" \ - "#{sanitize_input(match[2])} " \ + "#{sanitize_input(match[2])}" \ "#{link_to project.name, project_path(project)}" end @@ -140,13 +140,13 @@ module ApplicationHelper next unless experiment if experiment.archived? "" \ - "#{sanitize_input(match[2])} " \ + "#{sanitize_input(match[2])}" \ "#{link_to experiment.name, experiment_archive_project_path(experiment.project)} " \ "#{I18n.t('atwho.res.archived')}" else ""\ - "#{sanitize_input(match[2])} " \ + "#{sanitize_input(match[2])}" \ "#{link_to experiment.name, canvas_experiment_path(experiment)}" end @@ -155,25 +155,25 @@ module ApplicationHelper next unless my_module if my_module.archived? "" \ - "#{sanitize_input(match[2])} " \ + "#{sanitize_input(match[2])}" \ "#{link_to my_module.name, module_archive_experiment_path(my_module.experiment)} " \ "#{I18n.t('atwho.res.archived')}" else "" \ - "#{sanitize_input(match[2])} " \ + "#{sanitize_input(match[2])}" \ "#{link_to my_module.name, protocols_my_module_path(my_module)}" end when 'sam' sample = Sample.find_by_id(match[3].base62_decode) if sample - " " \ + "" \ "#{link_to(sample.name, sample_path(sample.id), class: 'sample-info-link')}" else - " " \ + "" \ "#{match[1]} #{I18n.t('atwho.res.deleted')}" end end diff --git a/app/helpers/permission_helper.rb b/app/helpers/permission_helper.rb index 907cb2ac8..ef403766f 100644 --- a/app/helpers/permission_helper.rb +++ b/app/helpers/permission_helper.rb @@ -64,7 +64,6 @@ module PermissionHelper :can_edit_connections, :can_create_modules, :can_edit_modules, - :can_edit_module_groups, :can_clone_modules, :can_archive_modules, :can_view_reports, @@ -149,7 +148,6 @@ module PermissionHelper :can_edit_connections, :can_create_modules, :can_edit_modules, - :can_edit_module_groups, :can_clone_modules, :can_archive_modules ] do |proxy, *args, &block| @@ -421,10 +419,6 @@ module PermissionHelper is_user_or_higher_of_project(experiment.project) end - def can_edit_module_groups(experiment) - is_user_or_higher_of_project(experiment.project) - end - def can_clone_modules(experiment) is_user_or_higher_of_project(experiment.project) end diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 334a2b626..8920484b4 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -96,26 +96,27 @@ class Experiment < ApplicationRecord end def modules_without_group - MyModule.where(experiment_id: id).where(my_module_group: nil) - .where(archived: false) + MyModule.where(experiment_id: id) + .where(my_module_group: nil) + .where(archived: false) end def active_module_groups - self.my_module_groups.joins(:my_modules) - .where('my_modules.archived = ?', false) - .distinct + my_module_groups.joins(:my_modules) + .where('my_modules.archived = ?', false) + .distinct end def active_modules - my_modules.where(:archived => false) + my_modules.where(archived: false) end def archived_modules - my_modules.where(:archived => true) + my_modules.where(archived: true) end def assigned_samples - Sample.joins(:my_modules).where(my_modules: {id: my_modules} ) + Sample.joins(:my_modules).where(my_modules: { id: my_modules }) end def unassigned_samples(assigned_samples) @@ -131,15 +132,15 @@ class Experiment < ApplicationRecord to_clone, connections, positions, - current_user, - module_groups + current_user ) cloned_modules = [] begin - Experiment.transaction do + with_lock do # First, add new modules new_ids, cloned_pairs, originals = add_modules( - to_add, to_clone, current_user) + to_add, to_clone, current_user + ) cloned_modules = cloned_pairs.collect { |mn, _| mn } # Rename modules @@ -147,39 +148,31 @@ class Experiment < ApplicationRecord # Add activities that modules were created originals.each do |m| - Activity.create( - type_of: :create_module, + Activity.create(type_of: :create_module, user: current_user, - project: self.project, + project: project, experiment: m.experiment, my_module: m, - message: I18n.t( - "activities.create_module", - user: current_user.full_name, - module: m.name - ) - ) + message: I18n.t('activities.create_module', + user: current_user.full_name, + module: m.name)) end # Add activities that modules were cloned cloned_pairs.each do |mn, mo| - Activity.create( - type_of: :clone_module, + Activity.create(type_of: :clone_module, project: mn.experiment.project, experiment: mn.experiment, my_module: mn, user: current_user, - message: I18n.t( - "activities.clone_module", - user: current_user.full_name, - module_new: mn.name, - module_original: mo.name - ) - ) + message: I18n.t('activities.clone_module', + user: current_user.full_name, + module_new: mn.name, + module_original: mo.name)) end # Then, archive modules that need to be archived - archive_modules(to_archive, current_user) + archive_modules(to_archive, current_user) if to_archive.any? # Update connections, positions & module group variables # with actual IDs retrieved from the new modules creation @@ -196,17 +189,13 @@ class Experiment < ApplicationRecord updated_to_move_groups[mapped] = value end updated_connections = [] - connections.each do |a,b| + connections.each do |a, b| updated_connections << [new_ids.fetch(a, a), new_ids.fetch(b, b)] end - updated_positions = Hash.new + updated_positions = {} positions.each do |id, pos| updated_positions[new_ids.fetch(id, id)] = pos end - updated_module_groups = {} - module_groups.each do |id, name| - updated_module_groups[new_ids.fetch(id, id)] = name - end # Update connections update_module_connections(updated_connections) @@ -218,7 +207,7 @@ class Experiment < ApplicationRecord normalize_module_positions # Finally, update module groups - update_module_groups(updated_module_groups, current_user) + update_module_groups(current_user) # Finally move any modules to another experiment move_modules(updated_to_move) @@ -226,11 +215,13 @@ class Experiment < ApplicationRecord # Everyhing is set, now we can move any module groups move_module_groups(updated_to_move_groups) end - rescue ActiveRecord::ActiveRecordError, ArgumentError, ActiveRecord::RecordNotSaved + rescue ActiveRecord::ActiveRecordError, + ArgumentError, + ActiveRecord::RecordNotSaved => ex + logger.error ex.message return false end - - return true + true end # This method generate the workflow image and saves it as @@ -431,22 +422,15 @@ class Experiment < ApplicationRecord # Archive all modules. Receives an array of module integer IDs. def archive_modules(module_ids) - module_ids.each do |m_id| - my_module = self.my_modules.find_by_id(m_id) - unless my_module.blank? - my_module.archive! - end - end - modules.reload + my_modules.where(id: module_ids).each(&:archive!) + my_modules.reload end - # Archive all modules. Receives an array of module integer IDs and current user. + # Archive all modules. Receives an array of module integer IDs + # and current user. def archive_modules(module_ids, current_user) - module_ids.each do |m_id| - my_module = self.my_modules.find_by_id(m_id) - unless my_module.blank? - my_module.archive!(current_user) - end + my_modules.where(id: module_ids).each do |m| + m.archive!(current_user) end my_modules.reload end @@ -460,15 +444,14 @@ class Experiment < ApplicationRecord def add_modules(to_add, to_clone, current_user) originals = [] cloned_pairs = {} - ids_map = Hash.new + ids_map = {} to_add.each do |m| original = MyModule.find_by_id(to_clone.fetch(m[:id], nil)) - if original.present? then + if original.present? my_module = original.deep_clone(current_user) cloned_pairs[my_module] = original else - my_module = MyModule.new( - experiment: self) + my_module = MyModule.new(experiment: self) originals << my_module end @@ -537,7 +520,7 @@ class Experiment < ApplicationRecord # to bottom left corner. def move_module_groups(to_move) to_move.each do |ids, experiment_id| - modules = my_modules.where(id: ids) + modules = my_modules.find(ids) groups = Set.new(modules.map(&:my_module_group)) experiment = project.experiments.find_by_id(experiment_id) @@ -577,10 +560,8 @@ class Experiment < ApplicationRecord # Generates workflow img when the workflow or module is moved # to other experiment def generate_workflow_img_for_moved_modules(to_move) - to_move.values.uniq.each do |id| - experiment = Experiment.find_by_id(id) - next unless experiment - experiment.delay.generate_workflow_img + Experiment.where(id: to_move.values.uniq).each do |exp| + exp.delay.generate_workflow_img end end @@ -592,42 +573,39 @@ class Experiment < ApplicationRecord require 'rgl/base' require 'rgl/adjacency' require 'rgl/topsort' - dg = RGL::DirectedAdjacencyGraph.new - connections.each do |a,b| + connections.each do |a, b| # Check if both vertices exist - if (my_modules.find_all {|m| [a.to_i, b.to_i].include? m.id }).count == 2 + if (my_modules.find_all { |m| [a.to_i, b.to_i].include? m.id }).count == 2 dg.add_edge(a, b) end end # Check if cycles exist! topsort = dg.topsort_iterator.to_a - if topsort.length == 0 and dg.edges.size > 1 - raise ArgumentError, "Cycles exist." + if topsort.length.zero? && dg.edges.size > 1 + raise ArgumentError, 'Cycles exist.' end # First, delete existing connections # but keep a copy of previous state previous_sources = {} previous_sources.default = [] - my_modules.each do |m| + + my_modules.includes(inputs: { from: [:inputs, outputs: :to] }).each do |m| previous_sources[m.id] = [] m.inputs.each do |c| previous_sources[m.id] << c.from end end - self.my_modules.each do |m| - unless m.outputs.destroy_all - raise ActiveRecord::ActiveRecordError - end - end + # There are no callbacks in Connection, so delete_all should be safe + Connection.delete_all(output_id: my_modules) # Add new connections filtered_edges = dg.edges.collect { |e| [e.source, e.target] } filtered_edges.each do |a, b| - Connection.create!(:input_id => b, :output_id => a) + Connection.create!(input_id: b, output_id: a) end # Unassign samples from former downstream modules @@ -637,47 +615,49 @@ class Experiment < ApplicationRecord visited = [] # Assign samples to all new downstream modules filtered_edges.each do |a, b| - source = self.my_modules.find(a.to_i) - target = self.my_modules.find(b.to_i) + source = my_modules.includes({ inputs: :from }, :samples).find(a.to_i) + target = my_modules.find(b.to_i) # Do this only for new edges - if previous_sources[target.id].exclude?(source) - # Go as high upstream as new edges take us - # and then assign samples to all downsteam samples - assign_samples_to_new_downstream_modules(previous_sources, visited, source) - end + next unless previous_sources[target.id].exclude?(source) + # Go as high upstream as new edges take us + # and then assign samples to all downsteam samples + assign_samples_to_new_downstream_modules(previous_sources, + visited, + source) end # Save topological order of modules (for modules without workflow, # leave them unordered) - self.my_modules.each do |m| - if topsort.include? m.id.to_s - m.workflow_order = topsort.find_index(m.id.to_s) - else - m.workflow_order = -1 - end + my_modules.includes(:my_module_group).each do |m| + m.workflow_order = + if topsort.include? m.id.to_s + topsort.find_index(m.id.to_s) + else + -1 + end m.save! end - # Make sure to reload my modules, which now have updated connections and samples - self.my_modules.reload + # Make sure to reload my modules, which now have updated connections + # and samples + my_modules.reload true end # When connections are deleted, unassign samples that # are not inherited anymore def unassign_samples_from_old_downstream_modules(sources) - self.my_modules.each do |my_module| - sources[my_module.id].each do |s| + my_modules.each do |my_module| + sources[my_module.id].each do |src| # Only do this for newly deleted connections - if s.outputs.map{|i| i.to}.exclude? my_module - my_module.get_downstream_modules.each do |dm| - # Get unique samples for all upstream modules - um = dm.get_upstream_modules - um.shift # remove current module - ums = um.map{|m| m.samples}.flatten.uniq - s.samples.each do |sample| - dm.samples.destroy(sample) if ums.exclude? sample - end + next unless src.outputs.map(&:to).exclude? my_module + my_module.downstream_modules.each do |dm| + # Get unique samples for all upstream modules + um = dm.upstream_modules + um.shift # remove current module + ums = um.map(&:samples).flatten.uniq + src.samples.find_each do |sample| + dm.samples.destroy(sample) if ums.exclude? sample end end end @@ -687,24 +667,21 @@ class Experiment < ApplicationRecord # Assign samples to new connections recursively def assign_samples_to_new_downstream_modules(sources, visited, my_module) # If samples are already assigned for this module, stop going upstream - if visited.include? (my_module) - return - end + return if visited.include?(my_module) visited << my_module - # Edge case, when module is source or it doesn't have any new input connections - if my_module.inputs.blank? or ( - my_module.inputs.map{|c| c.from} - - sources[my_module.id] - ).empty? - my_module.get_downstream_modules.each do |dm| - new_samples = my_module.samples.select { |el| dm.samples.exclude?(el) } - dm.samples.push(*new_samples) + # Edge case, when module is source or it doesn't have any new input + # connections + if my_module.inputs.blank? || + (my_module.inputs.map(&:from) - sources[my_module.id]).empty? + my_module.downstream_modules.each do |dm| + new_samples = my_module.samples.where.not(id: dm.samples) + dm.samples << new_samples end else my_module.inputs.each do |input| # Go upstream for new in connections if sources[my_module.id].exclude?(input.from) - assign_samples_to_new_downstream_modules(input.from) + assign_samples_to_new_downstream_modules(sources, visited, input.from) end end end @@ -714,102 +691,58 @@ class Experiment < ApplicationRecord # Input is a map where keys are module IDs, and values are # hashes like { x: , y: }. def update_module_positions(positions) - positions.each do |id, pos| - unless MyModule.update(id, x: pos[:x], y: pos[:y]) - raise ActiveRecord::ActiveRecordError - end + modules = my_modules.where(id: positions.keys) + modules.each do |m| + m.update_columns(x: positions[m.id.to_s][:x], y: positions[m.id.to_s][:y]) end - self.my_modules.reload + my_modules.reload end # Normalize module positions in this project. def normalize_module_positions # This method normalizes module positions so x-s and y-s # are all positive - x_diff = (self.my_modules.collect { |m| m.x }).min - y_diff = (self.my_modules.collect { |m| m.y }).min + x_diff = my_modules.pluck(:x).min + y_diff = my_modules.pluck(:y).min - self.my_modules.each do |m| - unless - m.update_attribute(:x, m.x - x_diff) and - m.update_attribute(:y, m.y - y_diff) - raise ActiveRecord::ActiveRecordError - end + my_modules.each do |m| + m.update_columns(x: m.x - x_diff, y: m.y - y_diff) end end # Recalculate module groups in this project. Input is # a hash of module ids and their corresponding module names. - def update_module_groups(module_groups, current_user) + def update_module_groups(current_user) require 'rgl/base' require 'rgl/adjacency' require 'rgl/connected_components' dg = RGL::DirectedAdjacencyGraph[] group_ids = Set.new - active_modules.each do |m| - unless m.my_module_group.blank? - group_ids << m.my_module_group.id - end - unless dg.has_vertex? m.id - dg.add_vertex m.id - end + active_modules.includes(:my_module_group, outputs: :to).each do |m| + group_ids << m.my_module_group.id unless m.my_module_group.blank? + dg.add_vertex m.id unless dg.has_vertex? m.id m.outputs.each do |o| dg.add_edge m.id, o.to.id end end workflows = [] - dg.to_undirected.each_connected_component { |w| workflows << w } - - # For each workflow, generate new names - new_index = 1 - wf_names = [] - suffix = I18n.t("my_module_groups.new.suffix") - cut_index = -(suffix.length + 1) - workflows.each do |w| - modules = MyModule.find(w) - - # Get an array of module names - names = [] - modules.each do |m| - names << module_groups.fetch(m.id.to_s, "") - end - names = names.uniq - name = (names.select { |v| v != "" }).join(", ") - - if w.length <= 1 - name = nil - elsif name.blank? - name = I18n.t("my_module_groups.new.name", index: new_index) - new_index += 1 - while MyModuleGroup.find_by(name: name).present? - name = I18n.t("my_module_groups.new.name", index: new_index) - new_index += 1 - end - elsif name.length > Constants::NAME_MAX_LENGTH - # If length is too long, shorten it - name = name[0..(Constants::NAME_MAX_LENGTH + cut_index)] + suffix - end - - wf_names << name + dg.to_undirected.each_connected_component do |w| + workflows << my_modules.find(w) end # Remove any existing module groups from modules - unless MyModuleGroup.where(id: group_ids.to_a).destroy_all + unless MyModuleGroup.destroy_all(id: group_ids.to_a) raise ActiveRecord::ActiveRecordError end # Second, create new groups - workflows.each_with_index do |w, i| + workflows.each do |modules| # Single modules are not considered part of any workflow - if w.length > 1 - group = MyModuleGroup.new( - name: wf_names[i], - experiment: self, - my_modules: MyModule.find(w)) - group.created_by = current_user - group.save! - end + next unless modules.length > 1 + MyModuleGroup.create!(experiment: self, + my_modules: modules, + created_by: current_user) end my_module_groups.reload diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 90df13b09..e4677a97f 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -278,28 +278,24 @@ class MyModule < ApplicationRecord end # Treat this module as root, get all modules of that subtree - def get_downstream_modules + def downstream_modules final = [] modules = [self] - while !modules.empty? + until modules.empty? my_module = modules.shift - if !final.include?(my_module) - final << my_module - end + final << my_module unless final.include?(my_module) modules.push(*my_module.my_modules) end final end # Treat this module as inversed root, get all modules of that inversed subtree - def get_upstream_modules + def upstream_modules final = [] modules = [self] - while !modules.empty? + until modules.empty? my_module = modules.shift - if !final.include?(my_module) - final << my_module - end + final << my_module unless final.include?(my_module) modules.push(*my_module.my_module_antecessors) end final diff --git a/app/models/my_module_group.rb b/app/models/my_module_group.rb index 251229da9..bfe4493af 100644 --- a/app/models/my_module_group.rb +++ b/app/models/my_module_group.rb @@ -1,10 +1,6 @@ class MyModuleGroup < ApplicationRecord include SearchableModel - auto_strip_attributes :name, nullify: false - validates :name, - presence: true, - length: { maximum: Constants::NAME_MAX_LENGTH } validates :experiment, presence: true belongs_to :experiment, inverse_of: :my_module_groups, optional: true @@ -14,39 +10,12 @@ class MyModuleGroup < ApplicationRecord optional: true has_many :my_modules, inverse_of: :my_module_group, dependent: :nullify - def self.search(user, - include_archived, - query = nil, - page = 1, - _current_team = nil, - options = {}) - exp_ids = - Experiment - .search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT) - .pluck(:id) - - new_query = MyModuleGroup - .distinct - .where('my_module_groups.experiment_id IN (?)', exp_ids) - .where_attributes_like('my_module_groups.name', query, options) - - # Show all results if needed - if page == Constants::SEARCH_NO_LIMIT - new_query - else - new_query - .limit(Constants::SEARCH_LIMIT) - .offset((page - 1) * Constants::SEARCH_LIMIT) - end - end - def ordered_modules my_modules.order(workflow_order: :asc) end def deep_clone_to_experiment(current_user, experiment) clone = MyModuleGroup.new( - name: name, created_by: created_by, experiment: experiment ) diff --git a/app/models/repository.rb b/app/models/repository.rb index e9ec2fc51..b82a8688e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -60,17 +60,6 @@ class Repository < ApplicationRecord end end - def open_spreadsheet(file) - filename = file.original_filename - file_path = file.path - - if file.class == Paperclip::Attachment && file.is_stored_on_s3? - fa = file.fetch - file_path = fa.path - end - generate_file(filename, file_path) - end - def available_repository_fields fields = {} # First and foremost add record name @@ -116,6 +105,7 @@ class Repository < ApplicationRecord name_index = -1 total_nr = 0 nr_of_added = 0 + header_skipped = false mappings.each.with_index do |(_k, value), index| if value == '-1' @@ -132,54 +122,63 @@ class Repository < ApplicationRecord unless col_compact.map(&:id).uniq.length == col_compact.length return { status: :error, nr_of_added: nr_of_added, total_nr: total_nr } end + rows = SpreadsheetParser.spreadsheet_enumerator(sheet) # Now we can iterate through record data and save stuff into db - transaction do - (2..sheet.last_row).each do |i| - total_nr += 1 - record_row = RepositoryRow.new(name: sheet.row(i)[name_index], - repository: self, - created_by: user, - last_modified_by: user) - record_row.transaction(requires_new: true) do - unless record_row.save - errors = true - raise ActiveRecord::Rollback - end + rows.each do |row| + # Skip empty rows + next if row.empty? + unless header_skipped + header_skipped = true + next + end + total_nr += 1 - row_cell_values = [] + # Creek XLSX parser returns Hash of the row, Roo - Array + row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s) - sheet.row(i).each.with_index do |value, index| - if columns[index] && value - cell_value = RepositoryTextValue.new( - data: value, - created_by: user, - last_modified_by: user, - repository_cell_attributes: { - repository_row: record_row, - repository_column: columns[index] - } - ) - cell = RepositoryCell.new(repository_row: record_row, - repository_column: columns[index], - value: cell_value) - cell.skip_on_import = true - cell_value.repository_cell = cell - unless cell.valid? && cell_value.valid? - errors = true - raise ActiveRecord::Rollback - end - row_cell_values << cell_value - end - end - if RepositoryTextValue.import(row_cell_values, - recursive: true, - validate: false).failed_instances.any? - errors = true - raise ActiveRecord::Rollback - end - nr_of_added += 1 + record_row = RepositoryRow.new(name: row[name_index], + repository: self, + created_by: user, + last_modified_by: user) + record_row.transaction do + unless record_row.save + errors = true + raise ActiveRecord::Rollback end + + row_cell_values = [] + + row.each.with_index do |value, index| + if columns[index] && value + cell_value = RepositoryTextValue.new( + data: value, + created_by: user, + last_modified_by: user, + repository_cell_attributes: { + repository_row: record_row, + repository_column: columns[index] + } + ) + cell = RepositoryCell.new(repository_row: record_row, + repository_column: columns[index], + value: cell_value) + cell.skip_on_import = true + cell_value.repository_cell = cell + unless cell.valid? && cell_value.valid? + errors = true + raise ActiveRecord::Rollback + end + row_cell_values << cell_value + end + end + if RepositoryTextValue.import(row_cell_values, + recursive: true, + validate: false).failed_instances.any? + errors = true + raise ActiveRecord::Rollback + end + nr_of_added += 1 end end @@ -188,22 +187,4 @@ class Repository < ApplicationRecord end { status: :ok, nr_of_added: nr_of_added, total_nr: total_nr } end - - private - - def generate_file(filename, file_path) - case File.extname(filename) - when '.csv' - Roo::CSV.new(file_path, extension: :csv) - when '.tsv' - Roo::CSV.new(file_path, csv_options: { col_sep: "\t" }) - when '.txt' - # This assumption is based purely on biologist's habits - Roo::CSV.new(file_path, csv_options: { col_sep: "\t" }) - when '.xlsx' - Roo::Excelx.new(file_path) - else - raise TypeError - end - end end diff --git a/app/models/team.rb b/app/models/team.rb index e8fdfca64..05f139d0d 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -32,30 +32,6 @@ class Team < ApplicationRecord has_many :protocol_keywords, inverse_of: :team, dependent: :destroy has_many :tiny_mce_assets, inverse_of: :team, dependent: :destroy has_many :repositories, dependent: :destroy - # Based on file's extension opens file (used for importing) - def self.open_spreadsheet(file) - filename = file.original_filename - file_path = file.path - - if file.class == Paperclip::Attachment and file.is_stored_on_s3? - fa = file.fetch - file_path = fa.path - end - - case File.extname(filename) - when '.csv' then - Roo::CSV.new(file_path, extension: :csv) - when '.tsv' then - Roo::CSV.new(file_path, csv_options: { col_sep: "\t" }) - when '.txt' then - # This assumption is based purely on biologist's habits - Roo::CSV.new(file_path, csv_options: { col_sep: "\t" }) - when '.xlsx' then - Roo::Excelx.new(file_path) - else - raise TypeError - end - end def search_users(query = nil) a_query = "%#{query}%" @@ -72,6 +48,7 @@ class Team < ApplicationRecord errors = false nr_of_added = 0 total_nr = 0 + header_skipped = false # First let's query for all custom_fields we're refering to custom_fields = [] @@ -97,10 +74,22 @@ class Team < ApplicationRecord custom_fields << cf end end + + rows = SpreadsheetParser.spreadsheet_enumerator(sheet) + # Now we can iterate through sample data and save stuff into db - (2..sheet.last_row).each do |i| + rows.each do |row| + # Skip empty rows + next if row.empty? + unless header_skipped + header_skipped = true + next + end total_nr += 1 - sample = Sample.new(name: sheet.row(i)[sname_index], + # Creek XLSX parser returns Hash of the row, Roo - Array + row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s) + + sample = Sample.new(name: row[sname_index], team: self, user: user) @@ -110,12 +99,14 @@ class Team < ApplicationRecord raise ActiveRecord::Rollback end - sheet.row(i).each.with_index do |value, index| + row.each.with_index do |value, index| if index == stype_index - stype = SampleType.where(name: value.strip, team: self).take + stype = SampleType.where(team: self) + .where('name ILIKE ?', value.strip) + .take unless stype - stype = SampleType.new(name: value, team: self) + stype = SampleType.new(name: value.strip, team: self) unless stype.save errors = true raise ActiveRecord::Rollback @@ -123,10 +114,12 @@ class Team < ApplicationRecord end sample.sample_type = stype elsif index == sgroup_index - sgroup = SampleGroup.where(name: value.strip, team: self).take + sgroup = SampleGroup.where(team: self) + .where('name ILIKE ?', value.strip) + .take unless sgroup - sgroup = SampleGroup.new(name: value, team: self) + sgroup = SampleGroup.new(name: value.strip, team: self) unless sgroup.save errors = true raise ActiveRecord::Rollback diff --git a/app/services/import_repository/import_records.rb b/app/services/import_repository/import_records.rb index e9deea8b4..ba80a4b0a 100644 --- a/app/services/import_repository/import_records.rb +++ b/app/services/import_repository/import_records.rb @@ -17,9 +17,11 @@ module ImportRepository private def run_import_actions - @repository.import_records(@repository.open_spreadsheet(@temp_file.file), - @mappings, - @user) + @repository.import_records( + SpreadsheetParser.open_spreadsheet(@temp_file.file), + @mappings, + @user + ) end def run_checks diff --git a/app/services/import_repository/parse_repository.rb b/app/services/import_repository/parse_repository.rb index 192f4b61e..222b71274 100644 --- a/app/services/import_repository/parse_repository.rb +++ b/app/services/import_repository/parse_repository.rb @@ -5,48 +5,40 @@ module ImportRepository @file = options.fetch(:file) @repository = options.fetch(:repository) @session = options.fetch(:session) - @sheet = @repository.open_spreadsheet(@file) + @sheet = SpreadsheetParser.open_spreadsheet(@file) end def data - # Get data (it will trigger any errors as well) - header = @sheet.row(1) - columns = @sheet.row(2) + header, columns = SpreadsheetParser.first_two_rows(@sheet) # Fill in fields for dropdown @repository.available_repository_fields.transform_values! do |name| truncate(name, length: Constants::NAME_TRUNCATION_LENGTH_DROPDOWN) end - @temp_file = TempFile.create(session_id: @session.id, file: @file) Data.new(header, columns, @repository.available_repository_fields, - @repository, - @temp_file) + @repository) end def too_large? @file.size > Constants::FILE_MAX_SIZE_MB.megabytes end - def empty? - @sheet.last_row.between?(0, 1) - end - - def generated_temp_file? + def generate_temp_file # Save file for next step (importing) - @temp_file = TempFile.new( + temp_file = TempFile.new( session_id: @session.id, file: @file ) - if @temp_file.save - @temp_file.destroy_obsolete - return true + if temp_file.save + temp_file.destroy_obsolete + return temp_file end end Data = Struct.new( - :header, :columns, :available_fields, :repository, :temp_file + :header, :columns, :available_fields, :repository ) end end diff --git a/app/services/spreadsheet_parser.rb b/app/services/spreadsheet_parser.rb new file mode 100644 index 000000000..3d5a64774 --- /dev/null +++ b/app/services/spreadsheet_parser.rb @@ -0,0 +1,55 @@ +class SpreadsheetParser + # Based on file's extension opens file (used for importing) + def self.open_spreadsheet(file) + filename = file.original_filename + file_path = file.path + + if file.class == Paperclip::Attachment && file.is_stored_on_s3? + fa = file.fetch + file_path = fa.path + end + + case File.extname(filename) + when '.csv' + Roo::CSV.new(file_path, extension: :csv) + when '.tsv' + Roo::CSV.new(file_path, csv_options: { col_sep: "\t" }) + when '.txt' + # This assumption is based purely on biologist's habits + Roo::CSV.new(file_path, csv_options: { col_sep: "\t" }) + when '.xlsx' + # Roo Excel parcel was replaced with Creek, but it can be enabled back, + # just swap lines below. But only one can be enabled at the same time. + # Roo::Excelx.new(file_path) + Creek::Book.new(file_path).sheets[0] + else + raise TypeError + end + end + + def self.spreadsheet_enumerator(sheet) + if sheet.is_a?(Roo::CSV) + sheet + elsif sheet.is_a?(Roo::Excelx) + sheet.each_row_streaming + else + sheet.rows + end + end + + def self.first_two_rows(sheet) + rows = spreadsheet_enumerator(sheet) + header = [] + columns = [] + i = 1 + rows.each do |row| + # Creek XLSX parser returns Hash of the row, Roo - Array + row = row.is_a?(Hash) ? row.values.map(&:to_s) : row.map(&:to_s) + header = row if i == 1 && row + columns = row if i == 2 && row + i += 1 + break if i > 2 + end + return header, columns + end +end diff --git a/app/utilities/delayed_uploader_tutorial.rb b/app/utilities/delayed_uploader_tutorial.rb index 993e519cb..87e349f1a 100644 --- a/app/utilities/delayed_uploader_tutorial.rb +++ b/app/utilities/delayed_uploader_tutorial.rb @@ -41,6 +41,7 @@ module DelayedUploaderTutorial my_module: my_module, user: current_user, created_at: temp_result.created_at, + updated_at: temp_result.created_at, message: I18n.t( 'activities.add_asset_result', user: current_user.full_name, diff --git a/app/utilities/first_time_data_generator.rb b/app/utilities/first_time_data_generator.rb index c77cf8ca0..c3a7933d2 100644 --- a/app/utilities/first_time_data_generator.rb +++ b/app/utilities/first_time_data_generator.rb @@ -150,7 +150,6 @@ module FirstTimeDataGenerator # Create a module group my_module_group = MyModuleGroup.create( - name: 'Potato qPCR workflow', experiment: experiment ) @@ -317,7 +316,7 @@ module FirstTimeDataGenerator samples_to_assign << sample end - my_modules[1].get_downstream_modules.each do |mm| + my_modules[1].downstream_modules.each do |mm| samples_to_assign.each do |s| SampleMyModule.create( sample: s, @@ -881,7 +880,7 @@ module FirstTimeDataGenerator ).sneaky_save # create thumbnail - experiment.generate_workflow_img + experiment.delay.generate_workflow_img # Lastly, create cookie with according ids # so tutorial steps can be properly positioned diff --git a/app/views/canvas/_edit.html.erb b/app/views/canvas/_edit.html.erb index 45b44cb37..a362c5066 100644 --- a/app/views/canvas/_edit.html.erb +++ b/app/views/canvas/_edit.html.erb @@ -1,7 +1,6 @@
" data-can-edit-modules="<%= can_edit_modules(@experiment) ? "yes" : "no" %>" - data-can-edit-module-groups="<%= can_edit_module_groups(@experiment) ? "yes" : "no" %>" data-can-clone-modules="<%= can_clone_modules(@experiment) ? "yes" : "no" %>" data-can-move-modules="<%= can_move_modules(@experiment) ? "yes" : "no" %>" data-can-delete-modules="<%= can_archive_modules(@experiment) ? "yes" : "no" %>" @@ -49,9 +48,6 @@ - @@ -87,9 +83,6 @@ <% if can_edit_modules(@experiment) %> <%= render partial: "canvas/edit/modal/edit_module", locals: {experiment: @experiment } %> <% end %> -<% if can_edit_module_groups(@experiment) %> - <%= render partial: "canvas/edit/modal/edit_module_group", locals: {experiment: @experiment } %> -<% end %> <% if can_move_modules(@experiment) %> <%= render partial: "canvas/edit/modal/move_module", locals: {experiment: @experiment } %> <%= render partial: "canvas/edit/modal/move_module_group", locals: {experiment: @experiment } %> diff --git a/app/views/canvas/edit/_my_module.html.erb b/app/views/canvas/edit/_my_module.html.erb index 54ee90c4e..d0a876290 100644 --- a/app/views/canvas/edit/_my_module.html.erb +++ b/app/views/canvas/edit/_my_module.html.erb @@ -2,11 +2,6 @@ id="<%= my_module.id %>" data-module-id="<%= my_module.id %>" data-module-name="<%= my_module.name %>" - <% if my_module.my_module_group.present? %> - data-module-group-name="<%= my_module.my_module_group.name %>" - <% else %> - data-module-group-name="" - <% end %> data-module-x="<%= my_module.x %>" data-module-y="<%= my_module.y %>" data-module-conns="<%= construct_module_connections(my_module) %>"> @@ -26,11 +21,6 @@ <%=t "experiments.canvas.edit.edit_module" %> <% end %> - <% if can_edit_module_groups(my_module.experiment) %> -
  • > - <%=t "experiments.canvas.edit.edit_module_group" %> -
  • - <% end %> <% if can_clone_modules(my_module.experiment) %>
  • <%=t "experiments.canvas.edit.clone_module" %> diff --git a/app/views/canvas/edit/modal/_edit_module_group.html.erb b/app/views/canvas/edit/modal/_edit_module_group.html.erb deleted file mode 100644 index d2e14a628..000000000 --- a/app/views/canvas/edit/modal/_edit_module_group.html.erb +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/app/views/experiments/canvas.html.erb b/app/views/experiments/canvas.html.erb index 798e1d845..ef6a09b04 100644 --- a/app/views/experiments/canvas.html.erb +++ b/app/views/experiments/canvas.html.erb @@ -58,7 +58,7 @@
  • - <%= render partial: 'canvas/full_zoom', locals: { experiment: @experiment, my_modules: @experiment.active_modules } %> + <%= render partial: 'canvas/full_zoom', locals: { experiment: @experiment, my_modules: @active_modules } %>
    diff --git a/app/views/protocols/header/_edit_keywords_modal_body.html.erb b/app/views/protocols/header/_edit_keywords_modal_body.html.erb index 51250f321..021e33ad1 100644 --- a/app/views/protocols/header/_edit_keywords_modal_body.html.erb +++ b/app/views/protocols/header/_edit_keywords_modal_body.html.erb @@ -4,4 +4,5 @@ <% end %> -<% end %> \ No newline at end of file + <%=t 'protocols.header.keywords_modal' %> +<% end %> diff --git a/app/views/reports/report.pdf.erb b/app/views/reports/report.pdf.erb index aaa96c774..6e96f962d 100644 --- a/app/views/reports/report.pdf.erb +++ b/app/views/reports/report.pdf.erb @@ -10,9 +10,7 @@ diff --git a/app/views/repositories/_parse_records_modal.html.erb b/app/views/repositories/_parse_records_modal.html.erb index ccab285b1..7a15968ba 100644 --- a/app/views/repositories/_parse_records_modal.html.erb +++ b/app/views/repositories/_parse_records_modal.html.erb @@ -58,7 +58,7 @@
    - <%= hidden_field_tag 'file_id', @import_data.temp_file.id %> + <%= hidden_field_tag 'file_id', @temp_file.id %>
    diff --git a/app/views/result_assets/_new.html.erb b/app/views/result_assets/_new.html.erb index fe28d123d..275e40cd9 100644 --- a/app/views/result_assets/_new.html.erb +++ b/app/views/result_assets/_new.html.erb @@ -11,6 +11,7 @@
    - <%# end %>
    diff --git a/app/views/search/index.html.erb b/app/views/search/index.html.erb index 92bfaea45..b00a61fcb 100644 --- a/app/views/search/index.html.erb +++ b/app/views/search/index.html.erb @@ -70,19 +70,6 @@ <%= t'Experiments' %> -