From 3184c3134595272d1acee0f0d962aa0e7b82a04f Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 28 Nov 2024 08:24:43 +0000 Subject: [PATCH 01/40] fix: generate plate volumes --- .../uat_actions/generate_plate_volumes.rb | 103 ++++++++++++++++++ .../generate_plate_volumes_spec.rb | 37 +++++++ 2 files changed, 140 insertions(+) create mode 100644 app/uat_actions/uat_actions/generate_plate_volumes.rb create mode 100644 spec/uat_actions/generate_plate_volumes_spec.rb diff --git a/app/uat_actions/uat_actions/generate_plate_volumes.rb b/app/uat_actions/uat_actions/generate_plate_volumes.rb new file mode 100644 index 0000000000..4a577abe11 --- /dev/null +++ b/app/uat_actions/uat_actions/generate_plate_volumes.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +# Will generate volumes for a given plate +class UatActions::GeneratePlateVolumes < UatActions + self.title = 'Generate volumes for a plate' + self.description = 'Generate a set of randomised volumes for a plate.' + self.category = :quality_control + + form_field :plate_barcode, + :text_field, + label: 'Plate barcode', + help: + 'Enter the barcode of the plate for which you want to add volumes. ' \ + 'NB. only well containing aliquots will have volumes set.' + form_field :minimum_volume, + :number_field, + label: 'Minimum volume (µl)', + help: 'The minimum volume the wells should have.', + options: { + minimum: 0 + } + form_field :maximum_volume, + :number_field, + label: 'Maximum volume (µl)', + help: 'The maximum volume the wells should have.', + options: { + minimum: 0 + } + + + # + # Returns a default copy of the UatAction which will be used to fill in the form, with values + # for the units, and min and max volumes. + # + # @return [UatActions::GeneratePlateVolumes] A default object for rendering a form + def self.default + new( minimum_volume: 0, maximum_volume: 100) + end + + + validates :plate_barcode, presence: { message: 'could not be found' } + validates :minimum_volume, numericality: { only_integer: false } + validates :maximum_volume, numericality: { greater_than: 0, only_integer: false } + validate :maximum_greater_than_minimum + + def perform + qc_assay_results = construct_qc_assay + report['number_well_volumes_written'] = qc_assay_results[:num_wells_written] + qc_assay_results[:qc_assay_success] + end + + private + + def maximum_greater_than_minimum + return true if max_vol > min_vol + + errors.add(:maximum_volume, 'needs to be greater than minimum volume') + false + end + + def labware + @labware ||= Plate.find_by_barcode(plate_barcode.strip) + end + + def key + @key ||= 'volume' + end + + def min_vol + @min_vol ||= minimum_volume.to_f + end + + def max_vol + @max_vol ||= maximum_volume.to_f + end + + def create_random_volume + value = (rand * (max_vol - min_vol)) + min_vol + format('%.3f', value) + end + + def construct_qc_assay + qc_assay = QcAssay.new + num_wells_written = 0 + + labware.wells.each do |well| + next if well.aliquots.empty? + + QcResult.create!( + asset: well, + key: key, + value: create_random_volume, + units: 'µl', + assay_type: 'UAT_Testing', + assay_version: 'Binning', + qc_assay: qc_assay + ) + num_wells_written += 1 + end + qc_assay_success = qc_assay.save + { qc_assay_success:, num_wells_written: } + end +end diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb new file mode 100644 index 0000000000..fcb826677d --- /dev/null +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe UatActions::GeneratePlateVolumes do + context 'with valid options' do + let(:plate) { create(:plate_with_untagged_wells, sample_count: 3) } + let(:uat_action) { described_class.new(parameters) } + let(:report) do + # A report is a hash of key value pairs which get returned to the user. + # It should include information such as barcodes and identifiers + { 'number_well_volumes_written' => 3 } + end + + context 'when ul volumes' do + let(:parameters) do + { + plate_barcode: plate.barcodes.first.barcode, + minimum_volume: 0, + maximum_volume: 30 + } + end + + it 'can be performed' do + expect(uat_action.perform).to be true + expect(uat_action.report).to eq report + expect(plate.wells.map(&:qc_results).size).to eq 3 + expect(plate.wells.first.qc_results.first.assay_type).to eq 'UAT_Testing' + end + end + + end + + it 'returns a default' do + expect(described_class.default).to be_a described_class + end +end From 12deaa2c01c3aefe227aed524e04c461b5a8923b Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 28 Nov 2024 08:33:33 +0000 Subject: [PATCH 02/40] style: lindt --- app/uat_actions/uat_actions/generate_plate_volumes.rb | 4 +--- spec/uat_actions/generate_plate_volumes_spec.rb | 9 +-------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/uat_actions/uat_actions/generate_plate_volumes.rb b/app/uat_actions/uat_actions/generate_plate_volumes.rb index 4a577abe11..c316881cf0 100644 --- a/app/uat_actions/uat_actions/generate_plate_volumes.rb +++ b/app/uat_actions/uat_actions/generate_plate_volumes.rb @@ -27,17 +27,15 @@ class UatActions::GeneratePlateVolumes < UatActions minimum: 0 } - # # Returns a default copy of the UatAction which will be used to fill in the form, with values # for the units, and min and max volumes. # # @return [UatActions::GeneratePlateVolumes] A default object for rendering a form def self.default - new( minimum_volume: 0, maximum_volume: 100) + new(minimum_volume: 0, maximum_volume: 100) end - validates :plate_barcode, presence: { message: 'could not be found' } validates :minimum_volume, numericality: { only_integer: false } validates :maximum_volume, numericality: { greater_than: 0, only_integer: false } diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index fcb826677d..115596e63f 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -13,13 +13,7 @@ end context 'when ul volumes' do - let(:parameters) do - { - plate_barcode: plate.barcodes.first.barcode, - minimum_volume: 0, - maximum_volume: 30 - } - end + let(:parameters) { { plate_barcode: plate.barcodes.first.barcode, minimum_volume: 0, maximum_volume: 30 } } it 'can be performed' do expect(uat_action.perform).to be true @@ -28,7 +22,6 @@ expect(plate.wells.first.qc_results.first.assay_type).to eq 'UAT_Testing' end end - end it 'returns a default' do From df603b40d9c980545801a9d75dae7b892487a9db Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 28 Nov 2024 08:51:56 +0000 Subject: [PATCH 03/40] tests: split out individual expectations --- spec/uat_actions/generate_plate_volumes_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index 115596e63f..1ef656c3e9 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -17,8 +17,17 @@ it 'can be performed' do expect(uat_action.perform).to be true + end + + it 'generates the correct report' do expect(uat_action.report).to eq report + end + + it 'creates the correct number of QC results' do expect(plate.wells.map(&:qc_results).size).to eq 3 + end + + it 'sets the correct assay type for the first QC result' do expect(plate.wells.first.qc_results.first.assay_type).to eq 'UAT_Testing' end end From b3084f7fd2d62e14760f78ddde638907a2e8508f Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 28 Nov 2024 09:12:28 +0000 Subject: [PATCH 04/40] tests: refactor and fix expectations --- .../generate_plate_volumes_spec.rb | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index 1ef656c3e9..043c108636 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -6,34 +6,51 @@ context 'with valid options' do let(:plate) { create(:plate_with_untagged_wells, sample_count: 3) } let(:uat_action) { described_class.new(parameters) } + let!(:performed_action) { uat_action.perform } let(:report) do # A report is a hash of key value pairs which get returned to the user. # It should include information such as barcodes and identifiers { 'number_well_volumes_written' => 3 } end - context 'when ul volumes' do - let(:parameters) { { plate_barcode: plate.barcodes.first.barcode, minimum_volume: 0, maximum_volume: 30 } } + let(:parameters) { { plate_barcode: plate.barcodes.first.barcode, minimum_volume: 0, maximum_volume: 30 } } - it 'can be performed' do - expect(uat_action.perform).to be true - end + it 'can be performed' do + expect(performed_action).to be true + end + + it 'generates the correct report' do + expect(uat_action.report).to eq report + end - it 'generates the correct report' do - expect(uat_action.report).to eq report - end + it 'creates the correct number of QC results' do + expect(plate.wells.map(&:qc_results).size).to eq 3 + end - it 'creates the correct number of QC results' do - expect(plate.wells.map(&:qc_results).size).to eq 3 - end + it 'sets the correct assay type for the first QC result' do + expect(plate.wells.first.qc_results.first.assay_type).to eq 'UAT_Testing' + end - it 'sets the correct assay type for the first QC result' do - expect(plate.wells.first.qc_results.first.assay_type).to eq 'UAT_Testing' - end + it 'sets the volumes to be within the specified range' do + expect(plate.wells.map { |well| well.qc_results.first.value.to_f }).to all(be_between(0, 30)) end end - it 'returns a default' do - expect(described_class.default).to be_a described_class + context 'with default options' do + it 'returns an instance of described_class' do + expect(described_class.default).to be_a described_class + end + + it 'has a nil plate_barcode' do + expect(described_class.default.plate_barcode).to be_nil + end + + it 'has a minimum_volume of 0' do + expect(described_class.default.minimum_volume).to eq 0 + end + + it 'has a maximum_volume of 100' do + expect(described_class.default.maximum_volume).to eq 100 + end end end From ca14fa166dd39107558769aa005c05c1e53f708c Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Mon, 2 Dec 2024 17:00:23 +0000 Subject: [PATCH 05/40] tests: add additional tests for validation --- .../generate_plate_volumes_spec.rb | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index 043c108636..e90521938e 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -3,17 +3,19 @@ require 'rails_helper' describe UatActions::GeneratePlateVolumes do + let(:plate) { create(:plate_with_untagged_wells, sample_count: 3) } + let(:plate_barcode) { plate.barcodes.first.barcode } + let(:uat_action) { described_class.new(parameters) } + let!(:performed_action) { uat_action.perform } + context 'with valid options' do - let(:plate) { create(:plate_with_untagged_wells, sample_count: 3) } - let(:uat_action) { described_class.new(parameters) } - let!(:performed_action) { uat_action.perform } let(:report) do # A report is a hash of key value pairs which get returned to the user. # It should include information such as barcodes and identifiers { 'number_well_volumes_written' => 3 } end - let(:parameters) { { plate_barcode: plate.barcodes.first.barcode, minimum_volume: 0, maximum_volume: 30 } } + let(:parameters) { { plate_barcode: plate_barcode, minimum_volume: 0, maximum_volume: 30 } } it 'can be performed' do expect(performed_action).to be true @@ -37,6 +39,8 @@ end context 'with default options' do + let(:parameters) { { plate_barcode: } } + it 'returns an instance of described_class' do expect(described_class.default).to be_a described_class end @@ -53,4 +57,29 @@ expect(described_class.default.maximum_volume).to eq 100 end end + + context 'with invalid options' do + let(:parameters) { {plate_barcode: plate_barcode, minimum_volume: 110, maximum_volume: 10 } } + let!(:saved_action) { uat_action.save } + + it 'has a minimum_volume of 110' do + expect(uat_action.minimum_volume).to eq 110 + end + + it 'has a maximum_volume of 10' do + expect(uat_action.maximum_volume).to eq 10 + end + + it 'is invalid' do + expect(uat_action.valid?).to be false + end + + it 'can not be saved' do + expect(saved_action).to be false + end + + it 'adds an error' do + expect(uat_action.errors.full_messages).to include('Maximum volume needs to be greater than minimum volume') + end + end end From 48a0ba159b6a6dd206b67db42521b59630f6238f Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Tue, 3 Dec 2024 09:25:47 +0000 Subject: [PATCH 06/40] style: lint --- spec/uat_actions/generate_plate_volumes_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index e90521938e..e655213412 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -59,7 +59,7 @@ end context 'with invalid options' do - let(:parameters) { {plate_barcode: plate_barcode, minimum_volume: 110, maximum_volume: 10 } } + let(:parameters) { { plate_barcode: plate_barcode, minimum_volume: 110, maximum_volume: 10 } } let!(:saved_action) { uat_action.save } it 'has a minimum_volume of 110' do From 9c82b866e11f196bde1f9fe64764cf0b15a22d78 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Tue, 3 Dec 2024 15:53:28 +0000 Subject: [PATCH 07/40] fix: allow maximum to equal minimum --- app/uat_actions/uat_actions/generate_plate_volumes.rb | 8 ++++---- spec/uat_actions/generate_plate_volumes_spec.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/uat_actions/uat_actions/generate_plate_volumes.rb b/app/uat_actions/uat_actions/generate_plate_volumes.rb index c316881cf0..0afd4cacb9 100644 --- a/app/uat_actions/uat_actions/generate_plate_volumes.rb +++ b/app/uat_actions/uat_actions/generate_plate_volumes.rb @@ -39,7 +39,7 @@ def self.default validates :plate_barcode, presence: { message: 'could not be found' } validates :minimum_volume, numericality: { only_integer: false } validates :maximum_volume, numericality: { greater_than: 0, only_integer: false } - validate :maximum_greater_than_minimum + validate :maximum_greater_than_or_equal_to_minimum def perform qc_assay_results = construct_qc_assay @@ -49,10 +49,10 @@ def perform private - def maximum_greater_than_minimum - return true if max_vol > min_vol + def maximum_greater_than_or_equal_to_minimum + return true if max_vol >= min_vol - errors.add(:maximum_volume, 'needs to be greater than minimum volume') + errors.add(:maximum_volume, 'needs to be greater than or equal to minimum volume') false end diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index e655213412..794fefafcb 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -79,7 +79,7 @@ end it 'adds an error' do - expect(uat_action.errors.full_messages).to include('Maximum volume needs to be greater than minimum volume') + expect(uat_action.errors.full_messages).to include('Maximum volume needs to be greater than or equal to minimum volume') end end end From 91b78f328d9ce2a432aedf3e24924034634bb392 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Tue, 3 Dec 2024 17:01:33 +0000 Subject: [PATCH 08/40] fix: allow maximum to equal minimum (concentrations) --- .../uat_actions/generate_plate_concentrations.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/uat_actions/uat_actions/generate_plate_concentrations.rb b/app/uat_actions/uat_actions/generate_plate_concentrations.rb index ac97aacb5a..aaf04ab299 100644 --- a/app/uat_actions/uat_actions/generate_plate_concentrations.rb +++ b/app/uat_actions/uat_actions/generate_plate_concentrations.rb @@ -48,7 +48,7 @@ def self.default validates :concentration_units, presence: { message: 'needs a choice' } validates :minimum_concentration, numericality: { only_integer: false } validates :maximum_concentration, numericality: { greater_than: 0, only_integer: false } - validate :maximum_greater_than_minimum + validate :maximum_greater_than_or_equal_to_minimum def perform qc_assay_results = construct_qc_assay @@ -58,10 +58,10 @@ def perform private - def maximum_greater_than_minimum - return true if max_conc > min_conc + def maximum_greater_than_or_equal_to_minimum + return true if max_conc >= min_conc - errors.add(:maximum_concentration, 'needs to be greater than minimum concentration') + errors.add(:maximum_concentration, 'needs to be greater than or equal to minimum concentration') false end From a2acfb96a1c46c446381e11c468fef2d5ee9e012 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Tue, 3 Dec 2024 17:02:34 +0000 Subject: [PATCH 09/40] style: lint --- spec/uat_actions/generate_plate_volumes_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index 794fefafcb..ed51fb389a 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -79,7 +79,9 @@ end it 'adds an error' do - expect(uat_action.errors.full_messages).to include('Maximum volume needs to be greater than or equal to minimum volume') + expect(uat_action.errors.full_messages).to include( + 'Maximum volume needs to be greater than or equal to minimum volume' + ) end end end From 5b0a3ed3144bbf353fe0b2b2341fe91e4752ddba Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 4 Dec 2024 10:05:48 +0000 Subject: [PATCH 10/40] tests: add more --- .../generate_plate_concentrations_spec.rb | 61 +++++++++++++++++++ .../generate_plate_volumes_spec.rb | 20 ++++++ 2 files changed, 81 insertions(+) diff --git a/spec/uat_actions/generate_plate_concentrations_spec.rb b/spec/uat_actions/generate_plate_concentrations_spec.rb index 0c15ece3cb..549fab29d1 100644 --- a/spec/uat_actions/generate_plate_concentrations_spec.rb +++ b/spec/uat_actions/generate_plate_concentrations_spec.rb @@ -49,6 +49,67 @@ end end + context 'with invalid options' do + let(:parameters) do + { + plate_barcode: plate_barcode, + concentration_units: 'ng/ul', + minimum_concentration: 30, + maximum_concentration: 10 + } + end + let!(:saved_action) { uat_action.save } + + it 'has a minimum_concentration of 30' do + expect(uat_action.minimum_concentration).to eq 30 + end + + it 'has a maximum_concentration of 10' do + expect(uat_action.maximum_concentration).to eq 10 + end + + it 'is invalid' do + expect(uat_action.valid?).to be false + end + + it 'can not be saved' do + expect(saved_action).to be false + end + + it 'adds an error' do + expect(uat_action.errors.full_messages).to include( + 'Maximum concentration needs to be greater than or equal to minimum concentration' + ) + end + end + + context 'with equal minimum and maximum concentrations' do + let(:parameters) do + { + plate_barcode: plate_barcode, + concentration_units: 'ng/ul', + minimum_concentration: 10, + maximum_concentration: 10 + } + end + + it 'can be performed' do + expect(performed_action).to be true + end + + it 'generates the correct report' do + expect(uat_action.report).to eq('number_weil_concentrations_written' => 3) + end + + it 'creates the correct number of QC results' do + expect(plate.wells.map(&:qc_results).size).to eq 3 + end + + it 'sets the concentrations to be within the specified range' do + expect(plate.wells.map { |well| well.qc_results.first.value.to_f }).to all(eq 10) + end + end + it 'returns a default' do expect(described_class.default).to be_a described_class end diff --git a/spec/uat_actions/generate_plate_volumes_spec.rb b/spec/uat_actions/generate_plate_volumes_spec.rb index ed51fb389a..c169b8d01f 100644 --- a/spec/uat_actions/generate_plate_volumes_spec.rb +++ b/spec/uat_actions/generate_plate_volumes_spec.rb @@ -84,4 +84,24 @@ ) end end + + context 'with equal minimum and maximum volumes' do + let(:parameters) { { plate_barcode: plate_barcode, minimum_volume: 10, maximum_volume: 10 } } + + it 'can be performed' do + expect(performed_action).to be true + end + + it 'generates the correct report' do + expect(uat_action.report).to eq('number_well_volumes_written' => 3) + end + + it 'creates the correct number of QC results' do + expect(plate.wells.map(&:qc_results).size).to eq 3 + end + + it 'sets the volumes to be within the specified range' do + expect(plate.wells.map { |well| well.qc_results.first.value.to_f }).to all(eq 10) + end + end end From d45119c713f6c9b32ba231b5ae29b9c85451beb4 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 4 Dec 2024 10:47:55 +0000 Subject: [PATCH 11/40] tests: repair broken tests --- .../generate_plate_concentrations_spec.rb | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/spec/uat_actions/generate_plate_concentrations_spec.rb b/spec/uat_actions/generate_plate_concentrations_spec.rb index 549fab29d1..ed1bb8d8c4 100644 --- a/spec/uat_actions/generate_plate_concentrations_spec.rb +++ b/spec/uat_actions/generate_plate_concentrations_spec.rb @@ -3,9 +3,36 @@ require 'rails_helper' describe UatActions::GeneratePlateConcentrations do + let(:plate) { create(:plate_with_untagged_wells, sample_count: 3) } + let(:plate_barcode) { plate.barcodes.first.barcode } + let(:uat_action) { described_class.new(parameters) } + let!(:saved_action) { uat_action.save } + + context 'with default options' do + let(:parameters) { {} } + + it 'returns a default' do + expect(described_class.default).to be_a described_class + end + + it 'has a nil plate_barcode' do + expect(described_class.default.plate_barcode).to be_nil + end + + it 'has a minimum_concentration of 0' do + expect(described_class.default.minimum_concentration).to eq 0 + end + + it 'has a maximum_concentration of 100' do + expect(described_class.default.maximum_concentration).to eq 100 + end + + it 'has a concentration_units of ng/ul' do + expect(described_class.default.concentration_units).to eq 'ng/ul' + end + end + context 'with valid options' do - let(:plate) { create(:plate_with_untagged_wells, sample_count: 3) } - let(:uat_action) { described_class.new(parameters) } let(:report) do # A report is a hash of key value pairs which get returned to the user. # It should include information such as barcodes and identifiers @@ -15,15 +42,15 @@ context 'when ng per ul concentrations' do let(:parameters) do { - plate_barcode: plate.barcodes.first.barcode, + plate_barcode: plate_barcode, concentration_units: 'ng/ul', minimum_concentration: 0, maximum_concentration: 30 } end - it 'can be performed' do - expect(uat_action.perform).to be true + it 'can be saved' do + expect(saved_action).to be true expect(uat_action.report).to eq report expect(plate.wells.map(&:qc_results).size).to eq 3 expect(plate.wells.first.qc_results.first.assay_type).to eq 'UAT_Testing' @@ -32,16 +59,11 @@ context 'when nM concentrations' do let(:parameters) do - { - plate_barcode: plate.barcodes.first.barcode, - concentration_units: 'nM', - minimum_concentration: 0, - maximum_concentration: 30 - } + { plate_barcode: plate_barcode, concentration_units: 'nM', minimum_concentration: 0, maximum_concentration: 30 } end - it 'can be performed' do - expect(uat_action.perform).to be true + it 'can be saved' do + expect(saved_action).to be true expect(uat_action.report).to eq report expect(plate.wells.map(&:qc_results).size).to eq 3 expect(plate.wells.first.qc_results.first.assay_type).to eq 'UAT_Testing' @@ -58,7 +80,6 @@ maximum_concentration: 10 } end - let!(:saved_action) { uat_action.save } it 'has a minimum_concentration of 30' do expect(uat_action.minimum_concentration).to eq 30 @@ -93,12 +114,12 @@ } end - it 'can be performed' do - expect(performed_action).to be true + it 'can be saved' do + expect(saved_action).to be true end it 'generates the correct report' do - expect(uat_action.report).to eq('number_weil_concentrations_written' => 3) + expect(uat_action.report).to eq('number_well_concentrations_written' => 3) end it 'creates the correct number of QC results' do @@ -109,8 +130,4 @@ expect(plate.wells.map { |well| well.qc_results.first.value.to_f }).to all(eq 10) end end - - it 'returns a default' do - expect(described_class.default).to be_a described_class - end end From d2a1e636cbe3cdb1e70ffab674b03f94024bd7d9 Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Wed, 4 Dec 2024 12:18:04 +0000 Subject: [PATCH 12/40] Add TODO for removal of order_template.rb --- app/models/order_template.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/order_template.rb b/app/models/order_template.rb index 17d4c6d0d4..215acc3a1d 100644 --- a/app/models/order_template.rb +++ b/app/models/order_template.rb @@ -3,3 +3,6 @@ # If we remove this, then we break our API endpoints. Some of which, at least at one point, actually had # external users. OrderTemplate = SubmissionTemplate + +# TODO: {API v1 removal} this alias was created for v1 and should not be in v2. When v1 is removed, we should try +# to remove this alias as well. From 2e73bc6029ed76459c6d502bb1df2d0162af875f Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 5 Dec 2024 11:33:49 +0000 Subject: [PATCH 13/40] fix: specify what b and v stand for --- app/views/batches/_cherrypick_single_worksheet.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/batches/_cherrypick_single_worksheet.html.erb b/app/views/batches/_cherrypick_single_worksheet.html.erb index 91e04b260e..4075619fde 100644 --- a/app/views/batches/_cherrypick_single_worksheet.html.erb +++ b/app/views/batches/_cherrypick_single_worksheet.html.erb @@ -92,6 +92,7 @@ From b99815b72dc6e2e9b35efe396658073ab2a1c1bc Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Thu, 5 Dec 2024 12:11:36 +0000 Subject: [PATCH 14/40] Move the overridden method to the top of TagLayoutProcessor --- .../api/v2/tag_layouts_controller.rb | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/app/controllers/api/v2/tag_layouts_controller.rb b/app/controllers/api/v2/tag_layouts_controller.rb index bc2472640e..d3330aea1f 100644 --- a/app/controllers/api/v2/tag_layouts_controller.rb +++ b/app/controllers/api/v2/tag_layouts_controller.rb @@ -10,6 +10,30 @@ class TagLayoutsController < JSONAPI::ResourceController end class TagLayoutProcessor < JSONAPI::Processor + # Override the default behaviour for a JSONAPI::Processor when creating a new resource. + # We need to check whether a template UUID was given, and, if so, copy its data into this + # new TagLayoutResource. The creation will be prevented if any data from the template is also + # included in the create request as additional values. e.g. a request has a template UUID and + # also specifies a direction or a tag_group. In this case, the error will indicate that the + # template UUID was not an allowed attribute. + def create_resource + errors = [] + template = find_template { |new_errors| errors += new_errors } + merge_template_data(template) { |new_errors| errors += new_errors } unless template.nil? + + return JSONAPI::ErrorsOperationResult.new(JSONAPI::BAD_REQUEST, errors) unless errors.empty? + + # Perform the usual create actions. + resource = TagLayoutResource.create(context) + result = resource.replace_fields(params[:data]) + + record_template_use(template, resource) + + JSONAPI::ResourceOperationResult.new((result == :completed ? :created : :accepted), resource) + end + + private + def find_template template_uuid = params[:data][:attributes][:tag_layout_template_uuid] return nil if template_uuid.nil? # No errors -- we just don't have a template. @@ -72,28 +96,6 @@ def enforce_uniqueness? def record_template_use(template, resource) template&.record_template_use(TagLayout.find(resource.id).plate, enforce_uniqueness?) end - - # Override the default behaviour for a JSONAPI::Processor when creating a new resource. - # We need to check whether a template UUID was given, and, if so, copy its data into this - # new TagLayoutResource. The creation will be prevented if any data from the template is also - # included in the create request as additional values. e.g. a request has a template UUID and - # also specifies a direction or a tag_group. In this case, the error will indicate that the - # template UUID was not an allowed attribute. - def create_resource - errors = [] - template = find_template { |new_errors| errors += new_errors } - merge_template_data(template) { |new_errors| errors += new_errors } unless template.nil? - - return JSONAPI::ErrorsOperationResult.new(JSONAPI::BAD_REQUEST, errors) unless errors.empty? - - # Perform the usual create actions. - resource = TagLayoutResource.create(context) - result = resource.replace_fields(params[:data]) - - record_template_use(template, resource) - - JSONAPI::ResourceOperationResult.new((result == :completed ? :created : :accepted), resource) - end end end end From 6abc012e4b8ab53f381f9dcb4e79bb3e3848b30f Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Thu, 5 Dec 2024 14:39:01 +0000 Subject: [PATCH 15/40] Make OrderResource work Particularly when it comes to creating new Orders from a SubmissionTemplate --- app/controllers/api/v2/orders_controller.rb | 98 +++++++++++++++ app/resources/api/v2/base_resource.rb | 1 + app/resources/api/v2/order_resource.rb | 127 +++++++++++++++++--- config/routes.rb | 2 +- 4 files changed, 212 insertions(+), 16 deletions(-) diff --git a/app/controllers/api/v2/orders_controller.rb b/app/controllers/api/v2/orders_controller.rb index 63a1e71e45..07b871d200 100644 --- a/app/controllers/api/v2/orders_controller.rb +++ b/app/controllers/api/v2/orders_controller.rb @@ -8,5 +8,103 @@ class OrdersController < JSONAPI::ResourceController # By default JSONAPI::ResourceController provides most the standard # behaviour, and in many cases this file may be left empty. end + + class OrderProcessor < JSONAPI::Processor + # Override the default behaviour for a JSONAPI::Processor when creating a new resource. + # We need to check whether a template UUID was given, and, if so, use it to create the Order directly. + # Where no template is given, the Order will be created via the base JSONAPI::Processor. + def create_resource + errors = [] + template = find_template { |new_errors| errors += new_errors } + attributes = template_attributes { |new_errors| errors += new_errors } unless template.nil? + + return JSONAPI::ErrorsOperationResult.new(JSONAPI::BAD_REQUEST, errors) unless errors.empty? + return super if template.nil? # No template means we should use the default behaviour. + + create_order_from_template(template, attributes) + end + + private + + def find_template + template_uuid = params[:data][:attributes][:submission_template_uuid] + return nil if template_uuid.nil? # No errors -- we just don't have a template. + + template = SubmissionTemplate.with_uuid(template_uuid).first + + if template.nil? + yield JSONAPI::Exceptions::InvalidFieldValue.new(:submission_template_uuid, template_uuid).errors + end + + template + end + + def template_attributes(&) + parameters = params[:data][:attributes][:submission_template_attributes] + + if parameters.nil? + yield JSONAPI::Exceptions::ParameterMissing.new(:submission_template_attributes).errors + return + end + + make_template_attributes(permitted_attributes(parameters), &) + end + + def permitted_attributes(attributes) + attributes.permit( + { asset_uuids: [], request_options: {} }, + :autodetect_projects, + :autodetect_studies, + :user_uuid + ) + end + + def make_template_attributes(attributes, &) + {}.tap do |result| + result[:assets] = extract_assets(attributes, &) + result[:autodetect_projects] = attributes[:autodetect_projects] unless attributes[:autodetect_projects].nil? + result[:autodetect_studies] = attributes[:autodetect_studies] unless attributes[:autodetect_studies].nil? + result[:request_options] = require_attribute(attributes, :request_options) + result[:user] = extract_user(attributes, &) + end + end + + def extract_assets(attributes, &) + asset_uuids = require_attribute(attributes, :asset_uuids, &) + return nil if asset_uuids.nil? + + asset_uuids.map do |uuid| + uuid = Uuid.find_by(external_id: uuid) + yield JSONAPI::Exceptions::InvalidFieldValue.new(:asset_uuids, uuid).errors if uuid.nil? + uuid&.resource + end + end + + def extract_user(attributes, &) + user_uuid = require_attribute(attributes, :user_uuid, &) + return nil if user_uuid.nil? + + user = User.with_uuid(user_uuid).first + yield JSONAPI::Exceptions::InvalidFieldValue.new(:user_uuid, user_uuid).errors if user.nil? + user + end + + def require_attribute(attributes, key) + value = attributes.require(key) + value = value.to_h if value.instance_of?(ActionController::Parameters) && value.permitted? + value + rescue ActionController::ParameterMissing + yield JSONAPI::Exceptions::ParameterMissing.new("submission_template_attributes.#{key}").errors + nil + end + + def create_order_from_template(template, attributes) + order = template.create_order!(attributes) + resource = OrderResource.new(order, context) + result = resource.replace_fields(params[:data]) + + JSONAPI::ResourceOperationResult.new((result == :completed ? :created : :accepted), resource) + end + end end end diff --git a/app/resources/api/v2/base_resource.rb b/app/resources/api/v2/base_resource.rb index 0a9dbd2495..a713b3dfaa 100644 --- a/app/resources/api/v2/base_resource.rb +++ b/app/resources/api/v2/base_resource.rb @@ -17,6 +17,7 @@ class BaseResource < JSONAPI::Resource abstract # Loaded on the base class so that they can be loaded globally. + Order.descendants.each { |subclass| model_hint model: subclass, resource: :order } Purpose.descendants.each { |subclass| model_hint model: subclass, resource: :purpose } Plate.descendants.each { |subclass| model_hint model: subclass, resource: :plate } Tube.descendants.each { |subclass| model_hint model: subclass, resource: :tube } diff --git a/app/resources/api/v2/order_resource.rb b/app/resources/api/v2/order_resource.rb index e06622ea27..9dbc55b8a0 100644 --- a/app/resources/api/v2/order_resource.rb +++ b/app/resources/api/v2/order_resource.rb @@ -2,37 +2,134 @@ module Api module V2 - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include detailed descriptions for relationships, attributes and filters. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. + # Provides a JSON:API representation of {Order} which are used as the main means of requesting work. + # Creation of this resource via a `POST` request which must include a {#submission_template_uuid=} and a + # {#submission_template_attributes=} hash. # - # @note Access this resource via the `/api/v2/orders/` endpoint. + # @note This resource cannot be modified after creation: its endpoint will not accept `PATCH` requests. + # @note Access this resource via the `/api/v2/tube_from_tube_creations/` endpoint. # - # Provides a JSON:API representation of {Order}. + # @example POST request with child purpose and parent tube specified by UUID (deprecated) + # POST /api/v2/tube_from_tube_creations/ + # { + # "data": { + # "type": "tube_from_tube_creations", + # "attributes": { + # "child_purpose_uuid": "11111111-2222-3333-4444-555555666666", + # "parent_uuid": "33333333-4444-5555-6666-777777888888", + # "user_uuid": "99999999-0000-1111-2222-333333444444" + # } + # } + # } + # + # @example POST request with child purpose and parent tube specified by relationship + # POST /api/v2/tube_from_tube_creations/ + # { + # "data": { + # "type": "tube_from_tube_creations", + # "attributes": {}, + # "relationships": { + # "child_purpose": { + # "data": { "type": "tube_purposes", "id": "123" } + # }, + # "parent": { + # "data": { "type": "tubes", "id": "456" } + # }, + # "user": { + # "data": { "type": "users", "id": "789" } + # } + # } + # } + # } + # + # @example GET request for all TubeFromTubeCreation resources + # GET /api/v2/tube_from_tube_creations/ + # + # @example GET request for a TubeFromTubeCreation with ID 123 + # GET /api/v2/tube_from_tube_creations/123/ # # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation # of the JSON:API standard. class OrderResource < BaseResource - # Constants... + ### + # Attributes + ### - # model_name / model_hint if required + # @!attribute [r] order_type + # @return [String] the STI type of the {Order}. + attribute :order_type, delegate: :sti_type, readonly: true - default_includes :uuid_object + # @!attribute [r] request_options + # @return [Hash] request options for the {Order}. + # @note These can only be set once upon creation of the {Order}. + attribute :request_options, readonly: true - # Associations: + # @!attribute [r] request_types + # @return [Array] the IDs of types of request in this {Order}. + # @note These can only be set once upon creation of the {Order}. + attribute :request_types, readonly: true # Attributes + # @!attribute [r] uuid + # @return [String] the UUID for this {Order}. attribute :uuid, readonly: true - attribute :request_options, write_once: true - # Filters + ### + # Relationships + ### + + # @!attribute [r] project + # @return [ProjectResource] the project associated with this Order. + # @note This can only be set once upon creation of the Order. + has_one :project, readonly: true + + # @!attribute [r] study + # @return [StudyResource] the study associated with this Order. + # @note This can only be set once upon creation of the {Order}. + has_one :study, readonly: true + + # @!attribute [r] user + # @return [UserResource] the user who created this {Order}. + # @note This can only be set once upon creation of the {Order}. + has_one :user, readonly: true + + ### + # Template attributes + ### + + # These are defined here to assist with the creation of Orders from SubmissionTemplates. + # The values given are not stored in the Order and will be consumed by the OrderProcessor. + + # @!attribute [w] submission_template_uuid + # The UUID of the {SubmissionTemplate} to use when creating this {Order}. + # @note This is mandatory when creating new {Order}s via the API. It is not stored. + attribute :submission_template_uuid, writeonly: true + + def submission_template_uuid=(value) + # Do nothing with this value as it's consumed by the OrderProcessor. + end - # Custom methods - # These shouldn't be used for business logic, and a more about - # I/O and isolating implementation details. + # @!attribute [w] submission_template_attributes + # A hash of additional attributes to use when creating this {Order} from a given {SubmissionTemplate}. + # The structure of this hash is as follows: + # + # ```json + # { + # "asset_uuids": [String], + # "autodetect_projects": Boolean, // optional + # "autodetect_studies": Boolean, // optional + # "request_options": Hash, + # "user_uuid": String + # } + # ``` + # + # @note This is mandatory when creating new {Order}s via the API. It is not stored. + attribute :submission_template_attributes, writeonly: true - # Class method overrides + def submission_template_attributes=(value) + # Do nothing with this value as it's consumed by the OrderProcessor. + end end end end diff --git a/config/routes.rb b/config/routes.rb index 2be4715c9e..3a2223498c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,7 +34,7 @@ jsonapi_resources :lanes jsonapi_resources :lot_types jsonapi_resources :lots - jsonapi_resources :orders + jsonapi_resources :orders, except: %i[update] jsonapi_resources :pick_lists jsonapi_resources :plate_conversions, except: %i[update] jsonapi_resources :plate_creations, except: %i[update] From dc4d9e2ce9d4242b3007799b7314c022dc2ae58c Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Thu, 5 Dec 2024 15:02:58 +0000 Subject: [PATCH 16/40] Clean up code for creating the template attributes --- app/controllers/api/v2/orders_controller.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/v2/orders_controller.rb b/app/controllers/api/v2/orders_controller.rb index 07b871d200..f05d8347c8 100644 --- a/app/controllers/api/v2/orders_controller.rb +++ b/app/controllers/api/v2/orders_controller.rb @@ -60,13 +60,13 @@ def permitted_attributes(attributes) end def make_template_attributes(attributes, &) - {}.tap do |result| - result[:assets] = extract_assets(attributes, &) - result[:autodetect_projects] = attributes[:autodetect_projects] unless attributes[:autodetect_projects].nil? - result[:autodetect_studies] = attributes[:autodetect_studies] unless attributes[:autodetect_studies].nil? - result[:request_options] = require_attribute(attributes, :request_options) - result[:user] = extract_user(attributes, &) - end + { + assets: extract_assets(attributes, &), + autodetect_projects: attributes[:autodetect_projects], + autodetect_studies: attributes[:autodetect_studies], + request_options: require_attribute(attributes, :request_options), + user: extract_user(attributes, &) + }.compact end def extract_assets(attributes, &) From 0e5e9c44e7d0088ca39a21784cff7339c4c4a272 Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Mon, 9 Dec 2024 15:25:26 +0000 Subject: [PATCH 17/40] Add tests for orders There are more to add, but this is WIP --- spec/factories/submission_factories.rb | 11 +- .../patient_consent_withdrawl_spec.rb | 2 +- spec/requests/api/v2/orders_spec.rb | 420 ++++++++++++++++-- spec/resources/api/v2/order_resource_spec.rb | 35 +- 4 files changed, 417 insertions(+), 51 deletions(-) diff --git a/spec/factories/submission_factories.rb b/spec/factories/submission_factories.rb index dfb79715f1..62d56d9dcb 100644 --- a/spec/factories/submission_factories.rb +++ b/spec/factories/submission_factories.rb @@ -12,13 +12,20 @@ factory :submission_template do transient do - request_type_ids_list { request_types.map { |rt| [rt.id] } } + project { nil } + study { nil } request_types { [] } end submission_class_name { LinearSubmission.name } sequence(:name) { |i| "Template #{i}" } - submission_parameters { { request_type_ids_list: } } + submission_parameters do + { + request_type_ids_list: request_types.map { |rt| [rt.id] }, + project_id: project&.id, + study_id: study&.id + }.compact + end product_catalogue { |pc| pc.association(:single_product_catalogue) } factory :cherrypick_submission_template do diff --git a/spec/features/patient_consent_withdrawl_spec.rb b/spec/features/patient_consent_withdrawl_spec.rb index 027e1eebf6..ee860f1931 100644 --- a/spec/features/patient_consent_withdrawl_spec.rb +++ b/spec/features/patient_consent_withdrawl_spec.rb @@ -55,7 +55,7 @@ context 'an order' do # Lifted straight from the feature test with minimal rspecification # and optimization - let(:submission_template) { create(:submission_template, request_type_ids_list: [[create(:request_type).id]]) } + let(:submission_template) { create(:submission_template, request_types: [create(:request_type)]) } let(:sample_tube) { create(:sample_tube, sample:) } let(:asset_group) { create(:asset_group, assets: [sample_tube.receptacle]) } diff --git a/spec/requests/api/v2/orders_spec.rb b/spec/requests/api/v2/orders_spec.rb index 0faeec1378..c34a23116b 100644 --- a/spec/requests/api/v2/orders_spec.rb +++ b/spec/requests/api/v2/orders_spec.rb @@ -2,56 +2,416 @@ require 'rails_helper' require './spec/requests/api/v2/shared_examples/api_key_authenticatable' +require './spec/requests/api/v2/shared_examples/requests' describe 'Orders API', with: :api_v2 do - let(:base_endpoint) { '/api/v2/orders' } + let(:model_class) { Order } + let(:base_endpoint) { "/api/v2/#{resource_type}" } + let(:resource_type) { model_class.name.demodulize.pluralize.underscore } it_behaves_like 'ApiKeyAuthenticatable' - context 'with multiple orders' do - before { create_list(:order, 5) } + context 'with a list of resources' do + let(:resource_count) { 5 } - it 'sends a list of orders' do - api_get base_endpoint + before { create_list(:order, resource_count) } - # test for the 200 status-code - expect(response).to have_http_status(:success) + describe '#GET all resources' do + before { api_get base_endpoint } - # check to make sure the right amount of messages are returned - expect(json['data'].length).to eq(5) + it 'responds with a success http code' do + expect(response).to have_http_status(:success) + end + + it 'returns all the resources' do + expect(json['data'].length).to eq(resource_count) + end end + end + + context 'with a single resource' do + describe '#GET resource by ID' do + let(:resource) { create(:order) } + + context 'without included relationships' do + before { api_get "#{base_endpoint}/#{resource.id}" } + + it 'responds with a success http code' do + expect(response).to have_http_status(:success) + end + + it 'returns the resource with the correct id' do + expect(json.dig('data', 'id')).to eq(resource.id.to_s) + end + + it 'returns the resource with the correct type' do + expect(json.dig('data', 'type')).to eq(resource_type) + end + + it 'returns the correct value for the order_type attributes' do + expect(json.dig('data', 'attributes', 'order_type')).to eq(resource.sti_type) + end + + it 'returns the correct value for the request_options attributes' do + expect(json.dig('data', 'attributes', 'request_options')).to eq(resource.request_options) + end + + it 'returns the correct value for the request_types attributes' do + expect(json.dig('data', 'attributes', 'request_types')).to eq(resource.request_types) + end - # Check filters, ESPECIALLY if they aren't simple attribute filters + it 'returns the correct value for the uuid attributes' do + expect(json.dig('data', 'attributes', 'uuid')).to eq(resource.uuid) + end + + it 'excludes the unfetchable submission_template_uuid' do + expect(json.dig('data', 'attributes', 'submission_template_uuid')).not_to be_present + end + + it 'excludes the unfetchable submission_template_attributes' do + expect(json.dig('data', 'attributes', 'submission_template_attributes')).not_to be_present + end + + it 'returns a reference to the project relationship' do + expect(json.dig('data', 'relationships', 'project')).to be_present + end + + it 'returns a reference to the study relationship' do + expect(json.dig('data', 'relationships', 'study')).to be_present + end + + it 'returns a reference to the user relationship' do + expect(json.dig('data', 'relationships', 'user')).to be_present + end + + it 'does not include attributes for related resources' do + expect(json['included']).not_to be_present + end + end + + context 'with included relationships' do + it_behaves_like 'a GET request including a has_one relationship', 'project' + it_behaves_like 'a GET request including a has_one relationship', 'study' + it_behaves_like 'a GET request including a has_one relationship', 'user' + end + end end - context 'with a order' do + describe '#PATCH a resource' do let(:resource_model) { create(:order) } - + let(:purpose) { create(:tube_purpose) } let(:payload) do - { - 'data' => { - 'id' => resource_model.id, - 'type' => 'orders', - 'attributes' => { - # Set new attributes + { data: { id: resource_model.id, type: resource_type, attributes: { child_purpose_uuid: [purpose.uuid] } } } + end + + it 'finds no route for the method' do + expect { api_patch "#{base_endpoint}/#{resource_model.id}", payload }.to raise_error( + ActionController::RoutingError + ) + end + end + + describe '#POST a create request' do + let(:project) { create(:project) } + let(:study) { create(:study) } + let(:request_types) { create_list(:request_type, 1, asset_type: 'Well') } + let(:template) { create(:submission_template, request_types:, project:, study:) } + let(:assets) { create(:plate_with_tagged_wells).wells[0..2] } + let(:user) { create(:user) } + + context 'with a valid payload' do + shared_examples 'a valid request' do + before { api_post base_endpoint, payload } + + it 'creates a new resource' do + expect { api_post base_endpoint, payload }.to change(model_class, :count).by(1) + end + + it 'responds with success' do + expect(response).to have_http_status(:success) + end + + it 'returns the resource with the correct type' do + expect(json.dig('data', 'type')).to eq(resource_type) + end + + it 'returns the correct value for the order_type attributes' do + new_record = model_class.last + expect(json.dig('data', 'attributes', 'order_type')).to eq(new_record.sti_type) + end + + it 'returns the correct value for the request_options attributes' do + new_record = model_class.last + expect(json.dig('data', 'attributes', 'request_options')).to eq(new_record.request_options) + end + + it 'returns the correct value for the request_types attributes' do + new_record = model_class.last + expect(json.dig('data', 'attributes', 'request_types')).to eq(new_record.request_types) + end + + it 'returns the correct value for the uuid attributes' do + new_record = model_class.last + expect(json.dig('data', 'attributes', 'uuid')).to eq(new_record.uuid) + end + + it 'excludes the unfetchable submission_template_uuid' do + expect(json.dig('data', 'attributes', 'submission_template_uuid')).not_to be_present + end + + it 'excludes the unfetchable submission_template_attributes' do + expect(json.dig('data', 'attributes', 'submission_template_attributes')).not_to be_present + end + + it 'returns a reference to the project relationship' do + expect(json.dig('data', 'relationships', 'project')).to be_present + end + + it 'returns a reference to the study relationship' do + expect(json.dig('data', 'relationships', 'study')).to be_present + end + + it 'returns a reference to the user relationship' do + expect(json.dig('data', 'relationships', 'user')).to be_present + end + + it "associates the template's project with the new record" do + new_record = model_class.last + expect(new_record.project).to eq(project) + end + + it "associates the template's study with the new record" do + new_record = model_class.last + expect(new_record.study).to eq(study) + end + + it 'associates the user with the new record' do + new_record = model_class.last + expect(new_record.user).to eq(user) + end + end + + context 'with complete attributes' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + autodetect_projects: false, + autodetect_studies: false, + request_options: { + library_type: 'Chromium single cell 3 prime v3', + fragment_size_required_from: '200', + fragment_size_required_to: '800' + }, + user_uuid: user.uuid + } + } + } + } + end + + it_behaves_like 'a valid request' + end + + context 'with minimal attributes' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } + } + } + end + + it_behaves_like 'a valid request' + end + end + + describe 'with the project specified by the template' do + let(:template) { create(:submission_template, request_types:, project:, study:) } + let(:autodetect_projects) { false } + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + autodetect_projects: autodetect_projects, + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } + } + } + end + + before { api_post base_endpoint, payload } + + context 'when autocomplete is false' do + let(:autodetect_projects) { false } + + it "associates the template's project with the new record" do + new_record = model_class.last + expect(new_record.project).to eq(project) + end + end + + context 'when autocomplete is true' do + let(:autodetect_projects) { true } + + it "associates the template's project with the new record" do + new_record = model_class.last + expect(new_record.project).to eq(project) + end + end + end + + describe 'without the project specified by the template' do + let(:template) { create(:submission_template, request_types:, study:) } + let(:autodetect_projects) { false } + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + autodetect_projects: autodetect_projects, + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } } } - } + end + + before { api_post base_endpoint, payload } + + context 'when autocomplete is false' do + let(:autodetect_projects) { false } + + it 'fails validation on the model' do + # Not ideal, but we haven't implemented handling of validation errors for Order from the model via this + # processor. + expect(json.dig('errors', 0, 'meta', 'exception')).to include("Project can't be blank") + end + end + + context 'when autocomplete is true' do + let(:autodetect_projects) { true } + + it 'associates the unique project from assets with the new record' do + new_record = model_class.last + expect(new_record.project).to eq(assets.first.projects.first) + end + end end - it 'sends an individual order' do - api_get "#{base_endpoint}/#{resource_model.id}" - expect(response).to have_http_status(:success) - expect(json.dig('data', 'type')).to eq('orders') + describe 'with the study specified by the template' do + let(:template) { create(:submission_template, request_types:, project:, study:) } + let(:autodetect_studies) { false } + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + autodetect_studies: autodetect_studies, + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } + } + } + end + + before { api_post base_endpoint, payload } + + context 'when autocomplete is false' do + let(:autodetect_studies) { false } + + it "associates the template's study with the new record" do + new_record = model_class.last + expect(new_record.study).to eq(study) + end + end + + context 'when autocomplete is true' do + let(:autodetect_studies) { true } + + it "associates the template's study with the new record" do + new_record = model_class.last + expect(new_record.study).to eq(study) + end + end end - # Remove if immutable - it 'allows update of a order' do - api_patch "#{base_endpoint}/#{resource_model.id}", payload - expect(response).to have_http_status(:success) - expect(json.dig('data', 'type')).to eq('orders') - # Double check at least one of the attributes - # eg. expect(json.dig('data', 'attributes', 'state')).to eq('started') + describe 'without the study specified by the template' do + let(:template) { create(:submission_template, request_types:, project:) } + let(:autodetect_studies) { false } + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + autodetect_studies: autodetect_studies, + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } + } + } + end + + before { api_post base_endpoint, payload } + + context 'when autocomplete is false' do + let(:autodetect_studies) { false } + + it 'fails validation on the model' do + # Not ideal, but we haven't implemented handling of validation errors for Order from the model via this + # processor. + expect(json.dig('errors', 0, 'meta', 'exception')).to include("Study can't be blank") + end + end + + context 'when autocomplete is true' do + let(:autodetect_studies) { true } + + it 'associates the unique study from assets with the new record' do + new_record = model_class.last + expect(new_record.study).to eq(assets.first.studies.first) + end + end end end end diff --git a/spec/resources/api/v2/order_resource_spec.rb b/spec/resources/api/v2/order_resource_spec.rb index 43f815ae86..d0334541fa 100644 --- a/spec/resources/api/v2/order_resource_spec.rb +++ b/spec/resources/api/v2/order_resource_spec.rb @@ -8,22 +8,21 @@ let(:resource_model) { build_stubbed(:order) } - # Test attributes - it 'works', :aggregate_failures do # rubocop:todo RSpec/ExampleWording - expect(resource).to have_attribute :uuid - expect(resource).not_to have_updatable_field(:id) - expect(resource).not_to have_updatable_field(:uuid) - end - - # Updatable fields - # eg. it { is_expected.to have_updatable_field(:state) } - - # Filters - # eg. it { is_expected.to filter(:order_type) } - - # Associations - # eg. it { is_expected.to have_many(:samples).with_class_name('Sample') } - - # Custom method tests - # Add tests for any custom methods you've added. + # Model Name + it { is_expected.to have_model_name 'Order' } + + # Attributes + it { is_expected.to have_readonly_attribute :order_type } + it { is_expected.to have_readonly_attribute :request_options } + it { is_expected.to have_readonly_attribute :request_types } + it { is_expected.to have_readonly_attribute :uuid } + + # Relationships + it { is_expected.to have_a_readonly_has_one(:project).with_class_name('Project') } + it { is_expected.to have_a_readonly_has_one(:study).with_class_name('Study') } + it { is_expected.to have_a_readonly_has_one(:user).with_class_name('User') } + + # Template attributes + it { is_expected.to have_writeonly_attribute :submission_template_uuid } + it { is_expected.to have_writeonly_attribute :submission_template_attributes } end From a8b9d8b486f228f08ac4272369f8fd65e3d47d92 Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Mon, 9 Dec 2024 16:32:21 +0000 Subject: [PATCH 18/40] Complete tests for OrderResource requests --- app/controllers/api/v2/orders_controller.rb | 8 +- spec/requests/api/v2/orders_spec.rb | 224 ++++++++++++++++++ .../api/v2/shared_examples/requests.rb | 16 ++ 3 files changed, 244 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v2/orders_controller.rb b/app/controllers/api/v2/orders_controller.rb index f05d8347c8..da30f615e6 100644 --- a/app/controllers/api/v2/orders_controller.rb +++ b/app/controllers/api/v2/orders_controller.rb @@ -64,7 +64,7 @@ def make_template_attributes(attributes, &) assets: extract_assets(attributes, &), autodetect_projects: attributes[:autodetect_projects], autodetect_studies: attributes[:autodetect_studies], - request_options: require_attribute(attributes, :request_options), + request_options: require_attribute(attributes, :request_options, &), user: extract_user(attributes, &) }.compact end @@ -74,9 +74,9 @@ def extract_assets(attributes, &) return nil if asset_uuids.nil? asset_uuids.map do |uuid| - uuid = Uuid.find_by(external_id: uuid) - yield JSONAPI::Exceptions::InvalidFieldValue.new(:asset_uuids, uuid).errors if uuid.nil? - uuid&.resource + uuid_obj = Uuid.find_by(external_id: uuid) + yield JSONAPI::Exceptions::InvalidFieldValue.new(:asset_uuids, uuid).errors if uuid_obj.nil? + uuid_obj&.resource end end diff --git a/spec/requests/api/v2/orders_spec.rb b/spec/requests/api/v2/orders_spec.rb index c34a23116b..89b9618da2 100644 --- a/spec/requests/api/v2/orders_spec.rb +++ b/spec/requests/api/v2/orders_spec.rb @@ -413,5 +413,229 @@ end end end + + context 'with a read-only attribute in the payload' do + context 'with order_type' do + let(:disallowed_value) { 'order_type' } + let(:payload) { { data: { type: resource_type, attributes: { order_type: 'read-only' } } } } + + it_behaves_like 'a POST request with a disallowed value' + end + + context 'with request_options' do + let(:disallowed_value) { 'request_options' } + let(:payload) { { data: { type: resource_type, attributes: { request_options: 'read-only' } } } } + + it_behaves_like 'a POST request with a disallowed value' + end + + context 'with request_types' do + let(:disallowed_value) { 'request_types' } + let(:payload) { { data: { type: resource_type, attributes: { request_types: 'read-only' } } } } + + it_behaves_like 'a POST request with a disallowed value' + end + + context 'with uuid' do + let(:disallowed_value) { 'uuid' } + let(:payload) { { data: { type: resource_type, attributes: { uuid: 'read-only' } } } } + + it_behaves_like 'a POST request with a disallowed value' + end + end + + context 'with a read-only relationship in the payload' do + context 'with project' do + let(:disallowed_value) { 'project' } + let(:payload) do + { data: { type: resource_type, relationships: { project: { data: { id: '1', type: 'projects' } } } } } + end + + it_behaves_like 'a POST request with a disallowed value' + end + + context 'with study' do + let(:disallowed_value) { 'study' } + let(:payload) do + { data: { type: resource_type, relationships: { study: { data: { id: '1', type: 'studies' } } } } } + end + + it_behaves_like 'a POST request with a disallowed value' + end + + context 'with user' do + let(:disallowed_value) { 'user' } + let(:payload) do + { data: { type: resource_type, relationships: { user: { data: { id: '1', type: 'users' } } } } } + end + + it_behaves_like 'a POST request with a disallowed value' + end + end + + context 'without a required attribute' do + let(:project) { create(:project) } + let(:study) { create(:study) } + let(:request_types) { create_list(:request_type, 1, asset_type: 'Well') } + let(:template) { create(:submission_template, request_types:, project:, study:) } + let(:assets) { create(:plate_with_tagged_wells).wells[0..2] } + let(:user) { create(:user) } + + context 'without a submission_template_uuid' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } + } + } + end + + it 'fails to create the resource' do + # Not providing a submission_template_uuid is a valid use case as we default to the normal JSONAPI::Resources + # behaviour, but it will fail validation on the new Order model as it needs certain attributes set, but + # they're all read-only on the API. + api_post base_endpoint, payload + + expect(json['errors'].map { |err| err['detail'] }).to match_array( + [ + 'user - must exist', + "study - can't be blank", + "project - can't be blank", + "request_types - can't be blank" + ] + ) + end + end + + context 'without submission_template_attributes' do + let(:payload) { { data: { type: resource_type, attributes: { submission_template_uuid: template.uuid } } } } + let(:error_detail_message) { 'The required parameter, submission_template_attributes, is missing.' } + + it_behaves_like 'a bad POST request with a specific error' + end + + context 'without submission_template_attributes.asset_uuids' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } + } + } + end + let(:error_detail_message) { 'The required parameter, submission_template_attributes.asset_uuids, is missing.' } + + it_behaves_like 'a bad POST request with a specific error' + end + + context 'without submission_template_attributes.request_options' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + user_uuid: user.uuid + } + } + } + } + end + let(:error_detail_message) do + 'The required parameter, submission_template_attributes.request_options, is missing.' + end + + it_behaves_like 'a bad POST request with a specific error' + end + + context 'without submission_template_attributes.user_uuid' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + request_options: { + no_options: true + } + } + } + } + } + end + let(:error_detail_message) { 'The required parameter, submission_template_attributes.user_uuid, is missing.' } + + it_behaves_like 'a bad POST request with a specific error' + end + end + + context 'with an invalid UUID' do + context 'with an invalid asset_uuid' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: ['not-a-valid-uuid'], + request_options: { + no_options: true + }, + user_uuid: user.uuid + } + } + } + } + end + let(:error_detail_message) { 'not-a-valid-uuid is not a valid value for asset_uuids.' } + + it_behaves_like 'a bad POST request with a specific error' + end + + context 'with an invalid user_uuid' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + submission_template_uuid: template.uuid, + submission_template_attributes: { + asset_uuids: assets.map(&:uuid), + request_options: { + no_options: true + }, + user_uuid: 'not-a-valid-uuid' + } + } + } + } + end + let(:error_detail_message) { 'not-a-valid-uuid is not a valid value for user_uuid.' } + + it_behaves_like 'a bad POST request with a specific error' + end + end end end diff --git a/spec/requests/api/v2/shared_examples/requests.rb b/spec/requests/api/v2/shared_examples/requests.rb index 829b65e79c..3e3ec9c9d6 100644 --- a/spec/requests/api/v2/shared_examples/requests.rb +++ b/spec/requests/api/v2/shared_examples/requests.rb @@ -50,6 +50,22 @@ end end +shared_examples 'a bad POST request with a specific error' do + before { api_post base_endpoint, payload } + + it 'does not create a new resource' do + expect { api_post base_endpoint, payload }.not_to change(model_class, :count) + end + + it 'responds with unprocessable_entity' do + expect(response).to have_http_status(:bad_request) + end + + it 'specifies the expected error message' do + expect(json.dig('errors', 0, 'detail')).to eq(error_detail_message) + end +end + shared_examples 'a GET request including a has_one relationship' do |related_name| before { api_get "#{base_endpoint}/#{resource.id}?include=#{related_name}" } From 44fb0657c4b3306f327e74ad181f60ba378afe4e Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Mon, 9 Dec 2024 16:58:41 +0000 Subject: [PATCH 19/40] Fix Rubocop issues --- spec/requests/api/v2/orders_spec.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/spec/requests/api/v2/orders_spec.rb b/spec/requests/api/v2/orders_spec.rb index 89b9618da2..d4ac5ae21a 100644 --- a/spec/requests/api/v2/orders_spec.rb +++ b/spec/requests/api/v2/orders_spec.rb @@ -498,6 +498,9 @@ } } end + let(:expected_error_details) do + ['user - must exist', "study - can't be blank", "project - can't be blank", "request_types - can't be blank"] + end it 'fails to create the resource' do # Not providing a submission_template_uuid is a valid use case as we default to the normal JSONAPI::Resources @@ -505,14 +508,7 @@ # they're all read-only on the API. api_post base_endpoint, payload - expect(json['errors'].map { |err| err['detail'] }).to match_array( - [ - 'user - must exist', - "study - can't be blank", - "project - can't be blank", - "request_types - can't be blank" - ] - ) + expect(json['errors'].pluck('detail')).to contain_exactly(*expected_error_details) end end From d26dfcebca3a5c15f6a1e1b60a9b94cb08728faf Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Tue, 10 Dec 2024 21:20:43 +0000 Subject: [PATCH 20/40] Fix rubocop complaint --- spec/requests/api/v2/orders_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/v2/orders_spec.rb b/spec/requests/api/v2/orders_spec.rb index d4ac5ae21a..fcda248506 100644 --- a/spec/requests/api/v2/orders_spec.rb +++ b/spec/requests/api/v2/orders_spec.rb @@ -508,7 +508,7 @@ # they're all read-only on the API. api_post base_endpoint, payload - expect(json['errors'].pluck('detail')).to contain_exactly(*expected_error_details) + expect(json['errors'].pluck('detail')).to match_array(expected_error_details) end end From 1e4dd24f2d163cbb646fb1396ccb2d97662eb28b Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Thu, 12 Dec 2024 11:57:55 +0000 Subject: [PATCH 21/40] add huMFre code field to the samples --- app/controllers/samples_controller.rb | 1 + app/models/api/sample_io.rb | 1 + app/models/sample.rb | 10 ++++++++ .../shared/metadata/edit/_sample.html.erb | 2 ++ .../shared/metadata/show/_sample.html.erb | 2 ++ config/locales/metadata/en.yml | 2 ++ ...36_add_hu_m_fre_code_to_sample_metadata.rb | 6 +++++ db/schema.rb | 3 ++- spec/models/api/sample_io_spec.rb | 6 +++-- spec/models/sample_spec.rb | 24 +++++++++++++++++++ 10 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb diff --git a/app/controllers/samples_controller.rb b/app/controllers/samples_controller.rb index 2e21f2782e..a9aa814e8f 100644 --- a/app/controllers/samples_controller.rb +++ b/app/controllers/samples_controller.rb @@ -248,6 +248,7 @@ def default_permitted_metadata_fields subject treatment donor_id + huMFre_code ] } end diff --git a/app/models/api/sample_io.rb b/app/models/api/sample_io.rb index 5acaac3742..b54b90443a 100644 --- a/app/models/api/sample_io.rb +++ b/app/models/api/sample_io.rb @@ -98,6 +98,7 @@ def json_root map_attribute_to_json_attribute(:treatment) map_attribute_to_json_attribute(:date_of_consent_withdrawn) map_attribute_to_json_attribute(:user_id_of_consent_withdrawn, 'marked_as_consent_withdrawn_by') + map_attribute_to_json_attribute(:huMFre_code) end extra_json_attributes do |_object, json_attributes| diff --git a/app/models/sample.rb b/app/models/sample.rb index 105b86ef96..2ea4e9d0da 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -147,6 +147,8 @@ class Current < ActiveSupport::CurrentAttributes custom_attribute(:date_of_consent_withdrawn) custom_attribute(:user_id_of_consent_withdrawn) + custom_attribute(:huMFre_code) + # These fields are warehoused, so need to match the encoding restrictions there # This excludes supplementary characters, which include emoji and rare kanji # @note phenotype isn't currently broadcast but has a field waiting in the warehouse @@ -226,6 +228,14 @@ class Current < ActiveSupport::CurrentAttributes # Sample::Metadata tracks sample information, either for use in the lab, or passing to # the EBI class Metadata + + validates :huMFre_code, + format: { + with: %r{\A\d{2}/\d{2,}|\d{2}/\d{4}-\d{3}\z}, + message: 'must match a valid format, e.g. 12/34 or 12/2023-001' + }, + allow_blank: true + # This constraint doesn't match that described in the manifest, and is more permissive # It was added on conversion of out database to utf8 to address concerns that this would # lead to an increase in characters that their pipeline cannot process. Only a handful diff --git a/app/views/shared/metadata/edit/_sample.html.erb b/app/views/shared/metadata/edit/_sample.html.erb index 999a30919e..02f1f264fc 100644 --- a/app/views/shared/metadata/edit/_sample.html.erb +++ b/app/views/shared/metadata/edit/_sample.html.erb @@ -80,6 +80,8 @@ <%= metadata_fields.text_field(:disease) %> <%= metadata_fields.text_field(:treatment)%> + <%= metadata_fields.text_field(:huMFre_code)%> + <% metadata_fields.finalize_related_fields %> <% end %> <% end %> diff --git a/app/views/shared/metadata/show/_sample.html.erb b/app/views/shared/metadata/show/_sample.html.erb index 23f5aa732f..d75a44439e 100644 --- a/app/views/shared/metadata/show/_sample.html.erb +++ b/app/views/shared/metadata/show/_sample.html.erb @@ -64,5 +64,7 @@ <%= metadata_fields.plain_value(:disease) %> <%= metadata_fields.plain_value(:treatment)%> + <%= metadata_fields.plain_value(:huMFre_code)%> + <% end %> <% end %> diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml index 2abb785013..db6ed0d4de 100644 --- a/config/locales/metadata/en.yml +++ b/config/locales/metadata/en.yml @@ -325,6 +325,8 @@ en: edit_info: "EGA" genome_size: label: Genome Size + huMFre_code: + label: HuMFre Code study: metadata: diff --git a/db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb b/db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb new file mode 100644 index 0000000000..d16094607d --- /dev/null +++ b/db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class AddHuMFreCodeToSampleMetadata < ActiveRecord::Migration[7.0] + def change + add_column :sample_metadata, :huMFre_code, :string, limit: 16 + end +end diff --git a/db/schema.rb b/db/schema.rb index 11da36d9d8..187e3c4e3e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_11_06_103710) do +ActiveRecord::Schema[7.0].define(version: 2024_12_11_143636) do create_table "aliquot_indices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", options: "ENGINE=InnoDB ROW_FORMAT=DYNAMIC", force: :cascade do |t| t.integer "aliquot_id", null: false t.integer "lane_id", null: false @@ -1395,6 +1395,7 @@ t.integer "user_id_of_consent_withdrawn" t.boolean "consent_withdrawn", default: false, null: false t.string "collected_by", comment: "Name of persons or institute who collected the specimen" + t.string "huMFre_code", limit: 16 t.index ["sample_ebi_accession_number"], name: "index_sample_metadata_on_sample_ebi_accession_number" t.index ["sample_id"], name: "index_sample_metadata_on_sample_id" t.index ["supplier_name"], name: "index_sample_metadata_on_supplier_name" diff --git a/spec/models/api/sample_io_spec.rb b/spec/models/api/sample_io_spec.rb index 3a24a4e9c7..a219f9150e 100644 --- a/spec/models/api/sample_io_spec.rb +++ b/spec/models/api/sample_io_spec.rb @@ -62,7 +62,8 @@ sample_strain_att: 'stuff about strain', consent_withdrawn: false, donor_id: 2, - developmental_stage: 'thing' + developmental_stage: 'thing', + huMFre_code: '23/21' } ) end @@ -133,7 +134,8 @@ 'developmental_stage' => 'thing', 'donor_id' => '2', 'reference_genome' => 'ReferenceGenome1', - 'component_sample_uuids' => [{ uuid: comp_sample1.uuid }, { uuid: comp_sample2.uuid }] + 'component_sample_uuids' => [{ uuid: comp_sample1.uuid }, { uuid: comp_sample2.uuid }], + 'huMFre_code' => '23/21' } end diff --git a/spec/models/sample_spec.rb b/spec/models/sample_spec.rb index daa588a53d..838ff5f62c 100644 --- a/spec/models/sample_spec.rb +++ b/spec/models/sample_spec.rb @@ -259,6 +259,30 @@ end end + context 'huMFre code' do + let(:sample) { create(:sample) } + + it 'defaults to null when not specified' do + expect(sample.sample_metadata.huMFre_code).to be_nil + end + + it 'fails to update/save when huMFre code value is invalid' do + expect(sample.sample_metadata.huMFre_code).to be_nil + sample.sample_metadata.huMFre_code = 'humFre1' + expect(sample.sample_metadata.save).to be false + end + + it 'contains huMFre code value when it is correctly specified' do + sample.sample_metadata.update!(huMFre_code: '12/12') + expect(sample.sample_metadata.huMFre_code).to eq '12/12' + end + + it 'can have the huMFre code blanked' do + sample.sample_metadata.update!(huMFre_code: nil) + expect(sample.sample_metadata.huMFre_code).to be_nil + end + end + context '(DPL-148) on updating sample metadata' do let(:sample) { create(:sample) } From 487419153735f7f3eb0d54c97e33f1fa6e6f9e0d Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Thu, 12 Dec 2024 12:01:53 +0000 Subject: [PATCH 22/40] run prettier --- app/models/sample.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/sample.rb b/app/models/sample.rb index 2ea4e9d0da..5ea3f68a78 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -228,7 +228,6 @@ class Current < ActiveSupport::CurrentAttributes # Sample::Metadata tracks sample information, either for use in the lab, or passing to # the EBI class Metadata - validates :huMFre_code, format: { with: %r{\A\d{2}/\d{2,}|\d{2}/\d{4}-\d{3}\z}, From 569962a0fd94decc086d1a5647c84011b4f7b738 Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Thu, 12 Dec 2024 13:20:37 +0000 Subject: [PATCH 23/40] fix cucumber test --- features/samples/xml_interface.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/samples/xml_interface.feature b/features/samples/xml_interface.feature index 26f533159e..960ec1e0d4 100644 --- a/features/samples/xml_interface.feature +++ b/features/samples/xml_interface.feature @@ -68,6 +68,7 @@ Feature: The XML interface to the samples Time Point Donor Id Collected By + HuMFre Code """ From 5a080e0e0a95fe4b5343ef1ab13f9eaf048ef97a Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 12 Dec 2024 13:23:04 +0000 Subject: [PATCH 24/40] fix: show units --- app/views/batches/_cherrypick_single_worksheet.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/batches/_cherrypick_single_worksheet.html.erb b/app/views/batches/_cherrypick_single_worksheet.html.erb index 4075619fde..f10384d5bb 100644 --- a/app/views/batches/_cherrypick_single_worksheet.html.erb +++ b/app/views/batches/_cherrypick_single_worksheet.html.erb @@ -92,7 +92,7 @@ From 6f66d94df59a5e874251139247a1b87227d8e8fb Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 12 Dec 2024 13:24:14 +0000 Subject: [PATCH 25/40] fix: reword receptacle volume -> picking volume --- app/views/batches/_cherrypick_single_worksheet.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/batches/_cherrypick_single_worksheet.html.erb b/app/views/batches/_cherrypick_single_worksheet.html.erb index f10384d5bb..6bb66c2c36 100644 --- a/app/views/batches/_cherrypick_single_worksheet.html.erb +++ b/app/views/batches/_cherrypick_single_worksheet.html.erb @@ -92,7 +92,7 @@ From 9da0e253ff7983124c78bb94d23cd025ad91cdd5 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 13 Dec 2024 12:11:18 +0000 Subject: [PATCH 26/40] fix: replace assay_version Binning with more suitable UAT_version --- app/uat_actions/uat_actions/generate_plate_volumes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/uat_actions/uat_actions/generate_plate_volumes.rb b/app/uat_actions/uat_actions/generate_plate_volumes.rb index 0afd4cacb9..ddb1033609 100644 --- a/app/uat_actions/uat_actions/generate_plate_volumes.rb +++ b/app/uat_actions/uat_actions/generate_plate_volumes.rb @@ -90,7 +90,7 @@ def construct_qc_assay value: create_random_volume, units: 'µl', assay_type: 'UAT_Testing', - assay_version: 'Binning', + assay_version: 'UAT_version', qc_assay: qc_assay ) num_wells_written += 1 From 95a65bb56a5ffe89cdd9a86d66f0a807f3a84a9c Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Mon, 16 Dec 2024 12:39:34 +0000 Subject: [PATCH 27/40] Clean up the OrdersProcessor and OrderResource --- app/controllers/api/v2/orders_controller.rb | 65 +++++++------------- app/resources/api/v2/order_resource.rb | 15 ++--- spec/resources/api/v2/order_resource_spec.rb | 34 ++++++++++ 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/app/controllers/api/v2/orders_controller.rb b/app/controllers/api/v2/orders_controller.rb index da30f615e6..553298c46c 100644 --- a/app/controllers/api/v2/orders_controller.rb +++ b/app/controllers/api/v2/orders_controller.rb @@ -10,44 +10,31 @@ class OrdersController < JSONAPI::ResourceController end class OrderProcessor < JSONAPI::Processor - # Override the default behaviour for a JSONAPI::Processor when creating a new resource. - # We need to check whether a template UUID was given, and, if so, use it to create the Order directly. - # Where no template is given, the Order will be created via the base JSONAPI::Processor. - def create_resource - errors = [] - template = find_template { |new_errors| errors += new_errors } - attributes = template_attributes { |new_errors| errors += new_errors } unless template.nil? - - return JSONAPI::ErrorsOperationResult.new(JSONAPI::BAD_REQUEST, errors) unless errors.empty? - return super if template.nil? # No template means we should use the default behaviour. - - create_order_from_template(template, attributes) - end + before_create_resource :prepare_context private + def prepare_context + context[:template] = find_template + context[:template_attributes] = template_attributes unless context[:template].nil? + end + def find_template template_uuid = params[:data][:attributes][:submission_template_uuid] return nil if template_uuid.nil? # No errors -- we just don't have a template. template = SubmissionTemplate.with_uuid(template_uuid).first - - if template.nil? - yield JSONAPI::Exceptions::InvalidFieldValue.new(:submission_template_uuid, template_uuid).errors - end + raise JSONAPI::Exceptions::InvalidFieldValue.new(:submission_template_uuid, template_uuid) if template.nil? template end - def template_attributes(&) + def template_attributes parameters = params[:data][:attributes][:submission_template_attributes] - if parameters.nil? - yield JSONAPI::Exceptions::ParameterMissing.new(:submission_template_attributes).errors - return - end + raise JSONAPI::Exceptions::ParameterMissing, :submission_template_attributes if parameters.nil? - make_template_attributes(permitted_attributes(parameters), &) + make_template_attributes(permitted_attributes(parameters)) end def permitted_attributes(attributes) @@ -59,33 +46,34 @@ def permitted_attributes(attributes) ) end - def make_template_attributes(attributes, &) + def make_template_attributes(attributes) { - assets: extract_assets(attributes, &), + assets: extract_assets(attributes), autodetect_projects: attributes[:autodetect_projects], autodetect_studies: attributes[:autodetect_studies], - request_options: require_attribute(attributes, :request_options, &), - user: extract_user(attributes, &) + request_options: require_attribute(attributes, :request_options), + user: extract_user(attributes) }.compact end - def extract_assets(attributes, &) - asset_uuids = require_attribute(attributes, :asset_uuids, &) + def extract_assets(attributes) + asset_uuids = require_attribute(attributes, :asset_uuids) return nil if asset_uuids.nil? asset_uuids.map do |uuid| uuid_obj = Uuid.find_by(external_id: uuid) - yield JSONAPI::Exceptions::InvalidFieldValue.new(:asset_uuids, uuid).errors if uuid_obj.nil? + raise JSONAPI::Exceptions::InvalidFieldValue.new(:asset_uuids, uuid) if uuid_obj.nil? uuid_obj&.resource end end - def extract_user(attributes, &) - user_uuid = require_attribute(attributes, :user_uuid, &) + def extract_user(attributes) + user_uuid = require_attribute(attributes, :user_uuid) return nil if user_uuid.nil? user = User.with_uuid(user_uuid).first - yield JSONAPI::Exceptions::InvalidFieldValue.new(:user_uuid, user_uuid).errors if user.nil? + raise JSONAPI::Exceptions::InvalidFieldValue.new(:user_uuid, user_uuid) if user.nil? + user end @@ -94,16 +82,7 @@ def require_attribute(attributes, key) value = value.to_h if value.instance_of?(ActionController::Parameters) && value.permitted? value rescue ActionController::ParameterMissing - yield JSONAPI::Exceptions::ParameterMissing.new("submission_template_attributes.#{key}").errors - nil - end - - def create_order_from_template(template, attributes) - order = template.create_order!(attributes) - resource = OrderResource.new(order, context) - result = resource.replace_fields(params[:data]) - - JSONAPI::ResourceOperationResult.new((result == :completed ? :created : :accepted), resource) + raise JSONAPI::Exceptions::ParameterMissing, "submission_template_attributes.#{key}" end end end diff --git a/app/resources/api/v2/order_resource.rb b/app/resources/api/v2/order_resource.rb index 9dbc55b8a0..52859bd4e8 100644 --- a/app/resources/api/v2/order_resource.rb +++ b/app/resources/api/v2/order_resource.rb @@ -95,7 +95,7 @@ class OrderResource < BaseResource has_one :user, readonly: true ### - # Template attributes + # Templated creation ### # These are defined here to assist with the creation of Orders from SubmissionTemplates. @@ -105,10 +105,7 @@ class OrderResource < BaseResource # The UUID of the {SubmissionTemplate} to use when creating this {Order}. # @note This is mandatory when creating new {Order}s via the API. It is not stored. attribute :submission_template_uuid, writeonly: true - - def submission_template_uuid=(value) - # Do nothing with this value as it's consumed by the OrderProcessor. - end + attr_writer :submission_template_uuid # Do not store this on the model. It's consumed by the OrderProcessor. # @!attribute [w] submission_template_attributes # A hash of additional attributes to use when creating this {Order} from a given {SubmissionTemplate}. @@ -126,9 +123,13 @@ def submission_template_uuid=(value) # # @note This is mandatory when creating new {Order}s via the API. It is not stored. attribute :submission_template_attributes, writeonly: true + attr_writer :submission_template_attributes # Do not store this on the model. It's consumed by the OrderProcessor. + + def self.create(context) + return super if context[:template].nil? - def submission_template_attributes=(value) - # Do nothing with this value as it's consumed by the OrderProcessor. + order = context[:template].create_order!(context[:template_attributes]) + new(order, context) end end end diff --git a/spec/resources/api/v2/order_resource_spec.rb b/spec/resources/api/v2/order_resource_spec.rb index d0334541fa..d8bad10f00 100644 --- a/spec/resources/api/v2/order_resource_spec.rb +++ b/spec/resources/api/v2/order_resource_spec.rb @@ -25,4 +25,38 @@ # Template attributes it { is_expected.to have_writeonly_attribute :submission_template_uuid } it { is_expected.to have_writeonly_attribute :submission_template_attributes } + + # Custom methods + describe '#self.create' do + context 'without a template in the context' do + let(:context) { {} } + let(:resource) { double('OrderResource') } + + it 'returns an OrderResource created by the super class' do + expect(described_class.superclass).to receive(:create).with(context).and_return(resource) + + expect(described_class.create(context)).to eq(resource) + end + end + + context 'with a template in the context' do + let(:context) { { template:, template_attributes: } } + let(:template) { double('SubmissionTemplate') } + let(:template_attributes) { {} } + let(:order) { create(:order) } + + it 'does not call create on the super class' do + expect(described_class.superclass).not_to receive(:create).with(context) + end + + it 'creates a new OrderResource with a new Order created by the SubmissionTemplate' do + allow(template).to receive(:create_order!).and_return(order) + allow(described_class).to receive(:new).and_call_original + + described_class.create(context) + + expect(described_class).to have_received(:new).with(order, context) + end + end + end end From 70c8ee39d02a442edfc0d3bc127d055f42f06fdb Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Mon, 16 Dec 2024 12:51:06 +0000 Subject: [PATCH 28/40] Fix Rubocop issues --- spec/resources/api/v2/order_resource_spec.rb | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/spec/resources/api/v2/order_resource_spec.rb b/spec/resources/api/v2/order_resource_spec.rb index d8bad10f00..5428ac9f7e 100644 --- a/spec/resources/api/v2/order_resource_spec.rb +++ b/spec/resources/api/v2/order_resource_spec.rb @@ -30,10 +30,9 @@ describe '#self.create' do context 'without a template in the context' do let(:context) { {} } - let(:resource) { double('OrderResource') } it 'returns an OrderResource created by the super class' do - expect(described_class.superclass).to receive(:create).with(context).and_return(resource) + allow(described_class.superclass).to receive(:create).with(context).and_return(resource) expect(described_class.create(context)).to eq(resource) end @@ -41,21 +40,25 @@ context 'with a template in the context' do let(:context) { { template:, template_attributes: } } - let(:template) { double('SubmissionTemplate') } + let(:template) { instance_double(SubmissionTemplate) } let(:template_attributes) { {} } - let(:order) { create(:order) } + + before { allow(template).to receive(:create_order!).with(template_attributes).and_return(resource_model) } it 'does not call create on the super class' do - expect(described_class.superclass).not_to receive(:create).with(context) + allow(described_class.superclass).to receive(:create) + + described_class.create(context) + + expect(described_class.superclass).not_to have_received(:create) end it 'creates a new OrderResource with a new Order created by the SubmissionTemplate' do - allow(template).to receive(:create_order!).and_return(order) - allow(described_class).to receive(:new).and_call_original + allow(described_class).to receive(:new) described_class.create(context) - expect(described_class).to have_received(:new).with(order, context) + expect(described_class).to have_received(:new).with(resource_model, context) end end end From 44df6507c041409fad9384d712a2152762b606c0 Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Thu, 12 Dec 2024 11:57:55 +0000 Subject: [PATCH 29/40] add huMFre code field to the samples --- app/controllers/samples_controller.rb | 1 + app/models/api/sample_io.rb | 1 + app/models/sample.rb | 10 ++++++++ .../shared/metadata/edit/_sample.html.erb | 2 ++ .../shared/metadata/show/_sample.html.erb | 2 ++ config/locales/metadata/en.yml | 2 ++ ...36_add_hu_m_fre_code_to_sample_metadata.rb | 6 +++++ db/schema.rb | 3 ++- spec/models/api/sample_io_spec.rb | 6 +++-- spec/models/sample_spec.rb | 24 +++++++++++++++++++ 10 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb diff --git a/app/controllers/samples_controller.rb b/app/controllers/samples_controller.rb index 2e21f2782e..a9aa814e8f 100644 --- a/app/controllers/samples_controller.rb +++ b/app/controllers/samples_controller.rb @@ -248,6 +248,7 @@ def default_permitted_metadata_fields subject treatment donor_id + huMFre_code ] } end diff --git a/app/models/api/sample_io.rb b/app/models/api/sample_io.rb index 5acaac3742..b54b90443a 100644 --- a/app/models/api/sample_io.rb +++ b/app/models/api/sample_io.rb @@ -98,6 +98,7 @@ def json_root map_attribute_to_json_attribute(:treatment) map_attribute_to_json_attribute(:date_of_consent_withdrawn) map_attribute_to_json_attribute(:user_id_of_consent_withdrawn, 'marked_as_consent_withdrawn_by') + map_attribute_to_json_attribute(:huMFre_code) end extra_json_attributes do |_object, json_attributes| diff --git a/app/models/sample.rb b/app/models/sample.rb index 105b86ef96..2ea4e9d0da 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -147,6 +147,8 @@ class Current < ActiveSupport::CurrentAttributes custom_attribute(:date_of_consent_withdrawn) custom_attribute(:user_id_of_consent_withdrawn) + custom_attribute(:huMFre_code) + # These fields are warehoused, so need to match the encoding restrictions there # This excludes supplementary characters, which include emoji and rare kanji # @note phenotype isn't currently broadcast but has a field waiting in the warehouse @@ -226,6 +228,14 @@ class Current < ActiveSupport::CurrentAttributes # Sample::Metadata tracks sample information, either for use in the lab, or passing to # the EBI class Metadata + + validates :huMFre_code, + format: { + with: %r{\A\d{2}/\d{2,}|\d{2}/\d{4}-\d{3}\z}, + message: 'must match a valid format, e.g. 12/34 or 12/2023-001' + }, + allow_blank: true + # This constraint doesn't match that described in the manifest, and is more permissive # It was added on conversion of out database to utf8 to address concerns that this would # lead to an increase in characters that their pipeline cannot process. Only a handful diff --git a/app/views/shared/metadata/edit/_sample.html.erb b/app/views/shared/metadata/edit/_sample.html.erb index 999a30919e..02f1f264fc 100644 --- a/app/views/shared/metadata/edit/_sample.html.erb +++ b/app/views/shared/metadata/edit/_sample.html.erb @@ -80,6 +80,8 @@ <%= metadata_fields.text_field(:disease) %> <%= metadata_fields.text_field(:treatment)%> + <%= metadata_fields.text_field(:huMFre_code)%> + <% metadata_fields.finalize_related_fields %> <% end %> <% end %> diff --git a/app/views/shared/metadata/show/_sample.html.erb b/app/views/shared/metadata/show/_sample.html.erb index 23f5aa732f..d75a44439e 100644 --- a/app/views/shared/metadata/show/_sample.html.erb +++ b/app/views/shared/metadata/show/_sample.html.erb @@ -64,5 +64,7 @@ <%= metadata_fields.plain_value(:disease) %> <%= metadata_fields.plain_value(:treatment)%> + <%= metadata_fields.plain_value(:huMFre_code)%> + <% end %> <% end %> diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml index 2abb785013..db6ed0d4de 100644 --- a/config/locales/metadata/en.yml +++ b/config/locales/metadata/en.yml @@ -325,6 +325,8 @@ en: edit_info: "EGA" genome_size: label: Genome Size + huMFre_code: + label: HuMFre Code study: metadata: diff --git a/db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb b/db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb new file mode 100644 index 0000000000..d16094607d --- /dev/null +++ b/db/migrate/20241211143636_add_hu_m_fre_code_to_sample_metadata.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class AddHuMFreCodeToSampleMetadata < ActiveRecord::Migration[7.0] + def change + add_column :sample_metadata, :huMFre_code, :string, limit: 16 + end +end diff --git a/db/schema.rb b/db/schema.rb index 11da36d9d8..187e3c4e3e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_11_06_103710) do +ActiveRecord::Schema[7.0].define(version: 2024_12_11_143636) do create_table "aliquot_indices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", options: "ENGINE=InnoDB ROW_FORMAT=DYNAMIC", force: :cascade do |t| t.integer "aliquot_id", null: false t.integer "lane_id", null: false @@ -1395,6 +1395,7 @@ t.integer "user_id_of_consent_withdrawn" t.boolean "consent_withdrawn", default: false, null: false t.string "collected_by", comment: "Name of persons or institute who collected the specimen" + t.string "huMFre_code", limit: 16 t.index ["sample_ebi_accession_number"], name: "index_sample_metadata_on_sample_ebi_accession_number" t.index ["sample_id"], name: "index_sample_metadata_on_sample_id" t.index ["supplier_name"], name: "index_sample_metadata_on_supplier_name" diff --git a/spec/models/api/sample_io_spec.rb b/spec/models/api/sample_io_spec.rb index 3a24a4e9c7..a219f9150e 100644 --- a/spec/models/api/sample_io_spec.rb +++ b/spec/models/api/sample_io_spec.rb @@ -62,7 +62,8 @@ sample_strain_att: 'stuff about strain', consent_withdrawn: false, donor_id: 2, - developmental_stage: 'thing' + developmental_stage: 'thing', + huMFre_code: '23/21' } ) end @@ -133,7 +134,8 @@ 'developmental_stage' => 'thing', 'donor_id' => '2', 'reference_genome' => 'ReferenceGenome1', - 'component_sample_uuids' => [{ uuid: comp_sample1.uuid }, { uuid: comp_sample2.uuid }] + 'component_sample_uuids' => [{ uuid: comp_sample1.uuid }, { uuid: comp_sample2.uuid }], + 'huMFre_code' => '23/21' } end diff --git a/spec/models/sample_spec.rb b/spec/models/sample_spec.rb index daa588a53d..838ff5f62c 100644 --- a/spec/models/sample_spec.rb +++ b/spec/models/sample_spec.rb @@ -259,6 +259,30 @@ end end + context 'huMFre code' do + let(:sample) { create(:sample) } + + it 'defaults to null when not specified' do + expect(sample.sample_metadata.huMFre_code).to be_nil + end + + it 'fails to update/save when huMFre code value is invalid' do + expect(sample.sample_metadata.huMFre_code).to be_nil + sample.sample_metadata.huMFre_code = 'humFre1' + expect(sample.sample_metadata.save).to be false + end + + it 'contains huMFre code value when it is correctly specified' do + sample.sample_metadata.update!(huMFre_code: '12/12') + expect(sample.sample_metadata.huMFre_code).to eq '12/12' + end + + it 'can have the huMFre code blanked' do + sample.sample_metadata.update!(huMFre_code: nil) + expect(sample.sample_metadata.huMFre_code).to be_nil + end + end + context '(DPL-148) on updating sample metadata' do let(:sample) { create(:sample) } From debfce3821877fe8eb28519c0e29dd95919cbb55 Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Thu, 12 Dec 2024 12:01:53 +0000 Subject: [PATCH 30/40] run prettier --- app/models/sample.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/sample.rb b/app/models/sample.rb index 2ea4e9d0da..5ea3f68a78 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -228,7 +228,6 @@ class Current < ActiveSupport::CurrentAttributes # Sample::Metadata tracks sample information, either for use in the lab, or passing to # the EBI class Metadata - validates :huMFre_code, format: { with: %r{\A\d{2}/\d{2,}|\d{2}/\d{4}-\d{3}\z}, From 7ef8fb8564bd5c61c77e7ddf281b466dfda8ec5e Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Thu, 12 Dec 2024 13:20:37 +0000 Subject: [PATCH 31/40] fix cucumber test --- features/samples/xml_interface.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/samples/xml_interface.feature b/features/samples/xml_interface.feature index 26f533159e..960ec1e0d4 100644 --- a/features/samples/xml_interface.feature +++ b/features/samples/xml_interface.feature @@ -68,6 +68,7 @@ Feature: The XML interface to the samples Time Point Donor Id Collected By + HuMFre Code """ From 168c92e638339062f6e2704e327b853e3220fc3f Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Wed, 18 Dec 2024 10:57:14 +0000 Subject: [PATCH 32/40] add length constrain to the huMFre code --- app/models/sample.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/sample.rb b/app/models/sample.rb index 5ea3f68a78..1dc5ee358b 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -228,9 +228,13 @@ class Current < ActiveSupport::CurrentAttributes # Sample::Metadata tracks sample information, either for use in the lab, or passing to # the EBI class Metadata + # HuMFre numbers contain tissue information, which is only relevant for human samples. validates :huMFre_code, + length: { + maximum: 16 + }, format: { - with: %r{\A\d{2}/\d{2,}|\d{2}/\d{4}-\d{3}\z}, + with: %r{\A(?:\d{2}/\d{2,}|\d{2}/\d{4}-\d{3})\z}, message: 'must match a valid format, e.g. 12/34 or 12/2023-001' }, allow_blank: true From 1ee65bb199f200583f6cd03c8fc08f189ebcf0bf Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:34:36 +0000 Subject: [PATCH 33/40] Update rails to version 7.0.8.7 --- Gemfile.lock | 133 ++++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 66 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 60c8c361b3..f9f71d0799 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,34 +34,34 @@ GEM specs: aasm (5.5.0) concurrent-ruby (~> 1.0) - actioncable (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + actioncable (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionmailbox (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.1) - actionpack (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionmailer (7.0.8.7) + actionpack (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.1) - actionview (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionpack (7.0.8.7) + actionview (= 7.0.8.7) + activesupport (= 7.0.8.7) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) @@ -69,37 +69,37 @@ GEM actionpack-xml_parser (2.0.1) actionpack (>= 5.0) railties (>= 5.0) - actiontext (7.0.8.1) - actionpack (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + actiontext (7.0.8.7) + actionpack (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.1) - activesupport (= 7.0.8.1) + actionview (7.0.8.7) + activesupport (= 7.0.8.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.1) - activesupport (= 7.0.8.1) + activejob (7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.3.6) - activemodel (7.0.8.1) - activesupport (= 7.0.8.1) - activerecord (7.0.8.1) - activemodel (= 7.0.8.1) - activesupport (= 7.0.8.1) + activemodel (7.0.8.7) + activesupport (= 7.0.8.7) + activerecord (7.0.8.7) + activemodel (= 7.0.8.7) + activesupport (= 7.0.8.7) activerecord-import (1.7.0) activerecord (>= 4.2) - activestorage (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activesupport (= 7.0.8.1) + activestorage (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activesupport (= 7.0.8.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.1) + activesupport (7.0.8.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -196,7 +196,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.0) + date (3.4.1) delayed_job (4.1.11) activesupport (>= 3.0, < 8.0) delayed_job_active_record (4.1.8) @@ -206,7 +206,7 @@ GEM docile (1.4.0) domain_name (0.6.20240107) dry-cli (1.0.0) - erubi (1.13.0) + erubi (1.13.1) exception_notification (4.5.0) actionmailer (>= 5.2, < 8) activesupport (>= 5.2, < 8) @@ -257,7 +257,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.6.1) - loofah (2.23.1) + loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -273,7 +273,7 @@ GEM mime-types-data (3.2024.0305) mini_magick (4.12.0) mini_mime (1.1.5) - minitest (5.25.1) + minitest (5.25.4) minitest-profiler (0.0.2) activesupport (>= 4.1.0) minitest (>= 5.3.3) @@ -285,7 +285,7 @@ GEM mustermann (3.0.0) ruby2_keywords (~> 0.0.1) mysql2 (0.5.6) - net-imap (0.5.1) + net-imap (0.5.5) date net-protocol net-ldap (0.19.0) @@ -297,11 +297,11 @@ GEM net-protocol netrc (0.11.0) nio4r (2.7.4) - nokogiri (1.16.7-arm64-darwin) + nokogiri (1.18.1-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-darwin) + nokogiri (1.18.1-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.18.1-x86_64-linux-gnu) racc (~> 1.4) ostruct (0.6.0) parallel (1.26.3) @@ -334,22 +334,22 @@ GEM rack (~> 2.2, >= 2.2.4) rack-proxy (0.7.7) rack - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rails (7.0.8.1) - actioncable (= 7.0.8.1) - actionmailbox (= 7.0.8.1) - actionmailer (= 7.0.8.1) - actionpack (= 7.0.8.1) - actiontext (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activemodel (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + rails (7.0.8.7) + actioncable (= 7.0.8.7) + actionmailbox (= 7.0.8.7) + actionmailer (= 7.0.8.7) + actionpack (= 7.0.8.7) + actiontext (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activemodel (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) bundler (>= 1.15.0) - railties (= 7.0.8.1) + railties (= 7.0.8.7) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -363,13 +363,13 @@ GEM activesupport (>= 4.2) choice (~> 0.2.0) ruby-graphviz (~> 1.2) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-perftest (0.0.7) - railties (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + railties (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) method_source rake (>= 12.2) thor (~> 1.0) @@ -515,7 +515,7 @@ GEM thor (1.3.2) tilt (2.4.0) timecop (0.9.10) - timeout (0.4.2) + timeout (0.4.3) traceroute (0.8.1) rails (>= 3.0.0) tzinfo (2.0.6) @@ -535,7 +535,8 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) - websocket-driver (0.7.6) + websocket-driver (0.7.7) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) whenever (1.0.0) From 1e862057780917791dbbcf17da4d72cf23b5d04a Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:06:10 +0000 Subject: [PATCH 34/40] Update factory_bot_rails to version 6.4.4 --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f9f71d0799..d6d9f67284 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -210,10 +210,10 @@ GEM exception_notification (4.5.0) actionmailer (>= 5.2, < 8) activesupport (>= 5.2, < 8) - factory_bot (6.4.6) + factory_bot (6.5.0) activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) railties (>= 5.0.0) ffi (1.16.3) flipper (0.25.4) From 7238682efc8ca76417f5014a4759447b1eec22bd Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 07:17:05 +0000 Subject: [PATCH 35/40] Update Node.js to version 22.13.0 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 7af24b7ddb..6fa8dec4cd 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.11.0 +22.13.0 From 350bf8d240bdbd9b972661738ae5596dba5d81d4 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:54:18 +0000 Subject: [PATCH 36/40] Update postcss to version 8.4.49 --- package.json | 2 +- yarn.lock | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f11641e6b..af53721034 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "jsbarcode": "^3.11.6", "jszip": "^3.10.1", "popper.js": "^1.16.1", - "postcss": "^8.4.39", + "postcss": "^8.4.49", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^14.1.0", "postcss-preset-env": "^7.8.3", diff --git a/yarn.lock b/yarn.lock index 5f00b9aca4..360d2c0f3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5769,6 +5769,11 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -6130,6 +6135,15 @@ postcss@^8.4.33: picocolors "^1.0.0" source-map-js "^1.2.0" +postcss@^8.4.49: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -6724,6 +6738,11 @@ sortablejs@^1.15.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" From 2875b4b59bfd643a66923450f65d0444e0cac79e Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:58:57 +0000 Subject: [PATCH 37/40] Update @rails/ujs to version 7.1.501 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7f11641e6b..542b2c006c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", - "@rails/ujs": "^7.1.3", + "@rails/ujs": "^7.1.501", "@ttskch/select2-bootstrap4-theme": "^1.5.2", "bootstrap": "^4.6.2", "css-loader": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 5f00b9aca4..85cabb6a73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1911,10 +1911,10 @@ resolved "https://registry.yarnpkg.com/@prettier/plugin-ruby/-/plugin-ruby-4.0.4.tgz#73d85fc2a1731a3f62b57ac3116cf1c234027cb6" integrity sha512-lCpvfS/dQU5WrwN3AQ5vR8qrvj2h5gE41X08NNzAAXvHdM4zwwGRcP2sHSxfu6n6No+ljWCVx95NvJPFTTjCTg== -"@rails/ujs@^7.1.3": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.3.tgz#6d94a68b7da5046147d31716e0c187a4ead4fb93" - integrity sha512-FxtgKNvvIonoBE1TK7U10VMf6CYvzq8SIZ1XZ1Q8zcn/BEXzPzid3zC9qFiojuI5WXVwWhO8GFqApq0stD+OqQ== +"@rails/ujs@^7.1.501": + version "7.1.501" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.501.tgz#e560a7b6885a12a659c4beb47f4336c8a9353056" + integrity sha512-7EDRGUlgns12IgP3SXVSaxA3CwRzbLOypPXn1EqEZiZ/NS/PwaQ/oa7Z2VRO4B46JifoVr0PYg+G5ERSGQJHxQ== "@rollup/pluginutils@^4.1.1": version "4.1.2" From bcf93b1883d9178e58e459091b353a278157f91c Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:41:57 +0000 Subject: [PATCH 38/40] Update Ruby to version 3.2.6 --- .ruby-version | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index 5ae69bd5f0..34cde5690e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.5 +3.2.6 diff --git a/Dockerfile b/Dockerfile index f64ba4780f..53d2b9e0ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ARG CHIPSET=default # Use the correct base image depending on the architecture # For Apple M1 Chip, run: docker build --build-arg CHIPSET=m1 . -FROM ruby:3.2.5-slim AS base_default +FROM ruby:3.2.6-slim AS base_default FROM --platform=linux/amd64 ruby:3.2.5-slim AS base_m1 FROM base_${CHIPSET} AS base From edecf3750d294b3658f3c7de8ee377941e027d50 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:47:57 +0000 Subject: [PATCH 39/40] Update Ruby to version 3.3.6 --- .ruby-version | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index 34cde5690e..9c25013dbb 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.6 +3.3.6 diff --git a/Dockerfile b/Dockerfile index 53d2b9e0ea..8209798ccf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ARG CHIPSET=default # Use the correct base image depending on the architecture # For Apple M1 Chip, run: docker build --build-arg CHIPSET=m1 . -FROM ruby:3.2.6-slim AS base_default +FROM ruby:3.3.6-slim AS base_default FROM --platform=linux/amd64 ruby:3.2.5-slim AS base_m1 FROM base_${CHIPSET} AS base From 4262bc5115e82cccbdce140db084356203dcc2a5 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 23:34:38 +0000 Subject: [PATCH 40/40] Update ruby-prof to version 1.7.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d6d9f67284..1c4933d500 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -454,7 +454,7 @@ GEM rubocop-rspec (~> 3, >= 3.0.1) ruby-graphviz (1.2.5) rexml - ruby-prof (1.7.0) + ruby-prof (1.7.1) ruby-progressbar (1.13.0) ruby-units (4.0.3) ruby-vips (2.2.1)