From d02d54ce196e036fa800da16fad70eb8245591ca Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 8 May 2024 12:31:40 +0200 Subject: [PATCH 001/131] Add 'assets/manage_specific_attributes' for assay streams --- app/views/isa_assays/_form.html.erb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/isa_assays/_form.html.erb b/app/views/isa_assays/_form.html.erb index 20fe51faaf..899e57ad4f 100644 --- a/app/views/isa_assays/_form.html.erb +++ b/app/views/isa_assays/_form.html.erb @@ -80,7 +80,11 @@ <%= assay_fields.hidden_field :assay_stream_id, value: assay_stream_id -%> <%= assay_fields.hidden_field :assay_class_id, value: assay_class_id -%> - <% unless is_assay_stream %> + <% if is_assay_stream %> + <% if User.current_user -%> + <%= render partial: 'assets/manage_specific_attributes', locals:{f:assay_fields} if show_form_manage_specific_attributes? %> + <% end %> + <% else %> <% if User.current_user -%> <%= render partial: 'assets/manage_specific_attributes', locals:{f:assay_fields} if show_form_manage_specific_attributes? %> <%= assay_fields.fancy_multiselect(:sops, other_projects_checkbox: true, name: "isa_assay[assay][sop_ids]")%> From ddd8a1b5def5095b06aaab7b359a5febae718fb4 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 8 May 2024 13:15:27 +0200 Subject: [PATCH 002/131] Hide assay design in case of assay stream --- app/views/assays/show.html.erb | 2 +- app/views/general/_show_page_tab_definitions.html.erb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/assays/show.html.erb b/app/views/assays/show.html.erb index fabd8c63f9..e0c12ee449 100644 --- a/app/views/assays/show.html.erb +++ b/app/views/assays/show.html.erb @@ -160,7 +160,7 @@ <%= tab_pane('related-items') do %> <%= render partial: 'general/items_related_to', object: @assay %> <% end %> - <% if Seek::Config.isa_json_compliance_enabled && @assay.is_isa_json_compliant? %> + <% if Seek::Config.isa_json_compliance_enabled && @assay.is_isa_json_compliant? && !@assay.is_assay_stream? %> <%= tab_pane('assay_design') do %> <%= render :partial=>"isa_assays/assay_design", locals: { assay: @assay} -%> <%= render partial: 'single_pages/change_batch_persmission_modal' %> diff --git a/app/views/general/_show_page_tab_definitions.html.erb b/app/views/general/_show_page_tab_definitions.html.erb index 5ea7c76dc3..612f1fecfe 100644 --- a/app/views/general/_show_page_tab_definitions.html.erb +++ b/app/views/general/_show_page_tab_definitions.html.erb @@ -18,7 +18,8 @@ <% end %> <% if resource %> - <% if Seek::Config.isa_json_compliance_enabled && resource.is_isa_json_compliant? %> + <% resource_is_assay_stream = resource_name == 'Assay' ? resource.is_assay_stream? : false %> + <% if Seek::Config.isa_json_compliance_enabled && resource.is_isa_json_compliant? && !resource_is_assay_stream %> <%= tab(resource_name&.downcase + "_design") do %> <%= resource.model_name.human %> design <% end %> From 5e8b1512e3ac878e62135d6351c5b236752f3409 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 11:10:51 +0100 Subject: [PATCH 003/131] new AuthLookupMaintenanceJob to check consistency, runs priority 3 on the authlookup queue every 8 hours #1866 --- app/jobs/auth_lookup_maintenance_job.rb | 33 ++++++++ app/jobs/regular_maintenance_job.rb | 23 ------ config/schedule.rb | 4 + .../jobs/auth_lookup_maintenance_job_test.rb | 76 +++++++++++++++++++ test/unit/jobs/regular_maintenace_job_test.rb | 59 -------------- 5 files changed, 113 insertions(+), 82 deletions(-) create mode 100644 app/jobs/auth_lookup_maintenance_job.rb create mode 100644 test/unit/jobs/auth_lookup_maintenance_job_test.rb diff --git a/app/jobs/auth_lookup_maintenance_job.rb b/app/jobs/auth_lookup_maintenance_job.rb new file mode 100644 index 0000000000..ca46761a34 --- /dev/null +++ b/app/jobs/auth_lookup_maintenance_job.rb @@ -0,0 +1,33 @@ +class AuthLookupMaintenanceJob < ApplicationJob + RUN_PERIOD = 8.hours.freeze + + queue_as QueueNames::AUTH_LOOKUP + queue_with_priority 3 + + def perform + check_authlookup_consistency + end + + # checks lookup_table_consistent? on each type for each user, and if not triggers a job to repopulate for that user + def check_authlookup_consistency + found_types = [].to_set + items_for_queue = [].to_set + User.where.not(person_id: nil).to_a.push(nil).each do |user| + + # will only deal with 1 type per user per run + found = Seek::Util.authorized_types.find do |type| + !type.lookup_table_consistent?(user) + end + + next unless found.present? + + found_types << found + missing = found.items_missing_from_authlookup(user) + items_for_queue.merge(missing) + end + + found_types.each(&:remove_invalid_auth_lookup_entries) + AuthLookupUpdateQueue.enqueue(items_for_queue.to_a) unless items_for_queue.empty? + end + +end \ No newline at end of file diff --git a/app/jobs/regular_maintenance_job.rb b/app/jobs/regular_maintenance_job.rb index 139fe3c52a..d42afdf85e 100644 --- a/app/jobs/regular_maintenance_job.rb +++ b/app/jobs/regular_maintenance_job.rb @@ -20,7 +20,6 @@ def perform clean_git_repositories resend_activation_emails remove_unregistered_users - check_authlookup_consistency end private @@ -73,26 +72,4 @@ def resend_activation_emails end end - # checks lookup_table_consistent? on each type for each user, and if not triggers a job to repopulate for that user - # if not already queued - def check_authlookup_consistency - found_types = [].to_set - items_for_queue = [].to_set - User.where.not(person_id: nil).to_a.push(nil).each do |user| - - # will only deal with 1 type per user per run - found = Seek::Util.authorized_types.find do |type| - !type.lookup_table_consistent?(user) - end - - next unless found.present? - - found_types << found - missing = found.items_missing_from_authlookup(user) - items_for_queue.merge(missing) - end - - found_types.each(&:remove_invalid_auth_lookup_entries) - AuthLookupUpdateQueue.enqueue(items_for_queue.to_a) unless items_for_queue.empty? - end end diff --git a/config/schedule.rb b/config/schedule.rb index 0718d9b395..ba8c045099 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -47,6 +47,10 @@ def offset(off_hours) runner "RegularMaintenanceJob.perform_later" end +every AuthLookupMaintenanceJob::RUN_PERIOD, at: offset(1) do + runner "AuthLookupMaintenanceJob.perform_later" +end + every LifeMonitorStatusJob::PERIOD, at: offset(2) do runner "LifeMonitorStatusJob.perform_later" end diff --git a/test/unit/jobs/auth_lookup_maintenance_job_test.rb b/test/unit/jobs/auth_lookup_maintenance_job_test.rb new file mode 100644 index 0000000000..95f1b543f2 --- /dev/null +++ b/test/unit/jobs/auth_lookup_maintenance_job_test.rb @@ -0,0 +1,76 @@ +require 'test_helper' + +class AuthLookupMaintenaceJobTest < ActiveSupport::TestCase + + test 'run period' do + assert_equal 8.hours, AuthLookupMaintenanceJob::RUN_PERIOD + end + + test 'priority' do + assert_equal 3, AuthLookupMaintenanceJob.priority + end + + test 'queue name' do + assert_equal QueueNames::AUTH_LOOKUP, AuthLookupMaintenanceJob.queue_name + end + + + test 'check authlookup consistency' do + #ensure a consistent initial state + disable_authorization_checks do + Seek::Util.authorized_types.each(&:destroy_all) + Seek::Util.authorized_types.each(&:clear_lookup_table) + end + + User.destroy_all + p = FactoryBot.create(:person) + p2 = FactoryBot.create(:person) + u = FactoryBot.create(:brand_new_user) + + assert_nil u.person + + with_config_value(:auth_lookup_enabled, true) do + assert AuthLookupUpdateQueue.queue_enabled? + + doc1 = FactoryBot.create(:document) + doc2 = FactoryBot.create(:document) + AuthLookupUpdateJob.perform_now + + assert Document.lookup_table_consistent?(p.user) + assert Document.lookup_table_consistent?(p2.user) + assert Document.lookup_table_consistent?(nil) + + assert_no_enqueued_jobs do + assert_no_difference('AuthLookupUpdateQueue.count') do + AuthLookupMaintenanceJob.perform_now + end + end + + # delete a record + Document.lookup_class.where(user_id:p.user.id,asset_id:doc1.id).last.delete + + #duplicate a record + Document.lookup_class.where(user_id:p2.user.id, asset_id:doc2.id).last.dup.save! + + refute Document.lookup_table_consistent?(p.user) + refute Document.lookup_table_consistent?(p2.user) + + #gets immmediately updated for anonymous user + assert Document.lookup_table_consistent?(nil) + + assert_enqueued_jobs(1) do + assert_difference('AuthLookupUpdateQueue.count',1) do + AuthLookupMaintenanceJob.perform_now + end + end + + # double check it will be fixed when the job runs + AuthLookupUpdateJob.perform_now + assert Document.lookup_table_consistent?(p.user) + assert Document.lookup_table_consistent?(p2.user) + assert Document.lookup_table_consistent?(nil) + end + + end + +end \ No newline at end of file diff --git a/test/unit/jobs/regular_maintenace_job_test.rb b/test/unit/jobs/regular_maintenace_job_test.rb index 0cff108369..7b627541b9 100644 --- a/test/unit/jobs/regular_maintenace_job_test.rb +++ b/test/unit/jobs/regular_maintenace_job_test.rb @@ -154,65 +154,6 @@ def setup assert_equal [person3, person4].sort, logs.collect(&:subject).sort end - test 'check authlookup consistency' do - #ensure a consistent initial state - disable_authorization_checks do - Seek::Util.authorized_types.each(&:destroy_all) - Seek::Util.authorized_types.each(&:clear_lookup_table) - end - - User.destroy_all - p = FactoryBot.create(:person) - p2 = FactoryBot.create(:person) - u = FactoryBot.create(:brand_new_user) - - assert_nil u.person - - with_config_value(:auth_lookup_enabled, true) do - assert AuthLookupUpdateQueue.queue_enabled? - - doc1 = FactoryBot.create(:document) - doc2 = FactoryBot.create(:document) - AuthLookupUpdateJob.perform_now - - assert Document.lookup_table_consistent?(p.user) - assert Document.lookup_table_consistent?(p2.user) - assert Document.lookup_table_consistent?(nil) - - assert_no_enqueued_jobs do - assert_no_difference('AuthLookupUpdateQueue.count') do - RegularMaintenanceJob.perform_now - end - end - - # delete a record - Document.lookup_class.where(user_id:p.user.id,asset_id:doc1.id).last.delete - - #duplicate a record - Document.lookup_class.where(user_id:p2.user.id, asset_id:doc2.id).last.dup.save! - - refute Document.lookup_table_consistent?(p.user) - refute Document.lookup_table_consistent?(p2.user) - - #gets immmediately updated for anonymous user - assert Document.lookup_table_consistent?(nil) - - assert_enqueued_jobs(1) do - assert_difference('AuthLookupUpdateQueue.count',1) do - RegularMaintenanceJob.perform_now - end - end - - # double check it will be fixed when the job runs - AuthLookupUpdateJob.perform_now - assert Document.lookup_table_consistent?(p.user) - assert Document.lookup_table_consistent?(p2.user) - assert Document.lookup_table_consistent?(nil) - end - - - end - test 'cleans redundant repositories' do redundant = FactoryBot.create(:blank_repository, created_at: 5.years.ago) redundant_but_in_grace = FactoryBot.create(:blank_repository, created_at: 1.second.ago) From b76db9e4d2540649b925d05a3a9d1dd1b8243ad9 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 11:23:28 +0100 Subject: [PATCH 004/131] skip users that are already queued when checking, probably a newly registered user #1866 --- app/jobs/auth_lookup_maintenance_job.rb | 3 ++ .../jobs/auth_lookup_maintenance_job_test.rb | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/app/jobs/auth_lookup_maintenance_job.rb b/app/jobs/auth_lookup_maintenance_job.rb index ca46761a34..2cfa216fed 100644 --- a/app/jobs/auth_lookup_maintenance_job.rb +++ b/app/jobs/auth_lookup_maintenance_job.rb @@ -14,6 +14,9 @@ def check_authlookup_consistency items_for_queue = [].to_set User.where.not(person_id: nil).to_a.push(nil).each do |user| + # skip if this user is queued up + next if AuthLookupUpdateQueue.where(item:user).any? + # will only deal with 1 type per user per run found = Seek::Util.authorized_types.find do |type| !type.lookup_table_consistent?(user) diff --git a/test/unit/jobs/auth_lookup_maintenance_job_test.rb b/test/unit/jobs/auth_lookup_maintenance_job_test.rb index 95f1b543f2..ae563886d4 100644 --- a/test/unit/jobs/auth_lookup_maintenance_job_test.rb +++ b/test/unit/jobs/auth_lookup_maintenance_job_test.rb @@ -73,4 +73,54 @@ class AuthLookupMaintenaceJobTest < ActiveSupport::TestCase end + test 'skip if user in the queue' do + #ensure a consistent initial state + disable_authorization_checks do + Seek::Util.authorized_types.each(&:destroy_all) + Seek::Util.authorized_types.each(&:clear_lookup_table) + end + + User.destroy_all + p = FactoryBot.create(:person) + + with_config_value(:auth_lookup_enabled, true) do + assert AuthLookupUpdateQueue.queue_enabled? + + doc1 = FactoryBot.create(:document) + AuthLookupUpdateJob.perform_now + + assert Document.lookup_table_consistent?(p.user) + + # delete a record + Document.lookup_class.where(user_id:p.user.id,asset_id:doc1.id).last.delete + + refute Document.lookup_table_consistent?(p.user) + + #gets immmediately updated for anonymous user + assert Document.lookup_table_consistent?(nil) + + refute AuthLookupUpdateQueue.any? + AuthLookupUpdateQueue.create!(item: p.user) + assert AuthLookupUpdateQueue.any? + + # nothing queued whilst user is queued + assert_no_enqueued_jobs do + assert_no_difference('AuthLookupUpdateQueue.count') do + AuthLookupMaintenanceJob.perform_now + end + end + + AuthLookupUpdateQueue.destroy_all + refute AuthLookupUpdateQueue.any? + + # queued after the user has been removed + assert_enqueued_jobs(1) do + assert_difference('AuthLookupUpdateQueue.count',1) do + AuthLookupMaintenanceJob.perform_now + end + end + + end + end + end \ No newline at end of file From beb0de6fa9cba72ae26f2c92eb5aa19f07fe2257 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 11:52:30 +0100 Subject: [PATCH 005/131] skip any types that are present in the queue #1866 --- app/jobs/auth_lookup_maintenance_job.rb | 3 + .../jobs/auth_lookup_maintenance_job_test.rb | 65 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/jobs/auth_lookup_maintenance_job.rb b/app/jobs/auth_lookup_maintenance_job.rb index 2cfa216fed..3b06b727c4 100644 --- a/app/jobs/auth_lookup_maintenance_job.rb +++ b/app/jobs/auth_lookup_maintenance_job.rb @@ -19,6 +19,9 @@ def check_authlookup_consistency # will only deal with 1 type per user per run found = Seek::Util.authorized_types.find do |type| + # skip if there are any queued items of this type + next if AuthLookupUpdateQueue.where(item_type:type.name).any? + !type.lookup_table_consistent?(user) end diff --git a/test/unit/jobs/auth_lookup_maintenance_job_test.rb b/test/unit/jobs/auth_lookup_maintenance_job_test.rb index ae563886d4..9c40175428 100644 --- a/test/unit/jobs/auth_lookup_maintenance_job_test.rb +++ b/test/unit/jobs/auth_lookup_maintenance_job_test.rb @@ -2,6 +2,16 @@ class AuthLookupMaintenaceJobTest < ActiveSupport::TestCase + def setup + #ensure a consistent initial state + disable_authorization_checks do + Seek::Util.authorized_types.each(&:destroy_all) + Seek::Util.authorized_types.each(&:clear_lookup_table) + end + + User.destroy_all + end + test 'run period' do assert_equal 8.hours, AuthLookupMaintenanceJob::RUN_PERIOD end @@ -16,13 +26,7 @@ class AuthLookupMaintenaceJobTest < ActiveSupport::TestCase test 'check authlookup consistency' do - #ensure a consistent initial state - disable_authorization_checks do - Seek::Util.authorized_types.each(&:destroy_all) - Seek::Util.authorized_types.each(&:clear_lookup_table) - end - User.destroy_all p = FactoryBot.create(:person) p2 = FactoryBot.create(:person) u = FactoryBot.create(:brand_new_user) @@ -74,13 +78,7 @@ class AuthLookupMaintenaceJobTest < ActiveSupport::TestCase end test 'skip if user in the queue' do - #ensure a consistent initial state - disable_authorization_checks do - Seek::Util.authorized_types.each(&:destroy_all) - Seek::Util.authorized_types.each(&:clear_lookup_table) - end - User.destroy_all p = FactoryBot.create(:person) with_config_value(:auth_lookup_enabled, true) do @@ -123,4 +121,47 @@ class AuthLookupMaintenaceJobTest < ActiveSupport::TestCase end end + test 'skip if particular type is on the queue' do + p = FactoryBot.create(:person) + + with_config_value(:auth_lookup_enabled, true) do + assert AuthLookupUpdateQueue.queue_enabled? + + doc1 = FactoryBot.create(:document) + AuthLookupUpdateJob.perform_now + + assert Document.lookup_table_consistent?(p.user) + + # delete a record + Document.lookup_class.where(user_id:p.user.id,asset_id:doc1.id).last.delete + + refute Document.lookup_table_consistent?(p.user) + + #gets immmediately updated for anonymous user + assert Document.lookup_table_consistent?(nil) + + refute AuthLookupUpdateQueue.any? + AuthLookupUpdateQueue.create!(item: doc1) + assert AuthLookupUpdateQueue.any? + + # nothing queued whilst doc1 is queued + assert_no_enqueued_jobs do + assert_no_difference('AuthLookupUpdateQueue.count') do + AuthLookupMaintenanceJob.perform_now + end + end + + AuthLookupUpdateQueue.destroy_all + refute AuthLookupUpdateQueue.any? + + # queued after the doc1 has been removed + assert_enqueued_jobs(1) do + assert_difference('AuthLookupUpdateQueue.count',1) do + AuthLookupMaintenanceJob.perform_now + end + end + + end + end + end \ No newline at end of file From 106df34434ad68ad33366b341f7a1f91aaacdab5 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 13:17:11 +0100 Subject: [PATCH 006/131] updated schedule test #1866 --- test/integration/schedule_test.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/integration/schedule_test.rb b/test/integration/schedule_test.rb index 54f930f38d..b7aa273ccb 100644 --- a/test/integration/schedule_test.rb +++ b/test/integration/schedule_test.rb @@ -24,6 +24,11 @@ class ScheduleTest < ActionDispatch::IntegrationTest assert regular assert_equal [RegularMaintenanceJob::RUN_PERIOD, { at: '1:00am' }], regular[:every] + # AuthLookupMaintenanceJob + auth = pop_task(runners, "AuthLookupMaintenanceJob.perform_later") + assert auth + assert_equal [AuthLookupMaintenanceJob::RUN_PERIOD, { at: '1:00am' }], auth[:every] + # LifeMonitor status lm_status = pop_task(runners, "LifeMonitorStatusJob.perform_later") assert lm_status From 31fde85f4ce8fca93688a86866be79d2493a45a0 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 14:26:23 +0100 Subject: [PATCH 007/131] check each type, rather than just taking the first failing type #1866 --- app/jobs/auth_lookup_maintenance_job.rb | 29 ++++++----- .../jobs/auth_lookup_maintenance_job_test.rb | 48 ++++++++++++++++++- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/app/jobs/auth_lookup_maintenance_job.rb b/app/jobs/auth_lookup_maintenance_job.rb index 3b06b727c4..fb9700549b 100644 --- a/app/jobs/auth_lookup_maintenance_job.rb +++ b/app/jobs/auth_lookup_maintenance_job.rb @@ -11,29 +11,28 @@ def perform # checks lookup_table_consistent? on each type for each user, and if not triggers a job to repopulate for that user def check_authlookup_consistency found_types = [].to_set - items_for_queue = [].to_set - User.where.not(person_id: nil).to_a.push(nil).each do |user| - # skip if this user is queued up - next if AuthLookupUpdateQueue.where(item:user).any? + Seek::Util.authorized_types.each do |type| + #skip if there are any items of this type queued + next if AuthLookupUpdateQueue.where(item_type: type.name).any? - # will only deal with 1 type per user per run - found = Seek::Util.authorized_types.find do |type| - # skip if there are any queued items of this type - next if AuthLookupUpdateQueue.where(item_type:type.name).any? + items_for_queue = [].to_set - !type.lookup_table_consistent?(user) - end + User.where.not(person_id: nil).to_a.push(nil).each do |user| - next unless found.present? + # skip if this user is queued up + next if AuthLookupUpdateQueue.where(item: user).any? + next if type.lookup_table_consistent?(user) - found_types << found - missing = found.items_missing_from_authlookup(user) - items_for_queue.merge(missing) + items_for_queue.merge(type.items_missing_from_authlookup(user)) + found_types << type + end + AuthLookupUpdateQueue.enqueue(items_for_queue.to_a) unless items_for_queue.empty? end found_types.each(&:remove_invalid_auth_lookup_entries) - AuthLookupUpdateQueue.enqueue(items_for_queue.to_a) unless items_for_queue.empty? end + + end \ No newline at end of file diff --git a/test/unit/jobs/auth_lookup_maintenance_job_test.rb b/test/unit/jobs/auth_lookup_maintenance_job_test.rb index 9c40175428..04d4dd8023 100644 --- a/test/unit/jobs/auth_lookup_maintenance_job_test.rb +++ b/test/unit/jobs/auth_lookup_maintenance_job_test.rb @@ -10,6 +10,7 @@ def setup end User.destroy_all + AuthLookupUpdateQueue.destroy_all end test 'run period' do @@ -94,7 +95,7 @@ def setup refute Document.lookup_table_consistent?(p.user) - #gets immmediately updated for anonymous user + #gets immediately updated for anonymous user assert Document.lookup_table_consistent?(nil) refute AuthLookupUpdateQueue.any? @@ -137,7 +138,7 @@ def setup refute Document.lookup_table_consistent?(p.user) - #gets immmediately updated for anonymous user + #gets immediately updated for anonymous user assert Document.lookup_table_consistent?(nil) refute AuthLookupUpdateQueue.any? @@ -164,4 +165,47 @@ def setup end end + test 'checks each type' do + p = FactoryBot.create(:person) + + with_config_value(:auth_lookup_enabled, true) do + assert AuthLookupUpdateQueue.queue_enabled? + + doc = FactoryBot.create(:document) + sample = FactoryBot.create(:sample) + sop = FactoryBot.create(:sop) + with_config_value(:auth_lookup_update_batch_size, 20) do + AuthLookupUpdateJob.perform_now + end + + assert Document.lookup_table_consistent?(p.user) + assert Sample.lookup_table_consistent?(p.user) + assert Sop.lookup_table_consistent?(p.user) + + # delete a record + Document.lookup_class.where(user_id:p.user.id,asset_id:doc.id).last.delete + Sample.lookup_class.where(user_id:p.user.id,asset_id:sample.id).last.delete + Sop.lookup_class.where(user_id:p.user.id,asset_id:sop.id).last.delete + + refute Document.lookup_table_consistent?(p.user) + refute Sample.lookup_table_consistent?(p.user) + refute Sop.lookup_table_consistent?(p.user) + + #gets immediately updated for anonymous user + assert Document.lookup_table_consistent?(nil) + assert Sample.lookup_table_consistent?(nil) + assert Sop.lookup_table_consistent?(nil) + + assert_enqueued_jobs(3) do + assert_difference('AuthLookupUpdateQueue.count',3) do + AuthLookupMaintenanceJob.perform_now + end + end + + assert AuthLookupUpdateQueue.where(item: doc).any? + assert AuthLookupUpdateQueue.where(item: sample).any? + assert AuthLookupUpdateQueue.where(item: sop).any? + end + end + end \ No newline at end of file From c7464fa52feaf03e958dcf140d7d33f160e97fc3 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 15:20:00 +0100 Subject: [PATCH 008/131] queue authupdate for user only after person_id is associated #1866 needs tests, but see what tests are affected first --- app/models/user.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index ed0e487e03..1f13990a0c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -67,7 +67,7 @@ class User < ApplicationRecord delegate :is_admin_or_project_administrator?, to: :person, allow_nil: true delegate :is_programme_administrator?, to: :person, allow_nil: true - after_commit :queue_update_auth_table, on: :create + after_save_commit :queue_update_auth_table after_destroy :queue_auth_lookup_delete_job @@ -336,7 +336,9 @@ def email_available? end def queue_update_auth_table - AuthLookupUpdateQueue.enqueue(self) + if saved_changes.keys.include?("person_id") + AuthLookupUpdateQueue.enqueue(self) + end end def queue_auth_lookup_delete_job From 9749e094e327f1f1c36079b40be1ae2dc4d870dc Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 16:09:40 +0100 Subject: [PATCH 009/131] updated to to check job is queued after registration #1866 --- app/models/user.rb | 2 +- .../permissions/auth_lookup_update_queue_test.rb | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 1f13990a0c..6d0f25e762 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -337,7 +337,7 @@ def email_available? def queue_update_auth_table if saved_changes.keys.include?("person_id") - AuthLookupUpdateQueue.enqueue(self) + AuthLookupUpdateQueue.enqueue(self, priority: 1) end end diff --git a/test/unit/permissions/auth_lookup_update_queue_test.rb b/test/unit/permissions/auth_lookup_update_queue_test.rb index a1cc616690..95b07d9a8c 100644 --- a/test/unit/permissions/auth_lookup_update_queue_test.rb +++ b/test/unit/permissions/auth_lookup_update_queue_test.rb @@ -161,10 +161,17 @@ def teardown end end - test 'updates when a user registers' do + test 'updates when a user registers but not until associated with a person' do + person = FactoryBot.create(:person) + user = assert_no_difference('AuthLookupUpdateQueue.count') do + FactoryBot.create(:brand_new_user) + end assert_difference('AuthLookupUpdateQueue.count', 1) do - user = FactoryBot.create(:brand_new_user) - assert_equal user, AuthLookupUpdateQueue.order(:id).last.item + user.person = person + user.save! + q = AuthLookupUpdateQueue.order(:id).last + assert_equal user, q.item + assert_equal 1, q.priority end end From 4aa490cf17c6e129322bb1295300171b1852515c Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Tue, 7 May 2024 17:35:10 +0100 Subject: [PATCH 010/131] give UserAuthLookupUpdateJob a longer timeout of 30 mins, and do in batches of 8000 #1866 --- app/jobs/user_auth_lookup_update_job.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/jobs/user_auth_lookup_update_job.rb b/app/jobs/user_auth_lookup_update_job.rb index 957c9320f0..d67ccf3d10 100644 --- a/app/jobs/user_auth_lookup_update_job.rb +++ b/app/jobs/user_auth_lookup_update_job.rb @@ -2,11 +2,21 @@ class UserAuthLookupUpdateJob < ApplicationJob queue_as QueueNames::AUTH_LOOKUP queue_with_priority 0 + BATCH_SIZE = 8000 - def perform(user, type) - type.constantize.includes(policy: :permissions).find_each do |item| + # needs longer, otherwise the samples can time out + def timelimit + 30.minutes + end + + def perform(user, type, offset = 0) + type.constantize.offset(offset).limit(BATCH_SIZE).includes(policy: :permissions).find_each do |item| item.update_lookup_table(user) end + + offset += BATCH_SIZE + + self.class.perform_later(user, type, offset) if offset < type.constantize.count end end From 12717cf18b20d28b1b22810f1ed9df6b0fd08232 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Wed, 8 May 2024 10:30:55 +0100 Subject: [PATCH 011/131] also skip if person is on the queue, and added test to check for anonymous user checks #1866 --- app/jobs/auth_lookup_maintenance_job.rb | 5 +- .../jobs/auth_lookup_maintenance_job_test.rb | 55 ++++++++++++++----- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/jobs/auth_lookup_maintenance_job.rb b/app/jobs/auth_lookup_maintenance_job.rb index fb9700549b..14c4d73fcd 100644 --- a/app/jobs/auth_lookup_maintenance_job.rb +++ b/app/jobs/auth_lookup_maintenance_job.rb @@ -20,8 +20,9 @@ def check_authlookup_consistency User.where.not(person_id: nil).to_a.push(nil).each do |user| - # skip if this user is queued up - next if AuthLookupUpdateQueue.where(item: user).any? + # skip if this user or person is queued up + next if user && AuthLookupUpdateQueue.where(item: user).or(AuthLookupUpdateQueue.where(item: user.person)).any? + next if type.lookup_table_consistent?(user) items_for_queue.merge(type.items_missing_from_authlookup(user)) diff --git a/test/unit/jobs/auth_lookup_maintenance_job_test.rb b/test/unit/jobs/auth_lookup_maintenance_job_test.rb index 04d4dd8023..102aa8c5e2 100644 --- a/test/unit/jobs/auth_lookup_maintenance_job_test.rb +++ b/test/unit/jobs/auth_lookup_maintenance_job_test.rb @@ -60,9 +60,6 @@ def setup refute Document.lookup_table_consistent?(p.user) refute Document.lookup_table_consistent?(p2.user) - #gets immmediately updated for anonymous user - assert Document.lookup_table_consistent?(nil) - assert_enqueued_jobs(1) do assert_difference('AuthLookupUpdateQueue.count',1) do AuthLookupMaintenanceJob.perform_now @@ -78,7 +75,32 @@ def setup end - test 'skip if user in the queue' do + test 'test for anonymous user' do + + with_config_value(:auth_lookup_enabled, true) do + assert AuthLookupUpdateQueue.queue_enabled? + + doc = FactoryBot.create(:document) + AuthLookupUpdateJob.perform_now + + assert Document.lookup_table_consistent?(nil) + + # delete a record + Document.lookup_class.where(user_id:0,asset_id:doc.id).last.delete + + refute Document.lookup_table_consistent?(nil) + + # queued after the user has been removed + assert_enqueued_jobs(1) do + assert_difference('AuthLookupUpdateQueue.count',1) do + AuthLookupMaintenanceJob.perform_now + end + end + + end + end + + test 'skip if user or person in the queue' do p = FactoryBot.create(:person) @@ -95,9 +117,6 @@ def setup refute Document.lookup_table_consistent?(p.user) - #gets immediately updated for anonymous user - assert Document.lookup_table_consistent?(nil) - refute AuthLookupUpdateQueue.any? AuthLookupUpdateQueue.create!(item: p.user) assert AuthLookupUpdateQueue.any? @@ -109,8 +128,22 @@ def setup end end + AuthLookupUpdateQueue.destroy_all + AuthLookupUpdateQueue.create!(item: p) + assert AuthLookupUpdateQueue.any? + + # nothing queued whilst person is queued + assert_no_enqueued_jobs do + assert_no_difference('AuthLookupUpdateQueue.count') do + AuthLookupMaintenanceJob.perform_now + end + end + AuthLookupUpdateQueue.destroy_all refute AuthLookupUpdateQueue.any? + #add another item to make sure it's only checking for user/person + AuthLookupUpdateQueue.create!(item: FactoryBot.create(:sop)) + assert AuthLookupUpdateQueue.any? # queued after the user has been removed assert_enqueued_jobs(1) do @@ -138,9 +171,6 @@ def setup refute Document.lookup_table_consistent?(p.user) - #gets immediately updated for anonymous user - assert Document.lookup_table_consistent?(nil) - refute AuthLookupUpdateQueue.any? AuthLookupUpdateQueue.create!(item: doc1) assert AuthLookupUpdateQueue.any? @@ -191,11 +221,6 @@ def setup refute Sample.lookup_table_consistent?(p.user) refute Sop.lookup_table_consistent?(p.user) - #gets immediately updated for anonymous user - assert Document.lookup_table_consistent?(nil) - assert Sample.lookup_table_consistent?(nil) - assert Sop.lookup_table_consistent?(nil) - assert_enqueued_jobs(3) do assert_difference('AuthLookupUpdateQueue.count',3) do AuthLookupMaintenanceJob.perform_now From 3b5731c2896f23046abcb11dd3f60d82947c2a75 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Wed, 8 May 2024 15:25:51 +0100 Subject: [PATCH 012/131] use each instead of find_each, due to an issue when combined with offset and limit #1866 --- app/jobs/user_auth_lookup_update_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/user_auth_lookup_update_job.rb b/app/jobs/user_auth_lookup_update_job.rb index d67ccf3d10..6ea614771d 100644 --- a/app/jobs/user_auth_lookup_update_job.rb +++ b/app/jobs/user_auth_lookup_update_job.rb @@ -10,7 +10,7 @@ def timelimit end def perform(user, type, offset = 0) - type.constantize.offset(offset).limit(BATCH_SIZE).includes(policy: :permissions).find_each do |item| + type.constantize.offset(offset).limit(BATCH_SIZE).includes(policy: :permissions).each do |item| item.update_lookup_table(user) end From 211896f092747f9b3fd1f6df6ab8fe1d9db01fe8 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Thu, 9 May 2024 18:39:06 +0100 Subject: [PATCH 013/131] upgrade tweaks WIP #1861 --- lib/tasks/seek_upgrades.rake | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/tasks/seek_upgrades.rake b/lib/tasks/seek_upgrades.rake index ebd435c9ef..44726d19b1 100644 --- a/lib/tasks/seek_upgrades.rake +++ b/lib/tasks/seek_upgrades.rake @@ -113,15 +113,17 @@ namespace :seek do disable_authorization_checks do - Sample.includes(:originating_data_file).find_each do |sample| - # check if the sample was extracted from a datafile and their policies are linked - if sample.extracted? && sample.policy_id == sample.originating_data_file&.policy_id - policy = sample.policy.deep_copy - policy.save - sample.update_column(:policy_id, policy.id) - putc('.') - affected_samples << sample + Sample.includes(:originating_data_file).in_batches(of: 250) do |batch| + batch.each do |sample| + # check if the sample was extracted from a datafile and their policies are linked + #if sample.extracted? && sample.policy_id == sample.originating_data_file&.policy_id + policy = sample.policy.deep_copy + # policy.save + # sample.update_column(:policy_id, policy.id) + affected_samples << sample + #end end + putc('.') end end puts "..... finished creating independent policies of #{affected_samples.count} extracted samples" @@ -137,18 +139,21 @@ namespace :seek do decoupled = 0 hash_array = [] disable_authorization_checks do - Sample.find_each do |sample| - # check if the sample was extracted from a datafile and their projects are linked - if sample.extracted? && sample.project_ids.empty? - sample.originating_data_file.project_ids.each do |project_id| - hash_array << { project_id: project_id, sample_id: sample.id } + Sample.includes(:originating_data_file).in_batches(of: 250) do |batch| + batch.each do |sample| + # check if the sample was extracted from a datafile and their projects are linked + if sample.extracted? && sample.project_ids.empty? + sample.originating_data_file.project_ids.each do |project_id| + hash_array << { project_id: project_id, sample_id: sample.id } + end + decoupled += 1 end - decoupled += 1 end + putc('.') end unless hash_array.empty? class ProjectsSample < ActiveRecord::Base; end; - ProjectsSample.insert_all(hash_array) + #ProjectsSample.insert_all(hash_array) end end puts " ... finished copying project ids of #{decoupled.to_s} extracted samples" From 4ebf317e870e832de7516b8422c0255fb886733d Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Mon, 13 May 2024 09:31:24 +0100 Subject: [PATCH 014/131] finished improving the upgrade tasks for updating samples #1861 show progress for decouple_extracted_samples_policies and decouple_extracted_samples_projects, after every batch of 250 also update the query for decouple_extracted_samples_projects to select those without projects, making reruns quicker --- lib/tasks/seek_upgrades.rake | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/tasks/seek_upgrades.rake b/lib/tasks/seek_upgrades.rake index 44726d19b1..14ce7516bf 100644 --- a/lib/tasks/seek_upgrades.rake +++ b/lib/tasks/seek_upgrades.rake @@ -112,16 +112,15 @@ namespace :seek do Permission.skip_callback :commit, :after, :queue_rdf_generation_job disable_authorization_checks do - Sample.includes(:originating_data_file).in_batches(of: 250) do |batch| batch.each do |sample| # check if the sample was extracted from a datafile and their policies are linked - #if sample.extracted? && sample.policy_id == sample.originating_data_file&.policy_id + if sample.extracted? && sample.policy_id == sample.originating_data_file&.policy_id policy = sample.policy.deep_copy - # policy.save - # sample.update_column(:policy_id, policy.id) + policy.save + sample.update_column(:policy_id, policy.id) affected_samples << sample - #end + end end putc('.') end @@ -136,27 +135,27 @@ namespace :seek do task(decouple_extracted_samples_projects: [:environment]) do puts '..... copying project ids for extracted samples...' - decoupled = 0 + decoupled_count = 0 hash_array = [] disable_authorization_checks do - Sample.includes(:originating_data_file).in_batches(of: 250) do |batch| + Sample.includes(:originating_data_file).where.missing(:projects).in_batches(of: 250) do |batch| batch.each do |sample| # check if the sample was extracted from a datafile and their projects are linked if sample.extracted? && sample.project_ids.empty? sample.originating_data_file.project_ids.each do |project_id| hash_array << { project_id: project_id, sample_id: sample.id } end - decoupled += 1 + decoupled_count += 1 end end putc('.') end unless hash_array.empty? class ProjectsSample < ActiveRecord::Base; end; - #ProjectsSample.insert_all(hash_array) + ProjectsSample.insert_all(hash_array) end end - puts " ... finished copying project ids of #{decoupled.to_s} extracted samples" + puts " ... finished copying project ids of #{decoupled_count.to_s} extracted samples" end task(link_sample_datafile_attributes: [:environment]) do From d209fa8686ea980d5a1d8619202f30d7e00faba1 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Mon, 13 May 2024 09:42:54 +0100 Subject: [PATCH 015/131] removed upgrade tasks from 1.14 #1861 --- lib/tasks/seek_upgrades.rake | 50 ------------------------------------ 1 file changed, 50 deletions(-) diff --git a/lib/tasks/seek_upgrades.rake b/lib/tasks/seek_upgrades.rake index 14ce7516bf..769e95aed1 100644 --- a/lib/tasks/seek_upgrades.rake +++ b/lib/tasks/seek_upgrades.rake @@ -11,9 +11,6 @@ namespace :seek do decouple_extracted_samples_policies decouple_extracted_samples_projects link_sample_datafile_attributes - strip_sample_attribute_pids - rename_registered_sample_multiple_attribute_type - remove_ontology_attribute_type db:seed:007_sample_attribute_types db:seed:001_create_controlled_vocabs db:seed:017_minimal_starter_isa_templates @@ -55,53 +52,6 @@ namespace :seek do end end - task(rename_registered_sample_multiple_attribute_type: [:environment]) do - attr = SampleAttributeType.find_by(title:'Registered Sample (multiple)') - if attr - puts "..... Renaming sample attribute type 'Registered Sample (multiple)' to 'Registered Sample List'." - attr.update_column(:title, 'Registered Sample List') - end - end - - task(strip_sample_attribute_pids: [:environment]) do - puts '..... Stripping Sample Attribute PIds ...' - n = 0 - SampleAttribute.where('pid is NOT NULL AND pid !=?','').each do |attribute| - new_pid = attribute.pid.strip - if attribute.pid != new_pid - attribute.update_column(:pid, new_pid) - n += 1 - end - end - puts "..... Finished stripping #{n} Sample Attribute PIds." - end - - task(remove_ontology_attribute_type: [:environment]) do - ontology_attr_type = SampleAttributeType.find_by(title:'Ontology') - cv_attr_type = SampleAttributeType.find_by(title:'Controlled Vocabulary') - if ontology_attr_type - puts '..... Removing the Ontology sample attribute type ...' - if cv_attr_type - if ontology_attr_type.sample_attributes.any? - puts "..... Moving #{ontology_attr_type.sample_attributes.count} sample attributes to Controlled Vocabulary" - ontology_attr_type.sample_attributes.each do |attr_type| - attr_type.update_column(:sample_attribute_type_id, cv_attr_type.id) - end - end - if ontology_attr_type.isa_template_attributes.any? - puts "..... Moving #{ontology_attr_type.isa_template_attributes.count} template attributes to Controlled Vocabulary" - ontology_attr_type.isa_template_attributes.each do |attr_type| - attr_type.update_column(:sample_attribute_type_id, cv_attr_type.id) - end - end - - ontology_attr_type.destroy - else - puts '..... Target Controlled Vocabulary attribute type not found' - end - end - end - task(decouple_extracted_samples_policies: [:environment]) do puts '..... creating independent policies for extracted samples (this can take a while if there are many samples) ...' affected_samples = [] From 8ab176c531e227317c27a66fa4258f4adce43cb5 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Mon, 13 May 2024 10:14:22 +0100 Subject: [PATCH 016/131] don't queue reindexing items or jobs, or create follow on jobs, if solr_enabled is false #1874 --- app/jobs/reindexing_job.rb | 2 +- app/models/reindexing_queue.rb | 6 ++++++ test/unit/jobs/reindexing_job_test.rb | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/jobs/reindexing_job.rb b/app/jobs/reindexing_job.rb index ff45e69e65..a19281775e 100644 --- a/app/jobs/reindexing_job.rb +++ b/app/jobs/reindexing_job.rb @@ -25,7 +25,7 @@ def gather_items end def follow_on_job? - ReindexingQueue.any? + Seek::Config.solr_enabled && ReindexingQueue.any? end def timelimit diff --git a/app/models/reindexing_queue.rb b/app/models/reindexing_queue.rb index e01186c0cd..40a988713c 100644 --- a/app/models/reindexing_queue.rb +++ b/app/models/reindexing_queue.rb @@ -1,7 +1,13 @@ class ReindexingQueue < ApplicationRecord include ResourceQueue + def self.enqueue(*items, priority: DEFAULT_PRIORITY, queue_job: true) + return unless Seek::Config.solr_enabled + super(items, priority: priority, queue_job: queue_job) + end + def self.job_class ReindexingJob end + end diff --git a/test/unit/jobs/reindexing_job_test.rb b/test/unit/jobs/reindexing_job_test.rb index 7bc3f20324..78e8804b89 100644 --- a/test/unit/jobs/reindexing_job_test.rb +++ b/test/unit/jobs/reindexing_job_test.rb @@ -27,6 +27,20 @@ class ReindexingJobTest < ActiveSupport::TestCase end end + test 'dont add items if search disabled' do + p = FactoryBot.create :person + ReindexingQueue.delete_all + + with_config_value(:solr_enabled, false) do + assert_no_enqueued_jobs(only: ReindexingJob) do + assert_no_difference('ReindexingQueue.count') do + ReindexingQueue.enqueue(p) + ReindexingQueue.enqueue(FactoryBot.create(:sop)) + end + end + end + end + test 'gather_items strips deleted (nil) items' do model1 = FactoryBot.create(:model) model2 = FactoryBot.create(:model) @@ -44,4 +58,14 @@ class ReindexingJobTest < ActiveSupport::TestCase assert_includes items, model2 assert_includes items, document end + + test 'follow on job' do + ReindexingQueue.delete_all + refute ReindexingJob.new.follow_on_job? + ReindexingQueue.enqueue(FactoryBot.create(:sop)) + assert ReindexingJob.new.follow_on_job? + with_config_value(:solr_enabled, false) do + refute ReindexingJob.new.follow_on_job? + end + end end From 0717937150a0b4518b5cfbc73e4177f9c9b1f682 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 15 May 2024 09:14:59 +0200 Subject: [PATCH 017/131] internationalize the names in the search types --- app/helpers/search_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 1960662ebf..39824e8ed8 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -3,7 +3,7 @@ module SearchHelper def search_type_options - [['All', '']] | Seek::Util.searchable_types.collect { |c| [(c.name.underscore.humanize == 'Sop' ? t('sop') : c.name.underscore.humanize.pluralize), c.name.underscore.pluralize] } + [['All', '']] | Seek::Util.searchable_types.collect { |c| [(c.name.underscore.humanize == 'Sop' ? t('sop') : t(c.name.underscore).humanize.pluralize), c.name.underscore.pluralize] } end def external_search_tooltip_text From bb7e941400c6c7fbdc1ec88ae7c568728ad6c0e3 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 15 May 2024 13:59:54 +0200 Subject: [PATCH 018/131] Add test --- test/functional/homes_controller_test.rb | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/functional/homes_controller_test.rb b/test/functional/homes_controller_test.rb index 51a9d86bc2..0c3ca05d19 100644 --- a/test/functional/homes_controller_test.rb +++ b/test/functional/homes_controller_test.rb @@ -726,6 +726,51 @@ class HomesControllerTest < ActionController::TestCase end + test 'Show aliases in search options' do + I18n.load_path += Dir[Rails.root.join('test', 'config', 'translation_override.en.yml')] + I18n.backend.load_translations + with_config_values({ isa_json_compliance_enabled: true, solr_enabled: true }) do + get :index + assert_response :success + assert_select 'select#search_type' do + assert_select 'option', text: 'Investigation tests', count: 1 + assert_select 'option', text: 'Study tests', count: 1 + assert_select 'option', text: 'Assay tests', count: 1 + assert_select 'option', text: 'Data file tests', count: 1 + assert_select 'option', text: 'Document tests', count: 1 + assert_select 'option', text: 'SOP_test', count: 1 # this is an exception, the alias is not in the translation file + assert_select 'option', text: 'Presentation tests', count: 1 + assert_select 'option', text: 'Event tests', count: 1 + assert_select 'option', text: 'Collection tests', count: 1 + assert_select 'option', text: 'Sample tests', count: 1 + assert_select 'option', text: 'Sample type tests', count: 1 + assert_select 'option', text: 'Template tests', count: 1 + assert_select 'option', text: 'Person tests', count: 1 + assert_select 'option', text: 'Project tests', count: 1 + assert_select 'option', text: 'Institution tests', count: 1 + assert_select 'option', text: 'Programme tests', count: 1 + + # Making sure the default values are not shown + assert_select 'option', text: 'Investigations', count: 0 + assert_select 'option', text: 'Studies', count: 0 + assert_select 'option', text: 'Assays', count: 0 + assert_select 'option', text: 'Data files', count: 0 + assert_select 'option', text: 'Documents', count: 0 + assert_select 'option', text: 'SOP', count: 0 + assert_select 'option', text: 'Presentations', count: 0 + assert_select 'option', text: 'Events', count: 0 + assert_select 'option', text: 'Collections', count: 0 + assert_select 'option', text: 'Samples', count: 0 + assert_select 'option', text: 'Sample types', count: 0 + assert_select 'option', text: 'Templates', count: 0 + assert_select 'option', text: 'People', count: 0 + assert_select 'option', text: 'Projects', count: 0 + assert_select 'option', text: 'Institutions', count: 0 + assert_select 'option', text: 'Programmes', count: 0 + end + end + end + test 'get dataset jsonld from index' do get :index, format: :jsonld assert_response :success From 45f779b2e8dfef5c357c8276096626ecb55acc7c Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 15 May 2024 14:38:09 +0200 Subject: [PATCH 019/131] Restore the internalisation settings afterwards --- test/functional/homes_controller_test.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/functional/homes_controller_test.rb b/test/functional/homes_controller_test.rb index 0c3ca05d19..a8bbc1c54f 100644 --- a/test/functional/homes_controller_test.rb +++ b/test/functional/homes_controller_test.rb @@ -727,6 +727,7 @@ class HomesControllerTest < ActionController::TestCase end test 'Show aliases in search options' do + original_load_path = I18n.load_path I18n.load_path += Dir[Rails.root.join('test', 'config', 'translation_override.en.yml')] I18n.backend.load_translations with_config_values({ isa_json_compliance_enabled: true, solr_enabled: true }) do @@ -769,6 +770,8 @@ class HomesControllerTest < ActionController::TestCase assert_select 'option', text: 'Programmes', count: 0 end end + I18n.load_path = original_load_path + I18n.backend.load_translations end test 'get dataset jsonld from index' do From e0d7cc5f26a4cd6e433f561a252a78b5eae23761 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer <82407142+kdp-cloud@users.noreply.github.com> Date: Wed, 15 May 2024 15:12:31 +0200 Subject: [PATCH 020/131] Issue 1783 fix add new button (#1872) * Add to the config --- lib/seek/config.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/seek/config.rb b/lib/seek/config.rb index 2f6df9fe27..a363d1986e 100644 --- a/lib/seek/config.rb +++ b/lib/seek/config.rb @@ -336,6 +336,10 @@ def templates_enabled isa_json_compliance_enabled end + def strains_enabled + organisms_enabled + end + def omniauth_elixir_aai_config if omniauth_elixir_aai_legacy_mode { From 51653bba80a0defca805da302c004f825529f580 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 16 May 2024 09:28:42 +0200 Subject: [PATCH 021/131] Rewrite can_create? function in more clear way. --- app/models/template.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/template.rb b/app/models/template.rb index de1beeaf88..12131d9518 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -24,8 +24,7 @@ def can_edit?(user = User.current_user) end def self.can_create? - can = User.logged_in_and_member? && Seek::Config.samples_enabled - can && User.current_user.is_admin_or_project_administrator? + super && Seek::Config.samples_enabled && User.current_user.is_admin_or_project_administrator? end def resolve_inconsistencies From c3c0485e159b5289b61e7e64e19059ec3e3cef2a Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 16 May 2024 09:37:53 +0200 Subject: [PATCH 022/131] Filter out administered projects in a very very messy way --- app/views/projects/_project_selector.html.erb | 15 ++++++++++++--- app/views/templates/_form.html.erb | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/views/projects/_project_selector.html.erb b/app/views/projects/_project_selector.html.erb index 5d12d5efd5..ea7340f819 100644 --- a/app/views/projects/_project_selector.html.erb +++ b/app/views/projects/_project_selector.html.erb @@ -6,6 +6,7 @@ <% allow_nil ||= false allow_all ||= false + allow_only_administered ||= false resource ||= nil selected_projects = resource ? resource.projects : [] @@ -13,10 +14,18 @@ possible_projects = Project.all else possible_projects = [] - possible_projects |= User.current_user.person.current_projects if resource - possible_projects |= resource.projects - possible_projects |= resource.contributor.current_projects if resource.respond_to?(:contributor) && resource.contributor + if resource.respond_to?(:contributor) && resource.contributor + if allow_only_administered + possible_projects |= resource.contributor.current_projects.select{ |p| @current_user.person.is_project_administrator? p } + else + possible_projects |= resource.contributor.current_projects + end + else + possible_projects |= resource.projects + end + else + possible_projects |= @current_user.person.current_projects end end diff --git a/app/views/templates/_form.html.erb b/app/views/templates/_form.html.erb index d7acd30e54..bc3720dc59 100644 --- a/app/views/templates/_form.html.erb +++ b/app/views/templates/_form.html.erb @@ -15,7 +15,7 @@ <%= f.text_area :description, class: "form-control rich-text-edit", rows: 5, id: "template-description" -%> - <%= render partial: "projects/project_selector", locals: { resource: @template } -%> + <%= render partial: "projects/project_selector", locals: { resource: @template, allow_only_administered: true } -%> <%= render partial: 'assets/manage_specific_attributes', locals:{f:f} if show_form_manage_specific_attributes? %> @@ -85,7 +85,7 @@ <% if @template.children.none? %> - <%= button_link_to('Add new attribute', 'add', '#', id: 'add-attribute') %> + <%= button_link_to('Add new attribute', 'add', '#', id: 'add-attribute') if @template.level %> <% end %> From 42e5890434b501acff148c4322a38a0f304d0cdf Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 16 May 2024 11:12:06 +0200 Subject: [PATCH 023/131] Make current_person available as helper method and simplify logic. --- app/controllers/application_controller.rb | 1 + app/views/projects/_project_selector.html.erb | 15 ++++----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6d98f1920e..b5da0a292c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -54,6 +54,7 @@ def with_current_user def current_person current_user.try(:person) end + helper_method :current_person def partially_registered? redirect_to register_people_path if current_user && !current_user.registration_complete? diff --git a/app/views/projects/_project_selector.html.erb b/app/views/projects/_project_selector.html.erb index ea7340f819..27a1ae2efc 100644 --- a/app/views/projects/_project_selector.html.erb +++ b/app/views/projects/_project_selector.html.erb @@ -14,21 +14,14 @@ possible_projects = Project.all else possible_projects = [] + possible_projects |= current_person.current_projects if resource - if resource.respond_to?(:contributor) && resource.contributor - if allow_only_administered - possible_projects |= resource.contributor.current_projects.select{ |p| @current_user.person.is_project_administrator? p } - else - possible_projects |= resource.contributor.current_projects - end - else - possible_projects |= resource.projects - end - else - possible_projects |= @current_user.person.current_projects + possible_projects |= resource.projects + possible_projects |= resource.contributor.current_projects if resource.respond_to?(:contributor) && resource.contributor end end + possible_projects = possible_projects.select { |p| current_person.is_project_administrator? p } if allow_only_administered possible_projects = possible_projects - selected_projects possible_projects_json, selected_projects_json = [possible_projects, selected_projects].map do |projects| From a768918fb029ba6f47093fc5f79d00dd0546d08e Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 16 May 2024 12:01:19 +0200 Subject: [PATCH 024/131] Hide 'Add new attribute button' when it's a new form and shows the button when an existing template is shown. --- app/assets/javascripts/templates.js | 5 ++++- app/views/templates/_form.html.erb | 16 ++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/templates.js b/app/assets/javascripts/templates.js index 4b0c733526..e1430fc40d 100644 --- a/app/assets/javascripts/templates.js +++ b/app/assets/javascripts/templates.js @@ -293,9 +293,12 @@ const applyTemplate = () => { const template_id_tag = $j(`#isa_study${suffix}template_parent_id`); if (template_id_tag) $j(template_id_tag).val(id); + // Removes the hidden from the new attribute button + $j(`${attribute_table} ${addAttributeRow}`).find('#add-attribute').removeClass("hidden"); + SampleTypes.recalculatePositions(); SampleTypes.bindSortable(); - $j(".sample-type-attribute-type").trigger("change", [false]); + $j(".sample-type-attribute-type").trigger("change", [false]); }; // Shows the modal form diff --git a/app/views/templates/_form.html.erb b/app/views/templates/_form.html.erb index bc3720dc59..82d74be6c4 100644 --- a/app/views/templates/_form.html.erb +++ b/app/views/templates/_form.html.erb @@ -27,12 +27,6 @@

Template Information

- <% if @template.new_record? %> -
- -
- <% end %> -
<%= f.label :level, 'ISA Level' %> <%= f.text_field :level, { class: 'form-control', readonly: true } %> @@ -61,7 +55,13 @@ To ensure compliance with the original template, please do not modify the existing ISA-tags and attributes in the form below. You can always add new attributes.
- + <% if @template.new_record? %> +
+ +
+ <% end %> + +
@@ -85,7 +85,7 @@ <% if @template.children.none? %> <% end %> From 7f6f50bd84026ca40d1696098bb9b81017b46990 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Thu, 16 May 2024 11:56:22 +0100 Subject: [PATCH 025/131] update version numbers for 1.15.0 --- config/version.yml | 2 +- docker-compose-relative-root.yml | 4 ++-- docker-compose-virtuoso.yml | 4 ++-- docker-compose-with-email.yml | 4 ++-- docker-compose.yml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/config/version.yml b/config/version.yml index 9fe2f4b4c9..4b76db861f 100644 --- a/config/version.yml +++ b/config/version.yml @@ -9,4 +9,4 @@ major: 1 minor: 15 -patch: 0-pre +patch: 0 diff --git a/docker-compose-relative-root.yml b/docker-compose-relative-root.yml index c9f214cbbf..7576847289 100644 --- a/docker-compose-relative-root.yml +++ b/docker-compose-relative-root.yml @@ -14,7 +14,7 @@ services: seek: # The SEEK application #build: . - image: fairdom/seek:main + image: fairdom/seek:1.15 container_name: seek command: docker/entrypoint.sh @@ -43,7 +43,7 @@ services: seek_workers: # The SEEK delayed job workers #build: . - image: fairdom/seek:main + image: fairdom/seek:1.15 container_name: seek-workers command: docker/start_workers.sh restart: always diff --git a/docker-compose-virtuoso.yml b/docker-compose-virtuoso.yml index 626485b5a5..8a3d78f05c 100644 --- a/docker-compose-virtuoso.yml +++ b/docker-compose-virtuoso.yml @@ -12,7 +12,7 @@ services: seek: # The SEEK application #build: . - image: fairdom/seek:main + image: fairdom/seek:1.15 container_name: seek command: docker/entrypoint.sh restart: always @@ -39,7 +39,7 @@ services: seek_workers: # The SEEK delayed job workers #build: . - image: fairdom/seek:main + image: fairdom/seek:1.15 container_name: seek-workers command: docker/start_workers.sh restart: always diff --git a/docker-compose-with-email.yml b/docker-compose-with-email.yml index 19beb35095..51685ce1d0 100644 --- a/docker-compose-with-email.yml +++ b/docker-compose-with-email.yml @@ -14,7 +14,7 @@ services: seek: # The SEEK application #build: . - image: fairdom/seek:main + image: fairdom/seek:1.15 container_name: seek command: docker/entrypoint.sh @@ -43,7 +43,7 @@ services: seek_workers: # The SEEK delayed job workers #build: . - image: fairdom/seek:main + image: fairdom/seek:1.15 container_name: seek-workers command: docker/start_workers.sh restart: always diff --git a/docker-compose.yml b/docker-compose.yml index e7dec9dd7e..c9dd6960b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: seek: # The SEEK application #build: . - image: fairdom/seek:1.15-dev + image: fairdom/seek:1.15 container_name: seek command: docker/entrypoint.sh @@ -42,7 +42,7 @@ services: seek_workers: # The SEEK delayed job workers #build: . - image: fairdom/seek:1.15-dev + image: fairdom/seek:1.15 container_name: seek-workers command: docker/start_workers.sh restart: always From b6bf4bbc752e24004c2a24a31692b6ed11106c85 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 16 May 2024 13:15:25 +0200 Subject: [PATCH 026/131] Only lock attribute fields when template is applied on a sample type --- app/assets/javascripts/templates.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/templates.js b/app/assets/javascripts/templates.js index e1430fc40d..a37cbe5e37 100644 --- a/app/assets/javascripts/templates.js +++ b/app/assets/javascripts/templates.js @@ -222,6 +222,7 @@ const applyTemplate = () => { $j('#template_level').val(data.level); $j('#template_parent_id').val(data.template_id); + const appliedToSampleType = $j('#template_level')[0] === undefined || $j('#template_level')[0] === null; // Make sure default sorted attributes are added to the table Templates.table.order([9, "asc"]).draw(); $j.each(Templates.table.rows().data(), (i, row) => { @@ -239,24 +240,25 @@ const applyTemplate = () => { row[7] === "Registered Sample List" && row[1].includes("Input") && row[11] === null; - const isInherited = row[14] !== "undefined" || row[14] !== null; const isRequired = row[0] ? "checked" : ""; newRow = $j(newRow.replace(/replace-me/g, index)); $j(newRow).find('[data-attr="required"]').prop("checked", row[0]); + if (appliedToSampleType) $j(newRow).find('[data-attr="required"]').prop("disabled", true); + $j(newRow).find(".sample-type-is-title").prop("checked", row[8]); + if (appliedToSampleType) $j(newRow).find('.sample-type-is-title').prop("disabled", true); $j(newRow).find('[data-attr="title"]').val(row[1]); - $j(newRow).find('[data-attr="title"]').addClass("disabled"); + if (appliedToSampleType) $j(newRow).find('[data-attr="title"]').addClass("disabled"); $j(newRow).find('[data-attr="description"]').val(row[2]); $j(newRow).find('[data-attr="type"]').val(row[3]); - $j(newRow).find('[data-attr="type"]').addClass("disabled"); + if (appliedToSampleType) $j(newRow).find('[data-attr="type"]').addClass("disabled"); $j(newRow).find('[data-attr="cv_id"]').val(row[4]); - $j(newRow).find('[data-attr="cv_id"]').parent().addClass("disabled"); + if (appliedToSampleType) $j(newRow).find('[data-attr="cv_id"]').parent().addClass("disabled"); $j(newRow).find('[data-attr="allow_cv_free_text"]').prop("checked", row[5]); - $j(newRow) - .find('[data-attr="allow_cv_free_text"]') - .addClass("disabled"); + if (appliedToSampleType) $j(newRow) + .find('[data-attr="allow_cv_free_text"]') + .addClass("disabled"); $j(newRow).find('[data-attr="unit"]').val(row[6]); - $j(newRow).find('[data-attr="unit"]').addClass("disabled"); - $j(newRow).find(".sample-type-is-title").prop("checked", row[8]); + if (appliedToSampleType) $j(newRow).find('[data-attr="unit"]').addClass("disabled"); $j(newRow).find('[data-attr="pid"]').val(row[9]); $j(newRow).find('[data-attr="isa_tag_id"]').val(row[11]); $j(newRow).find('[data-attr="isa_tag_title"]').val(row[11]); From 2a439007a322a15b6443fdc85c54e7440fd6229c Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 16 May 2024 13:40:33 +0200 Subject: [PATCH 027/131] Make sure template attributes can be removed, even when inherrited and/or mandatory --- app/assets/javascripts/templates.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/templates.js b/app/assets/javascripts/templates.js index a37cbe5e37..41d064d115 100644 --- a/app/assets/javascripts/templates.js +++ b/app/assets/javascripts/templates.js @@ -267,7 +267,10 @@ const applyTemplate = () => { .addClass("disabled"); $j(newRow).find('[data-attr="template_attribute_id"]').val(row[14]); // In case of a sample type $j(newRow).find('[data-attr="parent_attribute_id"]').val(row[14]); // In case of a template - if (isRequired) { + + // Hide the remove button if the attribute is required and it is applied to a sample type. + // Template attributes should always be removeable + if (isRequired && appliedToSampleType) { $j(newRow).find('label.btn.btn-danger').addClass("hidden"); } From 8b327a7b9fc32f94707830dd8b3dfd4ab738348d Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 16 May 2024 14:12:26 +0200 Subject: [PATCH 028/131] Add tests for the `Add new attribute` button --- test/functional/templates_controller_test.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/functional/templates_controller_test.rb b/test/functional/templates_controller_test.rb index 753d43a28c..7e512e6301 100644 --- a/test/functional/templates_controller_test.rb +++ b/test/functional/templates_controller_test.rb @@ -623,6 +623,19 @@ class TemplatesControllerTest < ActionController::TestCase end end + test 'Should not see add new attribute button at template creation time' do + get :new + assert_response :success + assert_select 'a#add-attribute.hidden', text: /Add new attribute/, count: 1 + end + + test 'Should see add new attribute button at template edit time' do + my_template = FactoryBot.create(:isa_source_template, project_ids: @project_ids, contributor: @person) + get :edit, params: { id: my_template.id } + assert_response :success + assert_select 'a#add-attribute.hidden', text: /Add new attribute/, count: 0 + end + def create_template_from_parent_template(parent_template, person= @person, linked_sample_type= nil) child_template_attributes = parent_template.template_attributes.map do |ta| FactoryBot.create(:template_attribute, parent_attribute_id: ta.id, title: ta.title, isa_tag_id: ta.isa_tag_id, sample_attribute_type: ta.sample_attribute_type, is_title: ta.is_title, required: ta.required, sample_controlled_vocab: ta.sample_controlled_vocab, pos: ta.pos) From 0055e6f3ae8535f0a7d4e0a0d31a0cbc4ace1178 Mon Sep 17 00:00:00 2001 From: AndrewWood94 Date: Tue, 21 May 2024 15:53:42 +0100 Subject: [PATCH 029/131] Add authorised_assay_assets function to data_files_helper --- app/helpers/data_files_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/data_files_helper.rb b/app/helpers/data_files_helper.rb index 67d9d12f46..917fe91da2 100644 --- a/app/helpers/data_files_helper.rb +++ b/app/helpers/data_files_helper.rb @@ -3,6 +3,10 @@ def authorised_data_files(projects = nil) authorised_assets(DataFile, projects) end + def authorised_assay_assets(data_file) + data_file.assay_assets.where(assay_id: authorised_assays.collect(&:id)) + end + def split_into_two(ahash = {}) return [{}, {}] if ahash.nil? return [ahash, {}] if ahash.length < 2 From a367ba69fe4f87665f6775da8339e684c1b89201 Mon Sep 17 00:00:00 2001 From: AndrewWood94 Date: Tue, 21 May 2024 15:55:49 +0100 Subject: [PATCH 030/131] Only show authorised assays when extracting samples --- app/views/data_files/confirm_extraction.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/data_files/confirm_extraction.html.erb b/app/views/data_files/confirm_extraction.html.erb index c163b63d65..66bc8d2084 100644 --- a/app/views/data_files/confirm_extraction.html.erb +++ b/app/views/data_files/confirm_extraction.html.erb @@ -56,11 +56,11 @@ <%= hidden_field_tag(:sample_type_id, @sample_type.id) if @sample_type %> <%= hidden_field_tag(:confirm, 'true') %> - <% if @data_file.assay_assets.any? %> + <% if authorised_assays.intersect?(@data_file.assays) %>

Link to assays

This data file is linked to the following assays. Check the corresponding checkbox to also link the above samples to that assay. - <% @data_file.assay_assets.each do |aa| %> + <% authorised_assay_assets(@data_file).each do |aa| %>
- + From e1505f1d093098ab8669f631aa0bedc7368fe9bf Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 27 May 2024 18:48:08 +0200 Subject: [PATCH 052/131] Revert last two commits --- app/models/sample_controlled_vocab_term.rb | 2 +- app/views/sample_controlled_vocabs/_term_form_row.html.erb | 5 +---- .../_term_form_row_disabled.html.erb | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/sample_controlled_vocab_term.rb b/app/models/sample_controlled_vocab_term.rb index 092c85a9a5..4f1791e66f 100644 --- a/app/models/sample_controlled_vocab_term.rb +++ b/app/models/sample_controlled_vocab_term.rb @@ -1,7 +1,7 @@ class SampleControlledVocabTerm < ApplicationRecord belongs_to :sample_controlled_vocab, inverse_of: :sample_controlled_vocab_terms - validates :label, presence: true, length: { maximum: 500 }, uniqueness: { scope: :sample_controlled_vocab_id } + validates :label, presence: true, length: { maximum: 500 } before_validation :truncate_label diff --git a/app/views/sample_controlled_vocabs/_term_form_row.html.erb b/app/views/sample_controlled_vocabs/_term_form_row.html.erb index ffb2edf3b5..aa600c85a7 100644 --- a/app/views/sample_controlled_vocabs/_term_form_row.html.erb +++ b/app/views/sample_controlled_vocabs/_term_form_row.html.erb @@ -1,8 +1,5 @@ - + From 57819ece160249d9c21d1a389032275a2ed3bcf1 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 29 May 2024 15:58:36 +0200 Subject: [PATCH 053/131] Revert changes --- app/helpers/bootstrap_helper.rb | 13 +++------- .../sharing/_group_permission_modal.html.erb | 14 ++--------- .../sharing/_person_permission_modal.html.erb | 24 ++++--------------- .../_programme_permission_modal.html.erb | 22 ++++------------- 4 files changed, 14 insertions(+), 59 deletions(-) diff --git a/app/helpers/bootstrap_helper.rb b/app/helpers/bootstrap_helper.rb index f222431a61..0ee54d45d2 100644 --- a/app/helpers/bootstrap_helper.rb +++ b/app/helpers/bootstrap_helper.rb @@ -161,17 +161,10 @@ def modal(options = {}, &block) end end - def modal_header(title, options = {}) - # If no_close_btn is set to true, the modal will not have a close button. - # Useful for multiple shown modals. - no_close_btn = options[:no_close_btn] if options[:no_close_btn] + def modal_header(title, _options = {}) content_tag(:div, class: 'modal-header') do - if no_close_btn - ''.html_safe - else - content_tag(:button, class: 'close', 'data-dismiss' => 'modal', 'aria-label' => 'Close' ) do - content_tag(:span, '×'.html_safe, 'aria-hidden' => 'true') - end + content_tag(:button, class: 'close', 'data-dismiss' => 'modal', 'aria-label' => 'Close') do + content_tag(:span, '×'.html_safe, 'aria-hidden' => 'true') end + content_tag(:h4, title, class: 'modal-title') end diff --git a/app/views/sharing/_group_permission_modal.html.erb b/app/views/sharing/_group_permission_modal.html.erb index 901d42de98..e31c09fb6f 100644 --- a/app/views/sharing/_group_permission_modal.html.erb +++ b/app/views/sharing/_group_permission_modal.html.erb @@ -1,7 +1,7 @@ <%= button_link_to("Share with a #{t('project')} / #{t('institution')}", 'add', '#', id: 'add-project-permission-button') %> <%= modal(id: 'add-project-permission-modal', size: 'm') do %> - <%= modal_header("Share with a #{t('project')} and/or #{t('institution')}", { no_close_btn: true }) %> + <%= modal_header("Share with a #{t('project')} and/or #{t('institution')}") %> <%= modal_body do %>
@@ -32,9 +32,7 @@
<% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-project-confirm', class: 'btn btn-primary') %> - or - <%= link_to('Cancel', '#', id: 'cancel-project-selection', class: 'btn btn-default') %> + <%= link_to('Add', '#', id: 'permission-project-confirm', class: 'btn btn-primary pull-right', 'data-dismiss' => 'modal') %> <% end %> <% end %> @@ -79,17 +77,9 @@ } } - // Close this modal after clicking add button - $j('#add-project-permission-modal').modal('hide'); - Sharing.addPermission(permission); }); - // Close modal when cancel button is clicked - $j('#cancel-project-selection').click(function () { - $j('#add-project-permission-modal').modal('hide'); - }); - // Update the institution list when a project is selected $j('#permission-project-id').change(function () { var institutionSelect = $j('#permission-institution-id'); diff --git a/app/views/sharing/_person_permission_modal.html.erb b/app/views/sharing/_person_permission_modal.html.erb index b26b88c8dc..f10f4bf61a 100644 --- a/app/views/sharing/_person_permission_modal.html.erb +++ b/app/views/sharing/_person_permission_modal.html.erb @@ -1,7 +1,7 @@ <%= button_link_to("Share with a #{t('person')}", 'add', '#', id: 'add-person-permission-button') %> <%= modal(id: 'add-person-permission-modal', size: 'm') do %> - <%= modal_header("Share with #{t('person').pluralize}", { no_close_btn: true }) %> + <%= modal_header("Share with #{t('person').pluralize}") %> <%= modal_body do %>
@@ -18,18 +18,11 @@
<% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-people-confirm', class: 'btn btn-primary') %> - or - <%= link_to('Cancel', '#', id: 'cancel-people-permission-selection', class: 'btn btn-default') %> + <%= link_to('Add', '#', id: 'permission-people-confirm', class: 'btn btn-primary pull-right', 'data-dismiss' => 'modal') %> <% end %> <% end %> diff --git a/app/views/sharing/_programme_permission_modal.html.erb b/app/views/sharing/_programme_permission_modal.html.erb index 994d35a336..94eb5e97e0 100644 --- a/app/views/sharing/_programme_permission_modal.html.erb +++ b/app/views/sharing/_programme_permission_modal.html.erb @@ -1,7 +1,7 @@ <%= button_link_to("Share with a #{t('programme')}", 'add', '#', id: 'add-programme-permission-button') %> <%= modal(id: 'add-programme-permission-modal', size: 'm') do %> - <%= modal_header("Share with #{t('programme').pluralize}", { no_close_btn: true }) %> + <%= modal_header("Share with #{t('programme').pluralize}") %> <%= modal_body do %>
@@ -18,18 +18,11 @@
<% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-programmes-confirm', class: 'btn btn-primary') %> - or - <%= link_to('Cancel', '#', id: 'cancel-programme-permission-selection', class: 'btn btn-default') %> + <%= link_to('Add', '#', id: 'permission-programmes-confirm', class: 'btn btn-primary pull-right', 'data-dismiss' => 'modal') %> <% end %> <% end %> From 668852c41faa548058828914a5db75b4deec8c23 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 29 May 2024 16:23:23 +0200 Subject: [PATCH 054/131] Add function to stop the propagation of the data-dismiss of the inner modals --- .../single_pages/sample_sharing_bulk_change_preview.html.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/single_pages/sample_sharing_bulk_change_preview.html.erb b/app/views/single_pages/sample_sharing_bulk_change_preview.html.erb index c0f86b7e72..75ef3baced 100644 --- a/app/views/single_pages/sample_sharing_bulk_change_preview.html.erb +++ b/app/views/single_pages/sample_sharing_bulk_change_preview.html.erb @@ -73,4 +73,7 @@ }); } + $j('#add-person-permission-modal').on('click', '[data-dismiss="modal"]', function(e){e.stopPropagation();}); + $j('#add-project-permission-modal').on('click', '[data-dismiss="modal"]', function(e){e.stopPropagation();}); + $j('#add-programme-permission-modal').on('click', '[data-dismiss="modal"]', function(e){e.stopPropagation();}); \ No newline at end of file From fdffeb9c2ff1a9145ab204b1b5ce37e8bdc2f6df Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 29 May 2024 16:32:08 +0200 Subject: [PATCH 055/131] Add static backdrop to the batch permissions modal --- app/views/isa_studies/_buttons.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/isa_studies/_buttons.html.erb b/app/views/isa_studies/_buttons.html.erb index 47466814c3..55d68466fb 100644 --- a/app/views/isa_studies/_buttons.html.erb +++ b/app/views/isa_studies/_buttons.html.erb @@ -48,7 +48,7 @@ uploadExcel||="studySampleUploadExcel()" if (res?.status == "unprocessable_entity"){ alert(res?.error) } else { - $j('#change-batch-permission-modal').modal('show').focus(); + $j('#change-batch-permission-modal').modal({backdrop: 'static', keyboard: false}).focus(); $j('#change-batch-permission').html(res); ObjectsInput.init(); } From 12935e0c93306b92daa3eba3a25469d74416dd65 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Wed, 29 May 2024 16:49:15 +0200 Subject: [PATCH 056/131] Group all helper methods together --- app/controllers/application_controller.rb | 35 +++++++++++------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b5da0a292c..6e2bc08abc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -42,6 +42,8 @@ class ApplicationController < ActionController::Base include FairSignposting helper :all + helper_method %i[current_person is_condensed_view page_and_sort_params controller_model + displaying_single_page? display_isa_graph? safe_class_lookup] layout Seek::Config.main_layout @@ -54,7 +56,6 @@ def with_current_user def current_person current_user.try(:person) end - helper_method :current_person def partially_registered? redirect_to register_people_path if current_user && !current_user.registration_complete? @@ -119,7 +120,7 @@ def page_and_sort_params params.permit(:page, :sort, :order, :view, :table_cols, permitted_filter_params) end - helper_method :page_and_sort_params + def controller_model begin @@ -128,7 +129,6 @@ def controller_model end end - helper_method :controller_model def self.api_actions(*actions) @api_actions ||= [] @@ -234,7 +234,8 @@ def find_and_authorize_requested_item case privilege when :publish, :manage, :edit, :download, :delete if current_user.nil? - flash[:error] = "You are not authorized to #{privilege} this #{name.humanize}, you may need to login first." + flash[:error] = +"You are not authorized to #{privilege} this #{name.humanize}, you may need to login first." else flash[:error] = "You are not authorized to #{privilege} this #{name.humanize}." end @@ -290,14 +291,16 @@ def render_not_found_error(e) def render_unknown_attribute_error(e) respond_to do |format| - format.json { render json: { errors: [{ title: 'Unknown attribute', details: e.message }] }, status: :unprocessable_entity } + format.json { + render json: { errors: [{ title: 'Unknown attribute', details: e.message }] }, status: :unprocessable_entity } format.all { render plain: e.message, status: :unprocessable_entity } end end def render_not_implemented_error(e) respond_to do |format| - format.json { render json: { errors: [{ title: 'Not implemented', details: e.message }] }, status: :not_implemented } + format.json { + render json: { errors: [{ title: 'Not implemented', details: e.message }] }, status: :not_implemented } format.all { render plain: e.message, status: :not_implemented } end end @@ -319,8 +322,6 @@ def is_condensed_view? (!params.has_key?(:view) && session.has_key?(:view) && !session[:view].nil? && session[:view]!="default") end - helper_method :is_condensed_view - def log_event # FIXME: why is needed to wrap in this block when the around filter already does ? @@ -383,7 +384,8 @@ def log_event user_agent: user_agent, data: activity_loggable.title) end - when *Seek::Util.authorized_types.map { |t| t.name.underscore.pluralize.split('/').last } + ["sample_types"] # TODO: Find a nicer way of doing this... + when *Seek::Util.authorized_types.map { |t| + t.name.underscore.pluralize.split('/').last } + ["sample_types"] # TODO: Find a nicer way of doing this... action = 'create' if action == 'create_metadata' || action == 'create_from_template' action = 'update' if action == 'create_version' action = 'inline_view' if action == 'explore' @@ -531,7 +533,8 @@ def rdf_enabled? return unless request.format.rdf? unless Seek::Util.rdf_capable_types.include?(controller_model) respond_to do |format| - format.rdf { render plain: 'This resource does not support RDF', status: :not_acceptable, content_type: 'text/plain' } + format.rdf { + render plain: 'This resource does not support RDF', status: :not_acceptable, content_type: 'text/plain' } end end end @@ -607,9 +610,11 @@ def relationify_collection(collection) def determine_extended_metadata_keys(asset = nil) keys = [] if asset - type_id = params.dig(controller_name.singularize.to_sym, asset, :extended_metadata_attributes, :extended_metadata_type_id) + type_id = params.dig(controller_name.singularize.to_sym, asset, :extended_metadata_attributes, + :extended_metadata_type_id) else - type_id = params.dig(controller_name.singularize.to_sym, :extended_metadata_attributes, :extended_metadata_type_id) + type_id = params.dig(controller_name.singularize.to_sym, :extended_metadata_attributes, + :extended_metadata_type_id) end if type_id.present? metadata_type = ExtendedMetadataType.find(type_id) @@ -651,14 +656,10 @@ def displaying_single_page? @single_page || false end - helper_method :displaying_single_page? - def display_isa_graph? !displaying_single_page? end - helper_method :display_isa_graph? - def creator_related_params [:other_creators, @@ -678,6 +679,4 @@ def creator_related_params def safe_class_lookup(class_name, raise: true) Seek::Util.lookup_class(class_name, raise: raise) end - - helper_method :safe_class_lookup end From d143bd4d212d3c6f2826ed0ff1a241e790d32117 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 27 May 2024 19:04:45 +0200 Subject: [PATCH 057/131] Add hidden id fields to the CV terms form and add a uniqueness clause to the validation --- app/models/sample_controlled_vocab_term.rb | 2 +- app/views/sample_controlled_vocabs/_term_form_row.html.erb | 5 ++++- .../_term_form_row_disabled.html.erb | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/sample_controlled_vocab_term.rb b/app/models/sample_controlled_vocab_term.rb index 4f1791e66f..092c85a9a5 100644 --- a/app/models/sample_controlled_vocab_term.rb +++ b/app/models/sample_controlled_vocab_term.rb @@ -1,7 +1,7 @@ class SampleControlledVocabTerm < ApplicationRecord belongs_to :sample_controlled_vocab, inverse_of: :sample_controlled_vocab_terms - validates :label, presence: true, length: { maximum: 500 } + validates :label, presence: true, length: { maximum: 500 }, uniqueness: { scope: :sample_controlled_vocab_id } before_validation :truncate_label diff --git a/app/views/sample_controlled_vocabs/_term_form_row.html.erb b/app/views/sample_controlled_vocabs/_term_form_row.html.erb index aa600c85a7..ffb2edf3b5 100644 --- a/app/views/sample_controlled_vocabs/_term_form_row.html.erb +++ b/app/views/sample_controlled_vocabs/_term_form_row.html.erb @@ -1,5 +1,8 @@
- + From 624f8d6ec1ea7f0ddcc404b5e9faf8aaa17a034c Mon Sep 17 00:00:00 2001 From: AndrewWood94 Date: Mon, 27 May 2024 16:56:57 +0100 Subject: [PATCH 058/131] Update data_files_controller.rb Set sample extraction status to 'cancelled' before re-extracting a datafile --- app/controllers/data_files_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/data_files_controller.rb b/app/controllers/data_files_controller.rb index d42906f834..0d0b66e81f 100644 --- a/app/controllers/data_files_controller.rb +++ b/app/controllers/data_files_controller.rb @@ -169,6 +169,7 @@ def extract_samples SampleDataPersistJob.new(@data_file, @sample_type, User.current_user, assay_ids: params["assay_ids"]).queue_job flash[:notice] = 'Started creating extracted samples' else + @data_file.sample_persistence_task.update(status: "cancelled") if @data_file.sample_persistence_task&.success? SampleDataExtractionJob.new(@data_file, @sample_type).queue_job end From 13e46470f2934e82a170e878db63350cae1f5a57 Mon Sep 17 00:00:00 2001 From: AndrewWood94 Date: Thu, 30 May 2024 17:18:43 +0100 Subject: [PATCH 059/131] Destroy existing sample_persistence_task if re-extracting --- app/controllers/data_files_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/data_files_controller.rb b/app/controllers/data_files_controller.rb index 0d0b66e81f..6143dfcee1 100644 --- a/app/controllers/data_files_controller.rb +++ b/app/controllers/data_files_controller.rb @@ -169,7 +169,7 @@ def extract_samples SampleDataPersistJob.new(@data_file, @sample_type, User.current_user, assay_ids: params["assay_ids"]).queue_job flash[:notice] = 'Started creating extracted samples' else - @data_file.sample_persistence_task.update(status: "cancelled") if @data_file.sample_persistence_task&.success? + @data_file.sample_persistence_task.destroy if @data_file.sample_persistence_task&.success? SampleDataExtractionJob.new(@data_file, @sample_type).queue_job end From f35182de0f0ef0e5c6941058175206e28ba53d16 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 31 May 2024 15:34:21 +0200 Subject: [PATCH 060/131] Simplify assay linkage --- app/controllers/isa_assays_controller.rb | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/app/controllers/isa_assays_controller.rb b/app/controllers/isa_assays_controller.rb index 7e93c82ab4..d6c12551dd 100644 --- a/app/controllers/isa_assays_controller.rb +++ b/app/controllers/isa_assays_controller.rb @@ -5,8 +5,8 @@ class IsaAssaysController < ApplicationController before_action :set_up_instance_variable before_action :find_requested_item, only: %i[edit update] before_action :initialize_isa_assay, only: :create - before_action :fix_assay_linkage_for_new_assays, only: :create after_action :rearrange_assay_positions_create_isa_assay, only: :create + after_action :fix_assay_linkage_for_new_assays, only: :create def new study = Study.find(params[:study_id]) @@ -93,23 +93,18 @@ def update def fix_assay_linkage_for_new_assays return unless @isa_assay.assay.is_isa_json_compliant? return if @isa_assay.assay.is_assay_stream? # Should not fix anything when creating an assay stream + return unless @isa_assay.sample_type.present? # Just to be sure previous_sample_type = SampleType.find(params[:isa_assay][:input_sample_type_id]) - previous_assay = previous_sample_type.assays.first + next_sample_types = previous_sample_type.next_linked_sample_types + next_sample_types.delete @isa_assay.sample_type + next_sample_type = next_sample_types.first - # In case an assay is inserted right at the beginning of an assay stream, - # the next assay is the current first one in the assay stream. - next_assay = previous_assay.nil? ? @isa_assay.assay.assay_stream.next_linked_child_assay : previous_assay.next_linked_child_assay + # In case an assay is inserted right at the end of an assay stream, + # there is no next sample type and also no linkage to fix + return if next_sample_type.nil? - # In case no next assay (an assay was appended to the end of the stream), assay linkage does not have to be fixed. - return unless next_assay - - next_assay_input_attribute_id = next_assay.sample_type.sample_attributes.detect(&:input_attribute?).id - return unless next_assay_input_attribute_id - - # Add link of next assay sample type to currently created assay sample type - updated_lsai = @isa_assay.assay.sample_type.linked_sample_attribute_ids.push(next_assay_input_attribute_id) - @isa_assay.assay.sample_type.update(linked_sample_attribute_ids: updated_lsai) + next_sample_type.sample_attributes.detect(&:input_attribute?).update_column(:linked_sample_type_id, @isa_assay.sample_type.id) end def rearrange_assay_positions_create_isa_assay From 8e37e73c27e913cf8d2ac7193dc7c95b38e49e97 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 31 May 2024 16:28:05 +0200 Subject: [PATCH 061/131] Make use of `:is_input?` instead --- app/forms/isa_assay.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/isa_assay.rb b/app/forms/isa_assay.rb index 7c6679d4b3..7e90b8751f 100644 --- a/app/forms/isa_assay.rb +++ b/app/forms/isa_assay.rb @@ -21,7 +21,7 @@ def save if valid? if @assay.new_record? && !@assay.is_assay_stream? # connect the sample type multi link attribute to the last sample type of the assay's study - input_attribute = @sample_type.sample_attributes.detect(&:seek_sample_multi?) + input_attribute = @sample_type.sample_attributes.detect(&:input_attribute?) input_attribute.linked_sample_type_id = @input_sample_type_id title = SampleType.find(@input_sample_type_id).sample_attributes.detect(&:is_title).title input_attribute.title = "Input (#{title})" From d1878731d59820b5e4ed560dc7c69c005ff781be Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 31 May 2024 16:54:23 +0200 Subject: [PATCH 062/131] Simplification + update even when not authorized --- app/controllers/assays_controller.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/assays_controller.rb b/app/controllers/assays_controller.rb index 140b5c0dd6..4aa210b8b5 100644 --- a/app/controllers/assays_controller.rb +++ b/app/controllers/assays_controller.rb @@ -171,15 +171,11 @@ def fix_assay_linkage_when_deleting_assays return unless @assay.is_isa_json_compliant? return unless @assay.has_linked_child_assay? - previous_assay_linked_st_id = @assay.previous_linked_sample_type&.id + previous_st = @assay.sample_type&.previous_linked_sample_type + next_st = @assay.sample_type&.next_linked_sample_types&.first + return unless previous_st && next_st - next_assay = @assay.next_linked_child_assay - - next_assay_st_attr = next_assay.sample_type&.sample_attributes&.detect(&:input_attribute?) - - return unless next_assay && previous_assay_linked_st_id && next_assay_st_attr - - next_assay_st_attr.update(linked_sample_type_id: previous_assay_linked_st_id) + next_st.sample_attributes.detect(&:input_attribute?).update_column(:linked_sample_type_id, previous_st&.id) end def rearrange_assay_positions_at_destroy From 77f8bdd1e46fbfffa84875ce5c40c52ff0a803ac Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 31 May 2024 17:34:27 +0200 Subject: [PATCH 063/131] Add unit test for uniqueness clause --- test/unit/sample_controlled_vocab_test.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/unit/sample_controlled_vocab_test.rb b/test/unit/sample_controlled_vocab_test.rb index 6918c77450..c9253225d3 100644 --- a/test/unit/sample_controlled_vocab_test.rb +++ b/test/unit/sample_controlled_vocab_test.rb @@ -312,4 +312,12 @@ class SampleControlledVocabTest < ActiveSupport::TestCase assert vocab.ontology_based? end + test 'should not allow to add term with same label' do + vocab = FactoryBot.create(:apples_sample_controlled_vocab) + vocab.sample_controlled_vocab_terms.create(label: 'Golden Delicious') + assert_raises ActiveRecord::RecordInvalid do + vocab.sample_controlled_vocab_terms.create!(label: 'Golden Delicious') + end + end + end From feb52ca1593ce0abd5d0881103ab1e105e5f0f80 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 31 May 2024 17:38:33 +0200 Subject: [PATCH 064/131] Fix current controller tests --- test/functional/sample_controlled_vocabs_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/sample_controlled_vocabs_controller_test.rb b/test/functional/sample_controlled_vocabs_controller_test.rb index f8a450e584..1bcab9cb8b 100644 --- a/test/functional/sample_controlled_vocabs_controller_test.rb +++ b/test/functional/sample_controlled_vocabs_controller_test.rb @@ -299,7 +299,7 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_select 'table#new-terms' do # 3 hidden fields for each field, and an extra one for the remove button default - assert_select 'tr.sample-cv-term input[type=hidden]', count:cv.sample_controlled_vocab_terms.length * 4 + assert_select 'tr.sample-cv-term input[type=hidden]', count:cv.sample_controlled_vocab_terms.length * 5 assert_select 'div.disabled-cv-field', count: cv.sample_controlled_vocab_terms.length * 3 end From 94c2964fa35dc46f9924bae5e00f7fc339ac55f1 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 31 May 2024 18:44:37 +0200 Subject: [PATCH 065/131] Add test for duplicated CV terms --- ...ample_controlled_vocabs_controller_test.rb | 261 ++++++++++++------ 1 file changed, 169 insertions(+), 92 deletions(-) diff --git a/test/functional/sample_controlled_vocabs_controller_test.rb b/test/functional/sample_controlled_vocabs_controller_test.rb index 1bcab9cb8b..4abdce9459 100644 --- a/test/functional/sample_controlled_vocabs_controller_test.rb +++ b/test/functional/sample_controlled_vocabs_controller_test.rb @@ -70,11 +70,10 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_difference('SampleControlledVocab.count') do assert_difference('SampleControlledVocabTerm.count', 2) do post :create, params: { sample_controlled_vocab: { title: 'fish', description: 'About fish', - sample_controlled_vocab_terms_attributes: { - '0' => { label: 'goldfish', _destroy: '0' }, - '1' => { label: 'guppy', _destroy: '0' } - } - } } + sample_controlled_vocab_terms_attributes: { + '0' => { label: 'goldfish', _destroy: '0' }, + '1' => { label: 'guppy', _destroy: '0' } + } } } end end assert cv = assigns(:sample_controlled_vocab) @@ -89,11 +88,10 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_no_difference('SampleControlledVocab.count') do assert_no_difference('SampleControlledVocabTerm.count') do post :create, params: { sample_controlled_vocab: { title: 'fish', description: 'About fish', - sample_controlled_vocab_terms_attributes: { - '0' => { label: 'goldfish', _destroy: '0' }, - '1' => { label: 'guppy', _destroy: '0' } - } - } } + sample_controlled_vocab_terms_attributes: { + '0' => { label: 'goldfish', _destroy: '0' }, + '1' => { label: 'guppy', _destroy: '0' } + } } } end end assert_response :redirect @@ -105,11 +103,10 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_no_difference('SampleControlledVocab.count') do assert_no_difference('SampleControlledVocabTerm.count', 2) do post :create, params: { sample_controlled_vocab: { title: 'fish', description: 'About fish', - sample_controlled_vocab_terms_attributes: { - '0' => { label: 'goldfish', _destroy: '0' }, - '1' => { label: 'guppy', _destroy: '0' } - } - } } + sample_controlled_vocab_terms_attributes: { + '0' => { label: 'goldfish', _destroy: '0' }, + '1' => { label: 'guppy', _destroy: '0' } + } } } end end assert_response :redirect @@ -123,14 +120,16 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_no_difference('SampleControlledVocab.count') do assert_no_difference('SampleControlledVocabTerm.count') do put :update, params: { id: cv, sample_controlled_vocab: { title: 'the apples', description: 'About apples', - sample_controlled_vocab_terms_attributes: { - '0' => { label: 'Granny Smith', _destroy: '0', id: term_ids[0] }, - '1' => { _destroy: '1', id: term_ids[1] }, - '2' => { label: 'Bramley', _destroy: '0', id: term_ids[2] }, - '3' => { label: 'Cox', _destroy: '0', id: term_ids[3] }, - '4' => { label: 'Jazz', _destroy: '0' } - } - } } + sample_controlled_vocab_terms_attributes: { + '0' => { label: 'Granny Smith', _destroy: '0', + id: term_ids[0] }, + '1' => { _destroy: '1', id: term_ids[1] }, + '2' => { label: 'Bramley', _destroy: '0', + id: term_ids[2] }, + '3' => { label: 'Cox', _destroy: '0', + id: term_ids[3] }, + '4' => { label: 'Jazz', _destroy: '0' } + } } } end end assert cv = assigns(:sample_controlled_vocab) @@ -147,14 +146,16 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_no_difference('SampleControlledVocab.count') do assert_no_difference('SampleControlledVocabTerm.count') do put :update, params: { id: cv, sample_controlled_vocab: { title: 'the apples', description: 'About apples', - sample_controlled_vocab_terms_attributes: { - '0' => { label: 'Granny Smith', _destroy: '0', id: term_ids[0] }, - '1' => { _destroy: '1', id: term_ids[1] }, - '2' => { label: 'Bramley', _destroy: '0', id: term_ids[2] }, - '3' => { label: 'Cox', _destroy: '0', id: term_ids[3] }, - '4' => { label: 'Jazz', _destroy: '0' } - } - } } + sample_controlled_vocab_terms_attributes: { + '0' => { label: 'Granny Smith', _destroy: '0', + id: term_ids[0] }, + '1' => { _destroy: '1', id: term_ids[1] }, + '2' => { label: 'Bramley', _destroy: '0', + id: term_ids[2] }, + '3' => { label: 'Cox', _destroy: '0', + id: term_ids[3] }, + '4' => { label: 'Jazz', _destroy: '0' } + } } } end end assert_response :redirect @@ -294,7 +295,7 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase login_as(person) cv = FactoryBot.create(:ontology_sample_controlled_vocab) assert cv.ontology_based? - get :edit, params:{id: cv.id} + get :edit, params:{ id: cv.id } assert_response :success assert_select 'table#new-terms' do @@ -310,7 +311,7 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase login_as(person) cv = FactoryBot.create(:apples_sample_controlled_vocab) refute cv.ontology_based? - get :edit, params:{id: cv.id} + get :edit, params:{ id: cv.id } assert_response :success assert_select 'table#new-terms' do @@ -329,8 +330,8 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase login_as(person) VCR.use_cassette('ols/fetch_obo_bad_term') do get :fetch_ols_terms_html, params: { source_ontology_id: 'go', - root_uris: 'http://purl.obolibrary.org/obo/banana', - include_root_term: '1' } + root_uris: 'http://purl.obolibrary.org/obo/banana', + include_root_term: '1' } assert_response :unprocessable_entity assert_equal '404 Not Found', response.body @@ -348,18 +349,29 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_response :success assert_select 'tr.sample-cv-term', count: 4 - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_parent_iri:not([value])' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]','plant cell papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090397' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]','stigma papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090396' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]','leaf papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090705' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_label[value=?]','trichome papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]', +'plant cell papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090397' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]', +'stigma papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090396' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]', +'leaf papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090705' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_label[value=?]', +'trichome papilla' end test 'create with root uris' do @@ -369,10 +381,11 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase post :create, params: { sample_controlled_vocab: { title: 'plant_cell_papilla and haustorium', description: 'multiple root uris', ols_root_term_uris: 'http://purl.obolibrary.org/obo/GO_0090395, http://purl.obolibrary.org/obo/GO_0085035', sample_controlled_vocab_terms_attributes: { - '0' => { label: 'plant cell papilla', iri:'http://purl.obolibrary.org/obo/GO_0090395', parent_iri:'', _destroy: '0' }, - '1' => { label: 'haustorium', iri:'http://purl.obolibrary.org/obo/GO_0085035', parent_iri:'', _destroy: '0' } - } - } } + '0' => { label: 'plant cell papilla', + iri: 'http://purl.obolibrary.org/obo/GO_0090395', parent_iri:'', _destroy: '0' }, + '1' => { label: 'haustorium', + iri: 'http://purl.obolibrary.org/obo/GO_0085035', parent_iri:'', _destroy: '0' } + } } } end end assert cv = assigns(:sample_controlled_vocab) @@ -381,8 +394,10 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_equal 'multiple root uris', cv.description assert_equal 2, cv.sample_controlled_vocab_terms.count assert_equal ['plant cell papilla','haustorium'], cv.labels - assert_equal ['http://purl.obolibrary.org/obo/GO_0090395','http://purl.obolibrary.org/obo/GO_0085035'], cv.sample_controlled_vocab_terms.collect(&:iri) - assert_equal 'http://purl.obolibrary.org/obo/GO_0090395, http://purl.obolibrary.org/obo/GO_0085035', cv.ols_root_term_uris + assert_equal ['http://purl.obolibrary.org/obo/GO_0090395','http://purl.obolibrary.org/obo/GO_0085035'], +cv.sample_controlled_vocab_terms.collect(&:iri) + assert_equal 'http://purl.obolibrary.org/obo/GO_0090395, http://purl.obolibrary.org/obo/GO_0085035', +cv.ols_root_term_uris end test 'fetch ols terms as HTML with multiple root uris and root term included' do @@ -398,25 +413,41 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_response :success assert_select 'tr.sample-cv-term', count: 6 - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_parent_iri:not([value])' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]','plant cell papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090397' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]','stigma papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090396' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]','leaf papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090705' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_label[value=?]','trichome papilla' - - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_4_iri[value=?]','http://purl.obolibrary.org/obo/GO_0085035' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]', +'plant cell papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090397' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]', +'stigma papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090396' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]', +'leaf papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090705' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_label[value=?]', +'trichome papilla' + + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_4_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0085035' assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_4_parent_iri:not([value])' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_4_label[value=?]','haustorium' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_5_iri[value=?]','http://purl.obolibrary.org/obo/GO_0085041' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_5_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0085035' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_5_label[value=?]','arbuscule' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_4_label[value=?]', +'haustorium' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_5_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0085041' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_5_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0085035' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_5_label[value=?]', +'arbuscule' end @@ -434,19 +465,31 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_response :success assert_select 'tr.sample-cv-term', count: 4 - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090397' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]','stigma papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090396' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]','leaf papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090705' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]','trichome papilla' - - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_iri[value=?]','http://purl.obolibrary.org/obo/GO_0085041' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0085035' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_label[value=?]','arbuscule' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090397' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]', +'stigma papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090396' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]', +'leaf papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090705' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]', +'trichome papilla' + + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0085041' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0085035' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_3_label[value=?]', +'arbuscule' end @@ -471,19 +514,28 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase login_as(person) VCR.use_cassette('ols/fetch_obo_plant_cell_papilla') do get :fetch_ols_terms_html, params: { source_ontology_id: 'go', - root_uris: 'http://purl.obolibrary.org/obo/GO_0090395' } + root_uris: 'http://purl.obolibrary.org/obo/GO_0090395' } end assert_response :success assert_select 'tr.sample-cv-term', count: 3 - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090397' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]','stigma papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090396' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]','leaf papilla' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090705' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]','http://purl.obolibrary.org/obo/GO_0090395' - assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]','trichome papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090397' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_0_label[value=?]', +'stigma papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090396' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_1_label[value=?]', +'leaf papilla' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090705' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_parent_iri[value=?]', +'http://purl.obolibrary.org/obo/GO_0090395' + assert_select 'input[type=hidden]#sample_controlled_vocab_sample_controlled_vocab_terms_attributes_2_label[value=?]', +'trichome papilla' end test 'can access typeahead with samples disabled' do @@ -498,4 +550,29 @@ class SampleControlledVocabsControllerTest < ActionController::TestCase assert_equal 'Sample collections', res.first['text'] end end + + test 'should not duplicate terms when updating' do + person = FactoryBot.create(:person) + login_as(person) + cv = FactoryBot.create(:apples_sample_controlled_vocab) + term_ids = cv.sample_controlled_vocab_terms.map(&:id) + + assert_no_difference('SampleControlledVocabTerm.count') do + put :update, params: { id: cv, sample_controlled_vocab: { title: 'the apples', description: 'About apples', + sample_controlled_vocab_terms_attributes: { + '0' => { label: 'Granny Smith', _destroy: '0', + id: term_ids[0] }, + '1' => { label: 'Red Delicious', _destroy: '0', + id: term_ids[1] }, + '2' => { label: 'Bramley', _destroy: '0', + id: term_ids[2] }, + '3' => { label: 'Cox', _destroy: '0', + id: term_ids[3] }, + '4' => { label: 'Granny Smith', _destroy: '0' } + } } } + end + assert_response :unprocessable_entity + assert_template :edit + assert flash[:error] = 'Validation failed: Labels have already been taken' + end end From fce8e3433a3b89d0611415417889df9e2f523a3c Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 16:33:16 +0200 Subject: [PATCH 066/131] Pass errors to partials --- app/views/single_pages/sample_upload_content.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/single_pages/sample_upload_content.html.erb b/app/views/single_pages/sample_upload_content.html.erb index 139062d970..b8b1f51dc2 100644 --- a/app/views/single_pages/sample_upload_content.html.erb +++ b/app/views/single_pages/sample_upload_content.html.erb @@ -9,7 +9,7 @@
<%# General information panel %> - <%= render partial: 'general_panel' %> + <%= render partial: 'general_panel', locals: {errors: } %> <%# New Samples panel %> <%= render partial: 'new_samples_panel' %> @@ -21,7 +21,7 @@ <%= render partial: 'duplicate_samples_panel' %> <%# Panel for Sample with wrong permissions %> - <%= render partial: 'unauthorized_samples_panel' %> + <%= render partial: 'unauthorized_samples_panel', locals: {errors: } %>
<% unless @can_upload %> From f666d69a8027f9e3e0544847aae6422ef179598d Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 17:13:42 +0200 Subject: [PATCH 067/131] Add test for unauthorized samples [JSON response] --- .../single_pages_controller_test.rb | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/functional/single_pages_controller_test.rb b/test/functional/single_pages_controller_test.rb index 7350c73411..f78e9785d0 100644 --- a/test/functional/single_pages_controller_test.rb +++ b/test/functional/single_pages_controller_test.rb @@ -221,6 +221,37 @@ def setup end end + test 'Should show permission conflicts for samples' do + with_config_value(:project_single_page_enabled, true) do + unauthorized_user = FactoryBot.create(:user) + login_as unauthorized_user + project, source_sample_type = setup_file_upload.values_at( + :project, :source_sample_type + ) + + file_path = 'upload_single_page/01_combo_update_sources_spreadsheet.xlsx' + file = fixture_file_upload(file_path, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + + post :upload_samples, as: :json, params: { file:, project_id: project.id, + sample_type_id: source_sample_type.id } + + response_data = JSON.parse(response.body)['uploadData'] + assert_response :success + + updated_samples = response_data['updateSamples'] + assert(updated_samples.size, 0) + + new_samples = response_data['unauthorized_samples'] + assert(new_samples.size, 2) + + new_samples = response_data['newSamples'] + assert(new_samples.size, 2) + + possible_duplicates = response_data['possibleDuplicates'] + assert(possible_duplicates.size, 1) + end + end + def setup_file_upload id_label = "#{Seek::Config.instance_name} id" person = @member.person From 2ffba238bd98e4e05c4b6247295e64233d2bc001 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 17:19:39 +0200 Subject: [PATCH 068/131] typo --- test/functional/single_pages_controller_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/single_pages_controller_test.rb b/test/functional/single_pages_controller_test.rb index f78e9785d0..c7746c13a5 100644 --- a/test/functional/single_pages_controller_test.rb +++ b/test/functional/single_pages_controller_test.rb @@ -241,8 +241,8 @@ def setup updated_samples = response_data['updateSamples'] assert(updated_samples.size, 0) - new_samples = response_data['unauthorized_samples'] - assert(new_samples.size, 2) + unauthorized_samples = response_data['unauthorized_samples'] + assert(unauthorized_samples.size, 2) new_samples = response_data['newSamples'] assert(new_samples.size, 2) From f1247bb5cdd3c8411b6b52e9688a1a4f25922f6f Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 18:05:42 +0200 Subject: [PATCH 069/131] Set empty cells to nil --- app/controllers/single_pages_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/single_pages_controller.rb b/app/controllers/single_pages_controller.rb index e25a7db2c6..004b48f372 100644 --- a/app/controllers/single_pages_controller.rb +++ b/app/controllers/single_pages_controller.rb @@ -244,7 +244,7 @@ def get_spreadsheet_data(samples_sheet) next if sample_cells.all? { |cell| (cell&.value == '' || cell&.value.nil?) } sample_cells.map do |cell| - cell&.value + cell&.value unless cell&.value == '' end.drop(1) end.compact From c371563650c9bc2c4100e95582409ac7463c85eb Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 18:06:22 +0200 Subject: [PATCH 070/131] Skip id and uuid columns when comparing metadata --- app/controllers/single_pages_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/single_pages_controller.rb b/app/controllers/single_pages_controller.rb index 004b48f372..3229d54df0 100644 --- a/app/controllers/single_pages_controller.rb +++ b/app/controllers/single_pages_controller.rb @@ -334,7 +334,7 @@ def separate_unauthorized_samples(existing_excel_samples, db_samples, authorized is_changed = false db_sample.map do |k, v| - unless ees[k] == v + unless ees[k] == v || %w[id uuid].include?(k) is_changed = true break end From ace380fc31197adab7f77121fffa6bf7583acc5a Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 20:08:41 +0200 Subject: [PATCH 071/131] Fix existing tests --- .../03_combo_update_samples_spreadsheet.xlsx | Bin 27970 -> 27981 bytes ...ombo_update_assay_samples_spreadsheet.xlsx | Bin 27918 -> 28043 bytes .../single_pages_controller_test.rb | 54 ++++++++---------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/test/fixtures/files/upload_single_page/03_combo_update_samples_spreadsheet.xlsx b/test/fixtures/files/upload_single_page/03_combo_update_samples_spreadsheet.xlsx index 357e00d721ae982c86652b2af6cbf4a8b9ce7828..18e303862283bed56125b5264e7346c1bc2091bd 100644 GIT binary patch delta 9377 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 delta 9181 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 V2Izmg$J&cgynbZc6@OkM{|CQF7Ek~H diff --git a/test/fixtures/files/upload_single_page/04_combo_update_assay_samples_spreadsheet.xlsx b/test/fixtures/files/upload_single_page/04_combo_update_assay_samples_spreadsheet.xlsx index fbc99d30c1e867375c7e3a7e7819d240c4b3e6f6..2e29324ac603b7ba5d6933ff0ff53ca785552a54 100644 GIT binary patch delta 8579 zcma*tc|25o-v@AOC|guY%!E)^3$hDil08vm52X?jB8*v1X(2;oiYwbx_9V$Nxhzx3 zo>a&dV;d8)&5ULCXU5#u{ap9`{PWD~HOI`E^F8N1-}CVserFyI3!+B^rJWswMRo{? zi;D}OA2xebN~;Nq@E%m@3!W6(e1t&?s{>0F77bw$`!0M;DwuPw9L{_hTzs~$-`r?i z)-^zFs@e`fhDwUa!K1V6)USxb1PJqyODij4m8Ujloc-tbUOacYqcg8@OYJ?|6TLZ? z4lyW}%(m~fe@NSH)lt8C8GPn4-V?v|5bD#fUTKx<55x<+hK_&ff+!c;N0;2{zviV1 zsG53rZf&=EbounTV%(O&o9Cd{;QVgY?Di{!I_onKEoAToHC-V|uqtLaO$wX1N;=cR#TD5S#~oSh)Mn$x~Z*7QJ2v*XV9Z z)s0z+vapKCv)|>}H*h$5>9%{qP>%8B0il<8xDGbk(dUm}R<%DOnnTe3v!nBGbGLLJ z88HBamn;fG_X^;l9R|N(wGZzbcq8TWBP~7u$e3dvD?R0w)toGs2+2GI-xhK2_ZR9z zlxBH^^2KVRs#I51ZbPJ(y}{#1uf&91Y7a`ZFPF+rm8+E?V_^b0hbr)$5aPX^Vi%jy3$IJAZbJ+tbA&0z1u&-$zC#FSc9@}!vy{G)a=aW}d zbsqsw+kGsrET+sl8e>zEx)H7+KXoc4T2EeC)VFMvpK*M9 zUTp-CzMwd?3lpN2vA|i=SS|imG-4YCPmBX>bc}p{zsfLu=`*xMoHbYs-C@8`qU4mM zM@l4KJ@dYn9ik%;Ykg}~W&EO=jPQIxd!8~T`@y=vB%bqk1l6_P*DB`5F zSfI!>uIKw8NFc*Hl{%aMRNFz#dU!QYydC=`(u8BSE;9KYrzdXOv+kH={U`%^>3jFO zpthB6jg9Il*rg)B-r=7kGuz16k36=aQDLV7 zY-mv+<09(r9!z8Ks5+lSh5gu6RzQ||o`OhR>Zf0OV)Hs8 z>3>elQe3TdSK}Mz^jRUS=;52A#>nJawuG~TnE1Htgr1&=fWS8m0fGM6J&AKlaM!{`sRe zW=1FzH3JVupwK}*vIlSMqBk2l_-~w$NPc);DF2S!ub68_ zfce||Q{1&bJ%y&O(=lqj*alA_jDr2Xb0&(DUb`U@gf|Sys%we6 z(ly0JuFW)+dWSz3QT^(rbubD1#e=M-_Hqwg=})qkytQP?S`I1h*5f#9e-Fmav%u^Z z_RG(igS9sbKcSHn)E`H;3XMLHF8K1{iyS51KT-p_t?b9gtJC}UJE}OH+QU@t9!BKu z2g+lJ$I-FrgPy@_g2a~MIvR7iYI?5MEIHzPUA)9X1}TvRL%VCgkf^`9Ci=WC$7)R4 zTFvVi{(gym#OL+B!rm9Ge2<@g7s^(3)Mm)=vg%<*hsJQT8Z7(YS*F|AVj zfdzL$Sg*D$id6B2`Ciy78+_W^_4_G#=Sl^=pauEK4})KVk5uY1&brBRj-T#*?&zgv zsgZK}$;+Z9R1ArMAgJ3pg>?y)->W=Zw&&q}73&i^-GzT;O%N}d$D$vJzBD>gEvMVC zeHZph#o23u#_pE`V?~`E@4SEezGraR!_SYUU*#iArzSDOzGB>+UIa*%MZa^ zm!Nk?ztO}X%p0@lhJwrI#H$BgKlWTu$q^QHqCTzZtFrEB^uM_vVd{QP>Q{=w+SHNV zodN)|sBk%|PFqp0S9CZdQPl}|JjyDiOZ4M2{UgEOMj(qs$A$*|S2+ptceT@ZC6-^c z%<}d~`&=KtcmQ=Xs%^PU;|oUOvZ>PD>5*7j>$_66dHe6!$!Wm!vc4idgSH%7jGSot zoqFHE@T#hkQLoRTt5Sj8*A=}_J9vgfm$ZKU4Se1DE>NQO`P%P=uATwIn)@;7xPhP@ z_amadAAKKmGw`}r`TY?>%x;Mm%e2sYuRqILl@kX#0z4;uJ-t4j(t2(3w9dWNWt!d< z`5@*ql7$(Gc^d*-pX+j>9~WB4VtBptMqfNpC%(^Uvd_oRY>D1LD1mLQgxiPcJrp-q zdY}ws6Y^gZ!2sC)W1zayZQ%f9ozJ zAdt1=zjG+CdBad-_5#SW=bi3o??A4*r!`s9{_NF%FQ*T`@u0&)uc`k=+e_!H+Px!q z+_3%N)3(@hf>&5Sc zEF*&tT$eg;Z1Mtg`HQtfoy+CGG+jw`*L}&Yw-v^#e%yLV?H5yyeE#a4vHiP0l-tiB zzan-RI8N!G(X3&KZG_Sq=B#hlU3G5}C|oJs{&9!1a9x0ut>1Nj-~6`XklWwC%BT(C zBkvrbEOlJ6elY}31{#4dDaJdupsu{zDUn}KMFjnpf|G4NPNpYa3{=RA4<9HyeXMBM zCBrql(2RcHk{wTCV=F&^{R{P2RQJZ-bnS-SLE69mI5bFETPnQp^_+6V zNbMLBA*9PXrS_(BqUf%<*b%9;qx5qT z)NcW9ZsVEw)7IQWSjxo0!ePw<(s*+`KR!)8UP5ZP_1CVuB9)@u!ly$qCXK6EZLV~w zN@RsdgQrfg_M^vEXXFQMwB;@>oEVFy_&ZD>!53D zV6Vd4X6|-)S87WgR04!i4=F^MB66bB_Pkz}%)~r7Hr2$)8#vGUb5t^FHo;H!k!#EJ zP1if09qg}Pup!ei&*XL}+I@d-$lyjWo7;u|`Y`_@*T?_2p-8^yUn8aS&OfZqW6qNz zie@RaosxNm57rq+5(*wh7aem6?K#!<4z6+W%T>9zyEP=FBIE}m- z80I^kfJM=0OTKtEi-4dJo%}e9#C1OwwWW-JXHImxqZo5-OZYg}Oc=l{)&l6PR4qJx zW>kyLuGeCq6&PJhc=}pG4}iLdr>zhuiWMlvNEjYf3RE<)o7c5~SQZ7%+#uIi_~Ky< z2y1bKz@bj`vx(Wjz&w&Nx+B*@o*xoF8(%x69Cv!E)zN$zb0mHW&}AuMTin{Qz7T+{7eSpjL`vRPeR zImMxgJqPAY5M#9PC>A+86&U5v5(Ws3#vKsm{5of2t{#GaZ>R+@`oT?b*4mK<;VFW(!(Z|xf3|e1KcLe zm^9W2fmtlh%3>yD0OwBSp1}ckAU4Kx3|L+0X@)e#t@eb?F3dN@!m(q>B%J%kx?dw} z2*x>T&iP$tNN>wrB6PDimH>7kmPO?>)@z0}t&zOf*6u;r^D?;BF=TH;9Wb&~@hQ7( znS)?-&l6Z=O0m`&qLM+USj9y;OcF=7v1iQIwZ@qKoE}5m z>tiUDjW8M(@-q{g!*a*RMp^-J>)5c0g&02;yBHU<#6qK330Pk|oy{7d)7Z?#k+3Eh zt+~0k={SiSV4y2IhBkxeo{CX`L#6u>#;`bdIw{lGFFD4YQ~_^gvy)jL_K;|kEx`MO zZ48%(V#SC#ziQe5gopq-X}?^)Xw3lci+?UT)q!%!EebKLRVUaOmTYQ5+?Z^k&11<; z5F#?X^Co_9y#nZ5n@E_R{b1b2n(Q9n&`Aj38w9bfk=A8Hcvp_Bq}LM~P;BG`2`E6V z(#yCZZF2|czuRIUy$~#$L^#1lwBqmztLzr^y>&8W0+?7YiwP6JlDiWCcoT=bX80(k z7)e0`arpqVpWTuPAQ;jF@Cwd!5)pTIIgv5Dy;CjE+-!2d^hM(aRTdr&*fOZHK)5Z3 znid=0zCleRm>OL|#kB*-@OC;i4G#}*=loUe*HlLUnr2yAKdA;|Y_-`6cn_mK#$c?m z3OwdFuP`-K!yHLXjpMMfgblF-Q#=I%J_a$Lrj?UaShR8uEsedR(i6r?aHo-*Q&7@) z+8$p7QZ1h0$?wIp_aw{MSvG(RD_!%aRkhk2sO<;Rs zQ)cRG!B6|7AEXj{=a)0t=a(VVCVD;zBvo0Y7T7O=bWt|pQyY2qW(j*B|3`#!9@?VX zjI*BNXDK-eq-}VZH%j+&+`eyl-5j|+v6ht29+16bwBJ0#slsxV8YVw5Q73+KgtC)) zEfK0DcVur*d7PQy=fFe@Y9j){-5GZ!xT|>Jd0d^rDfj=oR^Q;Wb)rS}1>+NvlWA9n z;zIfAXXGVOEt62(uwL%nv0Gk+nIq zAq-YY7Za;&B+|+8lgB()w#Gn})iE$-IgwXm7$29Mg z!|d9puF#3y`l(H5rOpDOEsK@>WFx#XWPvbp#%S!*#7L^#!qD~^VRow?z&uw;2G~lG z=IuF7$4L2~X@?`tKj%2j%#Xy~zY%%pr=G_(^N!j4Zh z5R+?u)<$Af8t^aepZs09bQcDC3# z54A5Q4sVt!(;UFp;B!VEvJ0U-kpv@%cCla)U1Z^cj+epxM60}3p383}i z5TH*8P#*#`jQ~**AQ2>J2NJX&`Mt>LP%2%|4QYGgd+QsS4%^d78Ldb!*l4lNlBHa_ z!7h}o^^LRD)#TnzT}VnUI6bXeXVsFMk{5+_Shq1okR+QMos@2H`lFm3M;6%C{UD6o6Zb@)rA5f&xSnFYVW~a&WLHHx=z06jGJ2+Z$=IU(NWS zSC&XH(eYX90a-iS;XzT=Jso!6z8g3nN)^`sahqqeklnOd$9uulOLXK^Krg?ofNASmMYJ7fp6QB?>43rEV+kgJEF$7?WrL@I}`A2)27I`4RZUIqS$NA zvyJj?+ic2BBl-r7cs5-dzRe_EtQ8`=Ha-3OZ9@WN_2qD z#-;IX@qL}TwkgW{dA56en{k2Lm}Rh}D$nM)pKnvGA#0xv*zFh;1EHGG7kB3Xq;N$da$j&YSrJIe@=iAryzD4kD8C;vO{*N4>H5F$1QRLcRB^B$-}MiaZiig#$dscW;|QDCf{aOLmqbtSlP6h^KBd4zF`%6op?4D z-?q)U+*GiC@YZJFocXr+VB(RI*4;aJwmW>AhU=kJ;Do;0a~@862fuO8yY3${=xLkyq(<-Y>se`xg%U@Ysn_=2_IL>#X0EzDCF7R^KI5eUa-qA zb#!^Qj3U0xpuaQHC8hBo&t}ZG4VAi$#klV&2^#WtZT7l(O~DsB5Bp(^<^jax9BbN!e} zpW3ST9)lv7_Z~*JbDXfH#kibz=|;)GPR*nnHCwgpGANoN2feM%S=D>KeSiJ6RBy9~ zPC%Q%v+3S2_?9dCWNeb$I_f;`ZR?>{ETD{wi$@h~WvVQEuM1+Ld@oj2U4jZ#J-g(} zUtpeHIuG=hXE&>z%dAHLw&wn8f|Nt>Kv#4B6~QhUkH)Y}wa5U#z){Q(l(D{)`?-SS z`P!Y*(?5fNVEei#%`2mL-x`nC1K9(Bw^k3pq)#<35=~Wmuj8Jsudw307K>X@UQRvI z=x$o|V9rtLz}}0r;5siY?CZcm-w;-}4G0XNe4YFN)6|u<6jM*-hvWs?8u7x)_yKUY za8qt{S964nQ*rZrkO?QYax~O8gw$MB)qQ1*cw=QQ77=E^8Jg@E3I+-?doQwy-|he` z#)yp!GarYAS7mlD(#VJ!Ku4x)Xr^Y#z?;y}l*d%_Rr(myi^3z!8*!79;uUUyk90G; zcr?nysUx>O>vy*5fCW?Zj<8@_hK zBc~P^h85H`Q}?Bwj~vdc!?j(b6ES&N4M;iH%+!&vDx^*> zIAgwrtu3>>WD32>xIXHM02nQ!c+Sl;3)fji9WSRYb!+({Xe1xjgdt9gGd?n0;SX@e zqH!h=(*~uHmZ54>&s+$>PkD(lIyVib46KMvuSCVdse##}bD7;MZ)<3ixzPiJezbe< z;+weEofO=3v^i%fqn=a-Ynz{q8XNBJS*)JoFbB_@Z*MrCLjqz^@f*KqGI2{jtZb}C z-gRlpRCB@)C8vPl^I_vrRmKff%qs+nM8nT7>mO)xum<{N5M`>W8&T5R()*cN+CR}T zlhEHW5pv%U9R_xrAx>$47LTq)9_wLYs%DuJh+2w1&RdytH&uQaKeVQHVr}gs7Oqu8L4d898MW3OS7CR1QmW z4k>b2*-d87%$&B_uHR;#`~KYD$M^TocaO)o4)4SBdc9xXyS%T(kOZF%3QC-_+#oC^ zuyNx?fn9c?_N5Yvg2KEFg0|pEq4h0H*al^Q?D8;(JYw`YV(070bESjH7t1p&)Gmhk zeNxD4I^E=n;V{d!wwX;241Be{X7^ldz80|}4+XZhNBG7oSJWxkNhf+wH}1Xa7@c!e z&8)5zX{bajzFjc(+l? z^U&MW%mw{@(Z=5THsfW2S$?Lide3~-Or(Ay3w^i6z>MrvN>098d$#l7XNi|02-R%0 ztrPO^9m=fg)07VOY!$4x9gb1?ByD}ptw8n_AoqMx&bD+{_(q*b#PG*Wr?sx{YOma@ zZ~pQYxt5OY$o|mEfosmPz)ri!1BEbZ^ox^Hh0Ba#+b6~+zvS7urUirN^>56V`Po~G z^+-vly1!^{IIS{kIG?6T5;v7>HoDgj8w(&ju@tFo`>E%wNji8bWaXnb6^m}I(D)Rx z4Y)-T{bYUay?AhHQSR%GpTEA0|2qHjN%{S%PE}0OPR(555$oQjA!8@SYMt6#viXhJ z6S4Q5a$HZ9QT~_*tUk1Pc7CWPP$od?b8%LAZsDrJiFQwH zEZrUbGr^-G~6ow zV^OW`cmiwRj&H_isa4hIHov;(@ax@$cKZ3e*Qh*Ooh{CgJ%Rh(U;*@BX{}$~Jq{m9 zP5r6?xW61q97=3&%;{HGX>7`nOuuU3ej50mOvf#Db4z~~a^!ZJb4eWa- zQ9YYlyS6z|yS?Ig`QfcH>ZFM4EMl!W8&x)OOp-01HDD7f0Z4}wC_~^n%eaLg8OX8CB$J0IW82mb zqF>6?UG0HV89@WZPs}ZKi!eZVp*(YgTXasYe1Ohpb!bDZf01dGzTE7FYx(05@{+MI zH97afv;v8l&PSDEO(Ta`E79+(G~S2mACK6Osh?53m}fbsfArfR zGEAST8aC10@o8159wpsBfQ~zHFWhXXdVuwoJB^=`kzrtp#}lf^!y!ejU{UOig;z+A zAf`c5_nBH4-s3Fs5;4Q)N?@l8<`gJ!&QffnfIEqzB`hG2uwQ_?|LoGghf+X61?C@Ob)Ma6R3wr+JH+j#Cel@&bnsxw|w(KlO|XYVGH*5K(F!Y3E^gRx=0kB zz_z$qarDh!<;?IC+ujz7AIZ|)Q}#vEZrWkIw>WI5(g;dkggZS~>6FR}NC_#qc~4)n zMpY7eDCqv~)u&&cK5+9&^Cs#5GV!7pskPOQfA(wv!u@*I9z3@!Hu_MU^!4tt{)5DT z-j6G3#yRSlERB*XMeBaqYXg&I9S_bgc^cm%-OVq~Rw#Z>yIvlHvJe-x5+Wc@kUfcQ0x9eS~;#|{wG2zH^7lkCBDFELV`sB8n zy+uvG&Rp(D7VKNbxY4BSw7M32O-t{BAT5g#zZOd!3nwUYw~(`_BAnFSz4HH zWOs<7t+U~h+~x4n^94`eVj?8ZIf>`bs(o~DHcd{v5r<^EpOoU3&l0h$K}3uQLDYeqwIlu19=9t^j{Sn;+L+Vh*k7TW8}N3oTO5Bv z7<( zb=fK!OGL4hC6<#hTtfJ>I|!)jw^!1?^I{IvR&P6FCi7)A3y|z_#*58Yz8T336%{rX zJ_2#vQ+v1hY?tcV-tn{v^$xnde@DK^@2Rx4>Eca${dG3NFJ>MjcwWfC=d|TRyzvJc z2QF@^KGqdT${<%Zx}2USKU?zUOpIao1`!)sO@%by(;hIum zEB4RpOjOlBZom2@&G5q)_uX;6<+W}X51b{1u|veS9J}|#cjZ?=@T~n0#_Qr5xtoeI zsS1)2EgM>84!NhkPQEs5{o;mt(kr{Th%IvUnJ6^oa8k+X27-^&O|?%!on~nf`CD!m zUfk3ac{}s%e!tBVE%&l-dCBzv%65ri-I9J%k(E`VmU`t+bDh#CVGMb&YA8`im1r&nCmSXZsP!Vyr66N6sd+BckY|O_&1d zqLvXV*h|S(CQoEmFFXgXqlu?AxRn~Aq)wMYhZstSK6@TcRi;fFHWG(`%E+)^<2Bd2 zJ~hg2`ElZMU;}pTg#@3Nk zKp)lEXujX)U2CIRxUv1^2CFgvu0klDj3!ldEdSY&GIK-bVN6lU&cz9ZHM#b1u4WeIcS}hh*q~Tw>*1Jk%+y6{o(_)GkzuO|GYkr@_C{ypeonWtR z-o3oFhsAyy>+aZ|G28jlMk=3C#tIMl4Fae(~%Q<9BJ z=aUw7gUr$qjNUFypv#3l-(gR#We!u|NU)E4bHKp?OGNgFH6Fc2#i3UgC|$T(^g}3` zNud&13@RDyNOs{YF3mZk!Cq^Z8Ya6P>I_gSxy3mJI2Ujx9$chY6G0lqo?OE|MFv)` z1GH-ZgKbad%rmO(SJR=OA)Yo_Nn|fiB?HXKDP$oLC?Ipz8vF2d0QhrN6;~I^ z91TE&OGIj17}y&Ru0@f-+&=gks}DL0C9-EpIB;nuWNpo!7{nR@XMpA|G;1ahNnE1= zz*5H)Yk`EXV@*(|@N7;25X^wU0S=T1u$FKjs{mY zHaOj!Ok`82;2U-axm+6VT%$KWqXn*1!okHKrT}}uni#UA62jgE&i}SYh6OcNjw8{m z4l=-M0NVSQ^mrgB9_%YXwie8dER_ebJCkhPu=;sXB{e_grc|So(kg^dS-@@%YameMSe# zEr~;hm9m)hoxx$q5lVUio4Iub?hM+3U41LV;Wfks{LqyVBEV=)Cf6c3GmeaYXX0|T z^)f{zm(vS(cIm_oaj5Cytl#!TaH69q1k7>)hjVc*VIaq_wpx$KT2A+M=FHER`F3$r z5(E~QSL!%(zCaL*jAXCjeSJF9+t+5VcS^evKO6;qA6Z_xF%9X|?UIIbHdyHm;rtDB zDHp)34kAaD%DT{Lit|Tnfg2DMG|;v*o(Qk2n!*EZbWS3*TeVpl-su7+uCP_-!Y>rF z8tVY|g$f8#$r{qBH!F=Na{7=|AciySIKJe9U^d;JiRmSCI+tcw9tVO3>`5vfy%Ym= zVNVdCWX>6)KD(NHf)m=LLzG_PG{!t$S(u$C&a-++IuEQF%m$(!89>p~TS!0ZIE~2K zpGb}uIMR-+Lq*NSuR6-#K^UG0`odn^NZD?Ulc(L1?{*FijyBW9b6k-9lRu$h6b zduVWrue|JGq~RzLq3a$N+(I_g9R;oo!d=L7Nb({_+9!CgFXzQ#UoDkAqrcaeJOG)) zFU8QN9XK=1rnLar<+~Pcf+aHoqrS#NQ0OU?8GxQ5%Y8>8Mj1um{6QCVRd?60&fH&( z&G7F~@^c#?)TF6l@4~eHimzDlePB654I+VB&L{}CXAK0-;-Rt&Ymu=4E3!u4JuI|^ zKz6uhKs@z_92Vn2(@2B(b`z$+N=zTf==#d-7yNq08KfeQaGOBKj1j|P#^|_IhVKx8 zY@kag`1XLzwd>%l;_B%B;*s>KAr}_J7nt_Nh1Z0$yL0z;6&E0MM+fe)wITJtQGPA- zYWnbVBsqgRpDqSpSaUW2wSkrGq5kmuwA%izxyFF!fqtCt$Sd+(qsrr`znp9IM>%go zXtU3A!FzCQgD>KfOOtc|t6lXt6X4ZoVW!4FhE;vGxFG&DUymLgc~5ljL^O zK7ni7qP{dQOx# z!Yhd0F$)FMan4bbfSTuqzTpoWPm<_1;H_w5SxIPnZr|%*X62Kqc_-nl8OCOg_7|AM zlb3N+yISntu_Gs8OlL%$L{}sCU_9l=)VW%xy>He2_feU>9*QSngC(-O-pafUD<&EJ zB~S&S?#NA>xUek5^&{^u&b34;Y}y`u%-fvTwFU`O8*F`l+C18*%+315mXYV<1K8=_ zkoVjj;ViijN$$m->{@n*gE$f|%xIUURo zd2^8)46t`6=BE51>ng6Uu;p~_p5L34kOjX&qI|8y%x1ZIqxgDGQ-6eKYin1wXOT4coS;i?l1u{+vY zbR_%Xsy7TFdjQh{Q^6D~IAH=695HhHUSpv9b^r%~XL?ZgPL));%~jfH<|SR;Y@X58O3 zOf&zfv3pTERO0yGUFW?ZEvfzmU)Bm zd4r8Kg99`JGg^*iIiiW4*+hpm(=(gtuoik|3mw)<&uqn*q9zLrAny$z9}OU%4Ipg> zknaYNK?BH$0V*RG(M1O`yXdfQ^vrK`ST{Yhn-2R<&-{+L5j?4H2svR0u`q<3H-tDC zLarG?ybK}!hA4b4qMx4GPlpZAGY9CfL3-vO9Y&&Ok}y#rlkJ9(?}m^;L&%6BWYQ3_ zUigRvG>o#$|%`Gp{| z$`?-CxaF+dr1-W1Zrq;A+&aaxm2hnUcTlrW71cQOq-gMP?Wg!SxEF!Yw&=!5cv+bAU$L)m!YIlBV?0Vc@@NLg~+QiM{o+$Bb3F|iC&6^xMtc91iG7tAb ziI3Y~zTk1*?dTI;!tLkV^z)ONF)~x;Je$!Ie!^K4)nxUg*sLd<1>bhTi=y|Y3A=8y z<=YCmwu9P{NjzHx-?p6OfV!GrT5?hn;3nKlKCX@o%q=UP?Hk{gZBKJYF?9-6fba3 z`8Fq8wYgK;qS-u~hZH|<*$yb(ccrqYcz>wbd>gE{P5gY^CRLto-zokCmv4tn@bWh2 z+4NKaKF*?i;jOKk?K;kak8^yN)ci(f>O9YOm2XqLP*iicHzjpF;V$rP>D(DEziDXQ z_KI(t=Gv^ZBXfDSWxj1W*8%n5U8xbzb~=x5JK5W2X?rwI^&k&-nvXm6&JLU7<$a!q zyL^yuyHLJx?4sMNb=!q?8{wRmvLFGeb?pE91B29ZCG5~y6nd{Cw&A@_vLKTuSR6vpuCtP9>jy>F^x zidd9PMWWd>*A;*H7oL@*udG$Hp~lLOYsjM98&Qp8p$^_w(oqD@($I3JqGap7Bi`@| z&X{661VG|*@knfQ5bHFyP{Ne49LicK3Gz#f9}^^7Ra1tmqH=!>FCEPQYWio7!7J#) z0|OpM=KMbd-7~1uNe8`Yqu%An@9myi;KF!4k;9(J7UOi2x@rg7Jyt?db%Vr|D2g|I zFfhHy*V)CIvmGumTVD3p{n z_0}vU7&XQDLy4IQ9t0N6L;YE^YyOs%gSC6@3&?e}Wp=uKa0s?w<;E}w*zF=wX6N;- z&koh46Ft2abhR0+MO|~D9BPl#*^XA)%gTYeluk(d(lsDOY&8&@`7x|=R-OBQ?8qz` z{FryY68*!y_s;vVWO&_LKrv@NoskIUUwJtaYLov|*E5&vLb9igo-x4#*B& zMib^3!p=Ta)?cAd44m1A;~BI84@A$zh^GgqXL8=1-Y_}pcdQ}3u@a(2U%0aa5-82m zOQmvJ2kvtG5F|z#spuK-xp_OA`Dex>uwTQTUbH+B%$RQW?X*S%x{h+i@Mz5Mg+=Bt zb)d|dG}cJlr5V4RsT%&0vJf&1&2BB8d#sUWofRJ^-?6I3_HYKdVwG+1Jtl@sB=%3I-~L@qn?-fT z;B~5@aO{U!P@1>gcEx;1JwE15YSoPA|%KPrXKhOOz zh+BuCyd(d9#e71=V`X2z`>o40tQ> h_c`4ClpPqE+Y0NNe@EqiUeyVV!)-Ou!(P00{~zZM0~`PV diff --git a/test/functional/single_pages_controller_test.rb b/test/functional/single_pages_controller_test.rb index c7746c13a5..e37bb58102 100644 --- a/test/functional/single_pages_controller_test.rb +++ b/test/functional/single_pages_controller_test.rb @@ -156,16 +156,16 @@ def setup sample_type_id: source_sample_type.id } response_data = JSON.parse(response.body)['uploadData'] - assert_response :success - + db_samples = response_data['dbSamples'] updated_samples = response_data['updateSamples'] - assert(updated_samples.size, 2) - new_samples = response_data['newSamples'] - assert(new_samples.size, 2) - possible_duplicates = response_data['possibleDuplicates'] - assert(possible_duplicates.size, 1) + + assert_response :success + assert_equal db_samples.size, 5 + assert_equal updated_samples.size, 2 + assert_equal new_samples.size, 2 + assert_equal possible_duplicates.size, 1 end end @@ -182,16 +182,14 @@ def setup sample_type_id: sample_collection_sample_type.id } response_data = JSON.parse(response.body)['uploadData'] - assert_response :success - updated_samples = response_data['updateSamples'] - assert(updated_samples.size, 2) - new_samples = response_data['newSamples'] - assert(new_samples.size, 2) - possible_duplicates = response_data['possibleDuplicates'] - assert(possible_duplicates.size, 1) + + assert_response :success + assert_equal updated_samples.size, 2 + assert_equal new_samples.size, 2 + assert_equal possible_duplicates.size, 1 end end @@ -208,16 +206,14 @@ def setup sample_type_id: assay_sample_type.id } response_data = JSON.parse(response.body)['uploadData'] - assert_response :success - updated_samples = response_data['updateSamples'] - assert(updated_samples.size, 2) - new_samples = response_data['newSamples'] - assert(new_samples.size, 1) - possible_duplicates = response_data['possibleDuplicates'] - assert(possible_duplicates.size, 1) + + assert_response :success + assert_equal updated_samples.size, 2 + assert_equal new_samples.size, 1 + assert_equal possible_duplicates.size, 1 end end @@ -236,16 +232,14 @@ def setup sample_type_id: source_sample_type.id } response_data = JSON.parse(response.body)['uploadData'] - assert_response :success - updated_samples = response_data['updateSamples'] - assert(updated_samples.size, 0) - unauthorized_samples = response_data['unauthorized_samples'] - assert(unauthorized_samples.size, 2) - new_samples = response_data['newSamples'] - assert(new_samples.size, 2) + + assert_response :success + assert_equal updated_samples.size, 0 + assert_equal unauthorized_samples.size, 2 + assert_equal new_samples.size, 2 possible_duplicates = response_data['possibleDuplicates'] assert(possible_duplicates.size, 1) @@ -289,12 +283,12 @@ def setup_file_upload FactoryBot.create( :sample, id: 10_010 + n, - title: "source#{n}", + title: "source_#{n}", sample_type: source_sample_type, project_ids: [project.id], contributor: person, data: { - 'Source Name': 'Source Name', + 'Source Name': "Source #{n}", 'Source Characteristic 1': 'Source Characteristic 1', 'Source Characteristic 2': source_sample_type From 04a82a46e525ad4d4e4569c67535bcbb8f10426c Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 20:42:45 +0200 Subject: [PATCH 072/131] Add thead and tbody elements --- .../_duplicate_samples_panel.html.erb | 69 +++++++++++-------- .../single_pages/_new_samples_panel.html.erb | 33 +++++---- .../_unauthorized_samples_panel.html.erb | 13 ++-- .../_update_samples_panel.html.erb | 29 +++++--- 4 files changed, 86 insertions(+), 58 deletions(-) diff --git a/app/views/single_pages/_duplicate_samples_panel.html.erb b/app/views/single_pages/_duplicate_samples_panel.html.erb index b05064fdd6..6a623a4286 100644 --- a/app/views/single_pages/_duplicate_samples_panel.html.erb +++ b/app/views/single_pages/_duplicate_samples_panel.html.erb @@ -1,8 +1,9 @@ <% unless @possible_duplicates.nil? or @possible_duplicates.compact.none? %> - <%= folding_panel("Possible Duplicates #{@possible_duplicates.size}", true, :id => "duplicate-samples-panel", :body_options => {:id => "duplicate-samples-panel-content"}, - :help_text => "These new samples have been matched to already existing samples.") do %> + <%= folding_panel("Possible Duplicates #{@possible_duplicates.size}", true, :id => "duplicate-samples-panel", :body_options => { :id => "duplicate-samples-panel-content" }, + :help_text => "These new samples have been matched to already existing samples.") do %>
Order
- <%= button_link_to('Add new attribute', 'add', '#', id: 'add-attribute') if @template.level %> + <%= button_link_to('Add new attribute', 'add', '#', id: 'add-attribute', class: @template.new_record? ? 'hidden' : '') %>
<%= hidden_field_tag "#{field_name_prefix}[required]", '0' %> - <%= check_box_tag "#{field_name_prefix}[required]", '1', required, disabled: !allow_required, data: { attr: "required" } %> + <%= check_box_tag "#{field_name_prefix}[required]", '1', required, class: "#{ allow_required ? '' : 'disabled' }", data: { attr: "required" } %>
<%= hidden_field_tag "#{field_name_prefix}[is_title]", '0' %> - <%= check_box_tag "#{field_name_prefix}[is_title]", '1', is_title, class:'sample-type-is-title',disabled: seek_sample_multi || !allow_required %> + <%= check_box_tag "#{field_name_prefix}[is_title]", '1', is_title, class: "sample-type-is-title #{ allow_required ? '' : 'disabled' }"%>
From 0fec474d2a7a2bc40feb0dd2050247699d11a1e4 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 23 May 2024 10:03:55 +0200 Subject: [PATCH 033/131] Make `is_title` and `required` attributes read-only if inherited --- lib/seek/samples/sample_type_editing_constraints.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/seek/samples/sample_type_editing_constraints.rb b/lib/seek/samples/sample_type_editing_constraints.rb index 39f5a64cd7..d5c35aa78d 100644 --- a/lib/seek/samples/sample_type_editing_constraints.rb +++ b/lib/seek/samples/sample_type_editing_constraints.rb @@ -22,6 +22,7 @@ def samples? def allow_required?(attr) if attr.is_a?(SampleAttribute) return true if attr.new_record? + return false if inherited?(attr) attr = attr.accessor_name end From 646919347a4756fb1e18e062099ab500d0ffb5f6 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 23 May 2024 10:04:27 +0200 Subject: [PATCH 034/131] Replace 'disabled' prop with bootstrap class --- app/assets/javascripts/templates.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/templates.js b/app/assets/javascripts/templates.js index 41d064d115..0947bfa8d1 100644 --- a/app/assets/javascripts/templates.js +++ b/app/assets/javascripts/templates.js @@ -243,9 +243,9 @@ const applyTemplate = () => { const isRequired = row[0] ? "checked" : ""; newRow = $j(newRow.replace(/replace-me/g, index)); $j(newRow).find('[data-attr="required"]').prop("checked", row[0]); - if (appliedToSampleType) $j(newRow).find('[data-attr="required"]').prop("disabled", true); + if (appliedToSampleType) $j(newRow).find('[data-attr="required"]').addClass("disabled"); $j(newRow).find(".sample-type-is-title").prop("checked", row[8]); - if (appliedToSampleType) $j(newRow).find('.sample-type-is-title').prop("disabled", true); + if (appliedToSampleType) $j(newRow).find('.sample-type-is-title').addClass("disabled"); $j(newRow).find('[data-attr="title"]').val(row[1]); if (appliedToSampleType) $j(newRow).find('[data-attr="title"]').addClass("disabled"); $j(newRow).find('[data-attr="description"]').val(row[2]); From 579f5e356e0b282265f9c0b066d0749c989c5404 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 23 May 2024 10:08:30 +0200 Subject: [PATCH 035/131] make isa_json_compliant sample_type editable when project member instead --- app/models/sample_type.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/sample_type.rb b/app/models/sample_type.rb index d64709da02..b7ba82c750 100644 --- a/app/models/sample_type.rb +++ b/app/models/sample_type.rb @@ -128,7 +128,13 @@ def self.can_create? def can_edit?(user = User.current_user) return false if user.nil? || user.person.nil? || !Seek::Config.samples_enabled return true if user.is_admin? - contributor == user.person || projects.detect { |project| project.can_manage?(user) }.present? + + # Make the ISA JSON compliant sample types editable when a user is a project member instead of a project admin + if is_isa_json_compliant? + contributor == user.person || projects.detect { |project| project.has_member? user.person }.present? + else + contributor == user.person || projects.detect { |project| project.can_manage?(user) }.present? + end end def can_delete?(user = User.current_user) From 66a9e6a5a04ddcaebe4911b825c9e261188c2f67 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Thu, 23 May 2024 10:31:19 +0200 Subject: [PATCH 036/131] Add sample type constraint test for `allow_required?` of inherited sample attributes --- .../sample_type_editing_constraints_test.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/unit/samples/sample_type_editing_constraints_test.rb b/test/unit/samples/sample_type_editing_constraints_test.rb index ace957c974..4cfdc03a7a 100644 --- a/test/unit/samples/sample_type_editing_constraints_test.rb +++ b/test/unit/samples/sample_type_editing_constraints_test.rb @@ -87,13 +87,29 @@ class SampleTypeEditingConstraintsTest < ActiveSupport::TestCase assert c.allow_required?(:age) assert c.allow_required?('full name') - # accespts attribute + # accepts attribute attr = c.sample_type.sample_attributes.detect { |t| t.accessor_name == 'address' } refute_nil attr refute c.allow_required?(attr) attr = c.sample_type.sample_attributes.detect { |t| t.accessor_name == 'age' } refute_nil attr assert c.allow_required?(attr) + + # should refute if inherited from a template + template = FactoryBot.create(:isa_source_template) + sample_type_from_template = create_sample_type_from_template(template, c.sample_type.projects.first, c.sample_type.contributor) + sample_type_from_template.sample_attributes << FactoryBot.create(:sample_attribute, title: 'Extra Source Characteristic', sample_attribute_type: FactoryBot.create(:string_sample_attribute_type), required: false, isa_tag_id: FactoryBot.create(:source_characteristic_isa_tag).id, sample_type: sample_type_from_template) + + c_inherited = Seek::Samples::SampleTypeEditingConstraints.new(sample_type_from_template) + sample_type_from_template.sample_attributes.each do |attr| + if attr.title == 'Extra Source Characteristic' + refute c_inherited.send(:inherited?, attr) + assert c_inherited.allow_required?(attr) + else + assert c_inherited.send(:inherited?, attr) + refute c_inherited.allow_required?(attr) + end + end end test 'allow_attribute_removal?' do From 52e30617c7d301557552833d7ecd187ca17e415e Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 24 May 2024 10:36:09 +0200 Subject: [PATCH 037/131] Remove all logic from the views and transfer to controller --- app/controllers/isa_assays_controller.rb | 49 +++++++++++++++--- app/views/isa_assays/_form.html.erb | 63 +++++------------------- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/app/controllers/isa_assays_controller.rb b/app/controllers/isa_assays_controller.rb index 8681f2a23f..710001a1d7 100644 --- a/app/controllers/isa_assays_controller.rb +++ b/app/controllers/isa_assays_controller.rb @@ -9,20 +9,53 @@ class IsaAssaysController < ApplicationController after_action :rearrange_assay_positions_create_isa_assay, only: :create def new - if params[:is_assay_stream] - @isa_assay = IsaAssay.new({ assay: { assay_class_id: AssayClass.assay_stream.id } }) - else - @isa_assay = IsaAssay.new({ assay: { assay_class_id: AssayClass.experimental.id } }) - end + new_position = + if params[:is_assay_stream] || params[:source_assay_id].nil? || (params[:source_assay_id] == params[:assay_stream_id]) + 0 + else + Assay.find(params[:source_assay_id]).position + 1 + end + + study = Study.find(params[:study_id]) + source_assay = Assay.find(params[:source_assay_id]) if params[:source_assay_id] + + input_sample_type_id = + if params[:is_assay_stream] + study.sample_types.second.id + else + source_assay.previous_linked_sample_type.id if source_assay + end + + projects = [] + + @isa_assay = + if params[:is_assay_stream] + IsaAssay.new({ assay: { assay_class_id: AssayClass.assay_stream.id, + study_id: study.id, + position: 0 }, + input_sample_type_id: input_sample_type_id}) + else + IsaAssay.new({ assay: { assay_class_id: AssayClass.experimental.id, + assay_stream_id: params[:assay_stream_id], + study_id: study.id, + position: new_position }, + input_sample_type_id: input_sample_type_id}) + end end def create if @isa_assay.save - redirect_to single_page_path(id: @isa_assay.assay.projects.first, item_type: 'assay', - item_id: @isa_assay.assay, notice: 'The ISA assay was created successfully!') + flash[:notice] = "The #{t('isa_assay')} was succesfully created.
".html_safe + respond_to do |format| + format.html do + redirect_to single_page_path(id: @isa_assay.assay.projects.first, item_type: 'assay', + item_id: @isa_assay.assay) + end + format.json { render json: @isa_assay, include: [params[:include]] } + end else respond_to do |format| - format.html { render action: 'new' } + format.html { render action: 'new', status: :unprocessable_entity } format.json { render json: json_api_errors(@isa_assay), status: :unprocessable_entity } end end diff --git a/app/views/isa_assays/_form.html.erb b/app/views/isa_assays/_form.html.erb index 899e57ad4f..020972f4d0 100644 --- a/app/views/isa_assays/_form.html.erb +++ b/app/views/isa_assays/_form.html.erb @@ -1,46 +1,6 @@ -<% assay = params[:isa_assay][:assay] if params.dig(:isa_assay, :assay) %> -<% study = Study.find(params[:study_id] || assay[:study_id]) %> -<% def first_assay_in_stream? - params[:source_assay_id].nil? || (params[:source_assay_id] == params[:assay_stream_id]) - end -%> <% - source_assay = Assay.find(params[:source_assay_id]) if params[:source_assay_id] - assay_stream_id = params[:assay_stream_id] if params[:assay_stream_id] - - if @isa_assay.assay.new_record? - if params[:is_assay_stream] - assay_position = study.assay_streams.any? ? study.assay_streams.map(&:position).max + 1 : 0 - assay_class_id = AssayClass.assay_stream.id - is_assay_stream = true - else - assay_position = first_assay_in_stream? ? 0 : source_assay.position + 1 - assay_class_id = AssayClass.experimental.id - is_assay_stream = false - end - else - assay_position = @isa_assay.assay.position - assay_class_id = @isa_assay.assay.assay_class_id - is_assay_stream = @isa_assay.assay.is_assay_stream? - end - - input_sample_type_id ||= - if is_assay_stream || source_assay&.assay_class&.is_assay_stream? - study.sample_types.second.id - else - source_assay.sample_type.id if source_assay - end - - show_extended_metadata = - if is_assay_stream - true - elsif source_assay&.position&.zero? && !@isa_assay.assay.new_record? - true # Custom metadata should be shown in edit as well if assay position is 0. - else - false - end + is_assay_stream = @isa_assay.assay.is_assay_stream? %> - <%= error_messages_for :isa_assay %> <%= f.fields_for @isa_assay.assay do |assay_fields| %> @@ -55,30 +15,28 @@ <%= assay_fields.text_area :description, rows: 5, class: "form-control rich-text-edit" -%> - <% if show_extended_metadata %> + # Show extended metadata is assay is of class assay_stream + <% if is_assay_stream %> <%= render partial: 'extended_metadata/extended_metadata_type_selection', locals:{f:assay_fields, resource:@isa_assay.assay} %> <%= render partial: 'extended_metadata/extended_metadata_attribute_input', locals:{f:assay_fields,resource:@isa_assay.assay, parent_resource: "isa_assay"} %> <% end %> - + <%= assay_fields.hidden_field :study_id %> <% if is_assay_stream %>
<%= assay_fields.label "Assay position" -%>
- <%= assay_fields.number_field :position, rows: 5, class: "form-control", value: assay_position %> + <%= assay_fields.number_field :position, rows: 5, class: "form-control" %>
<% else %> <% end %> - <%= assay_fields.hidden_field :assay_stream_id, value: assay_stream_id -%> - <%= assay_fields.hidden_field :assay_class_id, value: assay_class_id -%> + <%= assay_fields.hidden_field :assay_stream_id -%> + <%= assay_fields.hidden_field :assay_class_id -%> <% if is_assay_stream %> <% if User.current_user -%> @@ -97,7 +55,7 @@ <% end -%> <% end -%> -<%= f.hidden_field :input_sample_type_id, value: input_sample_type_id -%> +<%= f.hidden_field :input_sample_type_id -%> <% unless is_assay_stream %> <%= folding_panel("Define #{t(:sample_type)} for #{t(:assay)}") do %> @@ -110,7 +68,8 @@ <%= render partial: 'projects/implicit_project_selector', locals: { action: action, select_id: '#isa_assay_assay_study_id', - parents: Study.authorized_for('edit') } %> + parents: Study.authorized_for('edit'), + skip_on_page_load: true} %> From 723f6420125f7457ee196c13f0e533983130a96b Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 24 May 2024 13:17:20 +0200 Subject: [PATCH 041/131] Remove investigation list partial that conflicts with default policy prompt --- app/views/isa_studies/_form.html.erb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/isa_studies/_form.html.erb b/app/views/isa_studies/_form.html.erb index 77d5b45858..d59bc9887f 100644 --- a/app/views/isa_studies/_form.html.erb +++ b/app/views/isa_studies/_form.html.erb @@ -23,10 +23,6 @@ <% end %> - -
<%= study_fields.label "Study position" -%>
<%= study_fields.number_field :position, :rows => 5, :class=>"form-control" -%> From 3c32ab05643eaaae7a92133a6c1c34c346553107 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 24 May 2024 13:22:46 +0200 Subject: [PATCH 042/131] Prepopulate the investigation ID when assigning `@isa_study` --- app/controllers/isa_studies_controller.rb | 2 +- app/views/isa_studies/_form.html.erb | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/controllers/isa_studies_controller.rb b/app/controllers/isa_studies_controller.rb index a060bf16bc..990169a543 100644 --- a/app/controllers/isa_studies_controller.rb +++ b/app/controllers/isa_studies_controller.rb @@ -6,7 +6,7 @@ class IsaStudiesController < ApplicationController before_action :find_requested_item, only: %i[edit update] def new - @isa_study = IsaStudy.new + @isa_study = IsaStudy.new({ study: { investigation_id: params[:investigation_id] } }) end def create diff --git a/app/views/isa_studies/_form.html.erb b/app/views/isa_studies/_form.html.erb index d59bc9887f..92d47194a5 100644 --- a/app/views/isa_studies/_form.html.erb +++ b/app/views/isa_studies/_form.html.erb @@ -76,15 +76,6 @@ } $j(document).ready(function () { - const urlSearchParams = new URLSearchParams(window.location.search); - const params = Object.fromEntries(urlSearchParams.entries()); - const investigation_id = params["investigation_id"] - // Prevent setting the hidden field on redirect (query string parameters are missing) - if(investigation_id) { - $j("#isa_study_study_investigation_id").val(investigation_id); - $j("#study_investigation_id").val(investigation_id).change(); - } - if (templates.length > 0) { Templates.init($j('#template-attributes')); } From 2bf968aa736c21978d83fe98eb22fa151c272656 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 24 May 2024 13:56:39 +0200 Subject: [PATCH 043/131] Simplification + better error messages --- app/controllers/isa_assays_controller.rb | 4 +++- app/controllers/isa_studies_controller.rb | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/controllers/isa_assays_controller.rb b/app/controllers/isa_assays_controller.rb index dbf1ce9807..b343b2b2d4 100644 --- a/app/controllers/isa_assays_controller.rb +++ b/app/controllers/isa_assays_controller.rb @@ -212,7 +212,9 @@ def find_requested_item end if @isa_assay.errors.any? - error_messages = @isa_assay.errors.full_messages.map { |msg| "
  • [#{t('isa_assay')}]: #{msg}
  • " }.join('') + error_messages = @isa_assay.errors.map do |error| + "
  • [#{error.attribute.to_s}]: #{error.message}
  • " + end.join('') flash[:error] = "
      #{error_messages}
    ".html_safe redirect_to single_page_path(id: @isa_assay.assay.projects.first, item_type: 'assay', item_id: @isa_assay.assay) diff --git a/app/controllers/isa_studies_controller.rb b/app/controllers/isa_studies_controller.rb index 990169a543..791575e4d2 100644 --- a/app/controllers/isa_studies_controller.rb +++ b/app/controllers/isa_studies_controller.rb @@ -36,12 +36,7 @@ def create end def edit - @isa_study.source = nil unless requested_item_authorized?(@isa_study.source) - @isa_study.sample_collection = nil unless requested_item_authorized?(@isa_study.sample_collection) - - respond_to do |format| - format.html - end + respond_to(&:html) end def update @@ -132,8 +127,20 @@ def set_up_instance_variable def find_requested_item @isa_study = IsaStudy.new @isa_study.populate(params[:id]) - unless requested_item_authorized?(@isa_study.study) - flash[:error] = "You are not authorized to edit this #{t('isa_study')}" + + @isa_study.errors.add(:study, "The #{t('isa_study')} was not found.") if @isa_study.study.nil? + @isa_study.errors.add(:study, "You are not authorized to edit this #{t('isa_study')}.") unless requested_item_authorized?(@isa_study.study) + + @isa_study.errors.add(:sample_type, "'Study source' #{t('sample_type')} not found.") if @isa_study.source.nil? + @isa_study.errors.add(:sample_type, "You are not authorized to edit the 'study source' #{t('sample_type')}.") unless requested_item_authorized?(@isa_study.source) + @isa_study.errors.add(:sample_type, "'Study sample' #{t('sample_type')} not found.") if @isa_study.sample_collection.nil? + @isa_study.errors.add(:sample_type, "You are not authorized to edit the 'study sample' #{t('sample_type')}.") unless requested_item_authorized?(@isa_study.sample_collection) + + if @isa_study.errors.any? + error_messages = @isa_study.errors.map do |error| + "
  • [#{error.attribute.to_s}]: #{error.message}
  • " + end.join('') + flash[:error] = "
      #{error_messages}
    ".html_safe redirect_to single_page_path(id: @isa_study.study.projects.first, item_type: 'study', item_id: @isa_study.study) end From b926597d6e5dac7edf8b20a8c01fe880f32fd5d4 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 24 May 2024 14:18:46 +0200 Subject: [PATCH 044/131] Skip value if ID is blank --- app/helpers/samples_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/samples_helper.rb b/app/helpers/samples_helper.rb index bd4bfd36dd..5c37ad0c79 100644 --- a/app/helpers/samples_helper.rb +++ b/app/helpers/samples_helper.rb @@ -81,6 +81,8 @@ def sample_form_field(attribute, element_name, value, limit = 1) value = [value] unless value.is_a?(Array) value.compact.each do |v| id = v[:id] + next if id.blank? # Skip value if there is no ID + title = v[:title] title = 'Hidden' unless Sample.find(id).can_view? existing_objects << str.new(id, title) @@ -379,5 +381,3 @@ def attribute_form_element(attribute, value, element_name, element_class, depth= end end - - From a62a0c763b0d7390c72cb7e0446c0a07447017c2 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 24 May 2024 14:32:48 +0200 Subject: [PATCH 045/131] Rename registeredSamplesObjectsInput --- app/assets/javascripts/single_page/dynamic_table.js.erb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/single_page/dynamic_table.js.erb b/app/assets/javascripts/single_page/dynamic_table.js.erb index ae1a9cc598..a0c2f4556a 100644 --- a/app/assets/javascripts/single_page/dynamic_table.js.erb +++ b/app/assets/javascripts/single_page/dynamic_table.js.erb @@ -90,7 +90,7 @@ const handleSelect = (e) => { if(c.linked_sample_type){ data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData]; data = data[0]?.id ? data : []; - return inputObjectsInput(c, data, options, linkedSamplesUrl); + return registeredSamplesObjectsInput(c, data, options, linkedSamplesUrl); } else if(c.is_cv_list && sanitizedData !== "#HIDDEN"){ data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData]; data = data.map((e) => { @@ -569,7 +569,7 @@ function retrieveLinkedSamples(url){ return linkedSamples; } -function inputObjectsInput(column, data, options, url){ +function registeredSamplesObjectsInput(column, data, options, url){ const existingOptions = data.map((e) => { isHiddenInput = (e.title == '#HIDDEN'); if (isHiddenInput) { @@ -584,7 +584,6 @@ function inputObjectsInput(column, data, options, url){ const typeaheadTemplate = 'typeahead/single_pages_samples' const objectInputName = data.map((e) => e.id).join('-') + '-' + crypto.randomUUID(); - const unLinkedSamples = data.reduce(function(filtered, sample) { if(!column.linkedSampleIds.includes(parseInt(sample.id)) && sample.title != '#HIDDEN'){ filtered.push(sample); From e286ad2458fded128248a74a5a0c305984f94cf9 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Fri, 24 May 2024 15:09:57 +0200 Subject: [PATCH 046/131] Replace drop-down with objects input with limit of 1 for registered sample attributes --- .../javascripts/single_page/dynamic_table.js.erb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/single_page/dynamic_table.js.erb b/app/assets/javascripts/single_page/dynamic_table.js.erb index a0c2f4556a..63627432bc 100644 --- a/app/assets/javascripts/single_page/dynamic_table.js.erb +++ b/app/assets/javascripts/single_page/dynamic_table.js.erb @@ -48,7 +48,7 @@ function sanitizeData(data) { const objectInputTemp = '' + ''; const typeaheadSamplesUrl = "<%= typeahead_samples_path(linked_sample_type_id: '_LINKED_') %>"; @@ -86,7 +86,8 @@ const handleSelect = (e) => { } c["render"] = function(data_, type, full, meta) { - sanitizedData = sanitizeData(data_); + let sanitizedData = sanitizeData(data_); + let data; if(c.linked_sample_type){ data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData]; data = data[0]?.id ? data : []; @@ -592,7 +593,7 @@ function registeredSamplesObjectsInput(column, data, options, url){ }, []); const hasUnlinkedSamples = unLinkedSamples.length > 0 ? true : false; - const hasMultipleInputs = column.multi_link ? 'multiple="multiple"' : '' + const hasMultipleInputs = column.multi_link ? '100' : '1' const extraClass = hasUnlinkedSamples ? 'select2__error' : ''; const titleText = hasUnlinkedSamples ? `Sample(s) '${unLinkedSamples.map(uls => uls.title).join(', ')}' not recognised as input. Please correct this issue!` : ''; setTimeout(ObjectsInput.init); @@ -604,7 +605,7 @@ function registeredSamplesObjectsInput(column, data, options, url){ .replace('_OPTIONS_', existingOptions) .replace('_EXTRACLASS_', extraClass) .replace('_TITLE_', titleText) - .replace('_MULTIPLE?_', hasMultipleInputs) + .replace('_LIMIT?_', hasMultipleInputs) .replace('_ALLOW_FREE_TEXT_', false); } } @@ -628,7 +629,7 @@ function cvListObjectsInput(column, data, options, url){ .replace('_OPTIONS_', existingOptions) .replace('_EXTRACLASS_', extraClass) .replace('_TITLE_', titleText) - .replace('_MULTIPLE?_', 'multiple="multiple"') + .replace('_LIMIT?_', '') .replace('_ALLOW_FREE_TEXT_', allowNewItems); } } From 1bb74c5e25b1fc030fb3d315b09e9da8e61ef8d3 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 27 May 2024 09:32:46 +0200 Subject: [PATCH 047/131] Fix tests --- app/controllers/isa_assays_controller.rb | 26 ++++++++++++------- app/views/isa_assays/_form.html.erb | 2 +- test/functional/isa_assays_controller_test.rb | 11 ++++---- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/controllers/isa_assays_controller.rb b/app/controllers/isa_assays_controller.rb index b343b2b2d4..7e93c82ab4 100644 --- a/app/controllers/isa_assays_controller.rb +++ b/app/controllers/isa_assays_controller.rb @@ -9,17 +9,19 @@ class IsaAssaysController < ApplicationController after_action :rearrange_assay_positions_create_isa_assay, only: :create def new + study = Study.find(params[:study_id]) new_position = - if params[:is_assay_stream] || params[:source_assay_id].nil? || (params[:source_assay_id] == params[:assay_stream_id]) + if params[:is_assay_stream] || params[:source_assay_id].nil? # If first assay is of class assay stream + study.assay_streams.any? ? study.assay_streams.map(&:position).max + 1 : 0 + elsif params[:source_assay_id] == params[:assay_stream_id] # If first assay in the stream 0 else Assay.find(params[:source_assay_id]).position + 1 end - study = Study.find(params[:study_id]) source_assay = Assay.find(params[:source_assay_id]) if params[:source_assay_id] input_sample_type_id = - if params[:is_assay_stream] || source_assay.is_assay_stream? + if params[:is_assay_stream] || source_assay&.is_assay_stream? study.sample_types.second.id else source_assay&.sample_type&.id @@ -29,7 +31,7 @@ def new if params[:is_assay_stream] IsaAssay.new({ assay: { assay_class_id: AssayClass.assay_stream.id, study_id: study.id, - position: 0 }, + position: new_position }, input_sample_type_id: }) else IsaAssay.new({ assay: { assay_class_id: AssayClass.experimental.id, @@ -67,7 +69,7 @@ def update @isa_assay.assay.attributes = isa_assay_params[:assay] # update the sample_type - unless @isa_assay.assay.is_assay_stream? + unless @isa_assay&.assay&.is_assay_stream? if requested_item_authorized?(@isa_assay.sample_type) @isa_assay.sample_type.update(isa_assay_params[:sample_type]) @isa_assay.sample_type.resolve_inconsistencies @@ -202,13 +204,19 @@ def find_requested_item @isa_assay = IsaAssay.new @isa_assay.populate(params[:id]) - @isa_assay.errors.add(:assay, "The #{t('isa_assay')} was not found.") if @isa_assay.assay.nil? - @isa_assay.errors.add(:assay, "You are not authorized to edit this #{t('isa_assay')}.") unless requested_item_authorized?(@isa_assay.assay) + if @isa_assay.assay.nil? + @isa_assay.errors.add(:assay, "The #{t('isa_assay')} was not found.") + else + @isa_assay.errors.add(:assay, "You are not authorized to edit this #{t('isa_assay')}.") unless requested_item_authorized?(@isa_assay.assay) + end # Should not deal with sample type if assay has assay_class assay stream unless @isa_assay.assay&.is_assay_stream? - @isa_assay.errors.add(:sample_type, 'Sample type not found.') if @isa_assay.sample_type.nil? - @isa_assay.errors.add(:sample_type, "You are not authorized to edit this assay's #{t('sample_type')}.") unless requested_item_authorized?(@isa_assay.sample_type) + if @isa_assay.sample_type.nil? + @isa_assay.errors.add(:sample_type, 'Sample type not found.') + else + @isa_assay.errors.add(:sample_type, "You are not authorized to edit this assay's #{t('sample_type')}.") unless requested_item_authorized?(@isa_assay.sample_type) + end end if @isa_assay.errors.any? diff --git a/app/views/isa_assays/_form.html.erb b/app/views/isa_assays/_form.html.erb index 15d980b68e..81b4c064d1 100644 --- a/app/views/isa_assays/_form.html.erb +++ b/app/views/isa_assays/_form.html.erb @@ -15,7 +15,7 @@ <%= assay_fields.text_area :description, rows: 5, class: "form-control rich-text-edit" -%>
    - # Show extended metadata is assay is of class assay_stream + <%# Show extended metadata is assay is of class assay_stream %> <% if is_assay_stream %> <%= render partial: 'extended_metadata/extended_metadata_type_selection', locals:{f:assay_fields, resource:@isa_assay.assay} %> <%= render partial: 'extended_metadata/extended_metadata_attribute_input', locals:{f:assay_fields,resource:@isa_assay.assay, parent_resource: "isa_assay"} %> diff --git a/test/functional/isa_assays_controller_test.rb b/test/functional/isa_assays_controller_test.rb index ba746b6d70..1ad8afd286 100644 --- a/test/functional/isa_assays_controller_test.rb +++ b/test/functional/isa_assays_controller_test.rb @@ -76,8 +76,7 @@ def setup end isa_assay = assigns(:isa_assay) assert_redirected_to controller: 'single_pages', action: 'show', id: isa_assay.assay.projects.first.id, - params: { notice: 'The ISA assay was created successfully!', - item_type: 'assay', item_id: Assay.last.id } + params: { item_type: 'assay', item_id: Assay.last.id } sample_types = SampleType.last(2) title = sample_types[0].sample_attributes.detect(&:is_title).title @@ -139,7 +138,7 @@ def setup assay = FactoryBot.create(:assay, study:, contributor: person) put :update, params: { id: assay, isa_assay: { assay: { title: 'assay title' } } } assert_redirected_to single_page_path(id: project, item_type: 'assay', item_id: assay.id) - assert flash[:error].include?('Resource not found.') + assert flash[:error].include?('Sample type not found.') assay = FactoryBot.create(:assay, study:, sample_type: assay_type, contributor: person) @@ -359,7 +358,7 @@ def setup end isa_assay = assigns(:isa_assay) - assert_redirected_to single_page_path(id: project, item_type: 'assay', item_id: isa_assay.assay.id, notice: 'The ISA assay was created successfully!') + assert_redirected_to single_page_path(id: project, item_type: 'assay', item_id: isa_assay.assay.id) assert_equal isa_assay.assay.sample_type.previous_linked_sample_type, study.sample_types.second assert_equal isa_assay.assay.next_linked_child_assay, end_assay @@ -455,7 +454,7 @@ def setup end isa_assay = assigns(:isa_assay) - assert_redirected_to single_page_path(id: project, item_type: 'assay', item_id: isa_assay.assay.id, notice: 'The ISA assay was created successfully!') + assert_redirected_to single_page_path(id: project, item_type: 'assay', item_id: isa_assay.assay.id) assert_equal begin_assay.previous_linked_sample_type, study.sample_types.second assert_equal isa_assay.assay.sample_type.previous_linked_sample_type, begin_assay.sample_type @@ -619,7 +618,7 @@ def setup # New assay stream should have position 1 and is of type 'number' assert_select 'input[type=number][value=1]#isa_assay_assay_position', count: 1 - assay_stream2 = FactoryBot.create(:assay_stream, study: , contributor: person, position: 5) + FactoryBot.create(:assay_stream, study: , contributor: person, position: 5) get :new, params: { study_id: study.id, is_assay_stream: true } assert_response :success # New assay stream should have position 6 and is of type 'number' From 36ba2e339a360a076c3a6c2c2bfc6bccac0b4d5b Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 27 May 2024 14:21:12 +0200 Subject: [PATCH 048/131] Remove the data-dismiss attribute and add a function that hides the modal --- app/views/sharing/_group_permission_modal.html.erb | 5 ++++- app/views/sharing/_person_permission_modal.html.erb | 6 ++++-- app/views/sharing/_programme_permission_modal.html.erb | 9 ++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/views/sharing/_group_permission_modal.html.erb b/app/views/sharing/_group_permission_modal.html.erb index e31c09fb6f..fd0748ec2f 100644 --- a/app/views/sharing/_group_permission_modal.html.erb +++ b/app/views/sharing/_group_permission_modal.html.erb @@ -32,7 +32,7 @@ <% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-project-confirm', class: 'btn btn-primary pull-right', 'data-dismiss' => 'modal') %> + <%= link_to('Add', '#', id: 'permission-project-confirm', class: 'btn btn-primary pull-right') %> <% end %> <% end %> @@ -77,6 +77,9 @@ } } + // Close this modal + $j('#add-project-permission-modal').modal('hide'); + Sharing.addPermission(permission); }); diff --git a/app/views/sharing/_person_permission_modal.html.erb b/app/views/sharing/_person_permission_modal.html.erb index f10f4bf61a..5e9f0a4903 100644 --- a/app/views/sharing/_person_permission_modal.html.erb +++ b/app/views/sharing/_person_permission_modal.html.erb @@ -18,7 +18,7 @@ <% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-people-confirm', class: 'btn btn-primary pull-right', 'data-dismiss' => 'modal') %> + <% link_to('Add', '#', id: 'permission-people-confirm', class: 'btn btn-primary pull-right') %> <% end %> <% end %> @@ -44,9 +44,11 @@ access_type: parseInt($j('#permission-people-access-type').val()) }); }); - // Reset form $j('select#permission-people-ids').val([]).change(); $j('#permission-people-access-type').val('<%= Policy::ACCESSIBLE %>').change(); + + // Close this modal + $j('#add-person-permission-modal').modal('hide'); }); diff --git a/app/views/sharing/_programme_permission_modal.html.erb b/app/views/sharing/_programme_permission_modal.html.erb index 94eb5e97e0..3e35b25a79 100644 --- a/app/views/sharing/_programme_permission_modal.html.erb +++ b/app/views/sharing/_programme_permission_modal.html.erb @@ -18,7 +18,7 @@ <% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-programmes-confirm', class: 'btn btn-primary pull-right', 'data-dismiss' => 'modal') %> + <%= link_to('Add', '#', id: 'permission-programmes-confirm', class: 'btn btn-primary pull-right') %> <% end %> <% end %> @@ -44,7 +44,10 @@ }); // Reset form - $j('select#permission-programmes-ids').val([]).change(); - $j('#permission-programmes-access-type').val('<%= Policy::ACCESSIBLE %>').change(); + $j('select#permission-programmes-ids').val([]).change(); + $j('#permission-programmes-access-type').val('<%= Policy::ACCESSIBLE %>').change(); + + // Close this modal + $j('#add-programme-permission-modal').modal('hide'); }); From 52c636cf0f29db99783ac8411b951019e2dcf320 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 27 May 2024 16:21:16 +0200 Subject: [PATCH 049/131] Add functionality for cancel button --- app/helpers/bootstrap_helper.rb | 13 +++++++++--- .../sharing/_group_permission_modal.html.erb | 13 +++++++++--- .../sharing/_person_permission_modal.html.erb | 20 ++++++++++++++---- .../_programme_permission_modal.html.erb | 21 ++++++++++++++----- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/app/helpers/bootstrap_helper.rb b/app/helpers/bootstrap_helper.rb index 0ee54d45d2..f222431a61 100644 --- a/app/helpers/bootstrap_helper.rb +++ b/app/helpers/bootstrap_helper.rb @@ -161,10 +161,17 @@ def modal(options = {}, &block) end end - def modal_header(title, _options = {}) + def modal_header(title, options = {}) + # If no_close_btn is set to true, the modal will not have a close button. + # Useful for multiple shown modals. + no_close_btn = options[:no_close_btn] if options[:no_close_btn] content_tag(:div, class: 'modal-header') do - content_tag(:button, class: 'close', 'data-dismiss' => 'modal', 'aria-label' => 'Close') do - content_tag(:span, '×'.html_safe, 'aria-hidden' => 'true') + if no_close_btn + ''.html_safe + else + content_tag(:button, class: 'close', 'data-dismiss' => 'modal', 'aria-label' => 'Close' ) do + content_tag(:span, '×'.html_safe, 'aria-hidden' => 'true') + end end + content_tag(:h4, title, class: 'modal-title') end diff --git a/app/views/sharing/_group_permission_modal.html.erb b/app/views/sharing/_group_permission_modal.html.erb index fd0748ec2f..901d42de98 100644 --- a/app/views/sharing/_group_permission_modal.html.erb +++ b/app/views/sharing/_group_permission_modal.html.erb @@ -1,7 +1,7 @@ <%= button_link_to("Share with a #{t('project')} / #{t('institution')}", 'add', '#', id: 'add-project-permission-button') %> <%= modal(id: 'add-project-permission-modal', size: 'm') do %> - <%= modal_header("Share with a #{t('project')} and/or #{t('institution')}") %> + <%= modal_header("Share with a #{t('project')} and/or #{t('institution')}", { no_close_btn: true }) %> <%= modal_body do %>
    @@ -32,7 +32,9 @@
    <% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-project-confirm', class: 'btn btn-primary pull-right') %> + <%= link_to('Add', '#', id: 'permission-project-confirm', class: 'btn btn-primary') %> + or + <%= link_to('Cancel', '#', id: 'cancel-project-selection', class: 'btn btn-default') %> <% end %> <% end %> @@ -77,12 +79,17 @@ } } - // Close this modal + // Close this modal after clicking add button $j('#add-project-permission-modal').modal('hide'); Sharing.addPermission(permission); }); + // Close modal when cancel button is clicked + $j('#cancel-project-selection').click(function () { + $j('#add-project-permission-modal').modal('hide'); + }); + // Update the institution list when a project is selected $j('#permission-project-id').change(function () { var institutionSelect = $j('#permission-institution-id'); diff --git a/app/views/sharing/_person_permission_modal.html.erb b/app/views/sharing/_person_permission_modal.html.erb index 5e9f0a4903..b26b88c8dc 100644 --- a/app/views/sharing/_person_permission_modal.html.erb +++ b/app/views/sharing/_person_permission_modal.html.erb @@ -1,7 +1,7 @@ <%= button_link_to("Share with a #{t('person')}", 'add', '#', id: 'add-person-permission-button') %> <%= modal(id: 'add-person-permission-modal', size: 'm') do %> - <%= modal_header("Share with #{t('person').pluralize}") %> + <%= modal_header("Share with #{t('person').pluralize}", { no_close_btn: true }) %> <%= modal_body do %>
    @@ -18,11 +18,18 @@
    <% end %> <%= modal_footer do %> - <% link_to('Add', '#', id: 'permission-people-confirm', class: 'btn btn-primary pull-right') %> + <%= link_to('Add', '#', id: 'permission-people-confirm', class: 'btn btn-primary') %> + or + <%= link_to('Cancel', '#', id: 'cancel-people-permission-selection', class: 'btn btn-default') %> <% end %> <% end %> diff --git a/app/views/sharing/_programme_permission_modal.html.erb b/app/views/sharing/_programme_permission_modal.html.erb index 3e35b25a79..994d35a336 100644 --- a/app/views/sharing/_programme_permission_modal.html.erb +++ b/app/views/sharing/_programme_permission_modal.html.erb @@ -1,7 +1,7 @@ <%= button_link_to("Share with a #{t('programme')}", 'add', '#', id: 'add-programme-permission-button') %> <%= modal(id: 'add-programme-permission-modal', size: 'm') do %> - <%= modal_header("Share with #{t('programme').pluralize}") %> + <%= modal_header("Share with #{t('programme').pluralize}", { no_close_btn: true }) %> <%= modal_body do %>
    @@ -18,11 +18,18 @@
    <% end %> <%= modal_footer do %> - <%= link_to('Add', '#', id: 'permission-programmes-confirm', class: 'btn btn-primary pull-right') %> + <%= link_to('Add', '#', id: 'permission-programmes-confirm', class: 'btn btn-primary') %> + or + <%= link_to('Cancel', '#', id: 'cancel-programme-permission-selection', class: 'btn btn-default') %> <% end %> <% end %> From 60bcd97cbecbb64dbfd220dfbd32253e8186cc9c Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 27 May 2024 17:25:13 +0200 Subject: [PATCH 050/131] Make terms unique within a sample controlled vocab --- app/models/sample_controlled_vocab_term.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/sample_controlled_vocab_term.rb b/app/models/sample_controlled_vocab_term.rb index 4f1791e66f..092c85a9a5 100644 --- a/app/models/sample_controlled_vocab_term.rb +++ b/app/models/sample_controlled_vocab_term.rb @@ -1,7 +1,7 @@ class SampleControlledVocabTerm < ApplicationRecord belongs_to :sample_controlled_vocab, inverse_of: :sample_controlled_vocab_terms - validates :label, presence: true, length: { maximum: 500 } + validates :label, presence: true, length: { maximum: 500 }, uniqueness: { scope: :sample_controlled_vocab_id } before_validation :truncate_label From ec659ad0f0f750039806c619e4ccf03db4e3c871 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 27 May 2024 17:59:25 +0200 Subject: [PATCH 051/131] Add id as a hidden field to also pass the ID to the controller when updating the CV --- app/views/sample_controlled_vocabs/_term_form_row.html.erb | 5 ++++- .../_term_form_row_disabled.html.erb | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/sample_controlled_vocabs/_term_form_row.html.erb b/app/views/sample_controlled_vocabs/_term_form_row.html.erb index aa600c85a7..ffb2edf3b5 100644 --- a/app/views/sample_controlled_vocabs/_term_form_row.html.erb +++ b/app/views/sample_controlled_vocabs/_term_form_row.html.erb @@ -1,5 +1,8 @@
    <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %> + <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][id]", term.id %> + <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %> + <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][iri]", term.iri %> <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][parent_iri]", term.parent_iri %> diff --git a/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb b/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb index 2be8514245..a331b87914 100644 --- a/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb +++ b/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb @@ -8,6 +8,7 @@ %>
    + <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][id]", term.id %> <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %>
    <%= term.label %>
    - <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][id]", term.id %> - <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %> - <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %> <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][iri]", term.iri %> <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][parent_iri]", term.parent_iri %> diff --git a/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb b/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb index a331b87914..2be8514245 100644 --- a/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb +++ b/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb @@ -8,7 +8,6 @@ %>
    - <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][id]", term.id %> <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %>
    <%= term.label %>
    <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %> + <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][id]", term.id %> + <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %> + <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][iri]", term.iri %> <%= text_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][parent_iri]", term.parent_iri %> diff --git a/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb b/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb index 2be8514245..a331b87914 100644 --- a/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb +++ b/app/views/sample_controlled_vocabs/_term_form_row_disabled.html.erb @@ -8,6 +8,7 @@ %>
    + <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][id]", term.id %> <%= hidden_field_tag "sample_controlled_vocab[sample_controlled_vocab_terms_attributes][#{index}][label]", term.label %>
    <%= term.label %>
    + <% for key in @possible_duplicates[0].keys %> @@ -11,56 +12,64 @@ <% end %> <% end %> + + <% for dupl_sample in @possible_duplicates %> - ' > - - <% dupl_sample.map do |key, val| %> - <% val = '' if key =='id' %> - <% unless %w[uuid duplicate].include?(key) %> - <% if @multiple_input_fields.include?(key)%> - - <% elsif @cv_list_fields.include?(key) %> - - <% elsif @registered_sample_fields.include?(key) %> - - <% else %> - + '> + + <% dupl_sample.map do |key, val| %> + <% val = '' if key == 'id' %> + <% unless %w[uuid duplicate].include?(key) %> + <% if @multiple_input_fields.include?(key) %> + + <% elsif @cv_list_fields.include?(key) %> + + <% elsif @registered_sample_fields.include?(key) %> + + <% else %> + + <% end %> <% end %> <% end %> - <% end %> ' class="danger"> <% dupl_sample['duplicate'].map do |key, val| %> <% unless %w[uuid duplicate].include?(key) %> - <% if @multiple_input_fields.include?(key)%> + <% if @multiple_input_fields.include?(key) %> <% elsif @cv_list_fields.include?(key) %> <% elsif @registered_sample_fields.include?(key) %> - + <% else %> - + <% end %> <% end %> <% end %> - + <% end %> +
    - <% val.each do |sub_sample| %> - ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> - <% end %> - - <% val.each do |cv_term| %> - <%= cv_term %> - <% end %> - ' data-attr_type="seek-sample"><%= val['title'] %>' ><%= val %>
    + + <% val.each do |sub_sample| %> + ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> + <% end %> + + <% val.each do |cv_term| %> + <%= cv_term %> + <% end %> + + ' data-attr_type="seek-sample"><%= val['title'] %> + '><%= val %>
    <% val.each do |sub_sample| %> - '><%= sub_sample['title'] %> + '><%= sub_sample['title'] %> <% end %> <% val.each do |cv_term| %> - <%= cv_term %> + <%= cv_term %> <% end %> '><%= val['title'] %>'><%= val['title'] %><%= val%><%= val %>
    <% end %> diff --git a/app/views/single_pages/_new_samples_panel.html.erb b/app/views/single_pages/_new_samples_panel.html.erb index 1b163ec3da..4fd18ae27b 100644 --- a/app/views/single_pages/_new_samples_panel.html.erb +++ b/app/views/single_pages/_new_samples_panel.html.erb @@ -1,8 +1,9 @@ <% unless @new_samples.nil? or @new_samples.compact.none? %> - <%= folding_panel("New Samples #{@new_samples.size}", true, :id => "new-samples-panel", :body_options => {:id => "new-samples-panel-content"}, - :help_text => "These samples have been detected as new samples and will be created.") do %> + <%= folding_panel("New Samples #{@new_samples.size}", true, :id => "new-samples-panel", :body_options => { :id => "new-samples-panel-content" }, + :help_text => "These samples have been detected as new samples and will be created.") do %>
    + <% for key in @new_samples[0].keys %> @@ -11,36 +12,42 @@ <% end %> <% end %> + + <% for new_sample in @new_samples %> <% new_sample_id = UUID.generate %> - - <% new_sample.map do |key, val| %> - <% val = '' if key =='id' %> + + <% new_sample.map do |key, val| %> + <% val = '' if key == 'id' %> <% unless key == 'uuid' %> - <% if @multiple_input_fields.include?(key)%> - <% elsif @cv_list_fields.include?(key) %> - <% elsif @registered_sample_fields.include?(key) %> - <% else %> - + <% end %> <% end %> <% end %> <% end %> +
    + + <% if @multiple_input_fields.include?(key) %> + <% val.each do |sub_sample| %> - ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> + ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> <% end %> + <% val.each do |cv_term| %> - <%= cv_term %> + <%= cv_term %> <% end %> - ' data-attr_type="seek-sample"><%= val['title'] %> + + ' data-attr_type="seek-sample"><%= val['title'] %> <%= val %><%= val %>
    <% end %> diff --git a/app/views/single_pages/_unauthorized_samples_panel.html.erb b/app/views/single_pages/_unauthorized_samples_panel.html.erb index 8ad35705d2..4d69e2a741 100644 --- a/app/views/single_pages/_unauthorized_samples_panel.html.erb +++ b/app/views/single_pages/_unauthorized_samples_panel.html.erb @@ -1,15 +1,17 @@ <% unless @unauthorized_samples.nil? or @unauthorized_samples.compact.none? %> <% @can_upload = false %> <% errors.append("There are unauthorized samples present in the spreadsheet.") %> - <%= folding_panel("Unauthorized Samples", false, :id => "unauthorized-samples-panel", :body_options => {:id => "unauthorized-samples-panel-content"}, - :help_text => "Sample the current user does not have permission to edit them.") do %> + <%= folding_panel("Unauthorized Samples", false, :id => "unauthorized-samples-panel", :body_options => { :id => "unauthorized-samples-panel-content" }, + :help_text => "Sample the current user does not have permission to edit them.") do %>

    - You don't have permission to edit the samples listed below. Please contact the submitter of these samples or revert the changes in the spreadsheet to its original values. + You don't have permission to edit the samples listed below. Please contact the submitter of these samples or + revert the changes in the spreadsheet to its original values.

    + <% for key in @unauthorized_samples[0].keys %> <% unless key == 'uuid' %> @@ -17,6 +19,8 @@ <% end %> <% end %> + + <% for unauthorized_sample in @unauthorized_samples %> ' class=""> <% unauthorized_sample.map do |key, val| %> @@ -24,7 +28,7 @@ <% if @multiple_input_fields.include?(key) %> <% else %> @@ -34,6 +38,7 @@ <% end %> <% end %> +
    <% val.each do |sub_sample| %> - '><%= sub_sample['title'] %> + '><%= sub_sample['title'] %> <% end %>
    <% end %> diff --git a/app/views/single_pages/_update_samples_panel.html.erb b/app/views/single_pages/_update_samples_panel.html.erb index 4349edd71b..1d339a2bba 100644 --- a/app/views/single_pages/_update_samples_panel.html.erb +++ b/app/views/single_pages/_update_samples_panel.html.erb @@ -1,8 +1,9 @@ <% unless @update_samples.nil? or @update_samples.compact.none? %> - <%= folding_panel("Samples to Update #{@update_samples.size}", true, :id => "existing-samples-panel", :body_options => {:id => "existing-samples-panel-content"}, - :help_text => "These samples were detected existing samples and will be updated.") do %> + <%= folding_panel("Samples to Update #{@update_samples.size}", true, :id => "existing-samples-panel", :body_options => { :id => "existing-samples-panel-content" }, + :help_text => "These samples were detected existing samples and will be updated.") do %>
    + <% for key in @update_samples[0].keys %> @@ -11,38 +12,43 @@ <% end %> <% end %> + + <% for update_sample in @update_samples %> - ' > + '> <% db_sample = @authorized_db_samples.select { |s| s['id'] == update_sample['id'] }.first %> - + <% update_sample.map do |key, val| %> <% unless key == 'uuid' %> - <% if @multiple_input_fields.include?(key)%> - <% elsif @cv_list_fields.include?(key) %> - <% elsif @registered_sample_fields.include?(key) %> - <% else %> - + <% end %> <% end %> <% end %> - ' > + '> <% db_sample.map do |key, val| %> <% unless key == 'uuid' %> - <% if @multiple_input_fields.include?(key)%> + <% if @multiple_input_fields.include?(key) %> <% end %> +
    + ' > + <% if @multiple_input_fields.include?(key) %> + '> <% val.each do |sub_sample| %> ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> <% end %> ' > + '> <% val.each do |cv_term| %> <%= cv_term %> <% end %> '> + '> ' data-attr_type="seek-sample"><%= val['title'] %> ' ><%= val %>'><%= val %>
    <% val.each do |sub_sample| %> '><%= sub_sample['title'] %> @@ -63,6 +69,7 @@ <% end %>
    <% end %> From b76c1f6e967cad88d911c04ab11ec10225c5f6c4 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 20:43:02 +0200 Subject: [PATCH 073/131] Test the rendered HTML --- .../single_pages_controller_test.rb | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/test/functional/single_pages_controller_test.rb b/test/functional/single_pages_controller_test.rb index e37bb58102..e7fdb075e2 100644 --- a/test/functional/single_pages_controller_test.rb +++ b/test/functional/single_pages_controller_test.rb @@ -166,6 +166,35 @@ def setup assert_equal updated_samples.size, 2 assert_equal new_samples.size, 2 assert_equal possible_duplicates.size, 1 + + post :upload_samples, as: :html, params: { file:, project_id: project.id, + sample_type_id: source_sample_type.id } + + assert_response :success + + assert_select 'table#create-samples-table', count: 1 do + assert_select "tbody tr", count: new_samples.size + end + + assert_select 'table#update-samples-table', count: 1 do + update_sample_ids = updated_samples.map { |s| s['id'] } + update_sample_ids.map do |sample_id| + row_id_updated = "update-sample-#{sample_id}-updated" + assert_select "tr##{row_id_updated}", count: 1 + + row_id_original = "update-sample-#{sample_id}-original" + assert_select "tr##{row_id_original}", count: 1 + end + end + + assert_select 'table#duplicate-samples-table', count: 1 do + dup_sample_ids = possible_duplicates.map { |s| s['duplicate']['id'] } + dup_sample_ids.map do |sample_id| + row_id = "duplicate-sample-#{sample_id}" + assert_select "tr##{row_id}-1", count: 1 + assert_select "tr##{row_id}-2", count: 1 + end + end end end @@ -190,6 +219,35 @@ def setup assert_equal updated_samples.size, 2 assert_equal new_samples.size, 2 assert_equal possible_duplicates.size, 1 + + post :upload_samples, as: :html, params: { file:, project_id: project.id, + sample_type_id: sample_collection_sample_type.id } + + assert_response :success + + assert_select 'table#create-samples-table', count: 1 do + assert_select "tbody tr", count: new_samples.size + end + + assert_select 'table#update-samples-table', count: 1 do + update_sample_ids = updated_samples.map { |s| s['id'] } + update_sample_ids.map do |sample_id| + row_id_updated = "update-sample-#{sample_id}-updated" + assert_select "tr##{row_id_updated}", count: 1 + + row_id_original = "update-sample-#{sample_id}-original" + assert_select "tr##{row_id_original}", count: 1 + end + end + + assert_select 'table#duplicate-samples-table', count: 1 do + dup_sample_ids = possible_duplicates.map { |s| s['duplicate']['id'] } + dup_sample_ids.map do |sample_id| + row_id = "duplicate-sample-#{sample_id}" + assert_select "tr##{row_id}-1", count: 1 + assert_select "tr##{row_id}-2", count: 1 + end + end end end @@ -214,6 +272,35 @@ def setup assert_equal updated_samples.size, 2 assert_equal new_samples.size, 1 assert_equal possible_duplicates.size, 1 + + post :upload_samples, as: :html, params: { file:, project_id: project.id, + sample_type_id: assay_sample_type.id } + + assert_response :success + + assert_select 'table#create-samples-table', count: 1 do + assert_select "tbody tr", count: new_samples.size + end + + assert_select 'table#update-samples-table', count: 1 do + update_sample_ids = updated_samples.map { |s| s['id'] } + update_sample_ids.map do |sample_id| + row_id_updated = "update-sample-#{sample_id}-updated" + assert_select "tr##{row_id_updated}", count: 1 + + row_id_original = "update-sample-#{sample_id}-original" + assert_select "tr##{row_id_original}", count: 1 + end + end + + assert_select 'table#duplicate-samples-table', count: 1 do + dup_sample_ids = possible_duplicates.map { |s| s['duplicate']['id'] } + dup_sample_ids.map do |sample_id| + row_id = "duplicate-sample-#{sample_id}" + assert_select "tr##{row_id}-1", count: 1 + assert_select "tr##{row_id}-2", count: 1 + end + end end end @@ -243,6 +330,25 @@ def setup possible_duplicates = response_data['possibleDuplicates'] assert(possible_duplicates.size, 1) + + post :upload_samples, as: :html, params: { file:, project_id: project.id, + sample_type_id: source_sample_type.id } + + assert_response :success + + assert_select 'table#create-samples-table', count: 1 do + assert_select "tbody tr", count: new_samples.size + end + + assert_select 'table#update-samples-table', count: 0 + + assert_select 'table#unauthorized-samples-table', count: 1 do + unauthorized_sample_ids = unauthorized_samples.map { |s| s['id'] } + unauthorized_sample_ids.map do |sample_id| + row_id = "unauthorized-sample-#{sample_id}" + assert_select "tr##{row_id}", count: 1 + end + end end end From 10a321e2e530dfba606bb670b3318be5e51401e9 Mon Sep 17 00:00:00 2001 From: Kevin De Pelseneer Date: Mon, 3 Jun 2024 21:10:31 +0200 Subject: [PATCH 074/131] Redirect to correct item in SP view --- app/controllers/single_pages_controller.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/single_pages_controller.rb b/app/controllers/single_pages_controller.rb index 3229d54df0..b3015c123d 100644 --- a/app/controllers/single_pages_controller.rb +++ b/app/controllers/single_pages_controller.rb @@ -47,7 +47,7 @@ def dynamic_table_data rescue Exception => e render json: { status: :unprocessable_entity, error: e.message } 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) @@ -94,7 +94,10 @@ def download_samples_excel rescue StandardError => e flash[:error] = e.message respond_to do |format| - format.html { redirect_to single_page_path(@project.id) } + format.html do + redirect_to single_page_path(id: @project.id, item_type: @assay.nil? ? 'study' : 'assay', + item_id: @assay.nil? ? @study.id : @assay.id) + end format.json do render json: { parameters: { sample_ids:, sample_type_id:, study_id: }, errors: e }, status: :bad_request end From e6d7223c40127016caa40db5b23ad52199e2a9e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:09:42 +0000 Subject: [PATCH 075/131] Bump actionpack from 6.1.7.7 to 6.1.7.8 Bumps [actionpack](https://github.com/rails/rails) from 6.1.7.7 to 6.1.7.8. - [Release notes](https://github.com/rails/rails/releases) - [Changelog](https://github.com/rails/rails/blob/v7.1.3.4/actionpack/CHANGELOG.md) - [Commits](https://github.com/rails/rails/compare/v6.1.7.7...v6.1.7.8) --- updated-dependencies: - dependency-name: actionpack dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 126 ++++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0de2621a2b..1836f41939 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,40 +51,40 @@ GEM remote: https://rubygems.org/ specs: RedCloth (4.3.3) - actioncable (6.1.7.7) - actionpack (= 6.1.7.7) - activesupport (= 6.1.7.7) + actioncable (6.1.7.8) + actionpack (= 6.1.7.8) + activesupport (= 6.1.7.8) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.7) - actionpack (= 6.1.7.7) - activejob (= 6.1.7.7) - activerecord (= 6.1.7.7) - activestorage (= 6.1.7.7) - activesupport (= 6.1.7.7) + actionmailbox (6.1.7.8) + actionpack (= 6.1.7.8) + activejob (= 6.1.7.8) + activerecord (= 6.1.7.8) + activestorage (= 6.1.7.8) + activesupport (= 6.1.7.8) mail (>= 2.7.1) - actionmailer (6.1.7.7) - actionpack (= 6.1.7.7) - actionview (= 6.1.7.7) - activejob (= 6.1.7.7) - activesupport (= 6.1.7.7) + actionmailer (6.1.7.8) + actionpack (= 6.1.7.8) + actionview (= 6.1.7.8) + activejob (= 6.1.7.8) + activesupport (= 6.1.7.8) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.7) - actionview (= 6.1.7.7) - activesupport (= 6.1.7.7) + actionpack (6.1.7.8) + actionview (= 6.1.7.8) + activesupport (= 6.1.7.8) 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.7) - actionpack (= 6.1.7.7) - activerecord (= 6.1.7.7) - activestorage (= 6.1.7.7) - activesupport (= 6.1.7.7) + actiontext (6.1.7.8) + actionpack (= 6.1.7.8) + activerecord (= 6.1.7.8) + activestorage (= 6.1.7.8) + activesupport (= 6.1.7.8) nokogiri (>= 1.8.5) - actionview (6.1.7.7) - activesupport (= 6.1.7.7) + actionview (6.1.7.8) + activesupport (= 6.1.7.8) 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.7) - activesupport (= 6.1.7.7) + activejob (6.1.7.8) + activesupport (= 6.1.7.8) globalid (>= 0.3.6) - activemodel (6.1.7.7) - activesupport (= 6.1.7.7) - activerecord (6.1.7.7) - activemodel (= 6.1.7.7) - activesupport (= 6.1.7.7) + activemodel (6.1.7.8) + activesupport (= 6.1.7.8) + activerecord (6.1.7.8) + activemodel (= 6.1.7.8) + activesupport (= 6.1.7.8) 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.7) - actionpack (= 6.1.7.7) - activejob (= 6.1.7.7) - activerecord (= 6.1.7.7) - activesupport (= 6.1.7.7) + activestorage (6.1.7.8) + actionpack (= 6.1.7.8) + activejob (= 6.1.7.8) + activerecord (= 6.1.7.8) + activesupport (= 6.1.7.8) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.7) + activesupport (6.1.7.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -198,7 +198,7 @@ GEM execjs coffee-script-source (1.12.2) commonmarker (0.23.10) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.1) connection_pool (2.3.0) countries (5.2.0) unaccent (~> 0.3) @@ -322,7 +322,7 @@ GEM httpclient (2.8.3) httpi (1.1.1) rack - i18n (1.14.4) + i18n (1.14.5) concurrent-ruby (~> 1.0) i18n-js (3.9.0) i18n (>= 0.6.6) @@ -460,7 +460,7 @@ GEM nokogiri (~> 1) rake mini_mime (1.1.5) - mini_portile2 (2.8.5) + mini_portile2 (2.8.7) minitest (5.20.0) minitest-reporters (1.5.0) ansi @@ -492,7 +492,7 @@ GEM net-protocol netrc (0.11.0) nio4r (2.7.0) - nokogiri (1.16.2) + nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) nori (1.1.5) @@ -566,8 +566,8 @@ GEM puma (5.6.8) nio4r (~> 2.0) pyu-ruby-sasl (0.0.3.3) - racc (1.7.3) - rack (2.2.8.1) + racc (1.8.0) + rack (2.2.9) rack-attack (6.6.0) rack (>= 1.0, < 3) rack-cors (1.1.1) @@ -584,20 +584,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.7) - actioncable (= 6.1.7.7) - actionmailbox (= 6.1.7.7) - actionmailer (= 6.1.7.7) - actionpack (= 6.1.7.7) - actiontext (= 6.1.7.7) - actionview (= 6.1.7.7) - activejob (= 6.1.7.7) - activemodel (= 6.1.7.7) - activerecord (= 6.1.7.7) - activestorage (= 6.1.7.7) - activesupport (= 6.1.7.7) + rails (6.1.7.8) + actioncable (= 6.1.7.8) + actionmailbox (= 6.1.7.8) + actionmailer (= 6.1.7.8) + actionpack (= 6.1.7.8) + actiontext (= 6.1.7.8) + actionview (= 6.1.7.8) + activejob (= 6.1.7.8) + activemodel (= 6.1.7.8) + activerecord (= 6.1.7.8) + activestorage (= 6.1.7.8) + activesupport (= 6.1.7.8) bundler (>= 1.15.0) - railties (= 6.1.7.7) + railties (= 6.1.7.8) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -625,9 +625,9 @@ GEM json require_all (~> 3.0) ruby-progressbar - railties (6.1.7.7) - actionpack (= 6.1.7.7) - activesupport (= 6.1.7.7) + railties (6.1.7.8) + actionpack (= 6.1.7.8) + activesupport (= 6.1.7.8) method_source rake (>= 12.2) thor (~> 1.0) @@ -728,7 +728,8 @@ GEM netrc (~> 0.8) reverse_markdown (2.1.1) nokogiri - rexml (3.2.5) + rexml (3.2.8) + strscan (>= 3.0.9) rfc-822 (0.4.1) rmagick (5.3.0) pkg-config (~> 1.4) @@ -861,6 +862,7 @@ GEM sqlite3 (1.4.2) stackprof (0.2.25) stringio (3.0.1.1) + strscan (3.1.0) sunspot (2.6.0) pr_geohash (~> 1.0) rsolr (>= 1.1.1, < 3) @@ -950,7 +952,7 @@ GEM rails (>= 3.0) rake (>= 0.8.7) yard (0.9.36) - zeitwerk (2.6.13) + zeitwerk (2.6.15) zip-container (4.0.2) rubyzip (~> 2.0.0) @@ -1028,7 +1030,7 @@ DEPENDENCIES my_responds_to_parent! mysql2 net-ftp - nokogiri (~> 1.16.2) + nokogiri (~> 1.16.5) omniauth (~> 2.1.0) omniauth-github omniauth-rails_csrf_protection From 3ec31c0d94c05e3266db58150dcfdb21cf1104f3 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Wed, 5 Jun 2024 09:48:28 +0100 Subject: [PATCH 076/131] fix nokogiri version mismatch --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 1ba5c08b17..1077041082 100644 --- a/Gemfile +++ b/Gemfile @@ -52,7 +52,7 @@ gem 'will_paginate', '~> 3.1' gem 'yaml_db' gem 'rails_autolink' gem 'rfc-822' -gem 'nokogiri', '~> 1.16.2' +gem 'nokogiri', '~> 1.16' #necessary for newer hashie dependency, original api_smith is no longer active gem 'api_smith', git: 'https://github.com/youroute/api_smith.git', ref: '1fb428cebc17b9afab25ac9f809bde87b0ec315b' gem 'rdf-virtuoso', '>= 0.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 1836f41939..1a31a36a38 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1030,7 +1030,7 @@ DEPENDENCIES my_responds_to_parent! mysql2 net-ftp - nokogiri (~> 1.16.5) + nokogiri (~> 1.16) omniauth (~> 2.1.0) omniauth-github omniauth-rails_csrf_protection From 842fc554e0be7f0c75d2ab96a69255e2b9d74b08 Mon Sep 17 00:00:00 2001 From: Finn Bacall Date: Thu, 6 Jun 2024 10:28:38 +0100 Subject: [PATCH 077/131] Typo fix --- app/views/assets/_asset_buttons.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/assets/_asset_buttons.html.erb b/app/views/assets/_asset_buttons.html.erb index 9d26ddbc7f..cba08d71d2 100644 --- a/app/views/assets/_asset_buttons.html.erb +++ b/app/views/assets/_asset_buttons.html.erb @@ -33,7 +33,7 @@ <% if can_download_asset?(asset, params[:code]) -%> <% if asset.is_a?(Workflow) %> <% if display_asset.is_git_versioned? || asset.content_blob&.file_exists? %> - <%= button_link_to('Download RO Crate', 'ro_crate_file', ro_crate_workflow_path(asset, version: version, code: params[:code]), + <%= button_link_to('Download RO-Crate', 'ro_crate_file', ro_crate_workflow_path(asset, version: version, code: params[:code]), 'data-tooltip' => tooltip("The Workflow RO-Crate is a package containing the workflow definition, its metadata and supporting resources like test data")) %> <% if asset.can_run? %> <%= button_link_to("Run on Galaxy", 'run_galaxy', display_asset.run_url) %> From af0ecb4fd76c25a6c13226763b1292c657ec0a99 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Wed, 5 Jun 2024 11:22:02 +0100 Subject: [PATCH 078/131] fix pid uri validation, and handle gracefully in short_pid for existing entries #1865 --- app/models/sample_attribute.rb | 11 +++++++++-- test/unit/sample_attribute_test.rb | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/models/sample_attribute.rb b/app/models/sample_attribute.rb index 740f673e21..79de668b7b 100644 --- a/app/models/sample_attribute.rb +++ b/app/models/sample_attribute.rb @@ -11,7 +11,7 @@ class SampleAttribute < ApplicationRecord auto_strip_attributes :pid validates :sample_type, presence: true - validates :pid, format: { with: URI::regexp, allow_blank: true, allow_nil: true, message: 'not a valid URI' } + validates :pid, format: { with: URI::ABS_URI, 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 @@ -64,7 +64,14 @@ def template_column_definition def short_pid return '' unless pid.present? - URI.parse(pid).fragment || pid.gsub(/.*\//,'') || pid + begin + URI.parse(pid).fragment || pid.gsub(/.*\//,'') || pid + rescue URI::InvalidURIError + # likely a space that managed to pass through earlier uri validation + fixed_pid = pid.gsub(' ','_') + URI.parse(fixed_pid).fragment || fixed_pid.gsub(/.*\//,'') || fixed_pid + end + end def linked_extended_metadata_type diff --git a/test/unit/sample_attribute_test.rb b/test/unit/sample_attribute_test.rb index 4ab9f2f82e..006f8cdbc5 100644 --- a/test/unit/sample_attribute_test.rb +++ b/test/unit/sample_attribute_test.rb @@ -67,6 +67,9 @@ class SampleAttributeTest < ActiveSupport::TestCase sample_type: FactoryBot.create(:simple_sample_type) refute attribute.valid? + attribute.pid = 'Source:bacterial culture' + refute attribute.valid? + attribute.pid = 'http://somewhere.org#fish' assert attribute.valid? attribute.pid = 'dc:fish' @@ -333,6 +336,9 @@ class SampleAttributeTest < ActiveSupport::TestCase attribute = FactoryBot.create(:string_sample_attribute_with_description_and_pid, is_title: true, pid: 'http://pid.org/attr/title', sample_type: FactoryBot.create(:simple_sample_type)) assert_equal 'title',attribute.short_pid + attribute.pid = 'Source:bacterial culture' + assert_equal 'Source:bacterial_culture',attribute.short_pid + attribute = FactoryBot.create(:sample_sample_attribute, sample_type: FactoryBot.create(:simple_sample_type)) assert_equal '', attribute.short_pid end From 91d0dcf073c894f0bf853ac38f64f11a70ec9d2b Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Wed, 5 Jun 2024 16:06:49 +0100 Subject: [PATCH 079/131] fix factory pid to conform to fixed validation #1865 --- test/factories/sample_attributes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/factories/sample_attributes.rb b/test/factories/sample_attributes.rb index de36f107c7..9aad6646f9 100644 --- a/test/factories/sample_attributes.rb +++ b/test/factories/sample_attributes.rb @@ -50,7 +50,7 @@ factory(:string_sample_attribute_with_description_and_pid, parent: :sample_attribute) do association :sample_attribute_type, factory: :string_sample_attribute_type description { "sample_attribute_description" } - pid { "sample_attribute:pid" } + pid { "sample-attribute:pid" } required { true } end end From 24751349dbc29d10ba1bb20530398447a5b16ad3 Mon Sep 17 00:00:00 2001 From: Stuart Owen Date: Wed, 5 Jun 2024 17:14:05 +0100 Subject: [PATCH 080/131] replace space with - instead of _ #1865 --- app/models/sample_attribute.rb | 2 +- test/unit/sample_attribute_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/sample_attribute.rb b/app/models/sample_attribute.rb index 79de668b7b..9740e0c3fe 100644 --- a/app/models/sample_attribute.rb +++ b/app/models/sample_attribute.rb @@ -68,7 +68,7 @@ def short_pid URI.parse(pid).fragment || pid.gsub(/.*\//,'') || pid rescue URI::InvalidURIError # likely a space that managed to pass through earlier uri validation - fixed_pid = pid.gsub(' ','_') + fixed_pid = pid.gsub(' ','-') URI.parse(fixed_pid).fragment || fixed_pid.gsub(/.*\//,'') || fixed_pid end diff --git a/test/unit/sample_attribute_test.rb b/test/unit/sample_attribute_test.rb index 006f8cdbc5..abe11a0df8 100644 --- a/test/unit/sample_attribute_test.rb +++ b/test/unit/sample_attribute_test.rb @@ -337,7 +337,7 @@ class SampleAttributeTest < ActiveSupport::TestCase assert_equal 'title',attribute.short_pid attribute.pid = 'Source:bacterial culture' - assert_equal 'Source:bacterial_culture',attribute.short_pid + assert_equal 'Source:bacterial-culture',attribute.short_pid attribute = FactoryBot.create(:sample_sample_attribute, sample_type: FactoryBot.create(:simple_sample_type)) assert_equal '', attribute.short_pid From 2f122b065329095fba19a74d8822a3297653ba62 Mon Sep 17 00:00:00 2001 From: Finn Bacall Date: Thu, 6 Jun 2024 10:32:28 +0100 Subject: [PATCH 081/131] Account for git in `contains_downloadable_items?`. Fixes #1915 --- lib/seek/acts_as_asset.rb | 4 ++++ lib/seek/acts_as_asset/content_blobs.rb | 5 ----- lib/seek/versioned_resource.rb | 6 ------ test/unit/asset_test.rb | 21 ++++++++++++++------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/seek/acts_as_asset.rb b/lib/seek/acts_as_asset.rb index 1c5de8cb75..e8abef8742 100644 --- a/lib/seek/acts_as_asset.rb +++ b/lib/seek/acts_as_asset.rb @@ -23,6 +23,10 @@ def is_downloadable_asset? is_asset? && is_downloadable? end + def contains_downloadable_items? + respond_to?(:all_content_blobs) && all_content_blobs.compact.any?(&:is_downloadable?) || is_git_versioned? + end + def have_misc_links? self.class.have_misc_links? end diff --git a/lib/seek/acts_as_asset/content_blobs.rb b/lib/seek/acts_as_asset/content_blobs.rb index 6ff53183ba..2ddc88cc24 100644 --- a/lib/seek/acts_as_asset/content_blobs.rb +++ b/lib/seek/acts_as_asset/content_blobs.rb @@ -10,11 +10,6 @@ module ClassMethods end module InstanceMethods - - def contains_downloadable_items? - all_content_blobs.compact.any?(&:is_downloadable?) - end - def all_content_blobs if self.respond_to?(:content_blobs) content_blobs diff --git a/lib/seek/versioned_resource.rb b/lib/seek/versioned_resource.rb index 4bf7ab5794..31979c487e 100644 --- a/lib/seek/versioned_resource.rb +++ b/lib/seek/versioned_resource.rb @@ -59,12 +59,6 @@ def attributions_objects end end - # assumes all versioned resources are also taggable - - def contains_downloadable_items? - all_content_blobs.compact.any?(&:is_downloadable?) - end - def all_content_blobs blobs = [] blobs << content_blob if respond_to?(:content_blob) diff --git a/test/unit/asset_test.rb b/test/unit/asset_test.rb index fc966488e5..4d4a15ad3c 100644 --- a/test/unit/asset_test.rb +++ b/test/unit/asset_test.rb @@ -78,12 +78,12 @@ class AssetTest < ActiveSupport::TestCase assert df.latest_version.contains_downloadable_items? df = FactoryBot.create :data_file, content_blob: FactoryBot.create(:content_blob, url: 'http://webpage.com', external_link: true) - assert !df.contains_downloadable_items? - assert !df.latest_version.contains_downloadable_items? + refute df.contains_downloadable_items? + refute df.latest_version.contains_downloadable_items? model = FactoryBot.create :model_with_urls - assert !model.contains_downloadable_items? - assert !model.latest_version.contains_downloadable_items? + refute model.contains_downloadable_items? + refute model.latest_version.contains_downloadable_items? model = FactoryBot.create :teusink_model assert model.contains_downloadable_items? @@ -94,10 +94,10 @@ class AssetTest < ActiveSupport::TestCase assert model.latest_version.contains_downloadable_items? df = DataFile.new - assert !df.contains_downloadable_items? + refute df.contains_downloadable_items? model = Model.new - assert !model.contains_downloadable_items? + refute model.contains_downloadable_items? # test for versions model = FactoryBot.create :teusink_model @@ -112,7 +112,14 @@ class AssetTest < ActiveSupport::TestCase assert_equal(2, model.versions.count) assert model.find_version(1).contains_downloadable_items? - assert !model.find_version(2).contains_downloadable_items? + refute model.find_version(2).contains_downloadable_items? + + workflow = FactoryBot.create(:nfcore_git_workflow) + assert workflow.contains_downloadable_items? + assert workflow.git_version.contains_downloadable_items? + + workflow = Workflow.new + refute workflow.contains_downloadable_items? end test 'supports_spreadsheet_explore?' do From 69edef8bc337a17d6a594d55a877bb2cbcaf255c Mon Sep 17 00:00:00 2001 From: Finn Bacall Date: Wed, 5 Jun 2024 12:24:20 +0100 Subject: [PATCH 082/131] Bump nf-core rnaseq fixture --- .../git/nf-core-rnaseq/_git/FETCH_HEAD | 7 +------ .../git/nf-core-rnaseq/_git/ORIG_HEAD | 1 + test/fixtures/git/nf-core-rnaseq/_git/index | Bin 0 -> 32411 bytes .../git/nf-core-rnaseq/_git/logs/HEAD | 2 ++ .../_git/logs/refs/heads/master | 1 + .../_git/logs/refs/remotes/origin/TEMPLATE | 1 + .../_git/logs/refs/remotes/origin/arm | 1 + .../refs/remotes/origin/change_testing_logic | 1 + .../_git/logs/refs/remotes/origin/dev | 1 + .../remotes/origin/document_fastp_sampling | 1 + .../remotes/origin/feat/implement-ci-nf-tests | 1 + .../refs/remotes/origin/fix_bbsplit_config | 1 + .../logs/refs/remotes/origin/fix_gtf_unzip | 1 + .../logs/refs/remotes/origin/fix_salmon_args | 1 + .../remotes/origin/improve_rseqc_strandedness | 1 + .../_git/logs/refs/remotes/origin/master | 1 + .../logs/refs/remotes/origin/nf-test-cicd | 1 + .../refs/remotes/origin/optimized-resources | 1 + .../origin/update_default_pipeline_test | 1 + ...198e3e69261a07440f01e7393b0d0cfb8f9dc9.idx | Bin 0 -> 405224 bytes ...98e3e69261a07440f01e7393b0d0cfb8f9dc9.pack | Bin 0 -> 10599746 bytes .../git/nf-core-rnaseq/_git/refs/heads/master | 2 +- .../_git/refs/remotes/origin/TEMPLATE | 2 +- .../_git/refs/remotes/origin/arm | 1 + .../refs/remotes/origin/change_testing_logic | 1 + .../_git/refs/remotes/origin/dev | 2 +- .../remotes/origin/document_fastp_sampling | 1 + .../remotes/origin/feat/implement-ci-nf-tests | 1 + .../refs/remotes/origin/fix_bbsplit_config | 1 + .../_git/refs/remotes/origin/fix_gtf_unzip | 1 + .../_git/refs/remotes/origin/fix_salmon_args | 1 + .../remotes/origin/improve_rseqc_strandedness | 1 + .../_git/refs/remotes/origin/master | 2 +- .../_git/refs/remotes/origin/nf-test-cicd | 1 + .../refs/remotes/origin/optimized-resources | 1 + .../origin/update_default_pipeline_test | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.1 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.10 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.10.1 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.11.0 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.11.1 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.11.2 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.12.0 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.13.0 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.13.1 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.13.2 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.14.0 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.2 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.3 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.4 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.5 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.6 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.7 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.8 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.8.1 | 1 + .../git/nf-core-rnaseq/_git/refs/tags/3.9 | 1 + 56 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/ORIG_HEAD create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/index create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/HEAD create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/heads/master create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/arm create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/change_testing_logic create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/document_fastp_sampling create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/feat/implement-ci-nf-tests create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_bbsplit_config create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_gtf_unzip create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_salmon_args create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/improve_rseqc_strandedness create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/nf-test-cicd create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/optimized-resources create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/update_default_pipeline_test create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/objects/pack/pack-6e198e3e69261a07440f01e7393b0d0cfb8f9dc9.idx create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/objects/pack/pack-6e198e3e69261a07440f01e7393b0d0cfb8f9dc9.pack create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/arm create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/change_testing_logic create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/document_fastp_sampling create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/feat/implement-ci-nf-tests create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/fix_bbsplit_config create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/fix_gtf_unzip create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/fix_salmon_args create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/improve_rseqc_strandedness create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/nf-test-cicd create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/optimized-resources create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/remotes/origin/update_default_pipeline_test create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.1 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.10 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.10.1 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.11.0 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.11.1 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.11.2 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.12.0 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.13.0 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.13.1 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.13.2 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.14.0 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.2 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.3 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.4 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.5 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.6 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.7 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.8 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.8.1 create mode 100644 test/fixtures/git/nf-core-rnaseq/_git/refs/tags/3.9 diff --git a/test/fixtures/git/nf-core-rnaseq/_git/FETCH_HEAD b/test/fixtures/git/nf-core-rnaseq/_git/FETCH_HEAD index 007f9d59e1..4329f7922f 100644 --- a/test/fixtures/git/nf-core-rnaseq/_git/FETCH_HEAD +++ b/test/fixtures/git/nf-core-rnaseq/_git/FETCH_HEAD @@ -1,6 +1 @@ -88562f8745302ec159679369326c6db5e4bd6e3b not-for-merge branch 'TEMPLATE' of https://github.com/nf-core/rnaseq.git -825345ea0c17b3962066a181fc5b5e405bfee5aa not-for-merge branch 'dev' of https://github.com/nf-core/rnaseq.git -3643a94411b65f42bce5357c5015603099556ad9 not-for-merge branch 'master' of https://github.com/nf-core/rnaseq.git -ccfb140d0f21d144e65498edc098f89c0d66d961 not-for-merge branch 'nf-core-template-merge-1.13.2' of https://github.com/nf-core/rnaseq.git -88562f8745302ec159679369326c6db5e4bd6e3b not-for-merge branch 'nf-core-template-merge-1.13.3' of https://github.com/nf-core/rnaseq.git -5681aa9012608ed67c67073ccee07b27586ee36a not-for-merge branch 'pytest-workflow' of https://github.com/nf-core/rnaseq.git +b89fac32650aacc86fcda9ee77e00612a1d77066 branch 'master' of https://github.com/nf-core/rnaseq diff --git a/test/fixtures/git/nf-core-rnaseq/_git/ORIG_HEAD b/test/fixtures/git/nf-core-rnaseq/_git/ORIG_HEAD new file mode 100644 index 0000000000..cc7c16f3fc --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/ORIG_HEAD @@ -0,0 +1 @@ +3643a94411b65f42bce5357c5015603099556ad9 diff --git a/test/fixtures/git/nf-core-rnaseq/_git/index b/test/fixtures/git/nf-core-rnaseq/_git/index new file mode 100644 index 0000000000000000000000000000000000000000..4e51a11ae832865022d18b110599f4273c86eee8 GIT binary patch literal 32411 zcmbuI2|QHq+y9S!U&>ZgWJ_fyQpvuQh!6^6jKO3XX2w<&l_&|{N<~>xTBTJ~WG`)K zFIn2P*%GD2bLPysnKNV5@Bi=hdcN1MXU_S&@9Vnv<=j~>TV(}75H|#IO$qc{?yO-( zY6KR71i2v9S}q7mIs!pF-HiX^ZFXwD{(1qWci$}cO*zm$)8~3WOS2$F?EqplfgDc5 zlfsD<9p=AiucMN~VF=I#7SW#-CXdzUYA(AlT_C#q$@lF}kL|7ZaBXoE%W1Bq(~k!W}tjY9H|q7kVa z^;By{S@aG}S3Grpo_A7z8}v5h-TSG%TsAUOAz8X!aFoA}b^w_WLWNUG)Q$@a<*Zu^ z|HK^E8ReVrNe@EzUN-n!3xyBI=BQ`euS$UAfUea_C%0A0EZyCfIoWB41#t3TwU!5~ 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/HEAD b/test/fixtures/git/nf-core-rnaseq/_git/logs/HEAD new file mode 100644 index 0000000000..bd08a33ee1 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/HEAD @@ -0,0 +1,2 @@ +3643a94411b65f42bce5357c5015603099556ad9 3643a94411b65f42bce5357c5015603099556ad9 Finn Bacall 1717586509 +0100 checkout: moving from master to master +3643a94411b65f42bce5357c5015603099556ad9 b89fac32650aacc86fcda9ee77e00612a1d77066 Finn Bacall 1717586512 +0100 pull origin master: Fast-forward diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/heads/master b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/heads/master new file mode 100644 index 0000000000..f9a647c9c6 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/heads/master @@ -0,0 +1 @@ +3643a94411b65f42bce5357c5015603099556ad9 b89fac32650aacc86fcda9ee77e00612a1d77066 Finn Bacall 1717586512 +0100 pull origin master: Fast-forward diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/TEMPLATE b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/TEMPLATE index a59049b106..fd18dddb9a 100644 --- a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/TEMPLATE +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/TEMPLATE @@ -1,2 +1,3 @@ 0000000000000000000000000000000000000000 e9f315d51e88f55398b21b4ff0f4d5e04a7c1dc4 Finn Bacall 1616508146 +0000 fetch origin e9f315d51e88f55398b21b4ff0f4d5e04a7c1dc4 88562f8745302ec159679369326c6db5e4bd6e3b Finn Bacall 1617012765 +0100 fetch origin +88562f8745302ec159679369326c6db5e4bd6e3b eecf64e1f291a1aedbb23d96e95a3cdadd4640dc Finn Bacall 1717582518 +0100 fetch: fast-forward diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/arm b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/arm new file mode 100644 index 0000000000..4107e477eb --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/arm @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 d2f00635cee40ea206e53d2e49a7119bc9482307 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/change_testing_logic b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/change_testing_logic new file mode 100644 index 0000000000..962c24500e --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/change_testing_logic @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 21fe86c80fa9e57144522944a01405f95c7e58b0 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/dev b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/dev index 387e8ebbd9..27a7e6eb94 100644 --- a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/dev +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/dev @@ -1 +1,2 @@ 0000000000000000000000000000000000000000 825345ea0c17b3962066a181fc5b5e405bfee5aa Finn Bacall 1616508146 +0000 fetch origin +825345ea0c17b3962066a181fc5b5e405bfee5aa 21095f381e70b4b73dc392c36318218044a8bc95 Finn Bacall 1717582518 +0100 fetch: fast-forward diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/document_fastp_sampling b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/document_fastp_sampling new file mode 100644 index 0000000000..993008b311 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/document_fastp_sampling @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 cdc8f53f3515b1a7e982006a1b2a282a9fdffafb Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/feat/implement-ci-nf-tests b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/feat/implement-ci-nf-tests new file mode 100644 index 0000000000..7582277c43 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/feat/implement-ci-nf-tests @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 31a9d622bbc4eeb285a2424c1af7938712b4f925 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_bbsplit_config b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_bbsplit_config new file mode 100644 index 0000000000..4aab44fada --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_bbsplit_config @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 b5b728b9f36c650aea2245cfd893f7e2cfd8942a Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_gtf_unzip b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_gtf_unzip new file mode 100644 index 0000000000..d165b7d345 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_gtf_unzip @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 f018a0cf87c634a6a2eeb34d64a554a6fa852be0 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_salmon_args b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_salmon_args new file mode 100644 index 0000000000..dc45c695d2 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/fix_salmon_args @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 5bef11b969acb2f2091eb6c84745580b5f70b527 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/improve_rseqc_strandedness b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/improve_rseqc_strandedness new file mode 100644 index 0000000000..0b533dd975 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/improve_rseqc_strandedness @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 5ce89ae977e4017c63f6887034c469dae54ca763 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/master b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/master index f31dc14ed9..ec33116496 100644 --- a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/master +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/master @@ -1 +1,2 @@ 0000000000000000000000000000000000000000 3643a94411b65f42bce5357c5015603099556ad9 Finn Bacall 1616508146 +0000 fetch origin +3643a94411b65f42bce5357c5015603099556ad9 b89fac32650aacc86fcda9ee77e00612a1d77066 Finn Bacall 1717582518 +0100 fetch: fast-forward diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/nf-test-cicd b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/nf-test-cicd new file mode 100644 index 0000000000..0227a86d9b --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/nf-test-cicd @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 6694d694f9b43fb59b73d79b04e1cdbaf3d67ce2 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/optimized-resources b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/optimized-resources new file mode 100644 index 0000000000..7b62989ed3 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/optimized-resources @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 4ab3ecb3cfd734fcd149ee0a00a98b8da0c37ff2 Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/update_default_pipeline_test b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/update_default_pipeline_test new file mode 100644 index 0000000000..15365ecba2 --- /dev/null +++ b/test/fixtures/git/nf-core-rnaseq/_git/logs/refs/remotes/origin/update_default_pipeline_test @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 b264499527af3b4c349f258d38c31759d7497c8e Finn Bacall 1717582518 +0100 fetch: storing head diff --git a/test/fixtures/git/nf-core-rnaseq/_git/objects/pack/pack-6e198e3e69261a07440f01e7393b0d0cfb8f9dc9.idx b/test/fixtures/git/nf-core-rnaseq/_git/objects/pack/pack-6e198e3e69261a07440f01e7393b0d0cfb8f9dc9.idx new file mode 100644 index 0000000000000000000000000000000000000000..5197d77de9e2b4f7eb638f13adbc8d6a1994248c GIT binary patch literal 405224 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