From cbb9b5bd40cb8686c2ebd1df894b8584222f613f Mon Sep 17 00:00:00 2001 From: Shiv <44001656+borhara@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:39:12 +0000 Subject: [PATCH 1/8] fix: update find_lot method to use V2 API and handle nil case --- app/controllers/lots_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/lots_controller.rb b/app/controllers/lots_controller.rb index a69e6ad1..e2e87c64 100644 --- a/app/controllers/lots_controller.rb +++ b/app/controllers/lots_controller.rb @@ -102,7 +102,8 @@ def search private def find_lot - @lot = api.lot.find(params[:id]) + @lot = Sequencescape::Api::V2::Lot.includes(:lot_type, :qcables).where(uuid: params[:id]).first + raise Sequencescape::Api::ResourceNotFound if @lot.nil? rescue Sequencescape::Api::ResourceNotFound @message = "Could not find lot with uuid: #{params[:id]}" render 'pages/error', status: :not_found From e5fde1ca66f83367101a18fe1b1941e7ac607af6 Mon Sep 17 00:00:00 2001 From: Shiv <44001656+borhara@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:39:39 +0000 Subject: [PATCH 2/8] feat: implement compatible barcode interface in Qcable class --- app/resources/sequencescape/api/v2/qcable.rb | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/resources/sequencescape/api/v2/qcable.rb b/app/resources/sequencescape/api/v2/qcable.rb index 1480d3eb..b949efcb 100644 --- a/app/resources/sequencescape/api/v2/qcable.rb +++ b/app/resources/sequencescape/api/v2/qcable.rb @@ -3,4 +3,32 @@ # Represents a Qcable using the Sequencescape V2 API class Sequencescape::Api::V2::Qcable < Sequencescape::Api::V2::Base has_one :qcable_creator + + # Simple value object to provide V1-compatible .prefix and .number accessors + BarcodeWrapper = Struct.new(:prefix, :number) + + # Provides a V1-compatible barcode interface wrapping the V2 labware_barcode hash. + # The V2 API returns labware_barcode as { 'human_barcode' => 'DN1S', ... } whereas + # the V1 API returned barcode as an object with .prefix and .number accessors. + # This method bridges the gap so that Presenter::Qcable (and Presenter::Lot#prefix) + # continue to work without change. + def barcode + @barcode ||= BarcodeWrapper.new(*extract_prefix_and_number) + end + + private + + def extract_prefix_and_number + human = human_barcode_string + prefix = human[/\A([A-Za-z]+)/, 1] || '' + number = human[/[A-Za-z]+(\d+)/, 1] || '' + [prefix, number] + end + + def human_barcode_string + lb = labware_barcode + return '' unless lb.is_a?(Hash) + + (lb['human_barcode'] || lb[:human_barcode]).to_s + end end From b9aa9cc01ac8c76a95c03aca3c802d05c387efde Mon Sep 17 00:00:00 2001 From: Shiv <44001656+borhara@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:39:59 +0000 Subject: [PATCH 3/8] feat: add tests for showing tag and reporter plates with mock data --- test/controllers/lots_controller_test.rb | 65 ++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/controllers/lots_controller_test.rb b/test/controllers/lots_controller_test.rb index 753a4450..848cfadf 100644 --- a/test/controllers/lots_controller_test.rb +++ b/test/controllers/lots_controller_test.rb @@ -125,6 +125,46 @@ class LotsControllerTest < ActionController::TestCase # SHOW test 'show tag plate' do + mock_lot_type = Sequencescape::Api::V2::LotType.new + mock_lot_type.qcable_name = 'Tag Plate' + mock_lot_type.printer_type = '96 Well Plate' + + qcable_specs = [ + { state: 'created', human: 'DN1S', uuid: '11111111-2222-3333-4444-100000000001', stamp_index: nil }, + { state: 'created', human: 'DN2T', uuid: '11111111-2222-3333-4444-100000000002', stamp_index: nil }, + { state: 'qc_in_progress', human: 'DN3U', uuid: '11111111-2222-3333-4444-100000000003', stamp_index: 1 }, + { state: 'destroyed', human: 'DN4V', uuid: '11111111-2222-3333-4444-100000000004', stamp_index: 2 }, + { state: 'failed', human: 'DN5W', uuid: '11111111-2222-3333-4444-100000000005', stamp_index: 4 }, + { state: 'exhausted', human: 'DN6A', uuid: '11111111-2222-3333-4444-100000000006', stamp_index: 3 }, + { state: 'passed', human: 'DN7B', uuid: '11111111-2222-3333-4444-100000000007', stamp_index: 5 }, + { state: 'pending', human: 'DN8C', uuid: '11111111-2222-3333-4444-100000000008', stamp_index: 6 }, + { state: 'pending', human: 'DN9D', uuid: '11111111-2222-3333-4444-100000000009', stamp_index: 7 }, + { state: 'available', human: 'DN10I', uuid: '11111111-2222-3333-4444-100000000010', stamp_index: 8 } + ] + + mock_qcables = qcable_specs.map do |spec| + Sequencescape::Api::V2::Qcable.new( + state: spec[:state], + uuid: spec[:uuid], + labware_barcode: { 'human_barcode' => spec[:human] }, + stamp_index: spec[:stamp_index] + ) + end + + mock_lot = Sequencescape::Api::V2::Lot.new( + uuid: '11111111-2222-3333-4444-555555555556', + lot_number: '123456789', + lot_type_name: 'IDT Tags', + template_name: 'Example Tag Layout', + received_at: '2013-02-01' + ) + mock_lot.stubs(:lot_type).returns(mock_lot_type) + mock_lot.stubs(:qcables).returns(mock_qcables) + + scope = mock('v2_lot_scope') + Sequencescape::Api::V2::Lot.stubs(:includes).with(:lot_type, :qcables).returns(scope) + scope.stubs(:where).with(uuid: '11111111-2222-3333-4444-555555555556').returns([mock_lot]) + get :show, params: { id: '11111111-2222-3333-4444-555555555556' } assert_response :success @@ -138,6 +178,31 @@ class LotsControllerTest < ActionController::TestCase end test 'show reporter plate' do + mock_lot_type = Sequencescape::Api::V2::LotType.new + mock_lot_type.qcable_name = 'Reporter Plate' + mock_lot_type.printer_type = '96 Well Plate' + + mock_qcable = Sequencescape::Api::V2::Qcable.new( + state: 'created', + uuid: '11111111-2222-3333-4444-200000000010', + labware_barcode: { 'human_barcode' => 'DN11K' }, + stamp_index: nil + ) + + mock_lot = Sequencescape::Api::V2::Lot.new( + uuid: '11111111-2222-3333-4444-555555555557', + lot_number: '123456790', + lot_type_name: 'IDT Reporters', + template_name: 'Example Plate Layout', + received_at: '2013-02-01' + ) + mock_lot.stubs(:lot_type).returns(mock_lot_type) + mock_lot.stubs(:qcables).returns([mock_qcable]) + + scope = mock('v2_lot_scope') + Sequencescape::Api::V2::Lot.stubs(:includes).with(:lot_type, :qcables).returns(scope) + scope.stubs(:where).with(uuid: '11111111-2222-3333-4444-555555555557').returns([mock_lot]) + get :show, params: { id: '11111111-2222-3333-4444-555555555557' } assert_response :success From b2936ed5126e2f3060c5f8a56956b140afbb3133 Mon Sep 17 00:00:00 2001 From: Shiv <44001656+borhara@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:38:20 +0000 Subject: [PATCH 4/8] refactor: removed old v1 API 'ResourceNotFound' exception --- app/controllers/lots_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/lots_controller.rb b/app/controllers/lots_controller.rb index e2e87c64..3df9ff93 100644 --- a/app/controllers/lots_controller.rb +++ b/app/controllers/lots_controller.rb @@ -103,8 +103,9 @@ def search def find_lot @lot = Sequencescape::Api::V2::Lot.includes(:lot_type, :qcables).where(uuid: params[:id]).first - raise Sequencescape::Api::ResourceNotFound if @lot.nil? - rescue Sequencescape::Api::ResourceNotFound + + return if @lot.present? + @message = "Could not find lot with uuid: #{params[:id]}" render 'pages/error', status: :not_found end From 02665d32f4b1d656b0bdbff2613047d589ccef62 Mon Sep 17 00:00:00 2001 From: Shiv <44001656+borhara@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:38:53 +0000 Subject: [PATCH 5/8] refactor: use human_readable barcode instead of prefix + number --- app/assets/javascripts/qc.js.erb | 19 +++++++------- app/controllers/barcode_labels_controller.rb | 7 +++-- app/models/barcode_sheet/label.rb | 2 +- app/models/concerns/barcode_extensions.rb | 1 + app/models/presenter/lot.rb | 11 +++++++- app/models/presenter/qc_asset.rb | 18 +++++++++++-- app/models/presenter/qcable.rb | 27 ++++++++++++++++---- app/views/lots/_child_row.erb | 4 +-- app/views/lots/_children.erb | 1 - app/views/qc_assets/_asset_template.erb | 1 - app/views/qc_decisions/_qc_plates.erb | 2 +- 11 files changed, 65 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/qc.js.erb b/app/assets/javascripts/qc.js.erb index b547f39e..f58edbbc 100644 --- a/app/assets/javascripts/qc.js.erb +++ b/app/assets/javascripts/qc.js.erb @@ -106,25 +106,25 @@ }, setTitle: function(title) {this.element.find('.panel-heading h2').text(title);}, humanBarcode: function() { - return this.asset.barcode.prefix+this.asset.barcode.number + return this.asset.barcode.human_readable }, setBarcode: function() { this.element.find('.asset-ean13').val(this.asset.barcode.ean13); }, setBarcodeForm: function() { - var study, number; + var study, barcode; if (this.asset.handle.with==='plate_conversion') { return true; }; if (this.asset.handle.with==='qa_plate_conversion') { return true; }; study = document.createElement('input') study.type = 'hidden' study.value = this.asset.purpose; study.name = 'study' - number = document.createElement('input') - number.value = this.asset.barcode.number; - number.name = 'numbers['+this.asset.barcode.number+']'; - number.type = 'hidden'; - this.element.find('.barcode-printing-form').append(study).append(number); - new BarcodePrinter(this.element.find('.barcode-printing-form'),this.asset.handle.printer,this.asset.barcode.prefix); + barcode = document.createElement('input') + barcode.value = this.asset.barcode.human_readable; + barcode.name = 'barcodes['+this.asset.barcode.human_readable+']'; + barcode.type = 'hidden'; + this.element.find('.barcode-printing-form').append(study).append(barcode); + new BarcodePrinter(this.element.find('.barcode-printing-form'),this.asset.handle.printer); }, setPurpose: function(purpose) { this.element.find('.purpose').text(purpose); @@ -328,13 +328,12 @@ /* Ideally this would go elsewhere, but then I'd either pollute the global namespace, or need to add dependency management */ - BarcodePrinter = function(form,type,prefix) { + BarcodePrinter = function(form,type) { this.form = $(form); this.form.on('submit',this.formSubmit()) this.type = type||this.type; this.form.find(this.remove()).hide(); this.form.find(this.show()).show(); - this.form.find('.prefix-field').val([prefix]); this.form.find('select').val($(this.show()).val()); } BarcodePrinter.prototype = { diff --git a/app/controllers/barcode_labels_controller.rb b/app/controllers/barcode_labels_controller.rb index c5028607..3a093a00 100644 --- a/app/controllers/barcode_labels_controller.rb +++ b/app/controllers/barcode_labels_controller.rb @@ -27,11 +27,10 @@ def create private def generate_labels - permitted_params = params.permit(:prefix, :study, numbers: {}) - @labels = (permitted_params[:numbers] || {}).to_h.map do |_, number| + permitted_params = params.permit(:study, barcodes: {}) + @labels = (permitted_params[:barcodes] || {}).to_h.map do |_, human_readable| BarcodeSheet::Label.new( - prefix: permitted_params[:prefix], - number:, + human_readable:, study: permitted_params[:study] ) end diff --git a/app/models/barcode_sheet/label.rb b/app/models/barcode_sheet/label.rb index 04c5b2a4..81941e1c 100644 --- a/app/models/barcode_sheet/label.rb +++ b/app/models/barcode_sheet/label.rb @@ -73,7 +73,7 @@ def plate_double private def code39_barcode - @barcode || SBCF::SangerBarcode.new(prefix: @prefix, number: @number).human_barcode + @barcode || human_readable end def lot_template diff --git a/app/models/concerns/barcode_extensions.rb b/app/models/concerns/barcode_extensions.rb index 529c1fc8..ad5df7ed 100644 --- a/app/models/concerns/barcode_extensions.rb +++ b/app/models/concerns/barcode_extensions.rb @@ -4,4 +4,5 @@ module BarcodeExtensions def human_barcode "#{barcode.prefix}#{barcode.number}" end + alias human_readable human_barcode end diff --git a/app/models/presenter/lot.rb b/app/models/presenter/lot.rb index 77c122e2..3a60f269 100644 --- a/app/models/presenter/lot.rb +++ b/app/models/presenter/lot.rb @@ -44,7 +44,16 @@ def printer_type def prefix return '' if @lot.qcables.empty? - @lot.qcables.first.barcode.prefix + qcable = @lot.qcables.first + # V2 style + if qcable.respond_to?(:labware_barcode) + human = (qcable.labware_barcode['human_barcode'] || qcable.labware_barcode[:human_barcode]).to_s + return human[/\A([A-Za-z]+)/, 1] || '' + end + # V1/Mock style + return qcable.barcode.prefix if qcable.respond_to?(:barcode) && qcable.barcode.respond_to?(:prefix) + + '' end ## diff --git a/app/models/presenter/qc_asset.rb b/app/models/presenter/qc_asset.rb index 30ba552e..fcd625a3 100644 --- a/app/models/presenter/qc_asset.rb +++ b/app/models/presenter/qc_asset.rb @@ -31,8 +31,7 @@ def child_type def barcode { 'ean13' => @asset.barcode.ean13, - 'prefix' => @asset.barcode.prefix, - 'number' => @asset.barcode.number + 'human_readable' => human_readable } end @@ -61,6 +60,21 @@ def output } } end + def human_readable + return @asset.human_readable if @asset.respond_to?(:human_readable) + return @asset.human_barcode if @asset.respond_to?(:human_barcode) + + # Fallback for mock objects that only have the barcode hash + bc = @asset.barcode + if bc.respond_to?(:prefix) && bc.respond_to?(:number) + "#{bc.prefix}#{bc.number}" + elsif bc.is_a?(Hash) + "#{bc['prefix'] || bc[:prefix]}#{bc['number'] || bc[:number]}" + else + '' + end + end + private def own_purpose_config diff --git a/app/models/presenter/qcable.rb b/app/models/presenter/qcable.rb index 38ff1f2a..51e41d87 100644 --- a/app/models/presenter/qcable.rb +++ b/app/models/presenter/qcable.rb @@ -18,15 +18,32 @@ def index @qcable.stamp_index ? @qcable.stamp_index + 1 : '-' end - def barcode - "#{@qcable.barcode.prefix}#{@qcable.barcode.number}" + def human_readable + if @qcable.respond_to?(:labware_barcode) + lb = @qcable.labware_barcode + return (lb['human_barcode'] || lb[:human_barcode]).to_s if lb.is_a?(Hash) + end + + if @qcable.respond_to?(:barcode) + bc = @qcable.barcode + if bc.is_a?(Hash) + return (bc['machine'] || bc[:machine]).to_s if bc['machine'] || bc[:machine] + + return "#{bc['prefix'] || bc[:prefix]}#{bc['number'] || bc[:number]}" + elsif bc.respond_to?(:machine) + return bc.machine.to_s if bc.machine + elsif bc.respond_to?(:prefix) && bc.respond_to?(:number) + return "#{bc.prefix}#{bc.number}" + end + end + + '' end - def number - @qcable.barcode.number.to_s + def barcode + human_readable end delegate :uuid, to: :@qcable - alias id uuid end diff --git a/app/views/lots/_child_row.erb b/app/views/lots/_child_row.erb index 526327d1..6cb93233 100644 --- a/app/views/lots/_child_row.erb +++ b/app/views/lots/_child_row.erb @@ -1,5 +1,5 @@ <%= child.index %> - <%= child.barcode %> - + <%= child.human_readable %> + diff --git a/app/views/lots/_children.erb b/app/views/lots/_children.erb index 389f51e0..9cc07882 100644 --- a/app/views/lots/_children.erb +++ b/app/views/lots/_children.erb @@ -15,7 +15,6 @@ " /> -
diff --git a/app/views/qc_assets/_asset_template.erb b/app/views/qc_assets/_asset_template.erb index 0d135504..57336aa8 100644 --- a/app/views/qc_assets/_asset_template.erb +++ b/app/views/qc_assets/_asset_template.erb @@ -1,5 +1,4 @@ <%= form_tag({controller: 'barcode_labels',action: :create},{class: 'barcode-printing-form'}) do %> -
diff --git a/app/views/qc_decisions/_qc_plates.erb b/app/views/qc_decisions/_qc_plates.erb index a166d2b4..3c900fd7 100644 --- a/app/views/qc_decisions/_qc_plates.erb +++ b/app/views/qc_decisions/_qc_plates.erb @@ -6,7 +6,7 @@
<% presenter.each_plate_in('qc_in_progress') do |plate| -%>
- +