Skip to content
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
750c1f2
Add changes for generating screenshots
yoldas Feb 18, 2026
918d412
Update worksheet screenshot based on feedback
yoldas Feb 18, 2026
60301fc
Add poly_metadata association to Batch for storing buffer_volume_for_…
yoldas Feb 19, 2026
e9a372d
Add buffer volume for empty wells option to params for pass through
yoldas Feb 19, 2026
4b7b7e7
Remove pry
yoldas Feb 19, 2026
6a0d6db
Add HasPolyMetadata concent to set the option on batch
yoldas Feb 19, 2026
c5244aa
Call set_poly_metadata on the batch in cherrypick handler
yoldas Feb 19, 2026
f2d190d
Update worksheet with buffer_volume_for_empty_wells option from batch…
yoldas Feb 19, 2026
0247d43
Add buffer steps for empty wells in tecan behaviour
yoldas Feb 19, 2026
03927d5
Add buffer steps for empty wells in TecanV3 driver file generator
yoldas Feb 19, 2026
13e85a3
Check if the destination well is empty, in case of partial plate
yoldas Feb 19, 2026
e755668
Add guard for generator test without batch
yoldas Feb 19, 2026
521a3dd
Add the word volume to the note and legend in worksheet
yoldas Feb 20, 2026
0d0787c
Add Automatic buffer addition for empty wells required? checkbox
yoldas Feb 20, 2026
7349453
Add script to toggle buffer_volume_for_empty_wells textbox based on a…
yoldas Feb 20, 2026
df31627
Add code documentation about new data_object generation
yoldas Feb 20, 2026
416ab29
Add comment to explain why src_well is checked
yoldas Feb 20, 2026
85a6b2a
Add batch behaviour to handle poly_metadata for cherrypick options
yoldas Feb 20, 2026
6177332
Add task helper to set form params in batch polymetadata
yoldas Feb 20, 2026
e106709
Add PolyMetadataBehaviour to batch
yoldas Feb 20, 2026
0dad66a
Add option handling in pick helpers
yoldas Feb 20, 2026
0655cfe
Add pass through for automatic_buffer_addition
yoldas Feb 20, 2026
e70a38e
Tidy up the worksheet ERB
yoldas Feb 20, 2026
4ca4650
Use ERB comments instead of HTML comments
yoldas Feb 20, 2026
4f00987
Revert redundant change
yoldas Feb 20, 2026
b7183cd
Remove extra closing ERB tag
yoldas Feb 20, 2026
213445a
Handle checkbox value as 1 or on; passing through hidden form fields
yoldas Feb 20, 2026
c66d6ef
Fix broken TecanV3 tests
yoldas Feb 20, 2026
733ebc9
Fix broken Tecan generator tests
yoldas Feb 20, 2026
a37e275
Merge branch 'develop' into y26-012-automating-buffer-addition
andrewsparkes Feb 20, 2026
5656a8b
Merge branch 'y26-012-automating-buffer-addition' into Y26-680-automa…
andrewsparkes Feb 23, 2026
e897666
Merge branch 'develop' into Y26-680-automated-buffer-addition
andrewsparkes Feb 24, 2026
2226ad5
added jest and tests for cherrypick_strategies
andrewsparkes Feb 24, 2026
fb35abe
added tests for buffer volume for empty wells option module
andrewsparkes Feb 25, 2026
70d3e84
added tests for the check in buffers method for src_well
andrewsparkes Feb 25, 2026
9c6fa34
added tests for tecan default changes
andrewsparkes Feb 25, 2026
c656849
added tests for buffer addition, fixed plate barcodes in all test fil…
andrewsparkes Mar 2, 2026
f1c20a6
fix to pass eslint
andrewsparkes Mar 2, 2026
9650b30
linted
andrewsparkes Mar 2, 2026
adbaa8b
added tests for has poly metadata
andrewsparkes Mar 2, 2026
7a9c636
Merge branch 'develop' into Y26-680-automated-buffer-addition
andrewsparkes Mar 2, 2026
fe0d527
Merge branch 'develop' into Y26-680-automated-buffer-addition
andrewsparkes Mar 11, 2026
294edd1
changes after code review
andrewsparkes Mar 11, 2026
b9a9319
Merge branch 'develop' into Y26-680-automated-buffer-addition
andrewsparkes Mar 18, 2026
3bb09c9
various refactorings after code review feedback
andrewsparkes Mar 18, 2026
7b793f8
Merge branch 'develop' into Y26-680-automated-buffer-addition
andrewsparkes Mar 18, 2026
62c4ee4
linted js
andrewsparkes Mar 18, 2026
fe10426
fixed method calls
andrewsparkes Mar 19, 2026
9a9e022
Merge branch 'develop' into Y26-680-automated-buffer-addition
andrewsparkes Mar 26, 2026
63c92d9
passing through the template chosen for use in tecan behaviour to avo…
andrewsparkes Mar 27, 2026
eb15190
change to prevent buffer adding to wells that are set to empty in tem…
andrewsparkes Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/frontend/entrypoints/cherrypick_strategies.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
// Toggle buffer_volume_for_empty_wells input based on automatic_buffer_addition checkbox
document.addEventListener("DOMContentLoaded", function () {
const bufferInput = document.getElementById("buffer_volume_for_empty_wells");
const autoBufferCheckbox = document.getElementById("automatic_buffer_addition");
if (bufferInput && autoBufferCheckbox) {
function toggleBufferInput() {
bufferInput.disabled = !autoBufferCheckbox.checked;
}
autoBufferCheckbox.addEventListener("change", toggleBufferInput);
toggleBufferInput(); // Set initial state
}
});
// apply a border highlight to the card, based on the cherrypick strategy selected
// this is admittedly a gratuitous addition to the user experience, but it's a nice touch

Expand Down
33 changes: 33 additions & 0 deletions app/frontend/entrypoints/cherrypick_strategies.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// cherrypick_strategies.test.js
// Tests for cherrypick_strategies.js buffer input toggle logic

/* global describe, it, expect, beforeEach, jest */
/* @jest-environment jsdom */

describe("Buffer input toggle", () => {
let bufferInput, autoBufferCheckbox;

beforeEach(() => {
document.body.innerHTML = `
<input id="buffer_volume_for_empty_wells" type="number" />
<input id="automatic_buffer_addition" type="checkbox" />
`;
// Re-require the script to attach event listeners
jest.resetModules();
require("./cherrypick_strategies.js");
bufferInput = document.getElementById("buffer_volume_for_empty_wells");
autoBufferCheckbox = document.getElementById("automatic_buffer_addition");
});

it("enables and disables buffer input when checkbox is toggled", () => {
// register the change event handler
// bufferInput is initially disabled because autoBufferCheckbox is unchecked
document.dispatchEvent(new Event("DOMContentLoaded"));

autoBufferCheckbox.click(); // unchecked to checked
expect(bufferInput.disabled).toBe(false);

autoBufferCheckbox.click(); // checked to unchecked
expect(bufferInput.disabled).toBe(true);
});
});
13 changes: 7 additions & 6 deletions app/models/batch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class Batch < ApplicationRecord # rubocop:todo Metrics/ClassLength
include ::Batch::PipelineBehaviour
include ::Batch::StateMachineBehaviour
include UnderRepWellCommentsToBroadcast
# Added for storing buffer_volume_for_empty_wells option on Cherrypick batches.
include HasPolyMetadata
include ::Batch::PolyMetadataBehaviour
extend EventfulRecord

# The three states of {Batch} Also @see {SequencingQcBatch}
Expand Down Expand Up @@ -481,18 +484,16 @@ def swap(current_user, batch_info = {}) # rubocop:todo Metrics/CyclomaticComplex
# Finally record the fact that the batch was swapped
batch_request_left.batch.lab_events.create!(
description: 'Lane swap',
# rubocop:todo Layout/LineLength
message:
"Lane #{batch_request_right.position} moved to #{batch_request_left.batch_id} lane #{batch_request_left.position}",
# rubocop:enable Layout/LineLength
"Lane #{batch_request_right.position} moved to #{batch_request_left.batch_id} " \
"lane #{batch_request_left.position}",
user_id: current_user.id
)
batch_request_right.batch.lab_events.create!(
description: 'Lane swap',
# rubocop:todo Layout/LineLength
message:
"Lane #{batch_request_left.position} moved to #{batch_request_right.batch_id} lane #{batch_request_right.position}",
# rubocop:enable Layout/LineLength
"Lane #{batch_request_left.position} moved to #{batch_request_right.batch_id} " \
"lane #{batch_request_right.position}",
user_id: current_user.id
)
end
Expand Down
16 changes: 16 additions & 0 deletions app/models/batch/poly_metadata_behaviour.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true
module Batch::PolyMetadataBehaviour
# Returns whether the Cherrypick automatic_buffer_addition option is enabled
# @return [Boolean] whether the automatic_buffer_addition option is enabled
def automatic_buffer_addition?
# tests 1 and on to cover both visible and hidden option in the forms of successive pages
%w[1 on].include?(get_poly_metadata(:automatic_buffer_addition))
end

# Returns the Cherrypick buffer_volume_for_empty_wells option value if
# automatic_buffer_addition is enabled, nil otherwise.
# @return [Float, nil] the buffer_volume_for_empty_wells value
def buffer_volume_for_empty_wells
get_poly_metadata(:buffer_volume_for_empty_wells).to_f if automatic_buffer_addition?
end
end
39 changes: 39 additions & 0 deletions app/models/cherrypick/task/buffer_volume_for_empty_wells_option.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Cherrypick::Task::BufferVolumeForEmptyWellsOption
def create_buffer_volume_for_empty_wells_option(params)
return unless @batch

key = :automatic_buffer_addition

# The checkbox value is either '1', 'on' or nil if not checked.
# Store a consistent value
value = %w[1 on].include?(params[key]) ? '1' : params[key]
@batch.set_poly_metadata(key, value)

if value == '1'
record_buffer_addition_volume(params)
else
clear_buffer_addition_volume(params)
end
end

private

def record_buffer_addition_volume(params)
key = :buffer_volume_for_empty_wells

unless valid_float_param?(params[key])
raise Cherrypick::VolumeError,
"Invalid buffer volume for empty wells: #{params[key]}"
end
@batch.set_poly_metadata(key, params[key])
end

# Most likely it's not set, but if it is, clear it to avoid confusion.
def clear_buffer_addition_volume(_params)
return if @batch.get_poly_metadata(:buffer_volume_for_empty_wells).blank?

@batch.set_poly_metadata(:buffer_volume_for_empty_wells, nil)
end
end
1 change: 1 addition & 0 deletions app/models/cherrypick/task/pick_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ def self.included(base)
include Cherrypick::Task::PickByNanoGramsPerMicroLitre
include Cherrypick::Task::PickByNanoGrams
include Cherrypick::Task::PickByMicroLitre
include Cherrypick::Task::BufferVolumeForEmptyWellsOption
end
end

Expand Down
38 changes: 38 additions & 0 deletions app/models/concerns/has_poly_metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module HasPolyMetadata
extend ActiveSupport::Concern

included do
has_many :poly_metadata, as: :metadatable, dependent: :destroy, inverse_of: :metadatable
end

# Sets a PolyMetaDatum for the given key and value.
# If value is present, it will create or update the PolyMetaDatum with the
# given key and value, otherwise it will destroy the PolyMetaDatum with the
# given key if that exists.
# NB: this is because PolyMetaDatum validations prevent key duplication and blank values,
# although there are no such DB constraints.
# @param key [String] The key of the PolyMetaDatum to set.
# @param value [String] The value of the PolyMetaDatum to set. If nil or empty, the PolyMetaDatum will be destroyed.
# @return [void]
def set_poly_metadata(key, value)
record = poly_metadata.find_by(key:)
if value.present?
if record
record.update!(value:)
else
poly_metadata.create!(key:, value:)
end
else
record&.destroy!
end
end

# Returns the value of the PolyMetaDatum with the given key.
# @param key [String] The key of the PolyMetaDatum to retrieve.
# @return [String, nil] The value of the PolyMetaDatum, or nil if it does not exist.
def get_poly_metadata(key)
poly_metadata.find_by(key:)&.value
end
end
16 changes: 12 additions & 4 deletions app/models/map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def self.description_to_horizontal_plate_position(well_description, plate_size)
(width * split_well[:row]) + split_well[:col]
end

def self.description_to_vertical_plate_position(well_description, plate_size)
# e.g. B5 returns 34 for a 96 well plate
# So 4 columns of 8 rows each, plus 2 for the B row
def self.well_description_to_by_column_map_index(well_description, plate_size)
return nil unless Map.valid_well_description_and_plate_size?(well_description, plate_size)

split_well = Map.split_well_description(well_description)
Expand All @@ -62,7 +64,9 @@ def self.description_to_vertical_plate_position(well_description, plate_size)
(length * (split_well[:col] - 1)) + split_well[:row] + 1
end

def self.horizontal_plate_position_to_description(well_position, plate_size)
# e.g. 23 returns B11 for a 96 well plate
# So 1 full row of 12, plus 11, to give row B, column 11
def self.by_row_map_index_to_well_description(well_position, plate_size)
return nil unless Map.valid_plate_position_and_plate_size?(well_position, plate_size)

width = plate_width(plate_size)
Expand All @@ -71,7 +75,9 @@ def self.horizontal_plate_position_to_description(well_position, plate_size)
horizontal_position_to_description(well_position, width)
end

def self.vertical_plate_position_to_description(well_position, plate_size)
# e.g. 23 returns G3 for a 96 well plate
# So 2 full columns of 8, plus 7, to give row G, column 3
def self.by_column_map_index_to_well_description(well_position, plate_size)
return nil unless Map.valid_plate_position_and_plate_size?(well_position, plate_size)

length = plate_length(plate_size)
Expand All @@ -96,6 +102,8 @@ def self.plate_length(plate_size)
PLATE_DIMENSIONS[plate_size].last
end

# well number counting by columns, length is the number of rows in the plate
# e.g. B5 sends this 34 and 8
def self.vertical_position_to_description(well_position, length)
desc_letter = (((well_position - 1) % length) + 65).chr
desc_number = ((well_position - 1) / length) + 1
Expand All @@ -117,7 +125,7 @@ def self.vertical_to_horizontal(well_position, plate_size)
end

def self.location_from_index(index, size)
horizontal_plate_position_to_description(index + 1, size)
by_row_map_index_to_well_description(index + 1, size)
end

class << self
Expand Down
2 changes: 1 addition & 1 deletion app/models/robot/generator/behaviours/hamilton_default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def each_mapping(data_object) # rubocop:todo Metrics/AbcSize
mapping_by_well = Hash.new { |h, i| h[i] = [] }
plate_details['mapping'].each do |mapping|
destination_position =
Map::Coordinate.description_to_vertical_plate_position(mapping['dst_well'], plate_details['plate_size'])
Map::Coordinate.well_description_to_by_column_map_index(mapping['dst_well'], plate_details['plate_size'])
mapping_by_well[destination_position] << mapping
end

Expand Down
Loading
Loading