From b61a527a171872da178873415e52ed20ea26f79d Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Wed, 18 Dec 2024 09:48:24 +0000 Subject: [PATCH 1/5] Use V2 for Qcable resources --- app/controllers/tag_plates_controller.rb | 5 ++-- app/models/presenters/qcable_presenter.rb | 30 ++++++++++++++----- app/sequencescape/sequencescape/api/v2/lot.rb | 10 +++++++ .../sequencescape/api/v2/lot_type.rb | 6 ++++ .../sequencescape/api/v2/purpose.rb | 1 - .../sequencescape/api/v2/qcable.rb | 7 +++++ 6 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 app/sequencescape/sequencescape/api/v2/lot.rb create mode 100644 app/sequencescape/sequencescape/api/v2/lot_type.rb create mode 100644 app/sequencescape/sequencescape/api/v2/qcable.rb diff --git a/app/controllers/tag_plates_controller.rb b/app/controllers/tag_plates_controller.rb index 76dfec1f9..b06ded75f 100644 --- a/app/controllers/tag_plates_controller.rb +++ b/app/controllers/tag_plates_controller.rb @@ -5,7 +5,8 @@ # The front end makes a decision regarding suitability class TagPlatesController < ApplicationController def show - qcable = Presenters::QcablePresenter.new(api.qcable.find(params[:id])) - respond_to { |format| format.json { render json: { 'qcable' => qcable } } } + qcable_resource = Sequencescape::Api::V2::Qcable.find_by(uuid: params[:id]) + qcable_presenter = Presenters::QcablePresenter.new(qcable_resource) + respond_to { |format| format.json { render json: { 'qcable' => qcable_presenter } } } end end diff --git a/app/models/presenters/qcable_presenter.rb b/app/models/presenters/qcable_presenter.rb index 6fc495700..7ff453245 100644 --- a/app/models/presenters/qcable_presenter.rb +++ b/app/models/presenters/qcable_presenter.rb @@ -3,15 +3,31 @@ module Presenters # Used for tag plates / tag 2 tubes. Rendered with default to_json behaviour. class QcablePresenter - def initialize(qcable) # rubocop:todo Metrics/AbcSize - @uuid = qcable.uuid - @tag_layout = qcable.lot.template_name + def initialize(qcable) + init_qcable_attributes(qcable) + init_lot_attributes(qcable.lot) + init_lot_type_attributes(qcable.lot.lot_type) + init_template_attributes(qcable.lot.template) + end + + def init_qcable_attributes(qcable) @asset_uuid = qcable.asset.uuid @state = qcable.state - @type = qcable.lot.lot_type_name - @qcable_type = qcable.lot.lot_type.qcable_name - @template_uuid = qcable.lot.template.uuid - @lot_number = qcable.lot.lot_number + @uuid = qcable.uuid + end + + def init_lot_attributes(lot) + @lot_number = lot.lot_number + end + + def init_lot_type_attributes(lot_type) + @qcable_type = lot_type.target_purpose.name + @type = lot_type.name + end + + def init_template_attributes(template) + @tag_layout = template.name + @template_uuid = template.uuid end end end diff --git a/app/sequencescape/sequencescape/api/v2/lot.rb b/app/sequencescape/sequencescape/api/v2/lot.rb new file mode 100644 index 000000000..ef4728efd --- /dev/null +++ b/app/sequencescape/sequencescape/api/v2/lot.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# A Lot from sequencescape via the V2 API +class Sequencescape::Api::V2::Lot < Sequencescape::Api::V2::Base + has_one :lot_type + + # The template is is a polymorphic relationship in Sequencescape, but we only want to access the UUID + # and so we don't need a specific class since all properties are accessible via the base class. + has_one :template, class_name: 'Sequencescape::Api::V2::Base' +end diff --git a/app/sequencescape/sequencescape/api/v2/lot_type.rb b/app/sequencescape/sequencescape/api/v2/lot_type.rb new file mode 100644 index 000000000..fd815932f --- /dev/null +++ b/app/sequencescape/sequencescape/api/v2/lot_type.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# A LotType from sequencescape via the V2 API +class Sequencescape::Api::V2::LotType < Sequencescape::Api::V2::Base + has_one :target_purpose, class_name: 'Sequencescape::Api::V2::Purpose' +end diff --git a/app/sequencescape/sequencescape/api/v2/purpose.rb b/app/sequencescape/sequencescape/api/v2/purpose.rb index 83dccd9f0..9966d804f 100644 --- a/app/sequencescape/sequencescape/api/v2/purpose.rb +++ b/app/sequencescape/sequencescape/api/v2/purpose.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true class Sequencescape::Api::V2::Purpose < Sequencescape::Api::V2::Base - UNKNOWN = 'UNKNOWN' end diff --git a/app/sequencescape/sequencescape/api/v2/qcable.rb b/app/sequencescape/sequencescape/api/v2/qcable.rb new file mode 100644 index 000000000..fa0625e76 --- /dev/null +++ b/app/sequencescape/sequencescape/api/v2/qcable.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# A Qcable from sequencescape via the V2 API +class Sequencescape::Api::V2::Qcable < Sequencescape::Api::V2::Base + has_one :lot + has_one :asset +end From 6b643a2a78b0242c838e691a0291a42e656e5178 Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Wed, 18 Dec 2024 17:39:49 +0000 Subject: [PATCH 2/5] Use the non-deprecated labware relationship on Qcables --- app/models/presenters/qcable_presenter.rb | 2 +- app/sequencescape/sequencescape/api/v2/qcable.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/presenters/qcable_presenter.rb b/app/models/presenters/qcable_presenter.rb index 7ff453245..43ce7e97f 100644 --- a/app/models/presenters/qcable_presenter.rb +++ b/app/models/presenters/qcable_presenter.rb @@ -11,7 +11,7 @@ def initialize(qcable) end def init_qcable_attributes(qcable) - @asset_uuid = qcable.asset.uuid + @asset_uuid = qcable.labware.uuid @state = qcable.state @uuid = qcable.uuid end diff --git a/app/sequencescape/sequencescape/api/v2/qcable.rb b/app/sequencescape/sequencescape/api/v2/qcable.rb index fa0625e76..3afa57683 100644 --- a/app/sequencescape/sequencescape/api/v2/qcable.rb +++ b/app/sequencescape/sequencescape/api/v2/qcable.rb @@ -2,6 +2,6 @@ # A Qcable from sequencescape via the V2 API class Sequencescape::Api::V2::Qcable < Sequencescape::Api::V2::Base + has_one :labware has_one :lot - has_one :asset end From 049fc2335f6047b632c6d117aa74b77235f07135 Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Fri, 10 Jan 2025 11:52:26 +0000 Subject: [PATCH 3/5] Remove v1 stubs that aren't doing anything --- spec/features/creating_a_tag_plate_spec.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/features/creating_a_tag_plate_spec.rb b/spec/features/creating_a_tag_plate_spec.rb index fc2889b37..7d78aea53 100644 --- a/spec/features/creating_a_tag_plate_spec.rb +++ b/spec/features/creating_a_tag_plate_spec.rb @@ -91,10 +91,6 @@ stub_v2_barcode_printers(create_list(:v2_plate_barcode_printer, 3)) stub_v2_tag_layout_templates(templates) - # TODO: {Y24-190} Get rid of these v1 stubs after tag_layout_templates are moved to v2 in tagged_plate.rb - stub_api_get(tag_template_uuid, body: json(:tag_layout_template, uuid: tag_template_uuid)) - stub_api_post(tag_template_uuid, body: json(:tag_layout_template, uuid: tag_template_uuid)) - # API v1 UUID requests for a qcable via qcable_presenter. stub_api_get(tag_plate_qcable_uuid, body: tag_plate_qcable) stub_api_get('lot-uuid', body: json(:tag_lot, lot_number: tag_lot_number, template_uuid: tag_template_uuid)) From 4c1462c9cb990d2d6a34eecf7e90650908de0854 Mon Sep 17 00:00:00 2001 From: Harriet Craven Date: Fri, 10 Jan 2025 15:16:00 +0000 Subject: [PATCH 4/5] Added qcable test for v2 api --- app/controllers/tag_plates_controller.rb | 6 ++- spec/factories/qcable_factories.rb | 47 ++++++++++++++++++++++ spec/features/creating_a_tag_plate_spec.rb | 7 ++-- spec/support/api_url_helper.rb | 8 ++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/app/controllers/tag_plates_controller.rb b/app/controllers/tag_plates_controller.rb index b06ded75f..77538b7cc 100644 --- a/app/controllers/tag_plates_controller.rb +++ b/app/controllers/tag_plates_controller.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true # Receives AJAX requests when creating tag plates, returns the -# plate information eg. lot number, template, status +# plate information eg. lot number, template # The front end makes a decision regarding suitability class TagPlatesController < ApplicationController + INCLUDES = [:labware, { lot: [{ lot_type: :target_purpose }, :template] }].freeze + def show - qcable_resource = Sequencescape::Api::V2::Qcable.find_by(uuid: params[:id]) + qcable_resource = Sequencescape::Api::V2::Qcable.includes(*INCLUDES).find(uuid: params[:id]).first qcable_presenter = Presenters::QcablePresenter.new(qcable_resource) respond_to { |format| format.json { render json: { 'qcable' => qcable_presenter } } } end diff --git a/spec/factories/qcable_factories.rb b/spec/factories/qcable_factories.rb index 761c51598..ceb3fe90a 100644 --- a/spec/factories/qcable_factories.rb +++ b/spec/factories/qcable_factories.rb @@ -1,6 +1,53 @@ # frozen_string_literal: true FactoryBot.define do + # API V2 QCable + factory :v2_qcable, class: Sequencescape::Api::V2::Qcable, traits: [:barcoded_v2] do + skip_create + + uuid + state { 'available' } + + transient do + labware { create :v2_plate } + lot { create :v2_lot } + end + + after(:build) do |qcable, factory| + qcable._cached_relationship(:labware) { factory.labware } if factory.labware + qcable._cached_relationship(:lot) { factory.lot } if factory.lot + qcable._cached_relationship(:asset) { factory.labware } if factory.labware # alias for labware + end + end + + factory :v2_lot, class: Sequencescape::Api::V2::Lot do + skip_create + + sequence(:lot_number) { |n| "UAT12345.#{n}" } + + transient do + lot_type { create :v2_lot_type } + template { create :v2_tag_layout_template } + end + + after(:build) do |lot, factory| + lot._cached_relationship(:lot_type) { factory.lot_type } if factory.lot_type + lot._cached_relationship(:template) { factory.template } if factory.template + end + end + + factory :v2_lot_type, class: Sequencescape::Api::V2::LotType do + skip_create + + sequence(:name) { |n| "LotType#{n}" } + + transient { target_purpose { create :v2_purpose } } + + after(:build) do |lot_type, factory| + lot_type._cached_relationship(:target_purpose) { factory.target_purpose } if factory.target_purpose + end + end + # API V1 Qcable (Records the QC status of eg. a tag plate) factory :qcable, class: Limber::Qcable, traits: %i[api_object barcoded] do with_belongs_to_associations 'lot', 'qcable_creator', 'asset' diff --git a/spec/features/creating_a_tag_plate_spec.rb b/spec/features/creating_a_tag_plate_spec.rb index 7d78aea53..564b82938 100644 --- a/spec/features/creating_a_tag_plate_spec.rb +++ b/spec/features/creating_a_tag_plate_spec.rb @@ -20,6 +20,7 @@ purpose_uuid: 'stock-plate-purpose-uuid' ) end + let(:qcable) { create :v2_qcable } let(:tag_plate_barcode) { SBCF::SangerBarcode.new(prefix: 'DN', number: 2).machine_barcode.to_s } let(:tag_plate_qcable_uuid) { 'tag-plate-qcable' } @@ -91,10 +92,8 @@ stub_v2_barcode_printers(create_list(:v2_plate_barcode_printer, 3)) stub_v2_tag_layout_templates(templates) - # API v1 UUID requests for a qcable via qcable_presenter. - stub_api_get(tag_plate_qcable_uuid, body: tag_plate_qcable) - stub_api_get('lot-uuid', body: json(:tag_lot, lot_number: tag_lot_number, template_uuid: tag_template_uuid)) - stub_api_get('tag-lot-type-uuid', body: json(:tag_lot_type)) + # API v2 requests for the qcable + stub_v2_qcable(qcable) end shared_examples 'it supports the plate' do diff --git a/spec/support/api_url_helper.rb b/spec/support/api_url_helper.rb index 8ee3fc744..6ebe542da 100644 --- a/spec/support/api_url_helper.rb +++ b/spec/support/api_url_helper.rb @@ -258,6 +258,14 @@ def stub_v2_qc_file(qc_file) allow(Sequencescape::Api::V2::QcFile).to receive(:find).with(*arguments).and_return([qc_file]) end + def stub_v2_qcable(qcable) + arguments = [{ uuid: qcable.uuid }] + query_builder = double + + allow(query_builder).to receive(:find_by).with(*arguments).and_return(qcable) + allow(Sequencescape::Api::V2::Qcable).to receive(:includes).and_return(query_builder) + end + def stub_v2_study(study) arguments = [{ name: study.name }] allow(Sequencescape::Api::V2::Study).to receive(:find).with(*arguments).and_return([study]) From c52999d7871d5198e3608df677a32120d6b1f06c Mon Sep 17 00:00:00 2001 From: Stuart McHattie Date: Fri, 10 Jan 2025 16:31:05 +0000 Subject: [PATCH 5/5] Fix tests for QCable failures --- spec/features/creating_a_tag_plate_spec.rb | 42 ++++++++++++---------- spec/support/api_url_helper.rb | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/spec/features/creating_a_tag_plate_spec.rb b/spec/features/creating_a_tag_plate_spec.rb index 564b82938..cfac69bd0 100644 --- a/spec/features/creating_a_tag_plate_spec.rb +++ b/spec/features/creating_a_tag_plate_spec.rb @@ -20,14 +20,17 @@ purpose_uuid: 'stock-plate-purpose-uuid' ) end - let(:qcable) { create :v2_qcable } - - let(:tag_plate_barcode) { SBCF::SangerBarcode.new(prefix: 'DN', number: 2).machine_barcode.to_s } let(:tag_plate_qcable_uuid) { 'tag-plate-qcable' } let(:tag_plate_uuid) { 'tag-plate-uuid' } - let(:tag_plate_qcable) { json :tag_plate_qcable, uuid: tag_plate_qcable_uuid, lot_uuid: 'lot-uuid' } let(:tag_template_uuid) { 'tag-layout-template-0' } - let(:transfer_template_uuid) { 'custom-pooling' } + + let(:qcable_template) { create :v2_tag_layout_template, uuid: tag_template_uuid } + let(:qcable_lot) { create :v2_lot, template: qcable_template } + let(:qcable_labware) { create :v2_plate, uuid: tag_plate_uuid } + let(:qcable) { create :v2_qcable, lot: qcable_lot, labware: qcable_labware, uuid: tag_plate_qcable_uuid } + + let(:tag_plate_barcode) { qcable_labware.labware_barcode.machine } + let(:tag_plate_qcable) { json :tag_plate_qcable, uuid: tag_plate_qcable_uuid } let(:expected_transfers) { WellHelpers.stamp_hash(96) } let(:enforce_uniqueness) { true } @@ -60,7 +63,7 @@ user_uuid: user_uuid, source_uuid: parent_plate.uuid, destination_uuid: tag_plate_uuid, - transfer_template_uuid: transfer_template_uuid, + transfer_template_uuid: 'custom-pooling', transfers: expected_transfers } } @@ -69,7 +72,6 @@ let(:help_text) { 'This plate does not appear to be part of a larger pool. Dual indexing is optional.' } - let(:tag_lot_number) { 'tag_lot_number' } let(:enforce_same_template_within_pool) { false } # Setup stubs @@ -105,6 +107,12 @@ stub_v2_plate(create(:v2_plate, uuid: tag_plate_uuid, purpose_uuid: 'stock-plate-purpose-uuid')) stub_api_v2_post('StateChange') + + stub_search_and_single_result( + 'Find qcable by barcode', + { 'search' => { 'barcode' => tag_plate_barcode } }, + tag_plate_qcable + ) end scenario 'creation with the plate' do @@ -116,13 +124,8 @@ click_on('Add an empty Tag Purpose plate') expect(page).to have_content('Tag plate addition') expect(find('#tag-help')).to have_content(help_text) - stub_search_and_single_result( - 'Find qcable by barcode', - { 'search' => { 'barcode' => tag_plate_barcode } }, - tag_plate_qcable - ) swipe_in('Tag plate barcode', with: tag_plate_barcode) - expect(page).to have_content(tag_lot_number) + expect(page).to have_content(qcable_lot.lot_number) expect(find('#well_A2')).to have_content(a2_tag) click_on('Create Plate') expect(page).to have_content('New empty labware added to the system.') @@ -130,17 +133,20 @@ end shared_examples 'it rejects the candidate plate' do + before do + stub_search_and_single_result( + 'Find qcable by barcode', + { 'search' => { 'barcode' => tag_plate_barcode } }, + tag_plate_qcable + ) + end + scenario 'rejects the candidate plate' do fill_in_swipecard_and_barcode user_swipecard, plate_barcode plate_title = find('#plate-title') expect(plate_title).to have_text('Limber Cherrypicked') click_on('Add an empty Tag Purpose plate') expect(page).to have_content('Tag plate addition') - stub_search_and_single_result( - 'Find qcable by barcode', - { 'search' => { 'barcode' => tag_plate_barcode } }, - tag_plate_qcable - ) swipe_in('Tag plate barcode', with: tag_plate_barcode) expect(page).to have_button('Create Plate', disabled: true) expect(page).to have_content(tag_error) diff --git a/spec/support/api_url_helper.rb b/spec/support/api_url_helper.rb index 6ebe542da..af96a2421 100644 --- a/spec/support/api_url_helper.rb +++ b/spec/support/api_url_helper.rb @@ -262,8 +262,8 @@ def stub_v2_qcable(qcable) arguments = [{ uuid: qcable.uuid }] query_builder = double - allow(query_builder).to receive(:find_by).with(*arguments).and_return(qcable) allow(Sequencescape::Api::V2::Qcable).to receive(:includes).and_return(query_builder) + allow(query_builder).to receive(:find).with(*arguments).and_return([qcable]) end def stub_v2_study(study)