diff --git a/.gitignore b/.gitignore index d3fd6cc3ab..5c5f043efe 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ public/javascripts/i18n.js public/javascripts/translations.js public/stylesheets/*_cached.css public/system/ +public/sitemaps/* public/assets/ public/assets_dev/ diff --git a/Gemfile b/Gemfile index fd40d4e720..619ca2babd 100644 --- a/Gemfile +++ b/Gemfile @@ -109,6 +109,7 @@ gem 'ransack' gem 'terser', '~> 1.1', '>= 1.1.1' + # Rails 4 upgrade gem 'activerecord-session_store' gem 'rails-observers' @@ -177,6 +178,8 @@ group :development do gem 'web-console', '>= 4.1.0' gem 'rack-mini-profiler', '~> 2.0' + gem "flamegraph", "~> 0.9.5" + gem "stackprof", "~> 0.2.25" gem 'listen', '~> 3.3' end @@ -202,3 +205,5 @@ group :test, :development do gem 'teaspoon' gem 'teaspoon-mocha' end + +gem "sitemap_generator", "~> 6.3" diff --git a/Gemfile.lock b/Gemfile.lock index b2797432af..acecd6c9c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,40 +51,40 @@ GEM remote: https://rubygems.org/ specs: RedCloth (4.3.2) - actioncable (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + actioncable (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailbox (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) mail (>= 2.7.1) - actionmailer (6.1.7.3) - actionpack (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailer (6.1.7.6) + actionpack (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activesupport (= 6.1.7.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.3) - actionview (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionpack (6.1.7.6) + actionview (= 6.1.7.6) + activesupport (= 6.1.7.6) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.3) - actionpack (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actiontext (6.1.7.6) + actionpack (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) nokogiri (>= 1.8.5) - actionview (6.1.7.3) - activesupport (= 6.1.7.3) + actionview (6.1.7.6) + activesupport (= 6.1.7.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -94,14 +94,14 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.7.3) - activesupport (= 6.1.7.3) + activejob (6.1.7.6) + activesupport (= 6.1.7.6) globalid (>= 0.3.6) - activemodel (6.1.7.3) - activesupport (= 6.1.7.3) - activerecord (6.1.7.3) - activemodel (= 6.1.7.3) - activesupport (= 6.1.7.3) + activemodel (6.1.7.6) + activesupport (= 6.1.7.6) + activerecord (6.1.7.6) + activemodel (= 6.1.7.6) + activesupport (= 6.1.7.6) activerecord-import (1.3.0) activerecord (>= 4.2) activerecord-session_store (2.0.0) @@ -110,14 +110,14 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 3) railties (>= 5.2.4.1) - activestorage (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activesupport (= 6.1.7.3) + activestorage (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activesupport (= 6.1.7.6) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.3) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -286,6 +286,7 @@ GEM loofah (>= 2.3.1) sax-machine (>= 1.0) ffi (1.15.5) + flamegraph (0.9.5) fssm (0.2.10) gem-licenses (0.2.2) gitlab_omniauth-ldap (2.2.0) @@ -293,8 +294,8 @@ GEM omniauth (>= 1.3, < 3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) rubyntlm (~> 0.5) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) google-analytics-rails (1.1.1) gyoku (0.4.6) builder (>= 2.1.2) @@ -458,8 +459,9 @@ GEM mimemagic (0.3.10) nokogiri (~> 1) rake - mini_mime (1.1.2) - minitest (5.18.0) + mini_mime (1.1.5) + mini_portile2 (2.8.4) + minitest (5.20.0) minitest-reporters (1.5.0) ansi builder @@ -478,7 +480,7 @@ GEM net-http-digest_auth (1.4.1) net-http-persistent (4.0.1) connection_pool (~> 2.2) - net-imap (0.3.4) + net-imap (0.3.7) date net-protocol net-ldap (0.17.1) @@ -491,6 +493,7 @@ GEM netrc (0.11.0) nio4r (2.5.9) nokogiri (1.14.5) + mini_portile2 (~> 2.8.0) racc (~> 1.4) nori (1.1.5) oauth2 (2.0.9) @@ -563,7 +566,7 @@ GEM nio4r (~> 2.0) pyu-ruby-sasl (0.0.3.3) racc (1.7.1) - rack (2.2.7) + rack (2.2.8) rack-attack (6.6.0) rack (>= 1.0, < 3) rack-cors (1.1.1) @@ -580,26 +583,26 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.3) - actioncable (= 6.1.7.3) - actionmailbox (= 6.1.7.3) - actionmailer (= 6.1.7.3) - actionpack (= 6.1.7.3) - actiontext (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activemodel (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + rails (6.1.7.6) + actioncable (= 6.1.7.6) + actionmailbox (= 6.1.7.6) + actionmailer (= 6.1.7.6) + actionpack (= 6.1.7.6) + actiontext (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activemodel (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) bundler (>= 1.15.0) - railties (= 6.1.7.3) + railties (= 6.1.7.6) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.1.1) + rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) @@ -621,9 +624,9 @@ GEM json require_all (~> 3.0) ruby-progressbar - railties (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + railties (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) method_source rake (>= 12.2) thor (~> 1.0) @@ -828,6 +831,8 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) + sitemap_generator (6.3.0) + builder (~> 3.0) slop (3.6.0) snaky_hash (2.0.0) hashie @@ -844,7 +849,7 @@ GEM sparql-client (3.2.0) net-http-persistent (~> 4.0, >= 4.0.1) rdf (~> 3.2) - sprockets (4.2.0) + sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) @@ -852,6 +857,7 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) sqlite3 (1.4.2) + stackprof (0.2.25) stringio (3.0.1) sunspot (2.6.0) pr_geohash (~> 1.0) @@ -882,7 +888,7 @@ GEM tilt (2.0.10) time (0.2.2) date - timeout (0.3.2) + timeout (0.4.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) ucf (2.0.2) @@ -930,7 +936,7 @@ GEM hashdiff (>= 0.4.0, < 2.0.0) webrick (1.7.0) webrobots (0.1.2) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) whenever (1.0.0) @@ -943,7 +949,7 @@ GEM rake (>= 0.8.7) yard (0.9.27) webrick (~> 1.7.0) - zeitwerk (2.6.8) + zeitwerk (2.6.11) zip-container (4.0.2) rubyzip (~> 2.0.0) @@ -990,6 +996,7 @@ DEPENDENCIES exception_notification factory_bot (~> 6.2.1) feedjira + flamegraph (~> 0.9.5) fleximage! fssm gem-licenses @@ -1073,8 +1080,10 @@ DEPENDENCIES seedbank simple-spreadsheet-extractor (~> 0.18.0) simplecov + sitemap_generator (~> 6.3) sprockets-rails sqlite3 (~> 1.4) + stackprof (~> 0.2.25) stringio (= 3.0.1) sunspot_matchers sunspot_rails diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4f80e6a46e..eeb6c760ef 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -56,7 +56,7 @@ //= require calendar //= require inplace //= require strain -//= require checkbox +//= require batch_asset_selection //= require cytoscape.js-2.5.0/cytoscape //= require cytoscape_isa_graph //= require bives @@ -75,3 +75,4 @@ //= require git //= require jquery.splitter/jquery.splitter.min //= require select2.full.min +//= require licenses diff --git a/app/assets/javascripts/application_shared.js.erb b/app/assets/javascripts/application_shared.js.erb index a3c400fb40..3b8c59fe21 100644 --- a/app/assets/javascripts/application_shared.js.erb +++ b/app/assets/javascripts/application_shared.js.erb @@ -399,6 +399,8 @@ $j(document).ready(function () { $j('#sidebar-toggle').click(Sidebar.toggle); $j('#sidebar-close').click(Sidebar.toggle); $j(document).on('click', '.sidebar-backdrop', Sidebar.toggle); + + Licenses.init(); }); var URL_ROOT = '<%= Rails.application.config.relative_url_root %>'; diff --git a/app/assets/javascripts/batch_asset_selection.js b/app/assets/javascripts/batch_asset_selection.js new file mode 100644 index 0000000000..e6bb2e2c27 --- /dev/null +++ b/app/assets/javascripts/batch_asset_selection.js @@ -0,0 +1,121 @@ +$j(document).ready(function () { + $j('.batch-selection-select-children').click(BatchAssetSelection.selectChildren); + $j('.batch-selection-deselect-children').click(BatchAssetSelection.deselectChildren); + $j('.batch-selection-collapse-children').click(BatchAssetSelection.collapseRecursively); + $j('.batch-selection-expand-children').click(BatchAssetSelection.expandRecursively); + $j('.batch-selection-show-permissions').click(function (event) { + event.preventDefault(); + $j('.batch-selection-permission-list', $j(this).closest('.batch-selection-scope')).show(); + }) + $j('.batch-selection-hide-permissions').click(function (event) { + event.preventDefault(); + $j('.batch-selection-permission-list', $j(this).closest('.batch-selection-scope')).hide(); + }) + $j('.batch-selection-hide-blocked').click(BatchAssetSelection.hideBlocked).click(); // Trigger on page load + $j('.batch-selection-show-blocked').click(BatchAssetSelection.showBlocked); + $j('.batch-selection-collapse-toggle').click(function () { + BatchAssetSelection.toggleCollapse(this); + return false; + }); + $j('.batch-selection-check-btn').click(function (event) { + if (event.target.nodeName.includes('BUTTON')) { + $j(this).find(':checkbox').click(); + } + }); + $j('.batch-selection-check-btn :checkbox').click(function () { + BatchAssetSelection.checkRepeatedItems(this.className, this.checked); + }); + $j('.batch-selection-managed-by-toggle').click(function (event) { + event.preventDefault(); + $j('.batch-selection-managed-by-list:first', $j(this).closest('.batch-selection-scope')).toggle(); + }); + $j('.batch-selection-permissions-toggle').click(function (event) { + event.preventDefault(); + $j('.batch-selection-permission-list:first', $j(this).closest('.batch-selection-scope')).toggle(); + }); +}); + +const BatchAssetSelection = { + blockedSelectors: '.not-visible, .not-manageable, .already-published', + selectChildren: function (event) { + event.preventDefault(); + BatchAssetSelection.setChildren($j(this).closest('.batch-selection-scope'), true); + }, + + deselectChildren: function (event) { + event.preventDefault(); + BatchAssetSelection.setChildren($j(this).closest('.batch-selection-scope'), false); + }, + + setChildren: function (scope, value) { + const children = $j(':checkbox', scope); + const classes = new Set(); + for (let child of children) { + classes.add(child.className); + } + + classes.forEach(c => BatchAssetSelection.checkRepeatedItems(c, value)); + }, + + checkRepeatedItems: function (className, check) { + document.getElementById('batch-asset-selection') + .querySelectorAll('.' + className).forEach(e => e.checked = check); + }, + + toggleManagers: function () { + $j('.batch-selection-managed-by-list', $j(this).closest('.batch-selection-asset')).toggle(); + + return false; + }, + + toggleCollapse: function (element, state) { + if (state === undefined) { + state = !element.classList.contains('open'); + } + element.classList.toggle('open', state); + $j(element).closest('.batch-selection-scope').children('.batch-selection-collapse').toggle(state); + }, + + collapseRecursively: function () { + const scope = $j(this).closest('.batch-selection-scope').children('.batch-selection-collapse'); + const toggles = $j('.batch-selection-collapse-toggle', scope); + for (let toggle of toggles) { + BatchAssetSelection.toggleCollapse(toggle, false); + } + + return false; + }, + + expandRecursively: function () { + const scope = $j(this).closest('.batch-selection-scope').children('.batch-selection-collapse'); + const toggles = $j('.batch-selection-collapse-toggle', scope); + for (let toggle of toggles) { + BatchAssetSelection.toggleCollapse(toggle, true); + } + + return false; + }, + + hideBlocked: function () { + const children = $j(this).closest('.batch-selection-scope') + .find(BatchAssetSelection.blockedSelectors) + .closest('.batch-selection-asset'); + for (let child of children) { + const element = $j(child); + // Don't hide if any non-blocked children + if (!$j(':checkbox', element).length) { + element.hide(); + } + } + + return false; + }, + + showBlocked: function () { + $j(this).closest('.batch-selection-scope') + .find(BatchAssetSelection.blockedSelectors) + .closest('.batch-selection-asset').show(); + + return false; + } +} diff --git a/app/assets/javascripts/checkbox.js b/app/assets/javascripts/checkbox.js deleted file mode 100644 index d5751b2fa2..0000000000 --- a/app/assets/javascripts/checkbox.js +++ /dev/null @@ -1,42 +0,0 @@ -$j(document).ready(function () { - $j("a.selectChildren").click(function (event) { - event.preventDefault(); - selectChildren(this,$j(this).data("cb_parent_selector")) - }) - $j("a.deselectChildren").click(function (event) { - event.preventDefault(); - deselectChildren(this,$j(this).data("cb_parent_selector")) - }) - $j('#jstree').on('click', 'a.selectChildren', function (event) { - event.preventDefault(); - selectChildren(this,$j(this).data("cb_parent_selector")) - }) - $j('#jstree').on('click', 'a.deselectChildren', function (event) { - event.preventDefault(); - deselectChildren(this,$j(this).data("cb_parent_selector")) - }) -}) - -function selectChildren(select_all_element,cb_parent_selector){ - let children_checkboxes = $j(':checkbox', $j(select_all_element).parents(cb_parent_selector)) - for(let checkbox of children_checkboxes){ - let checkbox_element = { className: checkbox.className, checked: true } - checkRepeatedItems(checkbox_element) - } -} - -function deselectChildren(deselect_all_element,cb_parent_selector){ - let children_checkboxes = $j(':checkbox', $j(deselect_all_element).parents(cb_parent_selector)) - for(let checkbox of children_checkboxes){ - let checkbox_element = { className: checkbox.className, checked: false } - checkRepeatedItems(checkbox_element) - } -} - -function checkRepeatedItems(checkbox_element) { - let repeated_elements = document.getElementsByClassName(checkbox_element.className) - let check = checkbox_element.checked - for(let element of repeated_elements){ - element.checked = check - } -} \ No newline at end of file diff --git a/app/assets/javascripts/licenses.js b/app/assets/javascripts/licenses.js new file mode 100644 index 0000000000..cd3e115e8f --- /dev/null +++ b/app/assets/javascripts/licenses.js @@ -0,0 +1,23 @@ +var Licenses = { + init: function () { + $j('[data-seek-license-select="true"]').on('select2:select', Licenses.displayUrl); + $j('[data-seek-license-select="true"]').trigger('select2:select'); + }, + + displayUrl: function () { + var element = $j('option:selected', $j(this)); + var link = $j('#license-url'); + if (link.length) { + var block = link.parents('.license-url-block'); + + if (element.data('url')) { + block.show(); + } else { + block.hide(); + } + + link.attr('href', element.data('url')); + link.html(element.data('url')); + } + } +} diff --git a/app/assets/javascripts/single_page/dynamic_table.js.erb b/app/assets/javascripts/single_page/dynamic_table.js.erb index dbcbbb2601..cdd63249ee 100644 --- a/app/assets/javascripts/single_page/dynamic_table.js.erb +++ b/app/assets/javascripts/single_page/dynamic_table.js.erb @@ -230,6 +230,8 @@ const handleSelect = (e) => { sampleLinksTitles.map((x) => `"${x}"`).join(", ") + "\nYou need to manually input them." ); + }).catch((error) =>{ + alert(`Paste action aborted:\n\n${error}\n\nNote: Some browsers require to click 'paste' to paste the information in the table.`); }); } catch (error) { diff --git a/app/assets/javascripts/single_page/index.js.erb b/app/assets/javascripts/single_page/index.js.erb index f9beb902db..85994ee285 100644 --- a/app/assets/javascripts/single_page/index.js.erb +++ b/app/assets/javascripts/single_page/index.js.erb @@ -282,6 +282,25 @@ async function batchUpdateSample(sampleTypes) { } } +async function handleUploadSubmit(formData){ + $j.ajax({ + type: 'POST', + url: "<%= upload_samples_single_pages_path %>", + data: formData, + dataType: 'html', + processData: false, + contentType: false, + enctype: 'multipart/form-data', + success: function(response){ + $j('#upload-excel-modal').modal({backdrop: 'static', keyboard: false}, 'show').focus(); + $j('#upload-excel').html(response); + }, + error: function(err){ + location.reload(); // Page needs reloading for the notice message to appear + } + }); +} + function isMobile() { /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) } diff --git a/app/assets/javascripts/zenodo_form.js b/app/assets/javascripts/zenodo_form.js index 8f003bee94..30c43b5722 100644 --- a/app/assets/javascripts/zenodo_form.js +++ b/app/assets/javascripts/zenodo_form.js @@ -1,21 +1,6 @@ var zenodoExport = { creatorList: [], - setLicenseUrl: function () { - var element = $j('#license-select option:selected'); - var link = $j('#license-url'); - var block = link.parents('.license-url-block'); - - if(element.data('url') == '') { - block.hide(); - } else { - block.show(); - } - - link.attr('href', element.data('url')); - link.html(element.data('url')); - }, - enableSection: function (sectionElement) { sectionElement.show().children(":input").prop("disabled", false); }, diff --git a/app/assets/stylesheets/linked_custom_metadata.css b/app/assets/stylesheets/linked_custom_metadata.css index 265f876954..596f1cb2f4 100644 --- a/app/assets/stylesheets/linked_custom_metadata.css +++ b/app/assets/stylesheets/linked_custom_metadata.css @@ -28,4 +28,30 @@ padding: 10px 15px 10px 15px; border-radius: 5px; -} \ No newline at end of file +} + +.multi_linked_custom_metdata { + background-color: rgb(246, 246, 246); + padding: 10px 15px 10px 15px; + border-radius: 5px; + margin-bottom: 15px; +} + +.linked_custom_metdata_display .panel-default .panel-heading { + font-weight: bold; + border: none; + background-color: #ffffff; + padding: 0px 0px; +} + + +.linked_custom_metdata_display .panel { + border: none; + box-shadow: none; + margin-bottom: 0px; +} + +.linked_custom_metdata_display .panel-body { + padding: 0px 0px; + border-radius: 5px; +} diff --git a/app/assets/stylesheets/publishing.scss b/app/assets/stylesheets/publishing.scss index 1de7aec917..3a9e3bed42 100644 --- a/app/assets/stylesheets/publishing.scss +++ b/app/assets/stylesheets/publishing.scss @@ -17,14 +17,6 @@ ul.publishing_options li.secondary { } -.published { - color: limegreen; -} - -span.published { - color: green; -} - span.approve { color: limegreen; font-weight: bolder; @@ -64,13 +56,12 @@ ul.item_for_decision { .publishing_options { padding: 0.5em 1em; - &.publishable { - background-color: #fafaff; - border-left-color: $btn-success-bg; + &.not-manageable { + border-left-color: $btn-warning-bg; } - &.not-publishable { - border-left-color: $btn-warning-bg; + &.not-visible { + border-left-color: $btn-danger-bg; } &.already-published { @@ -78,18 +69,6 @@ ul.item_for_decision { } } -.publish-colour { - color: $btn-success-bg; -} - -.publish-checkbox { - background-color: $btn-success-bg; - color: $btn-success-color; - padding: 0px 10px; - border-radius: 5px; - display: inline-block; -} - ul.decided_item { padding-left: 1em; } @@ -108,3 +87,43 @@ ul.decided_item li.type_and_title { padding: 0.5em 15px; margin: 0.3em 0em; } + +.batch-selection-buttons { + margin-bottom: 1em; + display: flex; + gap: 1em; +} + +.batch-selection-asset-row { + display: flex; + gap: 1em; + align-items: center; + .visibility_icon { + margin: 0; + } +} + +.batch-selection-permission-list, +.batch-selection-managed-by-list, +.batch-selection-children { + margin-left: 3em; +} + +.batch-selection-check-btn { + input { + margin: 0; + vertical-align: middle; + } +} + +.batch-selection-collapse-toggle { + cursor: pointer; + + .glyphicon-menu-down { display: none; }; + .glyphicon-menu-right { display: inline; }; + + &.open { + .glyphicon-menu-down { display: inline; }; + .glyphicon-menu-right { display: none; }; + } +} diff --git a/app/assets/stylesheets/sharing.scss b/app/assets/stylesheets/sharing.scss index d396101a0c..6f56e31c5f 100644 --- a/app/assets/stylesheets/sharing.scss +++ b/app/assets/stylesheets/sharing.scss @@ -210,17 +210,3 @@ padding-left: 16px; } } - -.parent-btn-checkbox { - padding: 1px 6px 0px 6px; - border-radius: 5px; - display: inline-block; - height: 25px; -} -.parent-btn-dropdown { - padding: 2px 6px 10px 0px; - border-radius: 5px; - display: inline-block; - height: 25px; -} - diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 218c6a81b4..77d2b76e3d 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -163,6 +163,7 @@ def update_features_enabled Seek::Config.life_monitor_url = params[:life_monitor_url]&.strip&.chomp('/') Seek::Config.life_monitor_client_id = params[:life_monitor_client_id]&.strip Seek::Config.life_monitor_client_secret = params[:life_monitor_client_secret]&.strip + Seek::Config.life_monitor_ui_url = params[:life_monitor_ui_url]&.strip&.chomp('/') Seek::Config.bio_tools_enabled = string_to_boolean(params[:bio_tools_enabled]) @@ -331,8 +332,8 @@ def update_settings Seek::Config.default_license = params[:default_license] Seek::Config.metadata_license = params[:metadata_license] - Seek::Config.recommended_data_licenses = params[:recommended_data_licenses] - Seek::Config.recommended_software_licenses = params[:recommended_software_licenses] + Seek::Config.recommended_data_licenses = params[:recommended_data_licenses]&.compact_blank + Seek::Config.recommended_software_licenses = params[:recommended_software_licenses]&.compact_blank update_flag = (pubmed_email == '' || pubmed_email_valid) && (crossref_email == '' || crossref_email_valid) update_redirect_to update_flag, 'settings' end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 734f468bac..37c31e87a6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -597,33 +597,26 @@ def relationify_collection(collection) end def determine_custom_metadata_keys - - root_key = controller_name.singularize.to_sym - return [] unless params[root_key][:custom_metadata_attributes].present? - attribute_params = params[root_key][:custom_metadata_attributes] - recursive_determine_custom_metadata_keys(attribute_params) - + keys = [] + type_id = params.dig(controller_name.singularize.to_sym, :custom_metadata_attributes, :custom_metadata_type_id) + if type_id.present? + metadata_type = CustomMetadataType.find(type_id) + keys = [:custom_metadata_type_id, :id, data: recursive_determine_custom_metadata_keys(metadata_type)] + end + keys end - # todo currently 2-level nested attributes are tested, we would like to test if it also works with more level nested attributes - def recursive_determine_custom_metadata_keys(attribute_params) + def recursive_determine_custom_metadata_keys(metadata_type) keys = [] - if attribute_params && attribute_params[:custom_metadata_type_id].present? - metadata_type = CustomMetadataType.find(attribute_params[:custom_metadata_type_id]) - if metadata_type - keys = [:custom_metadata_type_id,:id,:custom_metadata_attribute_id] - cma= [] - metadata_type.custom_metadata_attributes.each do |attr| - if attr.sample_attribute_type.controlled_vocab? || attr.sample_attribute_type.seek_sample_multi? - cma << {attr.title=>[]} - cma << attr.title.to_s - elsif attr.linked_custom_metadata? - cma << { attr.title => recursive_determine_custom_metadata_keys(attribute_params[:data][attr.title.to_sym])} - else - cma << attr.title.to_s - end - end - keys = keys + [{data:cma}] + metadata_type.custom_metadata_attributes.each do |attr| + key = attr.title.to_sym + if attr.sample_attribute_type.controlled_vocab? || attr.sample_attribute_type.seek_sample_multi? || attr.sample_attribute_type.seek_sample? + keys << key + keys << { key => [] } + elsif attr.linked_custom_metadata? || attr.linked_custom_metadata_multi? + keys << { key => recursive_determine_custom_metadata_keys(attr.linked_custom_metadata_type) } + else + keys << key end end keys diff --git a/app/controllers/assays_controller.rb b/app/controllers/assays_controller.rb index 48c13e0dd1..796642c82d 100644 --- a/app/controllers/assays_controller.rb +++ b/app/controllers/assays_controller.rb @@ -6,6 +6,7 @@ class AssaysController < ApplicationController before_action :assays_enabled? before_action :find_assets, :only=>[:index] before_action :find_and_authorize_requested_item, :only=>[:edit, :update, :destroy, :manage, :manage_update, :show, :new_object_based_on_existing_one] + before_action :delete_linked_sample_types, only: [:destroy] #project_membership_required_appended is an alias to project_membership_required, but is necessary to include the actions #defined in the application controller @@ -18,7 +19,7 @@ class AssaysController < ApplicationController api_actions :index, :show, :create, :update, :destroy def new_object_based_on_existing_one - @existing_assay = Assay.find(params[:id]) + @existing_assay = Assay.find(params[:id]) @assay = @existing_assay.clone_with_associations if @existing_assay.can_view? @@ -62,9 +63,7 @@ def new_object_based_on_existing_one flash[:error]="You do not have the necessary permissions to copy this #{t('assays.assay')}" redirect_to @existing_assay end - - - end + end def new @assay=setup_new_asset @@ -112,6 +111,13 @@ def create end end + + def delete_linked_sample_types + return unless is_single_page_assay? + + @assay.sample_type.destroy + end + def update update_assay_organisms @assay, params update_assay_human_diseases @assay, params @@ -177,4 +183,10 @@ def assay_params assay_params[:model_ids].select! { |id| Model.find_by_id(id).try(:can_view?) } if assay_params.key?(:model_ids) end end + + def is_single_page_assay? + return false unless params.key?(:return_to) + + params[:return_to].start_with? '/single_pages/' + end end diff --git a/app/controllers/samples_controller.rb b/app/controllers/samples_controller.rb index 6e72ae8e23..91d53f5645 100644 --- a/app/controllers/samples_controller.rb +++ b/app/controllers/samples_controller.rb @@ -75,6 +75,11 @@ def show def edit @sample = Sample.find(params[:id]) + if !@sample.originating_data_file.nil? && @sample.edit_count.zero? + flash.now[:error] = 'Warning: This sample was extracted from a datafile. + If you edit the sample, it will no longer correspond to the original source data.
+ Unless you cancel, a label will be added to the sample\'s source field to indicate it is no longer valid.'.html_safe + end respond_with(@sample) end @@ -201,7 +206,7 @@ def typeahead query = params[:q] || '' sample_type = SampleType.find(params[:linked_sample_type_id]) results = sample_type.samples.where("LOWER(title) like :query", - query: "%#{query.downcase}%").limit(params[:limit] || 100) + query: "%#{query.downcase}%").limit(params[:limit] || 100).authorized_for(:view) items = results.map do |sa| { id: sa.id, text: sa.title } @@ -266,7 +271,7 @@ def sample_params(sample_type = nil, parameters = params) if sample_type sample_type.sample_attributes.each do |attr| - if attr.sample_attribute_type.controlled_vocab? || attr.sample_attribute_type.seek_sample_multi? + if attr.sample_attribute_type.controlled_vocab? || attr.sample_attribute_type.seek_sample_multi? || attr.sample_attribute_type.seek_sample? sample_type_param_keys << { attr.title => [] } sample_type_param_keys << attr.title.to_sym else diff --git a/app/controllers/single_pages_controller.rb b/app/controllers/single_pages_controller.rb index 1914f403bc..591eb2600c 100644 --- a/app/controllers/single_pages_controller.rb +++ b/app/controllers/single_pages_controller.rb @@ -1,8 +1,11 @@ require 'isatab_converter' +# Controller for the Single Page view class SinglePagesController < ApplicationController include Seek::AssetsCommon include Seek::Sharing::SharingCommon + include Seek::Publishing::PublishingCommon + include Seek::Data::SpreadsheetExplorerRepresentation before_action :set_up_instance_variable before_action :project_single_page_enabled? @@ -14,9 +17,7 @@ class SinglePagesController < ApplicationController def show @project = Project.find(params[:id]) @folders = project_folders - respond_to do |format| - format.html - end + respond_to(&:html) end def index; end @@ -61,21 +62,20 @@ def export_isa end def download_samples_excel - sample_ids, sample_type_id, study_id, assay_id = Rails.cache.read(params[:uuid]).values_at(:sample_ids, :sample_type_id, - :study_id, :assay_id) + :study_id, :assay_id) @study = Study.find(study_id) @assay = Assay.find(assay_id) unless assay_id.nil? @project = @study.projects.first - @samples = Sample.where(id: sample_ids)&.authorized_for(:view).sort_by(&:id) + @samples = Sample.where(id: sample_ids)&.authorized_for(:view)&.sort_by(&:id) - raise "Nothing to export to Excel." if @samples.nil? || @samples == [] || sample_type_id.nil? + raise 'Nothing to export to Excel.' if @samples.nil? || @samples == [] || sample_type_id.nil? @sample_type = SampleType.find(sample_type_id) sample_attributes = @sample_type.sample_attributes.map do |sa| - obj = if (sa.sample_controlled_vocab_id.nil?) + obj = if sa.sample_controlled_vocab_id.nil? { sa_cv_title: sa.title, sa_cv_id: nil } else { sa_cv_title: sa.title, sa_cv_id: sa.sample_controlled_vocab_id } @@ -83,17 +83,20 @@ def download_samples_excel obj.merge({ required: sa.required }) end - @sa_cv_terms = [{ "name" => "id", "has_cv" => false, "data" => nil, "allows_custom_input" => nil, "required" => nil }, - { "name" => "uuid", "has_cv" => false, "data" => nil, "allows_custom_input" => nil, "required" => nil }] + @sa_cv_terms = [{ 'name' => 'id', 'has_cv' => false, 'data' => nil, 'allows_custom_input' => nil, 'required' => nil }, + { 'name' => 'uuid', 'has_cv' => false, 'data' => nil, 'allows_custom_input' => nil, + 'required' => nil }] sample_attributes.map do |sa| - if sa[:sa_cv_id].nil? - @sa_cv_terms.push({ "name" => sa[:sa_cv_title], "has_cv" => false, "data" => nil, "allows_custom_input" => nil, "required" => sa[:required] }) - else - allows_custom_input = SampleControlledVocab.find(sa[:sa_cv_id])&.custom_input - sa_terms = SampleControlledVocabTerm.where(sample_controlled_vocab_id: sa[:sa_cv_id]).map(&:label) - @sa_cv_terms.push({ "name" => sa[:sa_cv_title], "has_cv" => true, "data" => sa_terms, "allows_custom_input" => allows_custom_input, "required" => sa[:required] }) - end + if sa[:sa_cv_id].nil? + @sa_cv_terms.push({ 'name' => sa[:sa_cv_title], 'has_cv' => false, 'data' => nil, + 'allows_custom_input' => nil, 'required' => sa[:required] }) + else + allows_custom_input = SampleControlledVocab.find(sa[:sa_cv_id])&.custom_input + sa_terms = SampleControlledVocabTerm.where(sample_controlled_vocab_id: sa[:sa_cv_id]).map(&:label) + @sa_cv_terms.push({ 'name' => sa[:sa_cv_title], 'has_cv' => true, 'data' => sa_terms, + 'allows_custom_input' => allows_custom_input, 'required' => sa[:required] }) + end end @template = Template.find(@sample_type.template_id) @@ -102,13 +105,15 @@ def download_samples_excel flash[:error] = e.message respond_to do |format| format.html { redirect_to single_page_path(@project.id) } - format.json { render json: { parameters: { sample_ids: sample_ids, sample_type_id: sample_type_id, study_id: study_id } } } + format.json do + render json: { parameters: { sample_ids:, sample_type_id:, study_id: } } + end end end def export_to_excel cache_uuid = UUID.new.generate - id_label = "#{Seek::Config::instance_name} id" + id_label = "#{Seek::Config.instance_name} id" sample_ids = JSON.parse(params[:sample_data]).map { |sample| sample[id_label] unless sample[id_label] == '#HIDDEN' } sample_type_id = JSON.parse(params[:sample_type_id]) study_id = JSON.parse(params[:study_id]) @@ -122,8 +127,232 @@ def export_to_excel end end + def upload_samples + uploaded_file = params[:file] + project_id = params[:project_id] + @project = Project.find(project_id) + + # Check file type if is xls or xlsx + case uploaded_file.content_type + when 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + spreadsheet_xml = spreadsheet_to_xml(uploaded_file.path, Seek::Config.jvm_memory_allocation) + wb = parse_spreadsheet_xml(spreadsheet_xml) + metadata_sheet = wb.sheet('Metadata') + samples_sheet = wb.sheet('Samples') + else + raise "Please upload a valid spreadsheet file with extension '.xlsx'" + end + + sample_type_id_ui = params[:sample_type_id].to_i + + unless valid_workbook?(wb) + raise 'Invalid workbook! Cannot process this spreadsheet. Consider first exporting the table as a spreadsheet for the proper format.' + end + + # Extract Samples metadata from spreadsheet + study_id = metadata_sheet.cell(2, 2).value.to_i + @study = Study.find(study_id) + sample_type_id = metadata_sheet.cell(5, 2).value.to_i + @sample_type = SampleType.find(sample_type_id) + is_assay = @sample_type.assays.any? + @assay = @sample_type.assays.first + + # Sample Type validation rules + unless sample_type_id_ui == @sample_type&.id + raise "Sample Type #{@sample_type&.id} from spreadsheet doesn't match Sample Type #{sample_type_id_ui} from the table. Please upload in the correct table." + end + unless @study.sample_types.include?(@sample_type) || is_assay + raise "Sample Type '#{@sample_type.id}' doesn't belong to Study #{@study.id}. Sample Upload aborted." + end + unless (@assay&.sample_type == @sample_type) || !is_assay + raise "Sample Type '#{@sample_type.id}' doesn't belong to Assay #{@assay.id}. Sample Upload aborted." + end + + @multiple_input_fields = @sample_type.sample_attributes.map do |sa_attr| + sa_attr.title if sa_attr.sample_attribute_type.base_type == 'SeekSampleMulti' + end + + sample_fields, samples_data = get_spreadsheet_data(samples_sheet) + + # Compare Excel header row to Sample Type Sample Attributes + # Should raise an error if they don't match + sample_type_attributes = %w[id uuid].concat(@sample_type.sample_attributes.map(&:title)) + has_unmapped_sample_attributes = sample_type_attributes.map { |sa| sample_fields.include?(sa) }.include?(false) + if has_unmapped_sample_attributes + raise "The Sample Attributes from the excel sheet don't match those of the Sample Type in the database. Sample upload was aborted!" + end + + # Construct Samples objects from Excel data + excel_samples = generate_excel_samples(samples_data, sample_fields) + + existing_excel_samples = excel_samples.map { |sample| sample unless sample['id'].nil? }.compact + new_excel_samples = excel_samples.map { |sample| sample if sample['id'].nil? }.compact + + # Retrieve all samples of the Sample Type, also the unauthorized ones + @db_samples = sample_type_samples(@sample_type) + # Retrieve the Sample Types samples wich are authorized for editing + @authorized_db_samples = sample_type_samples(@sample_type, :edit) + + # Determine whether samples have been modified or not, + # and checking whether the user is permitted to edit them + @unauthorized_samples, @update_samples = separate_unauthorized_samples(existing_excel_samples, @db_samples, + @authorized_db_samples) + + # Determine if the new samples are no duplicates of existing ones, + # based on the attribute values + @possible_duplicates, @new_samples = separate_possible_duplicates(new_excel_samples, @db_samples) + + upload_data = { study: @study, + assay: @assay, + sampleType: @sample_type, + excel_samples:, + existingExcelSamples: existing_excel_samples, + newExcelSamples: new_excel_samples, + updateSamples: @update_samples, + newSamples: @new_samples, + possibleDuplicates: @possible_duplicates, + dbSamples: @db_samples, + authorized_db_samples: @authorized_db_samples, + unauthorized_samples: @unauthorized_samples } + + respond_to do |format| + format.json { render json: { uploadData: upload_data } } + format.html { render 'single_pages/sample_upload_content', { layout: false } } + end + rescue StandardError => e + flash[:error] = e.message + respond_to do |format| + format.html { redirect_to single_page_path(@project), status: :bad_request } + format.json { render json: { error: e }, status: :bad_request } + end + end + private + def get_spreadsheet_data(samples_sheet) + sample_fields = samples_sheet.row(1).actual_cells.map { |field| field&.value&.sub(' *', '') }.compact + samples_data = (2..samples_sheet.actual_rows.size).map do |i| + sample_cells = samples_sheet.row(i).cells + next if sample_cells.all? { |cell| (cell&.value == '' || cell&.value.nil?) } + + sample_cells.map do |cell| + cell&.value + end.drop(1) + end.compact + + [sample_fields, samples_data] + end + + def generate_excel_samples(samples_data, sample_fields) + samples_data.map do |excel_sample| + obj = {} + (0..sample_fields.size - 1).map do |i| + if @multiple_input_fields.include?(sample_fields[i]) + parsed_excel_input_samples = JSON.parse(excel_sample[i].gsub(/"=>/x, '":')).map do |subsample| + # Uploader should at least have viewing permissions for the inputs he's using + unless Sample.find(subsample['id'])&.authorized_for_view? + raise "Unauthorized Sample was detected in spreadsheet: #{subsample.inspect}" + end + + subsample + end + obj.merge!(sample_fields[i] => parsed_excel_input_samples) + elsif sample_fields[i] == 'id' + if excel_sample[i] == '' + obj.merge!(sample_fields[i] => nil) + else + obj.merge!(sample_fields[i] => excel_sample[i]&.to_i) + end + else + obj.merge!(sample_fields[i] => excel_sample[i]) + end + end + obj + end + end + + def sample_type_samples(sample_type, authorization_method = nil) + if authorization_method + sample_type.samples&.authorized_for(authorization_method)&.map do |sample| + attributes = JSON.parse(sample[:json_metadata]) + { 'id' => sample.id, + 'uuid' => sample.uuid }.merge(attributes) + end + else + sample_type.samples&.map do |sample| + attributes = JSON.parse(sample[:json_metadata]) + { 'id' => sample.id, + 'uuid' => sample.uuid }.merge(attributes) + end + end + end + + def separate_unauthorized_samples(existing_excel_samples, db_samples, authorized_db_samples) + update_samples = [] + unauthorized_samples = [] + existing_excel_samples.map do |ees| + db_sample = db_samples.select { |s| s['id'] == ees['id'] }.first + + # An exception is raised if the ID of an existing Sample cannot be found in the DB + raise "Sample with id '#{ees['id']}' does not exist in the database. Sample upload was aborted!" if db_sample.nil? + + is_authorized_for_update = authorized_db_samples.select { |s| s['id'] == ees['id'] }.any? + + is_changed = false + + db_sample.map do |k, v| + unless ees[k] == v + is_changed = true + break + end + end + + if is_changed + if is_authorized_for_update + update_samples.append(ees) + else + unauthorized_samples.append(ees) + end + end + end + [unauthorized_samples, update_samples] + end + + def separate_possible_duplicates(new_excel_samples, db_samples) + possible_duplicates = [] + new_samples = [] + new_excel_samples.map do |nes| + is_duplicate = true + + db_samples.map do |dbs| + dbs.map do |k, v| + unless %w[id uuid].include?(k) + is_duplicate = (nes[k] == v) + break unless is_duplicate + end + end + + if is_duplicate + possible_duplicates.append(nes.merge({ 'duplicate' => dbs })) + break + end + end + + if db_samples.none? + new_samples.append(nes) + else + new_samples.append(nes) unless is_duplicate + end + end + [possible_duplicates, new_samples] + end + + def valid_workbook?(workbook) + !((workbook.sheet_names.map do |sheet| + %w[Metadata Samples cv_ontology].include? sheet + end.include? false) && (workbook.sheets.size != 3)) + end + def set_up_instance_variable @single_page = true end @@ -134,8 +363,8 @@ def find_authorized_investigation end def check_user_logged_in - unless current_user - render json: { status: :unprocessable_entity, error: 'You must be logged in to access batch sharing permission.' } - end + return if current_user + + render json: { status: :unprocessable_entity, error: 'You must be logged in to access batch sharing permission.' } end end diff --git a/app/controllers/snapshots_controller.rb b/app/controllers/snapshots_controller.rb index 7f1d9fb36a..ebeb52f528 100644 --- a/app/controllers/snapshots_controller.rb +++ b/app/controllers/snapshots_controller.rb @@ -15,8 +15,13 @@ class SnapshotsController < ApplicationController def create @snapshot = @resource.create_snapshot - flash[:notice] = "Snapshot created" - redirect_to polymorphic_path([@resource, @snapshot]) + if @snapshot + flash[:notice] = "Snapshot created" + redirect_to polymorphic_path([@resource, @snapshot]) + else + flash[:error] = @resource.errors.full_messages.join(', ') + redirect_to polymorphic_path(@resource) + end end def show diff --git a/app/controllers/studies_controller.rb b/app/controllers/studies_controller.rb index 2aed0d36a1..4386e966b9 100644 --- a/app/controllers/studies_controller.rb +++ b/app/controllers/studies_controller.rb @@ -6,6 +6,7 @@ class StudiesController < ApplicationController before_action :studies_enabled? before_action :find_assets, only: [:index] before_action :find_and_authorize_requested_item, only: %i[edit update destroy manage manage_update show new_object_based_on_existing_one] + before_action :delete_linked_sample_types, only: [:destroy] # project_membership_required_appended is an alias to project_membership_required, but is necesary to include the actions # defined in the application controller @@ -69,11 +70,10 @@ def update format.html { redirect_to(@study) } end else - @study.attributes = study_params + @study.assign_attributes(study_params) update_sharing_policies @study update_annotations(params[:tag_list], @study) update_relationships(@study, params) - update_linked_custom_metadatas @study respond_to do |format| if @study.save @@ -88,6 +88,16 @@ def update end end + def delete_linked_sample_types + return unless is_single_page_study? + + # The study sample types must be destroyed in reversed order + # otherwise the first sample type won't be removed becaused it is linked from the second + study_st_ids = @study.sample_types.map(&:id).sort { |a, b| b <=> a } + SampleType.destroy(study_st_ids) + end + + def show @study = Study.find(params[:id]) @@ -103,7 +113,6 @@ def create update_sharing_policies @study update_annotations(params[:tag_list], @study) update_relationships(@study, params) - update_linked_custom_metadatas @study ### TO DO: what about validation of person responsible? is it already included (for json?) if @study.save @@ -201,7 +210,7 @@ def batch_create study_params = { title: params[:studies][:title][index], description: params[:studies][:description][index], - investigation_id: params[:study][:investigation_id], + investigation_id: params[:study][:investigation_id], custom_metadata: CustomMetadata.new( custom_metadata_type: metadata_types, data: metadata @@ -351,3 +360,9 @@ def study_params { custom_metadata_attributes: determine_custom_metadata_keys }) end end + +def is_single_page_study? + return false unless params.key?(:return_to) + + params[:return_to].start_with? '/single_pages/' +end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 7e225e2201..9143eef2f3 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -91,7 +91,7 @@ def task_status def populate_template uploaded_file = params[:template_json_file] - dir = Rails.root.join('config', 'default_data', 'source_types') + dir = Seek::Config.append_filestore_path('source_types') if Dir.exist?(dir) `rm #{dir}/*` @@ -177,11 +177,11 @@ def set_status end def lockfile - Rails.root.join('tmp', 'populate_templates.lock') + Rails.root.join(Seek::Config.temporary_filestore_path, 'populate_templates.lock') end def resultfile - Rails.root.join('tmp', 'populate_templates.result') + Rails.root.join(Seek::Config.temporary_filestore_path, 'populate_templates.result') end def running! diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 3b8261316c..ab67f6ea1b 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -94,11 +94,11 @@ def git_link_tag end end - def admin_setting_block(title, description) + def admin_setting_block(title, description = nil) content_tag(:div, class: 'form-group') do - content_tag(:label, title) + - (description ? content_tag(:p, description.html_safe, class: 'help-block') : ''.html_safe) + - yield + concat content_tag(:label, title) + concat content_tag(:p, description.html_safe, class: 'help-block') if description + concat yield end end end diff --git a/app/helpers/assets_helper.rb b/app/helpers/assets_helper.rb index b3b74a9228..05c4d64ee1 100644 --- a/app/helpers/assets_helper.rb +++ b/app/helpers/assets_helper.rb @@ -94,18 +94,6 @@ def publishing_item_param(item) "publish[#{item.class.name}][#{item.id}]" end - def sharing_item_param(item) - if item.try(:is_isa?) - "share_isa[#{item.class.name}][#{item.id}]" - elsif (item.respond_to? (:investigations)) && (!item.investigations.any?) - "share_not_isa[#{item.class.name}][#{item.id}]" - elsif !item.respond_to? (:investigations) - "share_not_isa[#{item.class.name}][#{item.id}]" - else - "share_isa[#{item.class.name}][#{item.id}]" - end - end - def include_downloadable_item?(items) has_downloadable_item = false items.each do |item| @@ -346,4 +334,10 @@ def controlled_vocab_annotation_items(controlled_vocab_terms) end.join(', ').html_safe end + def batch_selection_collapse_toggle + content_tag(:span, class: 'batch-selection-collapse-toggle open') do + concat content_tag(:span, '', class: 'glyphicon glyphicon-menu-down', 'aria-hidden' => 'true') + concat content_tag(:span, '', class: 'glyphicon glyphicon-menu-right', 'aria-hidden' => 'true') + end + end end diff --git a/app/helpers/bootstrap_helper.rb b/app/helpers/bootstrap_helper.rb index 1c56e72242..d50ba165b9 100644 --- a/app/helpers/bootstrap_helper.rb +++ b/app/helpers/bootstrap_helper.rb @@ -127,20 +127,23 @@ def objects_input(element_name, existing_objects = [], options = {}, value_metho options['data-allow-new-items'] = options.delete(:allow_new) if options[:allow_new] options['data-placeholder'] = options.delete(:placeholder) if options[:placeholder] options[:include_blank] = '' - options[:multiple] = true - options[:name] = "#{element_name}[]" unless options.key?(:name) + options[:multiple] = true unless options.key?(:multiple) + options[:name] = "#{element_name}#{options[:multiple] ? '[]': ''}" unless options.key?(:name) options.merge!(typeahead_options(options.delete(:typeahead))) if options[:typeahead] - select_options = options_from_collection_for_select( - existing_objects, - value_method, text_method, - existing_objects.collect { |obj| obj.send(value_method) } - ) + select_options = options.delete(:select_options) || + options_from_collection_for_select( + existing_objects, + value_method, text_method, + existing_objects.collect { |obj| obj.send(value_method) } + ) - hidden_field_tag(element_name, '', name: options[:name]) + - select_tag(element_name, - select_options, - options) + tag = select_tag(element_name, select_options, options) + if options[:multiple] + hidden_field_tag(element_name, '', name: options[:name]) + tag + else + tag + end end def modal(options = {}, &block) diff --git a/app/helpers/custom_metadata_helper.rb b/app/helpers/custom_metadata_helper.rb index 1801554c4c..57267f2ba2 100644 --- a/app/helpers/custom_metadata_helper.rb +++ b/app/helpers/custom_metadata_helper.rb @@ -5,15 +5,15 @@ def custom_metadata_form_field_for_attribute(attribute, resource) element_class = "custom_metadata_attribute_#{attribute.sample_attribute_type.base_type.downcase}" element_name = "#{resource.class.name.underscore}[custom_metadata_attributes][data][#{attribute.title}]" - if attribute.linked_custom_metadata? + if attribute.linked_custom_metadata? || attribute.linked_custom_metadata_multi? content_tag(:span, class: 'linked_custom_metdata') do folding_panel(attribute.label, false, id:attribute.title) do - attribute_form_element(attribute, resource.custom_metadata, element_name, element_class) + attribute_form_element(attribute, resource.custom_metadata.get_attribute_value(attribute.title), element_name, element_class) end end else content_tag(:label,attribute.label, class: attribute.required? ? 'required' : '') + - attribute_form_element(attribute, resource.custom_metadata, element_name, element_class) + attribute_form_element(attribute, resource.custom_metadata.get_attribute_value(attribute.title), element_name, element_class) end end @@ -24,4 +24,22 @@ def custom_metadata_attribute_description(description) html.html_safe end -end \ No newline at end of file + def render_custom_metadata_value(attribute, resource) + + if resource.custom_metadata.data[attribute.title].blank? + return '' # Return an empty string if the custom metadata is blank. + end + + content_tag(:div, class: 'custom_metadata') do + if attribute.linked_custom_metadata? || attribute.linked_custom_metadata_multi? + content_tag(:span, class: 'linked_custom_metdata_display') do + folding_panel(attribute.label, true, id: attribute.title) do + display_attribute(resource.custom_metadata, attribute, link: true) + end + end + else + label_tag("#{attribute.label}:") + display_attribute(resource.custom_metadata, attribute, link: true) + end + end + end +end diff --git a/app/helpers/license_helper.rb b/app/helpers/license_helper.rb index db4f841fb0..6187ecbb43 100644 --- a/app/helpers/license_helper.rb +++ b/app/helpers/license_helper.rb @@ -3,15 +3,23 @@ module LicenseHelper def license_select(name, selected = nil, opts = {}) - select_tag(name, options_for_select(license_options(opts), selected), opts) - end + opts[:data] ||= {} + opts[:data]['seek-license-select'] ||= 'true' + opts[:multiple] = false + + recommended = opts.delete(:recommended) + source = opts.delete(:source) || Seek::License.combined + if recommended + opts[:select_options] = grouped_options_for_select(grouped_license_options(source.values, recommended), selected) + else + opts[:select_options] = options_for_select(license_options(source.values), selected) + end - def grouped_license_select(name, selected = nil, opts = {}) - select_tag(name, grouped_options_for_select(grouped_license_options(opts), selected), opts) + objects_input(name, [], opts) end - def describe_license(id, source = nil) - license = Seek::License.find(id, source) + def describe_license(id) + license = Seek::License.find(id) content = license_description_content(license) if !license || license.is_null_license? @@ -67,50 +75,34 @@ def license_description_content(license) end end - def license_values(opts = {}) - opts.delete(:source) || Seek::License.open_definition[:all] - end - - def license_options(opts = {}) - license_values(opts).map { |value| [value['title'], value['id'], { 'data-url' => value['url'] }] } - end - - def grouped_license_options(opts = {}) - grouped_licenses = sort_grouped_licenses(group_licenses(opts)) - - grouped_licenses.each do |g, licenses| - licenses.map! { |value| [value['title'], value['id'], { 'data-url' => value['url'] }] } - end - - grouped_licenses - end - - def sort_grouped_licenses(licenses) - s = licenses.sort_by do |pair| - case pair[0] - when 'recommended' - 0 -# when 'Generic' -# 2 + def license_options(licenses) + licenses.map do |value| + [value['title'], value['id'], { 'data-url' => value['url'] }] + end.sort_by do |value| + if value[1] == Seek::License::NULL_LICENSE # Put null license first + '-' else - 3 + value[0] # Otherwise sort by title end end - s.each do |pair| - pair[0] = "#{t('licenses.' + pair[0])}" - end - s end - def group_licenses(opts) - recommended = opts.delete(:recommended) - license_values(opts).group_by do |l| + def grouped_license_options(licenses, recommended) + grouped_licenses = licenses.group_by do |l| if recommended&.include?(l['id']) 'recommended' else 'other' end - end.to_a + end + + grouped_licenses.transform_values! do |l| + license_options(l) + end + + # Transform into array to ensure recommended licenses come first + ['recommended', 'other'].map do |key| + [t("licenses.#{key}"), grouped_licenses[key] || []] + end end - end diff --git a/app/helpers/policy_helper.rb b/app/helpers/policy_helper.rb index 1a70d38ff7..7c431caa93 100644 --- a/app/helpers/policy_helper.rb +++ b/app/helpers/policy_helper.rb @@ -159,7 +159,7 @@ def project_policy_json(project) hash.to_json.html_safe end - def permission_title(permission, member_prefix: false, icon: false) + def permission_title(permission, member_prefix: false, icon: false, link: false) if permission.is_a?(Permission) type = permission.contributor_type contributor = permission.contributor @@ -168,18 +168,31 @@ def permission_title(permission, member_prefix: false, icon: false) contributor = permission end - if type == 'Person' + option = { target: :_blank } + case type + when 'Person' text = "#{contributor.first_name} #{contributor.last_name}" - elsif type == 'WorkGroup' - text = "#{member_prefix ? 'Members of ' : ''}#{contributor.project.title} @ #{contributor.institution.title}" + text = link_to(h(text), contributor, option).html_safe if link + when 'WorkGroup' + institution = contributor.institution + project = contributor.project + if link + text = "#{member_prefix ? 'Members of ' : ''}#{link_to(h(project.title), project, option)} @ #{link_to(h(institution.title), institution, option)}".html_safe + else + text = "#{member_prefix ? 'Members of ' : ''}#{project.title} @ #{institution.title}" + end else - text = "#{member_prefix ? 'Members of ' : ''}#{contributor.title}" + if link + text = "#{member_prefix ? 'Members of ' : ''}#{link_to(h(contributor.title), contributor, option)}".html_safe + else + text = "#{member_prefix ? 'Members of ' : ''}#{contributor.title}" + end end if icon content_tag(:span, class: 'type-icon-wrapper') do image_tag(asset_path(icon_filename_for_key(type.underscore)), class: 'type-icon') - end.html_safe + " #{text}" + end.html_safe + " #{text}".html_safe else text end diff --git a/app/helpers/sample_types_helper.rb b/app/helpers/sample_types_helper.rb index acc6935501..957eadc832 100644 --- a/app/helpers/sample_types_helper.rb +++ b/app/helpers/sample_types_helper.rb @@ -91,7 +91,7 @@ def sample_attribute_pid_help_icon private def displayed_sample_attribute_types - SampleAttributeType.all.select{|x|!x.linked_custom_metadata?} + SampleAttributeType.all.reject{ |x|x.linked_custom_metadata? || x.linked_custom_metadata_multi? } end def attribute_type_link(sample_type_attribute) diff --git a/app/helpers/samples_helper.rb b/app/helpers/samples_helper.rb index cccf841afa..056158ceea 100644 --- a/app/helpers/samples_helper.rb +++ b/app/helpers/samples_helper.rb @@ -3,7 +3,7 @@ def sample_form_field_for_attribute(attribute, resource) element_class = "sample_attribute_#{attribute.sample_attribute_type.base_type.downcase}" element_name = "sample[data][#{attribute.title}]" - attribute_form_element(attribute, resource, element_name, element_class) + attribute_form_element(attribute, resource.get_attribute_value(attribute.title), element_name, element_class) end def controlled_vocab_form_field(sample_controlled_vocab, element_name, values, limit = 1) @@ -41,29 +41,25 @@ def controlled_vocab_list_form_field(sample_controlled_vocab, element_name, valu controlled_vocab_form_field(sample_controlled_vocab, element_name, values, nil) end - def linked_custom_metadata_form_field(attribute,resource,element_name, element_class,depth) - linked_cms = resource.linked_custom_metadatas.select{|cm|cm.custom_metadata_attribute==attribute} - - id = linked_cms.blank? ? nil : linked_cms.select{|cm| cm.custom_metadata_type.id == attribute.linked_custom_metadata_type.id}.first.id + def linked_custom_metadata_multi_form_field(attribute, value, element_name, element_class) + render partial: 'custom_metadata/fancy_linked_custom_metadata_multi_attribute_fields', + locals: { value: value, attribute: attribute, element_name: element_name, element_class: element_class, collapsed: false } + end + def linked_custom_metadata_form_field(attribute, value, element_name, element_class,depth) html = '' - html += hidden_field_tag "#{element_name}[id]",id - html += hidden_field_tag "#{element_name}[custom_metadata_type_id]", attribute.linked_custom_metadata_type.id - html += hidden_field_tag "#{element_name}[custom_metadata_attribute_id]", attribute.id attribute.linked_custom_metadata_type.custom_metadata_attributes.each do |attr| - linked_cm = linked_cms.select{|cm| cm.custom_metadata_type_id == attr.custom_metadata_type_id}.first - linked_cm ||= CustomMetadata.new(:custom_metadata_type_id => attr.custom_metadata_type_id) - - attr_element_name = "#{element_name}][data][#{attr.title}]" + attr_element_name = "#{element_name}[#{attr.title}]" html += '
' html += required_span if attr.required? + v = value ? value[attr.title] : nil if attr.linked_custom_metadata? html += '
' - html += attribute_form_element(attr, linked_cm, attr_element_name, element_class,depth+1) + html += attribute_form_element(attr, v, attr_element_name, element_class,depth+1) html += '
' else - html += attribute_form_element(attr, linked_cm, attr_element_name, element_class) + html += attribute_form_element(attr, v, attr_element_name, element_class) end unless attr.description.nil? @@ -75,13 +71,30 @@ def linked_custom_metadata_form_field(attribute,resource,element_name, element_c html.html_safe end - def sample_multi_form_field(attribute, element_name, value) + def sample_form_field(attribute, element_name, value, limit = 1) + existing_objects = [] str = Struct.new(:id, :title) - value.each {|v| existing_objects << str.new(v[:id], v[:title]) if v} if value + if value + value = [value] unless value.is_a?(Array) + value.compact.each do |v| + id = v[:id] + title = v[:title] + title = 'Hidden' unless Sample.find(id).can_view? + existing_objects << str.new(id, title) + end + end + + typeahead = { query_url: typeahead_samples_path + "?linked_sample_type_id=#{attribute.linked_sample_type.id}", + handlebars_template: 'typeahead/controlled_vocab_term' } objects_input(element_name, existing_objects, - typeahead: { query_url: typeahead_samples_path + "?linked_sample_type_id=#{attribute.linked_sample_type.id}", - handlebars_template: 'typeahead/controlled_vocab_term' }, class: 'form-control') + typeahead: typeahead, + limit: limit, + class: 'form-control') + end + + def sample_multi_form_field(attribute, element_name, value) + sample_form_field(attribute, element_name, value, nil) end def authorised_samples(projects = nil) @@ -101,8 +114,12 @@ def sample_attribute_display_title(attribute) title.html_safe end - def display_attribute(sample, attribute, options = {}) - value = sample.get_attribute_value(attribute) + def display_attribute(resource, attribute, options = {}) + value = resource.get_attribute_value(attribute) + display_attribute_value(value, attribute, options.merge(resource: resource)) + end + + def display_attribute_value(value, attribute, options = {}) if value.blank? text_or_not_specified(value) else @@ -124,9 +141,11 @@ def display_attribute(sample, attribute, options = {}) when Seek::Samples::BaseType::CV_LIST value.each{|v| seek_cv_attribute_display(v, attribute) }.join(', ') when Seek::Samples::BaseType::LINKED_CUSTOM_METADATA - linked_custom_metadata_attribute_display(value) + linked_custom_metadata_attribute_display(value, attribute) + when Seek::Samples::BaseType::LINKED_CUSTOM_METADATA_MULTI + linked_custom_metadata_multi_attribute_display(value, attribute) else - default_attribute_display(attribute, options, sample, value) + default_attribute_display(value, attribute, options) end end end @@ -140,19 +159,36 @@ def seek_cv_attribute_display(value, attribute) content end - def linked_custom_metadata_attribute_display(value) + def linked_custom_metadata_attribute_display(value, attribute) html = '' html += '' html.html_safe end + def linked_custom_metadata_multi_attribute_display(values, attribute) + html = '' + values.each do |value| + html += linked_custom_metadata_attribute_display(value, attribute) + end + html.html_safe + end + def seek_sample_attribute_display(value) if value.kind_of?(Array) value.map {|v| seek_resource_attribute_display(Sample,v)} .join(", ").html_safe @@ -178,14 +214,14 @@ def seek_resource_attribute_display(clz, value) end end - def default_attribute_display(attribute, options, sample, value) - resolution = attribute.resolve (value) - if (resolution != nil) + def default_attribute_display(value, attribute, options) + resolution = attribute.resolve(value) + if resolution link_to(value, resolution, target: :_blank) - else if options[:link] && attribute.is_title - link_to(value, sample) + else + if options[:link] && options[:resource] && attribute.is_title + link_to(value, options[:resource]) else - text_or_not_specified(value, auto_link: options[:link]) end end @@ -265,8 +301,7 @@ def show_sample_extraction_status?(data_file) private - def attribute_form_element(attribute, resource, element_name, element_class, depth=1) - value = resource.get_attribute_value(attribute.title) + def attribute_form_element(attribute, value, element_name, element_class, depth=1) placeholder = "e.g. #{attribute.sample_attribute_type.placeholder}" unless attribute.sample_attribute_type.placeholder.blank? case attribute.sample_attribute_type.base_type @@ -307,14 +342,13 @@ def attribute_form_element(attribute, resource, element_name, element_class, dep when Seek::Samples::BaseType::CV_LIST controlled_vocab_list_form_field attribute.sample_controlled_vocab, element_name, value when Seek::Samples::BaseType::SEEK_SAMPLE - terms = attribute.linked_sample_type.samples.authorized_for('view').to_a - options = options_from_collection_for_select(terms, :id, :title, value.try(:[], 'id')) - select_tag element_name, options, - include_blank: !attribute.required?, class: "form-control #{element_class}" + sample_form_field attribute, element_name, value when Seek::Samples::BaseType::SEEK_SAMPLE_MULTI sample_multi_form_field attribute, element_name, value when Seek::Samples::BaseType::LINKED_CUSTOM_METADATA - linked_custom_metadata_form_field attribute, resource, element_name, element_class,depth + linked_custom_metadata_form_field attribute, value, element_name, element_class,depth + when Seek::Samples::BaseType::LINKED_CUSTOM_METADATA_MULTI + linked_custom_metadata_multi_form_field attribute, value, element_name, element_class else text_field_tag element_name, value, class: "form-control #{element_class}", placeholder: placeholder end diff --git a/app/helpers/sharing_permissions_helper.rb b/app/helpers/sharing_permissions_helper.rb deleted file mode 100644 index 63389141bb..0000000000 --- a/app/helpers/sharing_permissions_helper.rb +++ /dev/null @@ -1,225 +0,0 @@ -module SharingPermissionsHelper - - ITEMS_NOT_IN_ISA_HASH = { - "id": "not_isa-tree", - "data": { - "loadable": false - }, - "li_attr": { - class:"root-node" - }, - "a_attr": { - - }, - "children": [ - ], - "text": "Not in ISA", - "state": { - "opened": true - } - } - - ALL_INVESTIGATIONS_HASH = { - "id": "isa-tree", - "data": { - "loadable": false - }, - "li_attr": { - class:"root-node" - }, - "a_attr": { - - }, - "children": [ - ], - "text": "ISA", - "state": { - "opened": true - } - } - - def build_tree_json(hash, root_item) - - objects = hash[:nodes].map(&:object) - real_edges = hash[:edges].select { |e| objects.include?(e[0]) } - - roots = hash[:nodes].select do |n| - real_edges.none? { |_parent, child| child == n.object } - end - - nodes = roots.map { |root| create_tree_node(hash, root.object, root_item) }.flatten - nodes.to_json - end - - def create_tree_node(hash, object, root_item = nil) - - child_edges = hash[:edges].select do |parent, _child| - parent == object - end - - node = hash[:nodes].detect { |n| n.object == object } - - entry = { - id: unique_node_id(object), - data: { loadable: false }, - li_attr: { 'data-node-id' => node_id(object) }, - children: [] - } - - entry[:text] = object.title - entry[:icon] = asset_path(resource_avatar_path(object) || icon_filename_for_key("#{object.class.name.downcase}_avatar")) - - filtered_child_edges = child_edges.reject { |c| (c[1].instance_of? Publication) || (c[1].instance_of?(Seek::ObjectAggregation))} - child_edges_with_permission = filtered_child_edges.select { |c| c[1].can_manage? } - - unless child_edges_with_permission.blank? - entry[:children] += child_edges_with_permission.map { |c| create_tree_node(hash, c[1], root_item) } - end - - if node.child_count > 0 - if node.child_count > child_edges.count - entry[:children] << { - id: unique_child_count_id(object), - parent: entry[:id], - text: "Show #{node.child_count - child_edges.count} more", - a_attr: { class: 'child-count-leaf' }, - li_attr: { 'data-node-id' => child_count_id(object) }, - data: { child_count: true } - } - end - entry[:state] = { opened: false } - else - entry[:state] = { opened: false } - end - entry - end - - def add_permissions_to_tree_json (parent_node) - - parent_node.each do |node| - node["li_attr"][:class] = "asset-node-row" - node["a_attr"] = {} - node["a_attr"][:class] = "asset-node" - - if !node["children"].nil? && node["children"].size > 0 - add_permissions_to_tree_json (node["children"]) - end - add_asset_permission_nodes(node) - end - parent_node - end - - def add_asset_permission_nodes(parent_node) - asset_type = parent_node["id"].split("-")[0] - asset_id = parent_node["id"].split("-")[1].to_i - - # get asset instance - asset = safe_class_lookup(asset_type.camelize).find(asset_id) - parent_node["text"] = "#{h(asset.title)} #{icon_link_to("", "new_window", asset , options = {target:'blank',class:'asset-icon',:onclick => 'window.open(this.href, "_blank");'})}" - - permissions_array = get_permission(asset) - parent_node["children"] = permissions_array + parent_node["children"] - parent_node - end - - def asset_node_json(resource_type, resource_items) - - parent = { - id: resource_type+"-not_isa", - data: { loadable: false }, - li_attr: {class:"asset-type-node"}, - a_attr: {}, - children: [], - text: resource_type - } - - resource_items.each do |item| - - entry_item = { - id: unique_node_id(item), - data: { loadable: false }, - li_attr: { 'data-node-id' => node_id(item), class:"asset-node-row"}, - a_attr: {class:"asset-node"}, - children: [] , - icon: asset_path(resource_avatar_path(item) || icon_filename_for_key("#{item.class.name.downcase}_avatar")), - text: "#{h(item.title)} #{icon_link_to("", "new_window", item , options = {target:'blank',class:'asset-icon',:onclick => 'window.open(this.href, "_blank");'})}" - } - - permissions_array = get_permission(item) - entry_item[:children] = permissions_array - parent[:children].append(entry_item) - end - parent - end - - - def create_policy_node(item, policy_text,sharing_policy_changed) - - entry = { - id: unique_policy_node_id(item), - data: { loadable: false }, - li_attr: { 'data-node-id' => "Permission-"+node_id(item), class:"hide_permission" }, - a_attr: { class:"permission-node #{sharing_policy_changed}"}, - children: [], - text:policy_text - } - entry - end - - def get_permission (item) - - policy =[] - - # policy - downloadable = item.try(:is_downloadable?) - policy_text = "#{Policy.get_access_type_wording(item.policy.access_type, downloadable)} by Public" - sharing_policy_changed = (@batch_sharing_permission_changed && (@items_for_sharing.include? item) && !@policy_params[:access_type].nil?)? "sharing_permission_changed" : "" - - policy.append(create_policy_node(item,policy_text,sharing_policy_changed)) - - #permission - option = {:onclick => 'window.open(this.href, "_blank");'} - - item.policy.permissions.map do |permission| - case permission.contributor_type - when Permission::PROJECT - m = Project.find(permission.contributor_id) - policy_text ="#{Policy.get_access_type_wording(permission.access_type, downloadable)} by Project #{link_to(h(m.title), m, option)}" - sharing_policy_changed = @batch_sharing_permission_changed && (@items_for_sharing.include? item) ? PolicyHelper::permission_changed_item_class(permission, @policy_params) : "" - policy.append(create_policy_node(item,policy_text,sharing_policy_changed)) - when Permission::WORKGROUP - m = WorkGroup.find(permission.contributor_id) - institution = Institution.find(m.institution_id) - project = Project.find(m.project_id) - policy_text ="#{Policy.get_access_type_wording(permission.access_type, downloadable)} by #{link_to(h(project.title), project,option)} @ #{link_to(h(institution.title), institution,option)}" - sharing_policy_changed = @batch_sharing_permission_changed && (@items_for_sharing.include? item) ? PolicyHelper::permission_changed_item_class(permission, @policy_params) : "" - policy.append(create_policy_node(item,policy_text,sharing_policy_changed)) - when Permission::INSTITUTION - m = Institution.find(permission.contributor_id) - policy_text ="#{Policy.get_access_type_wording(permission.access_type, downloadable)} by Institution #{link_to(h(m.title), m,option)}" - sharing_policy_changed = @batch_sharing_permission_changed && (@items_for_sharing.include? item) ? PolicyHelper::permission_changed_item_class(permission, @policy_params) : "" - policy.append(create_policy_node(item,policy_text,sharing_policy_changed)) - when Permission::PERSON - m = Person.find(permission.contributor_id) - policy_text ="#{Policy.get_access_type_wording(permission.access_type, downloadable)} by People #{link_to(h(m.title), m,option)}" - sharing_policy_changed = @batch_sharing_permission_changed && (@items_for_sharing.include? item) ? PolicyHelper::permission_changed_item_class(permission, @policy_params) : "" - policy.append(create_policy_node(item,policy_text,sharing_policy_changed)) - when Permission::PROGRAMME - m = Programme.find(permission.contributor_id) - policy_text ="#{Policy.get_access_type_wording(permission.access_type, downloadable)} by Programme #{link_to(h(m.title), m,option)}" - sharing_policy_changed = @batch_sharing_permission_changed && (@items_for_sharing.include? item) ? PolicyHelper::permission_changed_item_class(permission, @policy_params) : "" - policy.append(create_policy_node(item,policy_text,sharing_policy_changed)) - end - end - policy - end - - - private - - def unique_policy_node_id(object) - "Permission-#{node_id(object)}-#{rand(2**32).to_s(36)}" - end - - -end diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index ca875639ca..a447ff61e1 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -49,6 +49,10 @@ def maturity_badge(level) content_tag(:span, t("maturity_level.#{level}"), class: "maturity-level label #{label_class}") end + def life_monitor_status_page_url(resource, base: Seek::Config.life_monitor_ui_url) + URI.join(base, "/workflow;uuid=#{resource.uuid}").to_s + end + def test_status_badge(resource) status = resource.test_status case status @@ -65,9 +69,8 @@ def test_status_badge(resource) label_class = 'label-default' label = t('test_status.not_available') end - url = LifeMonitor::Rest::Client.status_page_url(resource) - link_to(url, class: 'lifemonitor-status btn btn-default', target: '_blank', rel: 'noopener', - 'data-tooltip' => 'Click to view in LifeMonitor') do + link_to(life_monitor_status_page_url(resource), class: 'lifemonitor-status btn btn-default', target: '_blank', + rel: 'noopener', 'data-tooltip' => 'Click to view in LifeMonitor') do image('life_monitor_icon', class: 'icon lifemonitor-logo') + 'Tests ' + content_tag(:span, label, class: "test-status label #{label_class}") diff --git a/app/models/assay.rb b/app/models/assay.rb index d8bb9b9a68..13b2b9279e 100644 --- a/app/models/assay.rb +++ b/app/models/assay.rb @@ -77,7 +77,7 @@ def short_description end def state_allows_delete?(*args) - assets.empty? && publications.empty? && super + assets.empty? && publications.empty? && associated_samples_through_sample_type.empty? && super end # returns true if this is a modelling class of assay @@ -90,6 +90,10 @@ def is_experimental? !assay_class.nil? && assay_class.key == 'EXP' end + def associated_samples_through_sample_type + (sample_type.nil? || sample_type.samples.nil?) ? [] : sample_type.samples + end + # Create or update relationship of this assay to another, with a specific relationship type and version def associate(asset, options = {}) if asset.is_a?(Organism) diff --git a/app/models/concerns/has_custom_metadata.rb b/app/models/concerns/has_custom_metadata.rb index c618cd5344..92585f76aa 100644 --- a/app/models/concerns/has_custom_metadata.rb +++ b/app/models/concerns/has_custom_metadata.rb @@ -9,7 +9,7 @@ def custom_metadata_attribute_values_for_search class_methods do def has_extended_custom_metadata - has_one :custom_metadata, as: :item, dependent: :destroy + has_one :custom_metadata, as: :item, dependent: :destroy, autosave: true accepts_nested_attributes_for :custom_metadata if Seek::Config.solr_enabled diff --git a/app/models/custom_metadata.rb b/app/models/custom_metadata.rb index 9831795302..26edcf2248 100644 --- a/app/models/custom_metadata.rb +++ b/app/models/custom_metadata.rb @@ -5,31 +5,10 @@ class CustomMetadata < ApplicationRecord belongs_to :custom_metadata_type, validate: true belongs_to :custom_metadata_attribute - has_many :custom_metadata_resource_links, inverse_of: :custom_metadata, dependent: :destroy - has_many :linked_custom_metadatas, through: :custom_metadata_resource_links, source: :resource, source_type: 'CustomMetadata', dependent: :destroy - accepts_nested_attributes_for :linked_custom_metadatas - validates_with CustomMetadataValidator - validates_associated :linked_custom_metadatas delegate :custom_metadata_attributes, to: :custom_metadata_type - - after_create :update_linked_custom_metadata_id, if: :has_linked_custom_metadatas? - - def update_linked_custom_metadata_id - linked_custom_metadatas.each do |cm| - attr_name = cm.custom_metadata_attribute.title - data.mass_assign(data.to_hash.update({attr_name => cm.id}), pre_process: false) - update_column(:json_metadata, data.to_json) - end - end - - def has_linked_custom_metadatas? - linked_custom_metadatas.any? - end - - # for polymorphic behaviour with sample alias_method :metadata_type, :custom_metadata_type @@ -43,36 +22,4 @@ def custom_metadata_type=(type) def attribute_class CustomMetadataAttribute end - - def update_linked_custom_metadata(parameters) - cmt_id = parameters[:custom_metadata_type_id] - - # return no custom metdata is filled - seek_cm_attrs = CustomMetadataType.find(cmt_id).custom_metadata_attributes.select(&:linked_custom_metadata?) - return if seek_cm_attrs.blank? - - seek_cm_attrs.each do |cma| - cma_params = parameters[:data][cma.title.to_sym] - self.set_linked_custom_metadatas(cma, cma_params) unless cma_params.nil? - - cma_linked_cmt = cma.linked_custom_metadata_type.attributes_with_linked_custom_metadata_type - - unless cma_linked_cmt.blank? - cm = self.linked_custom_metadatas.select{|cm| cm.custom_metadata_type.id == cma[:linked_custom_metadata_type_id]}.first - cm.update_linked_custom_metadata(cma_params) - end - - end - end - - def set_linked_custom_metadatas(cma, cm_params) - - if self.new_record? - self.linked_custom_metadatas.build(custom_metadata_type: cma.linked_custom_metadata_type, data: cm_params[:data], custom_metadata_attribute_id: cm_params[:custom_metadata_attribute_id]) - else - linked_cm = self.linked_custom_metadatas.select{|cm| cm.custom_metadata_type_id.to_s == cm_params[:custom_metadata_type_id]}.select{|cm|cm.custom_metadata_attribute==cma}.first - linked_cm.update(cm_params.permit!) - end - end - end diff --git a/app/models/custom_metadata_resource_link.rb b/app/models/custom_metadata_resource_link.rb deleted file mode 100644 index 2d77cb1984..0000000000 --- a/app/models/custom_metadata_resource_link.rb +++ /dev/null @@ -1,4 +0,0 @@ -class CustomMetadataResourceLink < ApplicationRecord - belongs_to :custom_metadata - belongs_to :resource, polymorphic: true -end diff --git a/app/models/data_file.rb b/app/models/data_file.rb index e77a9a6565..d46c400947 100644 --- a/app/models/data_file.rb +++ b/app/models/data_file.rb @@ -150,7 +150,7 @@ def extract_samples(sample_type, confirm = false, overwrite = false) sample.project_ids = project_ids sample.contributor = contributor sample.originating_data_file = self - sample.policy = policy + sample.policy = policy.deep_copy sample.save if sample.valid? && confirm extracted << sample diff --git a/app/models/event.rb b/app/models/event.rb index 912f6749fd..604e442be3 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -39,6 +39,9 @@ class Event < ApplicationRecord validates :country, country:true, allow_blank: true + has_filter :country + has_filter start_date: Seek::Filtering::DateFilter.new(field: :start_date) + validate :validate_data_files def validate_data_files df = data_files.to_a diff --git a/app/models/sample.rb b/app/models/sample.rb index 865129a58e..cb965a3a41 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -16,8 +16,6 @@ class Sample < ApplicationRecord acts_as_asset - validates :projects, presence: true, projects: { self: true } - belongs_to :sample_type, inverse_of: :samples alias_method :metadata_type, :sample_type @@ -32,18 +30,19 @@ class Sample < ApplicationRecord has_many :linked_samples, through: :sample_resource_links, source: :resource, source_type: 'Sample' has_many :linking_samples, through: :reverse_sample_resource_links, source: :sample + validates :projects, presence: true, projects: { self: true } validates :title, :sample_type, presence: true validates_with SampleAttributeValidator + validate :validate_added_linked_sample_permissions before_validation :set_title_to_title_attribute_value + before_validation :update_sample_resource_links - before_save :update_sample_resource_links after_save :queue_sample_type_update_job after_save :queue_linking_samples_update_job after_destroy :queue_sample_type_update_job - has_filter :sample_type def sample_type=(type) @@ -90,10 +89,6 @@ def referenced_samples referenced_resources.select { |r| r.is_a?(Sample) } end - def state_allows_edit?(*args) - (id.nil? || originating_data_file.nil?) && super - end - def extracted? !!originating_data_file end @@ -208,6 +203,7 @@ def queue_linking_samples_update_job end def update_sample_resource_links + return unless sample_type.present? self.strains = referenced_strains self.linked_samples = referenced_samples end @@ -225,4 +221,18 @@ def update_sample_type_hash(sample_type_hash, sample_type) end sample_type_hash end + + # checks and validates whether new linked samples have view permission, but ignores existing ones + def validate_added_linked_sample_permissions + return if $authorization_checks_disabled + return if linked_samples.empty? + previous_linked_samples = [] + unless new_record? + previous_linked_samples = Sample.find(id).referenced_samples + end + additions = linked_samples - previous_linked_samples + if additions.detect { |sample| !sample.can_view? } + errors.add(:linked_samples, 'includes a new private sample') + end + end end diff --git a/app/models/sample_attribute.rb b/app/models/sample_attribute.rb index 98b3416f75..81a4452df9 100644 --- a/app/models/sample_attribute.rb +++ b/app/models/sample_attribute.rb @@ -10,6 +10,7 @@ class SampleAttribute < ApplicationRecord validates :sample_type, presence: true validates :pid, format: { with: URI::regexp, allow_blank: true, allow_nil: true, message: 'not a valid URI' } + validate :validate_against_editing_constraints, if: -> { sample_type.present? } before_save :store_accessor_name before_save :default_pos, :force_required_when_is_title @@ -53,6 +54,10 @@ def short_pid URI.parse(pid).fragment || pid.gsub(/.*\//,'') || pid end + def linked_custom_metadata_type + nil + end + private def store_accessor_name @@ -70,4 +75,22 @@ def force_required_when_is_title true end + def validate_against_editing_constraints + c = sample_type.editing_constraints + error_message = "cannot be changed (#{title_was})" # Use pre-change title in error message. + + errors.add(:title, error_message) if title_changed? && !c.allow_name_change?(self) + + unless c.allow_required?(self) + errors.add(:is_title, error_message) if is_title_changed? + errors.add(:required, error_message) if required_changed? + end + + unless c.allow_type_change?(self) + errors.add(:sample_attribute_type, error_message) if sample_attribute_type_id_changed? + errors.add(:sample_controlled_vocab, error_message) if sample_controlled_vocab_id_changed? + errors.add(:linked_sample_type, error_message) if linked_sample_type_id_changed? + errors.add(:unit, error_message) if unit_id_changed? + end + end end diff --git a/app/models/sample_attribute_type.rb b/app/models/sample_attribute_type.rb index 79cdbdd895..af109086a0 100644 --- a/app/models/sample_attribute_type.rb +++ b/app/models/sample_attribute_type.rb @@ -88,6 +88,10 @@ def linked_custom_metadata? base_type == Seek::Samples::BaseType::LINKED_CUSTOM_METADATA end + def linked_custom_metadata_multi? + base_type == Seek::Samples::BaseType::LINKED_CUSTOM_METADATA_MULTI + end + def seek_sample? base_type == Seek::Samples::BaseType::SEEK_SAMPLE end diff --git a/app/models/sample_type.rb b/app/models/sample_type.rb index 850445d9bf..8433263181 100644 --- a/app/models/sample_type.rb +++ b/app/models/sample_type.rb @@ -47,8 +47,11 @@ class SampleType < ApplicationRecord validates :title, length: { maximum: 255 } validates :description, length: { maximum: 65_535 } validates :contributor, presence: true - validate :validate_one_title_attribute_present, :validate_attribute_title_unique, :validate_attribute_accessor_names_unique, - :validate_title_is_not_type_of_seek_sample_multi + validate :validate_one_title_attribute_present, + :validate_attribute_title_unique, + :validate_attribute_accessor_names_unique, + :validate_title_is_not_type_of_seek_sample_multi, + :validate_against_editing_constraints validates :projects, presence: true, projects: { self: true } accepts_nested_attributes_for :sample_attributes, allow_destroy: true @@ -196,6 +199,19 @@ def validate_attribute_accessor_names_unique end end + def validate_against_editing_constraints + c = editing_constraints + sample_attributes.each do |a| + if a.marked_for_destruction? && !c.allow_attribute_removal?(a) + errors.add(:sample_attributes, "cannot be removed, there are existing samples using this attribute (#{a.title})") + end + + if a.new_record? && !c.allow_new_attribute? + errors.add(:sample_attributes, "cannot be added, new attributes are not allowed (#{a.title})") + end + end + end + def attribute_search_terms attribute_titles end diff --git a/app/models/study.rb b/app/models/study.rb index 944373fc9b..bc1691852e 100644 --- a/app/models/study.rb +++ b/app/models/study.rb @@ -2,7 +2,7 @@ class Study < ApplicationRecord enum status: [:planned, :running, :completed, :cancelled, :failed] belongs_to :assignee, class_name: 'Person' - + searchable(:auto_index => false) do text :experimentalists end if Seek::Config.solr_enabled @@ -22,7 +22,7 @@ class Study < ApplicationRecord has_many :sop_versions, through: :assays has_one :external_asset, as: :seek_entity, dependent: :destroy - + has_and_belongs_to_many :sops has_and_belongs_to_many :sample_types @@ -41,7 +41,16 @@ def assets end def state_allows_delete? *args - assays.empty? && super + assays.empty? && associated_samples_through_sample_type.empty? && super + end + + def associated_samples_through_sample_type + return [] if sample_types.nil? + st_samples = [] + sample_types.map do |st| + st.samples.map { |sts| st_samples.push sts } + end + st_samples end def clone_with_associations diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb index 5083ce6965..09bbdd48c0 100644 --- a/app/serializers/base_serializer.rb +++ b/app/serializers/base_serializer.rb @@ -123,20 +123,9 @@ def BaseSerializer.permits policy attribute :extended_attributes, if: -> { object.respond_to?(:custom_metadata) && !object.custom_metadata.blank? } do { extended_metadata_type_id: object.custom_metadata.custom_metadata_type_id.to_s, - attribute_map: get_custom_metadata } + attribute_map: object.custom_metadata.data.to_hash } end - def get_custom_metadata - data = object.custom_metadata.data.to_hash - CustomMetadata.find(object.custom_metadata.id).custom_metadata_attributes.each do |attr| - if attr.linked_custom_metadata? - data[attr.title] = display_custom_metadata(data,attr) - end - end - data - end - - def show_policy? return false unless object.respond_to?('can_manage?') @@ -155,17 +144,6 @@ def submitter private - def display_custom_metadata(data,attribute) - linked_data = CustomMetadata.find(data[attribute.title]).data.to_hash - CustomMetadata.find(data[attribute.title]).custom_metadata_attributes.each do |attr| - if attr.linked_custom_metadata? - linked_data[attr.title] = display_custom_metadata(linked_data,attr) - end - end - - linked_data - end - def determine_submitter(object) return object.owner if object.respond_to?('owner') result = object.contributor if object.respond_to?('contributor') && !object.is_a?(Permission) diff --git a/app/validators/custom_metadata_validator.rb b/app/validators/custom_metadata_validator.rb index c73b9cb1d1..a2f6ae13e5 100644 --- a/app/validators/custom_metadata_validator.rb +++ b/app/validators/custom_metadata_validator.rb @@ -1,16 +1,33 @@ class CustomMetadataValidator < ActiveModel::Validator - def validate(record) record.custom_metadata_attributes.each do |attribute| val = record.get_attribute_value(attribute) - if attribute.test_blank?(val) - record.errors.add(attribute.title, 'is required') if attribute.required? - else - unless attribute.validate_value?(val) - record.errors.add(attribute.title, "is not a valid #{attribute.sample_attribute_type.title}") + validate_attribute(record, attribute, val) + end + end + + private + + def validate_attribute(record, attribute, value, prefix = '') + if attribute.test_blank?(value) + record.errors.add("#{prefix}#{attribute.title}", 'is required') if attribute.required? + else + unless attribute.validate_value?(value) + record.errors.add("#{prefix}#{attribute.title}", "is not a valid #{attribute.sample_attribute_type.title}") + end + end + + if attribute.linked_custom_metadata? + attribute.linked_custom_metadata_type.custom_metadata_attributes.each do |attr| + validate_attribute(record, attr, value ? value[attr.accessor_name.to_s] : nil, "#{attribute.title}.") + end + elsif attribute.linked_custom_metadata_multi? + linked_attributes = attribute.linked_custom_metadata_type.custom_metadata_attributes + value.each_with_index do |val, index| + linked_attributes.each do |attr| + validate_attribute(record, attr, val ? val[attr.accessor_name.to_s] : nil, "#{attribute.title}.#{index + 1}.") end end end end - end \ No newline at end of file diff --git a/app/validators/license_validator.rb b/app/validators/license_validator.rb index 1ca5fefb7d..047e83dd43 100644 --- a/app/validators/license_validator.rb +++ b/app/validators/license_validator.rb @@ -2,7 +2,15 @@ class LicenseValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return if Seek::License.find(value) - record.errors.add(attribute, options[:message] || "isn't a valid license ID") + # Try looking up by URI + if value.start_with? /https?:/ + id = Seek::License.uri_to_id(value) + if id + record.send("#{attribute}=", id) + return + end + end + record.errors.add(attribute, options[:message] || "isn't a recognized license") end end \ No newline at end of file diff --git a/app/views/admin/features_enabled.html.erb b/app/views/admin/features_enabled.html.erb index 63039940a4..f46346c9d5 100644 --- a/app/views/admin/features_enabled.html.erb +++ b/app/views/admin/features_enabled.html.erb @@ -193,11 +193,13 @@
<%= admin_text_setting(:life_monitor_url, Seek::Config.life_monitor_url, - 'LifeMonitor URL', "The URL of a LifeMonitor instance.") %> + 'LifeMonitor API URL', "The API URL of a LifeMonitor instance.") %> <%= admin_text_setting(:life_monitor_client_id, Seek::Config.life_monitor_client_id, 'LifeMonitor OAuth client ID', 'The ID for this application to authenticate users through the LifeMonitor OAuth provider.') %> <%= admin_text_setting(:life_monitor_client_secret, Seek::Config.life_monitor_client_secret, 'LifeMonitor OAuth client secret', 'The secret token for this application to authenticate users through the LifeMonitor OAuth provider.') %> + <%= admin_text_setting(:life_monitor_ui_url, Seek::Config.life_monitor_ui_url, + 'LifeMonitor UI URL', "The UI (app) URL of a LifeMonitor instance.") %>
diff --git a/app/views/admin/settings.html.erb b/app/views/admin/settings.html.erb index 87068624e8..5296ef1fb0 100644 --- a/app/views/admin/settings.html.erb +++ b/app/views/admin/settings.html.erb @@ -56,22 +56,29 @@ admin_dropdown_setting(:max_all_visitors_access_type, option_tags, 'Permission Limits', 'Maximum permission option for all visitors, including those without a login') %> - <%= admin_setting_block('Default License', "The default license to use when one is not specified by a #{t('project').pluralize}.") do - grouped_license_select(:default_license, Seek::Config.default_license, id: 'license-select', - class: 'form-control', source: Seek::License.open_definition[:data]) - end %> - - <%= admin_setting_block('Recommended data licenses', "The licenses to recommend when data, such as a #{t('data_file')} or #{t('document')}, is registered. Hold down CTRL (or Command on Mac) to select multiple or to deselect.") do - select_tag :recommended_data_licenses, options_for_select(Seek::License.open_definition[:data].map { |l| [l['title'], l['id']]}, Seek::Config.recommended_data_licenses), multiple: true, class: 'form-control' - end %> - - <%= admin_setting_block('Recommended software licenses', "The licenses to recommend when software such as a #{t('workflow')}, is registered. Hold down CTRL (or Command on Mac) to select multiple or to deselect.") do - select_tag :recommended_software_licenses, options_for_select(Seek::License.open_definition[:software].map { |l| [l['title'], l['id']]}, Seek::Config.recommended_software_licenses), multiple: true, class: 'form-control' - end %> + <%= admin_setting_block('Default License', "The default license to use when one is not specified by a #{t('project').pluralize}.") do %> + <%= license_select(:default_license, Seek::Config.default_license, id: 'license-select', class: 'form-control') %> + <% end %> + + <%= admin_setting_block('Recommended data licenses', "The licenses to recommend when data, such as a #{t('data_file')} or #{t('document')}, is registered.") do %> + <%= associations_list('recommended-data-licenses', 'associations/general', + Seek::Config.recommended_data_licenses.map { |id| { id: id, title: Seek::License.find(id).title } }.to_json, + 'data-field-name' => 'recommended_data_licenses', + empty_text: 'No recommended licenses') %> + <%= license_select(:_recommended_data_license_select, nil, prompt: 'Select a license to add it to the list...', class: 'form-control') %> + <% end %> + + <%= admin_setting_block('Recommended software licenses', "The licenses to recommend when software, such as a #{t('workflow')}, is registered.") do %> + <%= associations_list('recommended-software-licenses', 'associations/general', + Seek::Config.recommended_software_licenses.map { |id| { id: id, title: Seek::License.find(id).title } }.to_json, + 'data-field-name' => 'recommended_software_licenses', + empty_text: 'No recommended licenses') %> + <%= license_select(:_recommended_software_license_select, nil, prompt: 'Select a license to add it to the list...', class: 'form-control') %> + <% end %> <%= admin_setting_block('Metadata License', "The license granted on metadata produced by this SEEK instance.") do - grouped_license_select(:metadata_license, Seek::Config.metadata_license, id: 'metadata-license-select', - class: 'form-control', source: Seek::License.open_definition[:data]) + license_select(:metadata_license, Seek::Config.metadata_license, id: 'metadata-license-select', + class: 'form-control', source: Seek::License.combined) end %> <%= admin_dropdown_setting(:permissions_popup, @@ -122,3 +129,26 @@ <%= submit_tag "Update", data: { disable_with: 'Updating...' }, :class => 'btn btn-primary' -%> or <%= cancel_button admin_path %> <% end -%> + + + \ No newline at end of file diff --git a/app/views/assets/_batch_asset_selection.html.erb b/app/views/assets/_batch_asset_selection.html.erb new file mode 100644 index 0000000000..3f34a53470 --- /dev/null +++ b/app/views/assets/_batch_asset_selection.html.erb @@ -0,0 +1,108 @@ +<% + publishing ||= false + show_hide_blocked ||= false + show_permissions ||= false + show_managers ||= false +-%> + +
+ <%= render partial: 'assets/batch_selection/buttons', locals: { + text: "your items", + select_deselect_all: true, + collapse_expand: true, + show_hide_blocked: publishing, + show_hide_permissions: show_permissions + } %> + + + +
+
+ <% @assets.sort_by { |k, v| v.first.class.name }.each do |type, items| %> +
+

<%= batch_selection_collapse_toggle -%> <%= text_for_resource items.first %>(s)

+
+ <%= render partial: 'assets/batch_selection/buttons', locals: { + text: (text_for_resource items.first).downcase.pluralize, + show_hide_blocked: publishing, + select_deselect_all: true + } %> + <% items.each do |item| %> + <%= render partial: 'assets/batch_selection/asset_row', + object: item, + locals: { html_classes: 'publishing_options', + publishing: publishing, + show_permissions: show_permissions, + show_managers: show_managers + } -%> + <% end %> +
+
+ <% end %> +
+ +
+ <% unless @assets_not_in_isa.empty? %> +
+

<%= batch_selection_collapse_toggle -%> Items not in ISA

+
+ <%= render partial: 'assets/batch_selection/buttons', locals: { + text: "items not in ISA", + show_hide_blocked: publishing, + select_deselect_all: true + } %> + <% @assets_not_in_isa.each do |item| %> + <%= render partial: 'assets/batch_selection/asset_row', + object: item, + locals: { html_classes: "publishing_options", + publishing: publishing, + show_permissions: show_permissions, + show_managers: show_managers + } -%> + <% end %> +
+
+ <% end %> + + <% unless @investigations.empty? %> +
+

<%= batch_selection_collapse_toggle-%> Items in ISA

+
+ <%= render partial: 'assets/batch_selection/buttons', locals: { + text: "items in ISA", + select_deselect_all: true, + collapse_expand: true, + show_hide_blocked: show_hide_blocked + } %> +
+ <% @investigations.each do |inv| %> + <% collection = inv.assays.map(&:study).map(&:investigation).flatten.uniq %> + <% collection = inv.studies.map(&:investigation).flatten.uniq if collection.empty? %> + <% collection = [inv] if collection.empty?%> + <%= render partial: 'assets/batch_selection/asset_row', + locals: { publishing: publishing, + show_permissions: show_permissions, + show_managers: show_managers, + show_children: true + }, + collection: collection %> + <% end %> +
+
+
+ <% end %> +
+
+
+ diff --git a/app/views/assets/_license_selector.html.erb b/app/views/assets/_license_selector.html.erb index d8091c5ecd..43caab681f 100644 --- a/app/views/assets/_license_selector.html.erb +++ b/app/views/assets/_license_selector.html.erb @@ -27,10 +27,10 @@
- <%= grouped_license_select("#{resource.class.name.underscore}[#{accessor.to_s}]", selected_license, + <%= license_select("#{resource.class.name.underscore}[#{accessor.to_s}]", selected_license, id: 'license-select', class: 'form-control', data: { 'can-overwrite': using_default }, - source: Seek::License.open_definition[:all], + source: Seek::License.combined, recommended: recommended_licenses) %> @@ -38,10 +38,3 @@
<% end %> - - diff --git a/app/views/assets/batch_selection/_asset_row.html.erb b/app/views/assets/batch_selection/_asset_row.html.erb new file mode 100644 index 0000000000..0390f0982b --- /dev/null +++ b/app/views/assets/batch_selection/_asset_row.html.erb @@ -0,0 +1,104 @@ +<% + item = asset_row + preselected ||= nil + checked ||= (item == preselected) + publishing ||= false + can_manage = item.can_manage? + can_view = item.can_view? + published = false + html_classes ||= "publishing_options" + tree_class = 'not-manageable' + tree_class = 'manageable' if can_manage + tree_class = 'not-visible' unless can_view + if publishing + if item.is_published? + published = true + tree_class = 'already-published' + end + end + item_id = "#{item.class.name}_#{item.id}" + show_managers ||= false + show_permissions ||= false + show_managers = show_managers && (can_view || item.can_see_hidden_item?(current_user.person)) + show_children ||= false + children = [] + if show_children + case item + when Investigation + children = item.studies + when Study + children = item.assays + when Assay + children = item.assets + end + end +-%> +
+
+ <%= batch_selection_collapse_toggle if children.any? %> + + <%= render partial: 'assets/batch_selection/checkbox', + locals: { checkbox_id: publishing_item_param(item), + checkbox_class: item_id, + checked: can_view && checked, + not_visible: !can_view, + published: can_view && published, + cant_manage: !can_manage, + has_children: children.any? } -%> + + + <%= text_for_resource item -%>: + <% if can_view %> + <%= link_to item.title, item, target: "_blank" -%> + <% else %> + This item is hidden to you + <% end %> + + + <% if show_permissions %> + <% if can_view %> + + <%= list_item_visibility(item)-%> + + <% else %> + <%= list_item_visibility(item)-%> + <% end %> + <% end %> + + <% if show_managers %> + + + + <% end %> +
+ + <% if show_managers %> + + <% end %> + <% if show_permissions && can_view %> + <%= render partial: 'assets/batch_selection/permission_list', locals: { item: item } -%> + <% end %> + + <% if show_children && children.any? %> +
+ <% children.each do |child| %> + <%= render partial: 'assets/batch_selection/asset_row', + object: child, + locals: { preselected: preselected, + publishing: publishing, + html_classes: "publishing_options", + show_permissions: show_permissions, + show_managers: show_managers, + show_children: true + } -%> + <% end %> +
+ <% end %> +
diff --git a/app/views/assets/batch_selection/_buttons.html.erb b/app/views/assets/batch_selection/_buttons.html.erb new file mode 100644 index 0000000000..52b856cc25 --- /dev/null +++ b/app/views/assets/batch_selection/_buttons.html.erb @@ -0,0 +1,50 @@ +<% + text ||= "your items" + select_deselect_all ||= false + collapse_expand ||= false + show_hide_blocked ||= false + show_hide_permissions ||= false +%> + +
+ <% if select_deselect_all %> +
+ + Select all <%= text %> + + + Deselect all <%= text %> + +
+ <% end %> + <% if collapse_expand %> +
+ + Collapse all + + + Expand all + +
+ <% end %> + <% if show_hide_blocked %> +
+ + Show blocked items + + + Hide blocked items + +
+ <% end %> + <% if show_hide_permissions %> +
+ + Show all permissions + + + Hide all permissions + +
+ <% end %> +
diff --git a/app/views/assets/batch_selection/_checkbox.html.erb b/app/views/assets/batch_selection/_checkbox.html.erb new file mode 100644 index 0000000000..b3da44f54a --- /dev/null +++ b/app/views/assets/batch_selection/_checkbox.html.erb @@ -0,0 +1,44 @@ +<% + checkbox_class ||= "no_name" # <- Needed! + published ||= false + cant_manage ||= false + not_visible ||= false + checked ||= false + has_children ||= false +-%> +
+ <% if published %> + + <% elsif not_visible %> + + <% elsif cant_manage %> + + <% else %> + + <% end %> + + <% if has_children %> + + + <% end %> +
diff --git a/app/views/assets/batch_selection/_permission_list.html.erb b/app/views/assets/batch_selection/_permission_list.html.erb new file mode 100644 index 0000000000..643f7aed42 --- /dev/null +++ b/app/views/assets/batch_selection/_permission_list.html.erb @@ -0,0 +1,18 @@ +<% + item ||= permission_list + downloadable = item.try(:is_downloadable?) +-%> + diff --git a/app/views/assets/publishing/_isa_publishing_preview.html.erb b/app/views/assets/publishing/_isa_publishing_preview.html.erb deleted file mode 100644 index 53779ae75a..0000000000 --- a/app/views/assets/publishing/_isa_publishing_preview.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<% - item = isa_publishing_preview - - case item - when Investigation - children = item.studies - when Study - children = item.assays - when Assay - children = item.assets - else - children = [] - end --%> -
"> - <%= render :partial => "assets/publishing/options_for_publishing", - :object => item, - :locals => { :html_classes => "publishing_options", - :toggle => children.any?, - :cb_parent_selector => "div\##{item.class.name}_#{item.id}.split_button_parent", - :checked => (item == preselected) } -%> - - <% if children.any? %> -
- <% children.each do |child| %> - <%= render :partial => "assets/publishing/isa_publishing_preview", - :object => child, - :locals => { :preselected => preselected, - :html_classes => "publishing_options"} -%> - <% end %> -
- <% end %> -
\ No newline at end of file diff --git a/app/views/assets/publishing/_options_for_publishing.html.erb b/app/views/assets/publishing/_options_for_publishing.html.erb deleted file mode 100644 index 403fe471ea..0000000000 --- a/app/views/assets/publishing/_options_for_publishing.html.erb +++ /dev/null @@ -1,51 +0,0 @@ -<% - item = options_for_publishing - checked ||= false - - tree_class = 'not-publishable' - tree_class = 'publishable' if item.can_publish? - tree_class = 'already-published' if item.is_published? - - toggle ||=false - cb_parent_selector ||="div\##{item.class.name}_#{item.id}.split_button_parent" - --%> -
- <% if item.can_view? %> -
- <%= text_for_resource item -%>: <%= link_to item.title, item, :target => "_blank" -%> - <%=list_item_visibility(item)-%> -
-
Manageable by <%= item.managers.empty? ? "None".html_safe : item.managers.collect { |m| link_to(h(m.title), m) }.join(", ").html_safe -%>
- <% else %> -
<%= text_for_resource item -%>: This item is hidden to you
- <% if current_user.try(:person) && item.can_see_hidden_item?(current_user.person) %> -
Manageable by <%= item.managers.empty? ? "
None
".html_safe : item.managers.collect { |m| link_to(h(m.title), m) }.join(", ").html_safe -%>
- <% end %> - <% end %> - -
- <% if item.is_published? %> - <%= render :partial => 'general/split_button_checkbox', - locals: { checkbox_id: publishing_item_param(item), - checkbox_class: "#{item.class.name}_#{item.id}", - published: true, - toggle: toggle, - cb_parent_selector: cb_parent_selector} -%> - <% elsif item.can_publish? %> - <%= render :partial => 'general/split_button_checkbox', - locals: { checkbox_id: publishing_item_param(item), - checkbox_class: "#{item.class.name}_#{item.id}", - checkbox_text: "Publish?", - checked: checked, - toggle: toggle, - cb_parent_selector: cb_parent_selector} -%> - <% else %> - - - Can't publish - - <% end %> -
-
- diff --git a/app/views/assets/publishing/batch_publishing_preview.html.erb b/app/views/assets/publishing/batch_publishing_preview.html.erb index 625fca6bae..c51dc30cf1 100644 --- a/app/views/assets/publishing/batch_publishing_preview.html.erb +++ b/app/views/assets/publishing/batch_publishing_preview.html.erb @@ -11,45 +11,27 @@

- You can select an item to be published by checking the Publish - checkbox beside that item. + You can select an item to be published by checking the checkbox beside that item.

-
- | - - -<%= form_tag({:action => :check_related_items},:method=>:post) do -%> +<%= form_tag({action: :check_related_items},method: :post) do -%> <% if @assets.empty? %> All your assets are published or you have no assets in <%= Seek::Config.instance_name %>

<%= link_to "Back to profile", person_path(params[:id].to_i) -%> <% else %> - <% @assets.sort_by { |k, v| v.first.class.name }.each do |type, items| %> -
-

<%= text_for_resource items.first %>(s)

- | - - <% items.each do |item| %> - <%= render :partial => "assets/publishing/options_for_publishing", - :object => item, - :locals => { :html_classes => "publishing_options" } -%> -
- <% end %> -
- <% end %> + <%= render partial: "assets/batch_asset_selection", + locals: { publishing: true, + show_hide_blocked: true, + show_permissions: true, + show_managers: false + } + -%>
- - <%= submit_tag "Next",data: { disable_with: 'Next' }, :class => 'btn btn-primary' -%> + <%= submit_tag "Next",data: { disable_with: 'Next' }, class: 'btn btn-primary' -%> Or <%= cancel_button person_path(params[:id].to_i)-%> <% end -%> diff --git a/app/views/assets/publishing/publish_final_confirmation.html.erb b/app/views/assets/publishing/publish_final_confirmation.html.erb index fc24050d67..d8d815fc50 100644 --- a/app/views/assets/publishing/publish_final_confirmation.html.erb +++ b/app/views/assets/publishing/publish_final_confirmation.html.erb @@ -1,21 +1,74 @@ <%= show_title "Confirm publishing" -%> -

- You are about the publish the following items. -

-<%= form_tag :action => :publish do %> -