diff --git a/app/models/presenters/pipeline_info_presenter.rb b/app/models/presenters/pipeline_info_presenter.rb index 01787cabb..61f231d58 100644 --- a/app/models/presenters/pipeline_info_presenter.rb +++ b/app/models/presenters/pipeline_info_presenter.rb @@ -29,22 +29,20 @@ def initialize(labware) # # In some cases, an intersection of these three groups might be required to accurately determine # the pipeline and pipeline group, in others the parent purpose might be used to determine the pipeline. - def pipeline_groups - pipeline_groups = [ - pipeline_groups_by_purpose, # allows matching pipeline purposes with no active requests - pipeline_groups_by_parent_purposes, # any matching pipeline MIGHT match the labware's parent purposes - pipeline_groups_by_requests_and_libraries # any matching pipeline MIGHT match the active requests and library type - ] - - # Remove nil values and empty arrays from the pipeline_groups array - pipeline_groups.compact! - pipeline_groups.reject!(&:empty?) - - # intersect the pipeline groups to find the common pipelines - pipeline_groups = pipeline_groups.reduce { |acc, group| acc & group } - - # Return the remaining pipeline groups as a sorted array, or nil if none are found. - pipeline_groups.sort if pipeline_groups&.any? + def pipeline_groups # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize + # Build arrays from different methods, removing nils and empties + all_groups = [groups_by_purpose, groups_by_parent, groups_by_filters].compact.reject(&:empty?) + purpose_filter_groups = [groups_by_purpose, groups_by_filters].compact.reject(&:empty?) + + # Find intersection if possible + intersected_all_groups = all_groups.reduce(&:&) + intersected_purpose_filters = purpose_filter_groups.reduce(&:&) + + # Prefer full intersection, then partial, else empty array + return intersected_all_groups.sort if intersected_all_groups&.any? + return intersected_purpose_filters.sort if intersected_purpose_filters&.any? + + [] end # Returns the pipeline group name if there is only one pipeline group, otherwise nil. @@ -92,17 +90,12 @@ def child_purposes private - def cache_key - "pipeline_info_presenter/#{@labware.barcode}" - end - - def join_up_to(max_listed, array, separator = ', ') - return array.join(separator) if array.size <= max_listed - - "#{array[0..(max_listed - 1)].join(separator)}, ...(#{array.size - max_listed} more)" - end - - def pipeline_groups_by_purpose + # Returns the pipeline group names associated with the labware's purpose from the pipeline configuration. + # Some purposes are associated with multiple pipelines. + # Allows matching of pipeline purposes with no active requests. + # + # @return [Array] Array of unique pipeline group names. + def groups_by_purpose Settings .pipelines .select_pipelines_with_purpose(Settings.pipelines.list, @labware.purpose) @@ -110,7 +103,30 @@ def pipeline_groups_by_purpose .uniq end - def pipeline_groups_by_requests_and_libraries + # Returns the pipeline group names associated with the purposes of the labware's parents. + # Any matching pipeline MIGHT match the labware's parent purposes. + # This method is most useful for cases where the parent labware purpose is more specific to the pipeline group, + # such as with 'LB Lib Pool Norm' tubes. It finds all parent labware, extracts their purposes, + # and determines the intersection of pipeline groups associated with those purposes. + # This approach could cause problems if the parent labware is in a different pipeline, + # however, in this case it is normally in the same pipeline *group*. + # + # @return [Array] Array of unique pipeline group names. + def groups_by_parent + find_all_labware_parents_with_purposes + .map(&:purpose) + .map do |parent_purpose| + Settings.pipelines.select_pipelines_with_purpose(Settings.pipelines.list, parent_purpose).map(&:pipeline_group) + end + .reduce(:&)&.compact + end + + # Returns the pipeline group name determined by the active pipelines for the labware, + # based on its requests and library type. + # Any matching pipeline MIGHT match the active requests and library type. + # + # @return [Array] Array of unique pipeline group names. + def groups_by_filters active_pipelines.map(&:pipeline_group).uniq end @@ -130,17 +146,13 @@ def find_all_labware_parents_with_purposes_uncached(labwares) ) end - # On `LB Lib Pool Norm` tubes, it's hard to find the correct pipeline, - # because the same purpose and request type is used in many pipelines. - # This looks at the parent labware, as this is normally specific to the pipeline. - # This approach could cause problems if the parent labware is in a different pipeline, - # however, in this case it is normally in the same pipeline *group*. - def pipeline_groups_by_parent_purposes - find_all_labware_parents_with_purposes - .map(&:purpose) - .map do |parent_purpose| - Settings.pipelines.select_pipelines_with_purpose(Settings.pipelines.list, parent_purpose).map(&:pipeline_group) - end - .reduce(:&) + def cache_key + "pipeline_info_presenter/#{@labware.barcode}" + end + + def join_up_to(max_listed, array, separator = ', ') + return array.join(separator) if array.size <= max_listed + + "#{array[0..(max_listed - 1)].join(separator)}, ...(#{array.size - max_listed} more)" end end diff --git a/spec/models/presenters/pipeline_info_presenter_spec.rb b/spec/models/presenters/pipeline_info_presenter_spec.rb index 608ca3ee5..2ce200f97 100644 --- a/spec/models/presenters/pipeline_info_presenter_spec.rb +++ b/spec/models/presenters/pipeline_info_presenter_spec.rb @@ -56,8 +56,8 @@ def stub_labware_find_all_barcode(labwares) context 'when no pipelines match' do let(:pipelines_config) { {} } - it 'returns nil' do - expect(presenter.pipeline_groups).to be_nil + it 'returns an empty array' do + expect(presenter.pipeline_groups).to eq([]) end end @@ -134,6 +134,49 @@ def stub_labware_find_all_barcode(labwares) end end + context 'when pipelines match by purpose and library type' do + # Based on Bioscan pipeline + let(:pipelines_config) do + { + 'bioscan_lysate_prep' => { + filters: { + request_type_key: 'limber_bioscan_lysate_prep' + }, + relationships: { + 'LILYS-96 Stock' => 'LBSN-96 Lysate' + } + }, + 'bioscan_library_prep' => { + filters: { + request_type_key: 'limber_bioscan_library_prep', + library_type: 'Bioscan' + }, + relationships: { + 'LBSN-96 Lysate' => 'LBSN-384 PCR 1' + } + } + } + end + let(:purpose) { create(:purpose, name: 'LBSN-96 Lysate') } + let(:labware) { create(:plate, purpose:) } + + it 'returns the pipeline group matching the purpose' do + expect(presenter.pipeline_groups).to eq(%w[bioscan_library_prep bioscan_lysate_prep]) + end + + context 'with added request filter' do + let(:request_type) { create(:request_type, key: 'limber_bioscan_library_prep') } + let(:request) { [create(:request, request_type: request_type, library_type: 'Bioscan', state: 'passed')] } + let(:aliquot) { [create(:aliquot, request:)] } + let(:wells) { [create(:well, aliquots: aliquot, location: 'A1')] } + let(:labware) { create(:plate, purpose:, wells:) } + + it 'returns the pipeline group matching the purpose and library type' do + expect(presenter.pipeline_groups).to eq(['bioscan_library_prep']) + end + end + end + context 'when the scenario is a linear config' do # simple_linear_config: a simple chain of 3 purposes in a row, belonging to the same pipeline group. # All three should return the same pipeline group.