diff --git a/app/helpers/page_helper.rb b/app/helpers/page_helper.rb index a3bb45818..11e5ba153 100644 --- a/app/helpers/page_helper.rb +++ b/app/helpers/page_helper.rb @@ -53,7 +53,6 @@ def jumbotron(jumbotron_id = nil, options = {}, &) # eg. state_badge('pending') # Pending def state_badge(state, title: 'Labware State') - return if state.blank? # added as TubeRack has a nil state tag.span(state.titleize, class: "state-badge #{state}", title: title, data: { toggle: 'tooltip' }) end diff --git a/app/models/presenters/tube_rack_presenter.rb b/app/models/presenters/tube_rack_presenter.rb index 9ed18e07f..fbb757930 100644 --- a/app/models/presenters/tube_rack_presenter.rb +++ b/app/models/presenters/tube_rack_presenter.rb @@ -23,16 +23,7 @@ class TubeRackPresenter # Returns an augmented state of the tube rack. # All states take precedence over canceled and failed (in that order or priority) # however if we still have a mixed state after that, we display it as such. - def state - states = all_tubes.pluck(:state).uniq - return states.first if states.one? - - %w[cancelled failed].each do |filter| - states.delete(filter) - return states.first if states.one? - end - 'mixed' - end + delegate :state, to: :labware def priority all_tubes.map(&:priority).max diff --git a/app/sequencescape/sequencescape/api/v2/tube.rb b/app/sequencescape/sequencescape/api/v2/tube.rb index 26240852c..b2d5fcaf9 100644 --- a/app/sequencescape/sequencescape/api/v2/tube.rb +++ b/app/sequencescape/sequencescape/api/v2/tube.rb @@ -29,6 +29,9 @@ class Sequencescape::Api::V2::Tube < Sequencescape::Api::V2::Base has_one :custom_metadatum_collection has_one :receptacle, class_name: 'Sequencescape::Api::V2::Receptacle' + has_many :racked_tubes, class_name: 'Sequencescape::Api::V2::RackedTube' + has_many :tube_racks, through: :racked_tubes, class_name: 'Sequencescape::Api::V2::TubeRack' + property :created_at, type: :time property :updated_at, type: :time diff --git a/app/sequencescape/sequencescape/api/v2/tube_rack.rb b/app/sequencescape/sequencescape/api/v2/tube_rack.rb index 1e28cc6ae..5a2fd583b 100644 --- a/app/sequencescape/sequencescape/api/v2/tube_rack.rb +++ b/app/sequencescape/sequencescape/api/v2/tube_rack.rb @@ -12,6 +12,8 @@ class Sequencescape::Api::V2::TubeRack < Sequencescape::Api::V2::Base self.tube_rack = true + STATES_TO_FILTER_OUT = %w[cancelled failed].freeze + # This is needed in order for the URL helpers to work correctly def to_param uuid @@ -28,6 +30,8 @@ def model_name end has_many :racked_tubes, class_name: 'Sequencescape::Api::V2::RackedTube' + has_many :tubes, through: :racked_tubes, class_name: 'Sequencescape::Api::V2::Tube' + has_many :parents, class_name: 'Sequencescape::Api::V2::Asset' property :name @@ -42,6 +46,33 @@ def stock_plate nil end + # This method determines the state of the tube rack based on the states of the racked tubes. + # It returns a single state if all racked tubes have the same state. + # If there are multiple states, it filters out first 'cancelled' and then 'failed' states and + # returns the remaining state if only one remains. + # If there are still multiple states after filtering, it returns 'mixed'. + # i.e. if all tubes are pending, the state will be pending + # i.e. if all tubes are failed, the state will be failed + # i.e. if we have a mix of cancelled and failed tubes, the state will be failed as we filter out + # the cancelled tubes first + # i.e. if we have a mix of cancelled, failed and pending tubes, the state will be pending + # i.e. if we have a mix of cancelled, failed, pending and passed tubes, the state will be mixed + # + # @return [String] the state of the tube rack + def state + states = racked_tubes.map { |racked_tube| racked_tube.tube.state }.uniq + return states.first if states.one? + + # filter out cancelled tubes first, and then if still a mix filter out the failed tubes + STATES_TO_FILTER_OUT.each do |filter| + states.delete(filter) + return states.first if states.one? + end + + # if we still have a mixed state after that, we display it as such + 'mixed' + end + private # This method iterates over all racked tubes in the tube rack and retrieves the diff --git a/spec/factories/tube_rack_factories.rb b/spec/factories/tube_rack_factories.rb index 807f3a847..e7642a340 100644 --- a/spec/factories/tube_rack_factories.rb +++ b/spec/factories/tube_rack_factories.rb @@ -43,17 +43,15 @@ end factory :racked_tube, class: Sequencescape::Api::V2::RackedTube do + # skips normal create and uses the after(:build) block to set up the relationships skip_create initialize_with { Sequencescape::Api::V2::RackedTube.load(attributes) } id coordinate { 'A1' } - - transient do - tube_rack { create :tube_rack } - tube { create :v2_tube } - end + tube_rack { create :tube_rack } + tube { create :v2_tube } after(:build) do |racked_tube, evaluator| Sequencescape::Api::V2::RackedTube.associations.each do |association| diff --git a/spec/sequencescape/api/v2/tube_rack_spec.rb b/spec/sequencescape/api/v2/tube_rack_spec.rb index 96b750fa7..cdc68d1f5 100644 --- a/spec/sequencescape/api/v2/tube_rack_spec.rb +++ b/spec/sequencescape/api/v2/tube_rack_spec.rb @@ -113,4 +113,69 @@ expect(model_name.singular_route_key).to eq('limber_tube_rack') end end + + describe '#state' do + let(:tube1) { create :v2_tube, uuid: 'tube1_uuid', state: state_for_tube1, barcode_number: 1 } + let(:tube2) { create :v2_tube, uuid: 'tube2_uuid', state: state_for_tube2, barcode_number: 2 } + let(:tube3) { create :v2_tube, uuid: 'tube3_uuid', state: state_for_tube3, barcode_number: 3 } + + let(:tubes) { { 'A1' => tube1, 'B1' => tube2, 'C1' => tube3 } } + + let!(:test_tube_rack) { build :tube_rack, barcode_number: 5, tubes: tubes } + + context 'when all racked tubes have the same state' do + let(:state_for_tube1) { 'pending' } + let(:state_for_tube2) { 'pending' } + let(:state_for_tube3) { 'pending' } + + it 'returns the state of the racked tubes' do + expect(test_tube_rack.state).to eq('pending') + end + end + + context 'when racked tubes have mixed states' do + let(:state_for_tube1) { 'pending' } + let(:state_for_tube2) { 'passed' } + let(:state_for_tube3) { 'pending' } + + it 'returns "mixed"' do + expect(test_tube_rack.state).to eq('mixed') + end + end + + context 'when racked tubes have mixed states including cancelled and failed' do + let(:state_for_tube1) { 'pending' } + let(:state_for_tube2) { 'cancelled' } + let(:state_for_tube3) { 'failed' } + + it 'returns the remaining state after filtering out cancelled and failed' do + expect(test_tube_rack.state).to eq('pending') + end + end + + context 'when racked tubes have only cancelled and failed states' do + let(:state_for_tube1) { 'failed' } + let(:state_for_tube2) { 'cancelled' } + let(:state_for_tube3) { 'failed' } + + it 'returns "failed" as we first filter the cancelled one out' do + expect(test_tube_rack.state).to eq('failed') + end + end + + context 'when there are still mixed states after filtering out cancelled and failed' do + let(:state_for_tube1) { 'pending' } + let(:state_for_tube2) { 'passed' } + let(:state_for_tube3) { 'failed' } + let(:state_for_tube4) { 'cancelled' } + + let(:tube4) { create :v2_tube, uuid: 'tube3_uuid', state: state_for_tube4, barcode_number: 4 } + + let(:tubes) { { 'A1' => tube1, 'B1' => tube2, 'C1' => tube3, 'D1' => tube4 } } + + it 'returns "mixed"' do + expect(test_tube_rack.state).to eq('mixed') + end + end + end end