From 84f4bdad9c01a062b039084ca1144f67bc8d4693 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 21 Dec 2023 14:22:31 +0000 Subject: [PATCH 01/37] Fixed expect syntax --- spec/tasks/create_mbrave_tags_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/tasks/create_mbrave_tags_spec.rb b/spec/tasks/create_mbrave_tags_spec.rb index 8a6e75b15d..c84d3d2f44 100644 --- a/spec/tasks/create_mbrave_tags_spec.rb +++ b/spec/tasks/create_mbrave_tags_spec.rb @@ -53,8 +53,9 @@ end it 'creates the tag group with the right indexing' do - expect(MbraveTagsCreator).to - receive(:process_create_tag_groups).with('forward', 'reverse', 'v1').at_least(:once) + expect(MbraveTagsCreator).to receive(:process_create_tag_groups) + .with('forward', 'reverse', 'v1') + .at_least(:once) run_task end end From 223db0df2cfc13a83ceab7d7472140dbd516946d Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 7 Jan 2024 00:10:48 +0000 Subject: [PATCH 02/37] Added 16-well Chromium Chip to the standard plate dimensions --- app/models/map.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/models/map.rb b/app/models/map.rb index 307f35a542..40d352311d 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -18,7 +18,18 @@ module Coordinate # TODO: These methods are only valid for standard plates. Moved them here to make that more explicit # (even if its not strictly appropriate) They could do with refactoring/removing. - PLATE_DIMENSIONS = Hash.new { |_h, _k| [] }.merge(96 => [12, 8], 384 => [24, 16]) + # A hash representing the dimensions of different types of plates. + # The keys are the total number of wells in the plate, and the values are + # arrays, where the first element is the number of columns and the second + # element is the number of rows. + # + # @note + # - 96 represents a 96-well plate, arranged in 12 columns and 8 rows. + # - 384 represents a 384-well plate, arranged in 24 columns and 16 rows. + # - 16 represents a 16-well Chromium Chip , which has 8 columns and 2 rows. + # + # @return [Hash{Integer => Array}] the dimensions of the plates + PLATE_DIMENSIONS = Hash.new { |_h, _k| [] }.merge(96 => [12, 8], 384 => [24, 16], 16 => [8, 2]) # Seems to expect row to be zero-indexed but column to be 1 indexed def self.location_from_row_and_column(row, column, _ = nil, __ = nil) From dec1157c4345524feaabc06ef40db3252f85823a Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 7 Jan 2024 00:15:16 +0000 Subject: [PATCH 03/37] Added tests for the use of Coordinate module for 16-well Chromium Chip --- spec/models/map_spec.rb | 238 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 spec/models/map_spec.rb diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb new file mode 100644 index 0000000000..cd39fa2797 --- /dev/null +++ b/spec/models/map_spec.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Map, type: :model do + context 'with Chromium Chip 16-well' do + # The Map class contains a nested module called Coordinate. For clarity, we + # use map_class to refer to the Map class and coordinate_module to refer to + # the Coordinate module. + # + # The Chromium Chip 16-well has 16 wells arranged in 2 rows and 8 columns. + # The following diagrams show the horizontal and vertical positions of the + # wells used by the methods in Map and Map::Coordinate. + # + # horizontal positions (order in rows) + # 1 2 3 4 5 6 7 8 + # ╔════╦════╦════╦════╦════╦════╦════╦════╗ + # A ║ 1 ║ 2 ║ 3 ║ 4 ║ 5 ║ 6 ║ 7 ║ 8 ║ + # ║════╬════╬════╬════╬════╬════╬════╬════╣ + # B ║ 9 ║ 10 ║ 11 ║ 12 ║ 13 ║ 14 ║ 15 ║ 16 ║ + # ╚════╩════╩════╩════╩════╩════╩════╩════╝ + + # vertical positions (order in columns) + # 1 2 3 4 5 6 7 8 + # ╔════╦════╦════╦════╦════╦════╦════╦════╗ + # A ║ 1 ║ 3 ║ 5 ║ 7 ║ 9 ║ 11 ║ 13 ║ 15 ║ + # ║════╬════╬════╬════╬════╬════╬════╬════╣ + # B ║ 2 ║ 4 ║ 6 ║ 8 ║ 10 ║ 12 ║ 14 ║ 16 ║ + # ╚════╩════╩════╩════╩════╩════╩════╩════╝ + + let(:map_class) { described_class } + let(:plate_size) { 16 } # Chromium Chip 16-well + + describe Map::Coordinate do + let(:coordinate_module) { described_class } + + describe '.location_from_row_and_column' do + it 'returns the name of a well by row and colum' do + # Rows are using zero-based index; columns are using one-based index. + (1..8).each do |column| + expect(coordinate_module.location_from_row_and_column(0, column)).to eq("A#{column}") + expect(coordinate_module.location_from_row_and_column(1, column)).to eq("B#{column}") + end + end + end + + describe '.description_to_horizontal_plate_position' do + it 'returns nil if well description is invalid' do + expect(coordinate_module.description_to_horizontal_plate_position('', plate_size)).to be_nil + expect(coordinate_module.description_to_horizontal_plate_position(nil, plate_size)).to be_nil + end + + it 'returns nil if plate size is invalid' do + expect(coordinate_module.description_to_horizontal_plate_position('A1', '16')).to be_nil # string + expect(coordinate_module.description_to_horizontal_plate_position('A1', 0)).to be_nil # zero + expect(coordinate_module.description_to_horizontal_plate_position('A1', -1)).to be_nil # negative + end + + it 'returns one-based index of a well in rows' do + # Indexes of wells in rows: 1 to 16 for A1, A2, A3, ..., A8, B1, B2, B3, ..., B8 + (1..8).each do |column| + expect(coordinate_module.description_to_horizontal_plate_position("A#{column}", plate_size)).to eq(column) + expect(coordinate_module.description_to_horizontal_plate_position("B#{column}", plate_size)).to eq( + column + 8 + ) + end + end + end + + describe '.description_to_vertical_plate_position' do + it 'returns nil if well description is invalid' do + expect(coordinate_module.description_to_vertical_plate_position('A1', '16')).to be_nil # string + expect(coordinate_module.description_to_vertical_plate_position('A1', 0)).to be_nil # zero + expect(coordinate_module.description_to_vertical_plate_position('A1', -1)).to be_nil # negative + end + + it 'returns nil if plate size is invalid' do + expect(coordinate_module.description_to_vertical_plate_position('', plate_size)).to be_nil + expect(coordinate_module.description_to_vertical_plate_position(nil, plate_size)).to be_nil + end + + it 'returns one-based index of a well columns' do + # Indexes of wells in columns: 1 to 16 for A1, B1, A2, B2, A3, B3, ..., A8, B8 + (1..8).each do |column| + expect(coordinate_module.description_to_vertical_plate_position("A#{column}", plate_size)).to eq( + (2 * column) - 1 + ) + expect(coordinate_module.description_to_vertical_plate_position("B#{column}", plate_size)).to eq(2 * column) + end + end + end + + describe '.horizontal_plate_position_to_description' do + it 'returns nil if well position is invalid' do + expect(coordinate_module.horizontal_plate_position_to_description('1', plate_size)).to be_nil # string + expect(coordinate_module.horizontal_plate_position_to_description(0, plate_size)).to be_nil # zero + expect(coordinate_module.horizontal_plate_position_to_description(-1, plate_size)).to be_nil # negative + expect(coordinate_module.horizontal_plate_position_to_description(17, plate_size)).to be_nil # out of bound + end + + it 'returns nil if plate size is invalid' do + expect(coordinate_module.horizontal_plate_position_to_description(1, '16')).to be_nil # string + expect(coordinate_module.horizontal_plate_position_to_description(1, 0)).to be_nil # zero + expect(coordinate_module.horizontal_plate_position_to_description(1, -1)).to be_nil # negative + end + + it 'returns name of a well in rows by one-based index' do + # Names of wells in rows: A1, A2, A3, ..., A8, B1, B2, B3, ..., B8 for indexes 1 to 16 + (1..8).each do |column| + expect(coordinate_module.horizontal_plate_position_to_description(column, plate_size)).to eq("A#{column}") + expect(coordinate_module.horizontal_plate_position_to_description(column + 8, plate_size)).to eq( + "B#{column}" + ) + end + end + end + + describe '.vertical_plate_position_to_description' do + it 'returns nil if well position is invalid' do + expect(coordinate_module.vertical_plate_position_to_description('1', plate_size)).to be_nil # string + expect(coordinate_module.vertical_plate_position_to_description(0, plate_size)).to be_nil # zero + expect(coordinate_module.vertical_plate_position_to_description(-1, plate_size)).to be_nil # negative + expect(coordinate_module.vertical_plate_position_to_description(17, plate_size)).to be_nil # out of bound + end + + it 'returns nil if plate size is invalid' do + expect(coordinate_module.vertical_plate_position_to_description(1, '16')).to be_nil # string + expect(coordinate_module.vertical_plate_position_to_description(1, 0)).to be_nil # zero + expect(coordinate_module.vertical_plate_position_to_description(1, -1)).to be_nil # negative + end + + it 'returns name of a well in columns by one-based index' do + # Names of wells in columns: A1, B1, A2, B2, A3, B3, ..., A8, B8 for indexes 1 to 16 + (1..8).each do |column| + expect(coordinate_module.vertical_plate_position_to_description((2 * column) - 1, plate_size)).to eq( + "A#{column}" + ) + expect(coordinate_module.vertical_plate_position_to_description(2 * column, plate_size)).to eq("B#{column}") + end + end + end + + describe '.descriptions_for_row' do + it 'returns names of wells in a row' do + expect(coordinate_module.descriptions_for_row('A', plate_size)).to eq((1..8).map { |column| "A#{column}" }) + expect(coordinate_module.descriptions_for_row('B', plate_size)).to eq((1..8).map { |column| "B#{column}" }) + end + end + + describe '.descriptions_for_column' do + it 'returns names of wells in a column' do + (1..8).each do |column| + expect(coordinate_module.descriptions_for_column(column, plate_size)).to eq(["A#{column}", "B#{column}"]) + end + end + end + + describe '.plate_width' do + it 'returns the width of a plate' do + expect(coordinate_module.plate_width(plate_size)).to eq(8) + end + end + + describe '.plate_length' do + it 'returns the height of a plate' do + expect(coordinate_module.plate_length(plate_size)).to eq(2) + end + end + + describe '.vertical_position_to_description' do + it 'returns the name of a well by well position and height' do + # The well position is in column order. Instead of plate size, the + # length (height, number of rows) of the plate is used to find the well. + expect(coordinate_module.vertical_position_to_description(1, 2)).to eq('A1') + expect(coordinate_module.vertical_position_to_description(2, 2)).to eq('B1') + expect(coordinate_module.vertical_position_to_description(3, 2)).to eq('A2') + + # ... + expect(coordinate_module.vertical_position_to_description(15, 2)).to eq('A8') + expect(coordinate_module.vertical_position_to_description(16, 2)).to eq('B8') + end + end + + describe '.horizontal_position_to_description' do + it 'returns the name of a well by well position and width' do + # The well position is in row order. Instead of plate size, the width + # (number of columns) of the plate is used to find the well. + expect(coordinate_module.horizontal_position_to_description(1, 8)).to eq('A1') + expect(coordinate_module.horizontal_position_to_description(2, 8)).to eq('A2') + + # ... + expect(coordinate_module.horizontal_position_to_description(8, 8)).to eq('A8') + expect(coordinate_module.horizontal_position_to_description(9, 8)).to eq('B1') + + # ... + expect(coordinate_module.horizontal_position_to_description(15, 8)).to eq('B7') + expect(coordinate_module.horizontal_position_to_description(16, 8)).to eq('B8') + end + end + + describe '.horizontal_to_vertical' do + it 'returns the vertical position of a well by horizontal position' do + input = (1..16).to_a + expected = input.select(&:odd?) + input.select(&:even?) + + input + .zip(expected) + .each do |horizontal, vertical| + expect(coordinate_module.horizontal_to_vertical(horizontal, plate_size)).to eq(vertical) + end + end + end + + describe '.vertical_to_horizontal' do + it 'returns the horizontal position of a well by vertical position' do + expected = (1..16).to_a + input = expected.select(&:odd?) + expected.select(&:even?) + + input + .zip(expected) + .each do |vertical, horizontal| + expect(coordinate_module.vertical_to_horizontal(vertical, plate_size)).to eq(horizontal) + end + end + end + + describe '.location_from_index' do + it 'returns the name of a well by zero-based index in row order' do + # Names of wells in rows: A1, A2, A3, ..., A8, B1, B2, B3, ..., B8 for indexes 0 to 15 + 8.times do |index| + expect(coordinate_module.location_from_index(index, plate_size)).to eq("A#{index + 1}") # first row + expect(coordinate_module.location_from_index(index + 8, plate_size)).to eq("B#{index + 1}") # second row + end + end + end + end + end +end From b5bae37613e11da4321b721ce560e7f1ec60d605 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 8 Jan 2024 00:11:10 +0000 Subject: [PATCH 04/37] Added comment about standard plate ratio in plate dimensions --- app/models/map.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/map.rb b/app/models/map.rb index 40d352311d..3719715c95 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -27,7 +27,8 @@ module Coordinate # - 96 represents a 96-well plate, arranged in 12 columns and 8 rows. # - 384 represents a 384-well plate, arranged in 24 columns and 16 rows. # - 16 represents a 16-well Chromium Chip , which has 8 columns and 2 rows. - # + # Although a 16-well Chromium Chip does not have 4:3 ratio to be a standard plate, + # the methods here still apply. # @return [Hash{Integer => Array}] the dimensions of the plates PLATE_DIMENSIONS = Hash.new { |_h, _k| [] }.merge(96 => [12, 8], 384 => [24, 16], 16 => [8, 2]) From 57a9ce23e1d022caa7be14c1f6beab45077e01cf Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 8 Jan 2024 00:12:12 +0000 Subject: [PATCH 05/37] Added tests for the Map model class methods --- spec/models/map_spec.rb | 122 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index cd39fa2797..4c1bd3c533 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -234,5 +234,127 @@ end end end + + describe 'class methods' do + describe '.valid_plate_size?' do + # This method checks if the plate size is a positive integer only. + it 'returns true for a valid plate size' do + expect(map_class.valid_plate_size?(plate_size)).to be true + end + + it 'returns false for an invalid plate size' do + expect(map_class.valid_plate_size?('16')).to be false # string + expect(map_class.valid_plate_size?(0)).to be false # zero + expect(map_class.valid_plate_size?(-1)).to be false # negative + end + end + + describe '.valid_plate_position_and_plate_size?' do + # This method is able to check if the position is out of bounds. + it 'returns true for a valid plate position and plate size' do + expect(map_class.valid_plate_position_and_plate_size?(1, plate_size)).to be true + expect(map_class.valid_plate_position_and_plate_size?(16, plate_size)).to be true + end + + it 'returns false for an invalid plate position' do + expect(map_class.valid_plate_position_and_plate_size?(0, plate_size)).to be false # zero + expect(map_class.valid_plate_position_and_plate_size?(17, plate_size)).to be false # out of bound + end + + it 'returns false for an invalid plate size' do + expect(map_class.valid_plate_position_and_plate_size?(1, 0)).to be false + end + end + + describe '.valid_well_description_and_plate_size?' do + # This method is not able to check if the well is out of bounds. + # It is called by Coordinate methods for basic validation. + it 'returns true for a valid well description and plate size' do + expect(map_class.valid_well_description_and_plate_size?('A1', plate_size)).to be true + end + + it 'returns false if well description is invalid' do + expect(map_class.valid_well_description_and_plate_size?('', plate_size)).to be false # empty string + expect(map_class.valid_well_description_and_plate_size?(nil, plate_size)).to be false # nil + end + + it 'returns false if plate size is invalid' do + expect(map_class.valid_well_description_and_plate_size?('A1', '16')).to be false # string + expect(map_class.valid_well_description_and_plate_size?('A1', 0)).to be false # zero + expect(map_class.valid_well_description_and_plate_size?('A1', -1)).to be false # negative + end + end + + describe '.valid_well_position?' do + # This method checks if the well position is a positive integer only. + it 'returns true for a valid well position' do + expect(map_class.valid_well_position?(1)).to be true + expect(map_class.valid_well_position?(16)).to be true + end + + it 'returns false of an invalid well position' do + expect(map_class.valid_well_position?('1')).to be false # string + expect(map_class.valid_well_position?(0)).to be false # zero + expect(map_class.valid_well_position?(nil)).to be false # string + end + end + + describe '.location_from_row_and_column' do + # This method calls Map::Coordinate.location_from_row_and_column . + it 'returns the name of a well by row and colum' do + # Rows are using zero-based index; columns are using one-based index. + (1..8).each do |column| + expect(map_class.location_from_row_and_column(0, column)).to eq("A#{column}") + expect(map_class.location_from_row_and_column(1, column)).to eq("B#{column}") + end + end + end + + describe '.horizontal_to_vertical' do + # This method calls Map::Coordinate.horizontal_to_vertical . + it 'returns the vertical position of a well by horizontal position' do + input = (1..16).to_a + expected = input.select(&:odd?) + input.select(&:even?) + + input + .zip(expected) + .each do |horizontal, vertical| + expect(map_class.horizontal_to_vertical(horizontal, plate_size)).to eq(vertical) + end + end + end + + describe '.vertical_to_horizontal' do + # This method calls Map::Coordinate.vertical_to_horizontal . + it 'returns the vertical position of a well by horizontal position' do + expected = (1..16).to_a + input = expected.select(&:odd?) + expected.select(&:even?) + + input + .zip(expected) + .each do |vertical, horizontal| + expect(map_class.vertical_to_horizontal(vertical, plate_size)).to eq(horizontal) + end + end + end + + describe '.split_well_description' do + it 'returns the row and column of a well in a hash' do + # Rows are using zero-based index; columns are using one-based index. + expect(map_class.split_well_description('A1')).to eq({row: 0, col: 1}) + expect(map_class.split_well_description('A8')).to eq({row: 0, col: 8}) + expect(map_class.split_well_description('B1')).to eq({row: 1, col: 1}) + expect(map_class.split_well_description('B8')).to eq({row: 1, col: 8}) + end + end + + describe '.strip_description' do + # Removes the leading zero from column if there is one. + it 'returns well description by removing the leading zero from column' do + expect(map_class.strip_description('A01')).to eq('A1') # one leading zero + expect(map_class.strip_description('B1')).to eq('B1') # no leading zeros + end + end + end end end From e51497acf1b092ef1be5bafb17549fefde9d7703 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 8 Jan 2024 00:15:04 +0000 Subject: [PATCH 06/37] Prettier --- spec/models/map_spec.rb | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index 4c1bd3c533..3e1fa9a7e1 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -293,9 +293,9 @@ end it 'returns false of an invalid well position' do - expect(map_class.valid_well_position?('1')).to be false # string - expect(map_class.valid_well_position?(0)).to be false # zero - expect(map_class.valid_well_position?(nil)).to be false # string + expect(map_class.valid_well_position?('1')).to be false # string + expect(map_class.valid_well_position?(0)).to be false # zero + expect(map_class.valid_well_position?(nil)).to be false # string end end @@ -313,15 +313,15 @@ describe '.horizontal_to_vertical' do # This method calls Map::Coordinate.horizontal_to_vertical . it 'returns the vertical position of a well by horizontal position' do - input = (1..16).to_a - expected = input.select(&:odd?) + input.select(&:even?) - - input - .zip(expected) - .each do |horizontal, vertical| - expect(map_class.horizontal_to_vertical(horizontal, plate_size)).to eq(vertical) - end - end + input = (1..16).to_a + expected = input.select(&:odd?) + input.select(&:even?) + + input + .zip(expected) + .each do |horizontal, vertical| + expect(map_class.horizontal_to_vertical(horizontal, plate_size)).to eq(vertical) + end + end end describe '.vertical_to_horizontal' do @@ -341,17 +341,17 @@ describe '.split_well_description' do it 'returns the row and column of a well in a hash' do # Rows are using zero-based index; columns are using one-based index. - expect(map_class.split_well_description('A1')).to eq({row: 0, col: 1}) - expect(map_class.split_well_description('A8')).to eq({row: 0, col: 8}) - expect(map_class.split_well_description('B1')).to eq({row: 1, col: 1}) - expect(map_class.split_well_description('B8')).to eq({row: 1, col: 8}) + expect(map_class.split_well_description('A1')).to eq({ row: 0, col: 1 }) + expect(map_class.split_well_description('A8')).to eq({ row: 0, col: 8 }) + expect(map_class.split_well_description('B1')).to eq({ row: 1, col: 1 }) + expect(map_class.split_well_description('B8')).to eq({ row: 1, col: 8 }) end end describe '.strip_description' do # Removes the leading zero from column if there is one. it 'returns well description by removing the leading zero from column' do - expect(map_class.strip_description('A01')).to eq('A1') # one leading zero + expect(map_class.strip_description('A01')).to eq('A1') # one leading zero expect(map_class.strip_description('B1')).to eq('B1') # no leading zeros end end From 4055b89ac138ef57622f29896e82be2ac88dfeaf Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 9 Jan 2024 22:58:09 +0000 Subject: [PATCH 07/37] Added ChromiumChip to plate map generation --- lib/plate_map_generation.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/plate_map_generation.rb b/lib/plate_map_generation.rb index 4a0a92a2c1..366d047189 100644 --- a/lib/plate_map_generation.rb +++ b/lib/plate_map_generation.rb @@ -22,7 +22,8 @@ def self.maps vertical_ratio: 8, description_strategy: 'Sequential', sizes: [8] - } + }, + { name: 'ChromiumChip', horizontal_ratio: 4, vertical_ratio: 1, description_strategy: 'Coordinate', sizes: [16] } ] end From 61aebd08cabb7efeb12de1aa3a74fb3d9ac97693 Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 10 Jan 2024 16:01:14 +0000 Subject: [PATCH 08/37] Add alias for walk_plate_in_row_major_order method --- app/models/map.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/map.rb b/app/models/map.rb index 3719715c95..27bbaa967c 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -281,5 +281,6 @@ def walk_plate_in_row_major_order(size, asset_shape = nil) .order(:row_order) .each { |position| yield(position, position.row_order) } end + alias walk_plate_horizontally walk_plate_in_row_major_order end end From e10ff3a3a54e7486a68956a9bb65084f1015cb1a Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 11 Jan 2024 08:46:54 +0000 Subject: [PATCH 09/37] Added tests for map walking methods --- spec/models/map_spec.rb | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index 3e1fa9a7e1..e65e495cf4 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -355,6 +355,76 @@ expect(map_class.strip_description('B1')).to eq('B1') # no leading zeros end end + + describe '.pad_description' do + # Returns well description with a leading zero for a given map. + # AssetShapes and Maps are created before the test suite runs and + # they are available in the test database. + let(:chromium_chip_maps) { map_class.joins(:asset_shape).where(asset_shapes: { name: 'ChromiumChip' }) } + + it 'returns well description by adding a leading zero' do + expect(map_class.pad_description(chromium_chip_maps.first)).to eq('A01') + expect(map_class.pad_description(chromium_chip_maps.last)).to eq('B08') + end + end + + describe 'walk_plate_in_column_major_order' do + let(:shape) { AssetShape.find_by(name: 'ChromiumChip') } + + it 'walks vertically' do + # Generate a hash of well descriptions and their column order (zero-based) for testing. + hash = {} + map_class.walk_plate_vertically(plate_size, shape.id) do |map, column_order| + hash[map.description] = column_order + end + expected = + { + A1: 0, + B1: 1, + A2: 2, + B2: 3, + A3: 4, + B3: 5, + A4: 6, + B4: 7, + A5: 8, + B5: 9, + A6: 10, + B6: 11, + A7: 12, + B7: 13, + A8: 14, + B8: 15 + }.transform_keys(&:to_s) + expect(hash).to eq(expected) + end + + it 'walks horizontally' do + # Generate a hash of well descriptions and their column order (zero-based) for testing. + hash = {} + map_class.walk_plate_horizontally(plate_size, shape.id) { |map, row_order| hash[map.description] = row_order } + expected = + { + A1: 0, + A2: 1, + A3: 2, + A4: 3, + A5: 4, + A6: 5, + A7: 6, + A8: 7, + B1: 8, + B2: 9, + B3: 10, + B4: 11, + B5: 12, + B6: 13, + B7: 14, + B8: 15 + }.transform_keys(&:to_s) + expect(hash).to eq(expected) + end + end end end end From b3ce4b96692b27a790cf09b5ecc9ebc6db1cdb62 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 11 Jan 2024 08:52:40 +0000 Subject: [PATCH 10/37] Fixed comment in test for walk_plate_horizontally method --- spec/models/map_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index e65e495cf4..814e28b6bf 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -400,7 +400,7 @@ end it 'walks horizontally' do - # Generate a hash of well descriptions and their column order (zero-based) for testing. + # Generate a hash of well descriptions and their row order (zero-based) for testing. hash = {} map_class.walk_plate_horizontally(plate_size, shape.id) { |map, row_order| hash[map.description] = row_order } expected = From 512eb8bfb64ed050b8e5b3784fd5a7f8f5b8db1e Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 11 Jan 2024 14:36:21 +0000 Subject: [PATCH 11/37] Specify the asset shape and size as well while finding the map by description --- .../submissions_controller_spec.rb | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb index 63efe6562e..8b1895cdfc 100644 --- a/spec/controllers/submissions_controller_spec.rb +++ b/spec/controllers/submissions_controller_spec.rb @@ -17,12 +17,23 @@ session[:user] = @user + # We need to specify the details of the map while using Map.find_by + # to avoid picking up another map with the same description. + @asset_shape = AssetShape.default + @asset_size = 96 + @plate = build :plate, barcode: 'SQPD-123456' %w[A1 A2 A3 B1 B2 B3 C1 C2 C3].each do |location| - well = build :well_with_sample_and_without_plate, map: Map.find_by(description: location) + well = + build :well_with_sample_and_without_plate, + map: Map.find_by(description: location, asset_shape: @asset_shape, asset_size: @asset_size) @plate.wells << well end - build(:well, map: Map.find_by(description: 'C5'), plate: @plate) + build( + :well, + map: Map.find_by(description: 'C5', asset_shape: @asset_shape, asset_size: @asset_size), + plate: @plate + ) @plate.save @study = create :study, name: 'A study' @project = create :project, name: 'A project' @@ -142,7 +153,10 @@ context 'with a more recent plate' do before do @new_plate = create :plate, plate_purpose: @plate.purpose - @well = create :well, map: Map.find_by(description: 'A1'), plate: @new_plate + @well = + create :well, + map: Map.find_by(description: 'A1', asset_shape: @asset_shape, asset_size: @asset_size), + plate: @new_plate create(:aliquot, sample: Sample.find_by(name: @samples.first), receptacle: @well) post( :create, @@ -184,7 +198,7 @@ @order_count = Order.count @wd_plate = create :working_dilution_plate %w[A1 A2 A3 B1 B2 B3 C1 C2 C3].each do |location| - well = create :empty_well, map: Map.find_by(description: location) + well = create :empty_well, map: Map.find_by(description: location, asset_shape: @asset_shape) well.aliquots.create(sample: @plate.wells.located_at(location).first.aliquots.first.sample) @wd_plate.wells << well end From feeca8c5a75ef305c0bc4e8d3bad2fc5f5a2b4cf Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 11 Jan 2024 14:39:58 +0000 Subject: [PATCH 12/37] Added asset_size to Map query --- spec/controllers/submissions_controller_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb index 8b1895cdfc..ffc227eed2 100644 --- a/spec/controllers/submissions_controller_spec.rb +++ b/spec/controllers/submissions_controller_spec.rb @@ -198,7 +198,9 @@ @order_count = Order.count @wd_plate = create :working_dilution_plate %w[A1 A2 A3 B1 B2 B3 C1 C2 C3].each do |location| - well = create :empty_well, map: Map.find_by(description: location, asset_shape: @asset_shape) + well = + create :empty_well, + map: Map.find_by(description: location, asset_shape: @asset_shape, asset_size: @asset_size) well.aliquots.create(sample: @plate.wells.located_at(location).first.aliquots.first.sample) @wd_plate.wells << well end From 8753f173f544f427b338038266910670c176d34b Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 11 Jan 2024 16:19:14 +0000 Subject: [PATCH 13/37] Find the asset shape and set in config --- lib/record_loader/plate_purpose_loader.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/record_loader/plate_purpose_loader.rb b/lib/record_loader/plate_purpose_loader.rb index c9cc8e8c5a..1136f16784 100644 --- a/lib/record_loader/plate_purpose_loader.rb +++ b/lib/record_loader/plate_purpose_loader.rb @@ -27,6 +27,7 @@ def create_purpose(name, config) creator = config.delete('plate_creator') config['barcode_printer_type'] = barcode_printer_type(config.delete('barcode_printer_type')) config['name'] = name + config['asset_shape'] = AssetShape.find_by(name: config.delete('asset_shape')) purpose = PlatePurpose.create!(config) build_creator(purpose, creator) if creator end From 2448716d46ff5a1c098ae769788d4dbfcefa0174 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 11 Jan 2024 16:58:31 +0000 Subject: [PATCH 14/37] Use the default (Standard) if asset_shape not specified in purpose config --- lib/record_loader/plate_purpose_loader.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/record_loader/plate_purpose_loader.rb b/lib/record_loader/plate_purpose_loader.rb index 1136f16784..f338413d97 100644 --- a/lib/record_loader/plate_purpose_loader.rb +++ b/lib/record_loader/plate_purpose_loader.rb @@ -27,7 +27,7 @@ def create_purpose(name, config) creator = config.delete('plate_creator') config['barcode_printer_type'] = barcode_printer_type(config.delete('barcode_printer_type')) config['name'] = name - config['asset_shape'] = AssetShape.find_by(name: config.delete('asset_shape')) + config['asset_shape'] = AssetShape.find_by(name: config.delete('asset_shape')) || AssetShape.default purpose = PlatePurpose.create!(config) build_creator(purpose, creator) if creator end From 4445a0055d6869187910fb6a636ceea5169e4a1f Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 11 Jan 2024 19:51:17 +0000 Subject: [PATCH 15/37] Added test for creating a ChromiumChip plate purpose using record loader --- .../plate_purposes/003_chromium_chip.yml | 6 ++++++ .../lib/record_loader/plate_purpose_loader_spec.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 spec/data/record_loader/plate_purposes/003_chromium_chip.yml diff --git a/spec/data/record_loader/plate_purposes/003_chromium_chip.yml b/spec/data/record_loader/plate_purposes/003_chromium_chip.yml new file mode 100644 index 0000000000..a9669f0855 --- /dev/null +++ b/spec/data/record_loader/plate_purposes/003_chromium_chip.yml @@ -0,0 +1,6 @@ +--- +ChromiumChip Plate: + asset_shape: ChromiumChip + cherrypickable_target: false + target_type: plate + size: 16 diff --git a/spec/lib/record_loader/plate_purpose_loader_spec.rb b/spec/lib/record_loader/plate_purpose_loader_spec.rb index d7f0d14259..3af20776b8 100644 --- a/spec/lib/record_loader/plate_purpose_loader_spec.rb +++ b/spec/lib/record_loader/plate_purpose_loader_spec.rb @@ -61,4 +61,18 @@ def a_new_record_loader expect(the_creator.parent_plate_purposes).to eq(PlatePurpose.where(name: 'Stock Plate')) end end + + context 'with ChromimumChip Plate purpose' do + let(:selected_files) { '003_chromium_chip' } + let(:purpose_name) { 'ChromiumChip Plate' } + + before { record_loader.create! } + + it 'creates a plate purpose' do + expect(Purpose.where(name: purpose_name).count).to eq(1) + purpose = Purpose.where(name: purpose_name).first + expect(purpose.asset_shape).to eq(AssetShape.find_by(name: 'ChromiumChip')) + expect(purpose.size).to eq(16) + end + end end From da9c9ec39c36bde269cea363564ced93cb13c8f7 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 12 Jan 2024 02:02:59 +0000 Subject: [PATCH 16/37] Update the comment about plate dimensions in Map module with ratios --- app/models/map.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index 27bbaa967c..e322819396 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -26,9 +26,9 @@ module Coordinate # @note # - 96 represents a 96-well plate, arranged in 12 columns and 8 rows. # - 384 represents a 384-well plate, arranged in 24 columns and 16 rows. - # - 16 represents a 16-well Chromium Chip , which has 8 columns and 2 rows. - # Although a 16-well Chromium Chip does not have 4:3 ratio to be a standard plate, - # the methods here still apply. + # - 16 represents a 16-well Chromium Chip, which has 8 columns and 2 rows. + # Although a 16-well Chromium Chip does not have 3:2 ratio to be a + # standard plate, i.e. it has 4:1 ratio, the methods here still apply. # @return [Hash{Integer => Array}] the dimensions of the plates PLATE_DIMENSIONS = Hash.new { |_h, _k| [] }.merge(96 => [12, 8], 384 => [24, 16], 16 => [8, 2]) From 5c89833bfec50aea03ffedbea14474d3a7451d04 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 12 Jan 2024 02:04:33 +0000 Subject: [PATCH 17/37] Added record loader config file for asset shapes --- .../asset_shapes/default_records.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 config/default_records/asset_shapes/default_records.yml diff --git a/config/default_records/asset_shapes/default_records.yml b/config/default_records/asset_shapes/default_records.yml new file mode 100644 index 0000000000..d73929ee24 --- /dev/null +++ b/config/default_records/asset_shapes/default_records.yml @@ -0,0 +1,62 @@ +# This file contains the AssetShape and associated Map records to be created +# in deployment environments by RecordLoader. The loading is triggered by +# the "post_deploy" task. The records are checked against the database by +# their names. If the records already exist, they are not created. +# AssetShape records are created in the "asset_shapes" table, and Map records +# are created in the "maps" table. Each section in this file starts with the +# name of the AssetShape record. If the name is defined again in the options, +# it takes priority. The "horizontal_ratio" and "vertical_ratio" options +# define the shape of the plates. For example, a "Standard" 96-well plate has +# a horizontal ratio of 3 and a vertical ratio of 2 because it has 12 columns +# (1 to 12) and 8 rows (A to H). The "sizes" option defines different plate +# sizes with that shape, for example, 96 and 384. Each size determines the +# number of Map records to be created for that shape and size. +# The "description_strategy" option defines the nested module in the Map model +# that is used for handling positions, rows, columns, and wells in plate +# geometry. AssetShapeLoader uses PlateMapGeneration to create records in the +# database. Each "Well" on a "Plate" is associated with a "Map". +# +# When configuring a "Purpose", both "asset_shape" and "size" need to be +# specified in that configuration in order to use the correct labware. If not +# specified, they will default to Standard and 96 respectively. For example, +# LRC HT 5p Chip: +# asset_shape: ChromiumChip +# size: 16 +# +# The information in this file is duplicated in a couple of places in the +# codebase. When the local development environment is "setup" or "reset", a +# database "seed" is executed. This results in using the maps hash in +# PlateMapGeneration. The same hash is used when the RSpec before suite hook +# calls PlateMapGeneration to create records in the local test environment. +# Plate sizes and number or rows and columns are also defined separately in +# a hash in the Map model, which is used by the nested Coordinate module. +--- +Standard: + horizontal_ratio: 3 + vertical_ratio: 2 + description_strategy: 'Coordinate' + sizes: [96, 384] + +Fluidigm96: + horizontal_ratio: 3 + vertical_ratio: 8 + description_strategy: 'Sequential' + sizes: [96] + +Fluidigm192: + horizontal_ratio: 3 + vertical_ratio: 4 + description_strategy: 'Sequential' + sizes: [192] + +StripTubeColumn: + horizontal_ratio: 1, + vertical_ratio: 8, + description_strategy: 'Sequential' + sizes: [8] + +ChromiumChip: + horizontal_ratio: 4 + vertical_ratio: 1 + description_strategy: 'Coordinate' + sizes: [16] From ec4198e94e7c4b653df56091d8c75d3e5b3ce913 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 12 Jan 2024 02:07:00 +0000 Subject: [PATCH 18/37] Added rake task for asset shape loader --- lib/tasks/record_loader/asset_shape.rake | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/tasks/record_loader/asset_shape.rake diff --git a/lib/tasks/record_loader/asset_shape.rake b/lib/tasks/record_loader/asset_shape.rake new file mode 100644 index 0000000000..b1fc1f41f8 --- /dev/null +++ b/lib/tasks/record_loader/asset_shape.rake @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +namespace :record_loader do + desc 'Automatically generate AssetShapes and Maps through AssetShapesLoader' + task asset_shape: :environment do + RecordLoader::AssetShapeLoader.new.create! + end +end + +# Automatically run this record loader as part of record_loader:all +# Remove this line if the task should only run when invoked explicitly +task 'record_loader:all' => 'record_loader:asset_shape' From 34897518c9dfeabd08a2f1d91839fb25a152881c Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 12 Jan 2024 02:08:46 +0000 Subject: [PATCH 19/37] Added AssetShapeLoader which uses PlateMapGenerator to create asset_shapes and maps --- lib/record_loader/asset_shape_loader.rb | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/record_loader/asset_shape_loader.rb diff --git a/lib/record_loader/asset_shape_loader.rb b/lib/record_loader/asset_shape_loader.rb new file mode 100644 index 0000000000..0fbf096025 --- /dev/null +++ b/lib/record_loader/asset_shape_loader.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# RecordLoader handles automatic population and updating of database records +# across different environments +# @see https://rubydoc.info/github/sanger/record_loader/ +module RecordLoader + class AssetShapeLoader < ApplicationRecordLoader + config_folder 'asset_shapes' + + # Creates an AssetShape record with the given name and options. If a record + # with the same name already exists, it is skipped. If not, a new one is + # created. For each size specified in the options, Map records are generated + # up to that size, unless they already exist. + # + # @param name [String] the name of the AssetShape record + # @param options [Hash] the options to be used for creating or updating the record + # @param options [String] :name If provided, this will override the 'name' parameter + # @option options [Integer] :horizontal_ratio The horizontal ratio of the plate + # @option options [Integer] :vertical_ratio The vertical ratio of the plate + # @option options [String] :description_strategy The strategy for describing the plate + # @option options [Array] :sizes The sizes of the plates to generate Maps for + def create_or_update!(name, options) + config = { name: name }.merge(options.symbolize_keys) + config[:description_strategy] = config[:description_strategy].delete_prefix('Map::') + PlateMapGeneration.new(**config).save! + end + end +end From 6f3037ca8a7f8f36f3163d681e2c5fb3afc142a0 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 12 Jan 2024 02:16:11 +0000 Subject: [PATCH 20/37] Prettier record loader config --- .../asset_shapes/default_records.yml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/config/default_records/asset_shapes/default_records.yml b/config/default_records/asset_shapes/default_records.yml index d73929ee24..6c9ac47110 100644 --- a/config/default_records/asset_shapes/default_records.yml +++ b/config/default_records/asset_shapes/default_records.yml @@ -12,17 +12,17 @@ # sizes with that shape, for example, 96 and 384. Each size determines the # number of Map records to be created for that shape and size. # The "description_strategy" option defines the nested module in the Map model -# that is used for handling positions, rows, columns, and wells in plate -# geometry. AssetShapeLoader uses PlateMapGeneration to create records in the +# that is used for handling positions, rows, columns, and wells in plate +# geometry. AssetShapeLoader uses PlateMapGeneration to create records in the # database. Each "Well" on a "Plate" is associated with a "Map". -# -# When configuring a "Purpose", both "asset_shape" and "size" need to be +# +# When configuring a "Purpose", both "asset_shape" and "size" need to be # specified in that configuration in order to use the correct labware. If not # specified, they will default to Standard and 96 respectively. For example, # LRC HT 5p Chip: # asset_shape: ChromiumChip # size: 16 -# +# # The information in this file is duplicated in a couple of places in the # codebase. When the local development environment is "setup" or "reset", a # database "seed" is executed. This results in using the maps hash in @@ -34,29 +34,29 @@ Standard: horizontal_ratio: 3 vertical_ratio: 2 - description_strategy: 'Coordinate' + description_strategy: Coordinate sizes: [96, 384] Fluidigm96: horizontal_ratio: 3 vertical_ratio: 8 - description_strategy: 'Sequential' + description_strategy: Sequential sizes: [96] Fluidigm192: horizontal_ratio: 3 vertical_ratio: 4 - description_strategy: 'Sequential' + description_strategy: Sequential sizes: [192] StripTubeColumn: horizontal_ratio: 1, vertical_ratio: 8, - description_strategy: 'Sequential' + description_strategy: Sequential sizes: [8] ChromiumChip: horizontal_ratio: 4 vertical_ratio: 1 - description_strategy: 'Coordinate' + description_strategy: Coordinate sizes: [16] From ca59d2c7579e1b0854086820bde84286fa16f7c8 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 12 Jan 2024 02:50:18 +0000 Subject: [PATCH 21/37] Fixed the comment as the method does not update records --- lib/record_loader/asset_shape_loader.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/record_loader/asset_shape_loader.rb b/lib/record_loader/asset_shape_loader.rb index 0fbf096025..97bda6db42 100644 --- a/lib/record_loader/asset_shape_loader.rb +++ b/lib/record_loader/asset_shape_loader.rb @@ -13,7 +13,7 @@ class AssetShapeLoader < ApplicationRecordLoader # up to that size, unless they already exist. # # @param name [String] the name of the AssetShape record - # @param options [Hash] the options to be used for creating or updating the record + # @param options [Hash] the options to be used for creating the record # @param options [String] :name If provided, this will override the 'name' parameter # @option options [Integer] :horizontal_ratio The horizontal ratio of the plate # @option options [Integer] :vertical_ratio The vertical ratio of the plate From d75aac26729378d92065c4635c85855ac201ac9f Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 12 Jan 2024 15:47:46 +0000 Subject: [PATCH 22/37] Used description and asset_size to find Maps in tests --- .../7119309_pulldown_cherrypick_pipeline_steps.rb | 3 ++- features/support/step_definitions/plate_steps.rb | 5 +++-- spec/controllers/submissions_controller_spec.rb | 4 +++- .../api/messages/well_stock_resource_io_spec.rb | 2 +- spec/models/api/well_io_spec.rb | 13 +++++++++++-- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/features/support/step_definitions/7119309_pulldown_cherrypick_pipeline_steps.rb b/features/support/step_definitions/7119309_pulldown_cherrypick_pipeline_steps.rb index 7f7903773a..920df971a4 100644 --- a/features/support/step_definitions/7119309_pulldown_cherrypick_pipeline_steps.rb +++ b/features/support/step_definitions/7119309_pulldown_cherrypick_pipeline_steps.rb @@ -38,7 +38,8 @@ Given(/^I have a plate "([^"]*)" with the following wells:$/) do |plate_barcode, well_details| plate = FactoryBot.create :plate, barcode: plate_barcode well_details.hashes.each do |well_detail| - well = Well.create!(map: Map.find_by(description: well_detail[:well_location], asset_size: 96), plate: plate) + well = + Well.create!(map: Map.find_by(description: well_detail[:well_location], asset_size: plate.size), plate: plate) well.well_attribute.update!( concentration: well_detail[:measured_concentration], measured_volume: well_detail[:measured_volume] diff --git a/features/support/step_definitions/plate_steps.rb b/features/support/step_definitions/plate_steps.rb index 40763e6625..4ce301e7ea 100644 --- a/features/support/step_definitions/plate_steps.rb +++ b/features/support/step_definitions/plate_steps.rb @@ -111,7 +111,7 @@ Given /^well "([^"]*)" is holded by plate "([^"]*)"$/ do |well_uuid, plate_uuid| well = Uuid.find_by(external_id: well_uuid).resource plate = Uuid.find_by(external_id: plate_uuid).resource - well.update!(plate: plate, map: Map.find_by(description: 'A1')) + well.update!(plate: plate, map: Map.find_by(description: 'A1', asset_size: plate.size)) Plate.find(plate_id).primary_barcode.update!(barcode: 'DN1S') end @@ -213,7 +213,8 @@ # plate = FactoryBot.create :plate, :barcode => plate_barcode plate = Uuid.find_by(external_id: uuid).resource well_details.hashes.each do |well_detail| - well = Well.create!(map: Map.find_by(description: well_detail[:well_location], asset_size: 96), plate: plate) + well = + Well.create!(map: Map.find_by(description: well_detail[:well_location], asset_size: plate.size), plate: plate) well.well_attribute.update!( concentration: well_detail[:measured_concentration], measured_volume: well_detail[:measured_volume] diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb index ffc227eed2..697b26739c 100644 --- a/spec/controllers/submissions_controller_spec.rb +++ b/spec/controllers/submissions_controller_spec.rb @@ -18,7 +18,9 @@ session[:user] = @user # We need to specify the details of the map while using Map.find_by - # to avoid picking up another map with the same description. + # to avoid picking up another map with the same description. As of + # the current records, the 'description' and 'asset_size' attributes + # can uniquely identify a map; asset_shape is added for completeness. @asset_shape = AssetShape.default @asset_size = 96 diff --git a/spec/models/api/messages/well_stock_resource_io_spec.rb b/spec/models/api/messages/well_stock_resource_io_spec.rb index a21a9da822..575171a93c 100644 --- a/spec/models/api/messages/well_stock_resource_io_spec.rb +++ b/spec/models/api/messages/well_stock_resource_io_spec.rb @@ -14,7 +14,7 @@ let(:plate_barcode) { build(:plate_barcode) } let(:well) do create :well, - map: Map.find_by!(description: 'A1'), + map: Map.find_by!(description: 'A1', asset_shape: AssetShape.default, asset_size: 96), plate: create(:plate, barcode: plate_barcode.barcode), well_attribute: create(:complete_well_attribute) end diff --git a/spec/models/api/well_io_spec.rb b/spec/models/api/well_io_spec.rb index acdc92b213..f581e945cb 100644 --- a/spec/models/api/well_io_spec.rb +++ b/spec/models/api/well_io_spec.rb @@ -4,7 +4,12 @@ RSpec.describe Api::WellIO do context 'with one sample' do - subject { create :well_with_sample_and_without_plate, map: Map.find_by(description: 'A1'), plate: plate } + # As of the current records, the 'description' and 'asset_size' attributes can uniquely identify a map. + subject do + create :well_with_sample_and_without_plate, + map: Map.find_by(description: 'A1', asset_size: plate.size), + plate: plate + end let(:plate) { create :plate, barcode: 'SQPD-1' } let(:sample) { subject.samples.first } @@ -37,7 +42,11 @@ context 'with multiple samples' do subject do - create :well_with_sample_and_without_plate, map: Map.find_by(description: 'A1'), plate: plate, aliquot_count: 2 + # As of the current records, the 'description' and 'asset_size' attributes can uniquely identify a map. + create :well_with_sample_and_without_plate, + map: Map.find_by(description: 'A1', asset_size: plate.size), + plate: plate, + aliquot_count: 2 end let(:plate) { create :plate, barcode: 'SQPD-1' } From c577651d503fcb66d13a2f56e167789177c5ec57 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 15 Jan 2024 01:43:15 +0000 Subject: [PATCH 23/37] Added chromium_chip_purpose factory --- spec/factories/purpose_factories.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/factories/purpose_factories.rb b/spec/factories/purpose_factories.rb index 77de1bc374..230b781533 100644 --- a/spec/factories/purpose_factories.rb +++ b/spec/factories/purpose_factories.rb @@ -65,6 +65,11 @@ size { 192 } asset_shape factory: %i[fluidigm_192_shape] end + + factory :chromium_chip_purpose do + size { 16 } + asset_shape { AssetShape.find_by(name: 'ChromiumChip') } + end end factory :dilution_plate_purpose do From 4686f6440094cd3d085c37bebe1996f4f08c6963 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 15 Jan 2024 01:45:31 +0000 Subject: [PATCH 24/37] Added test for displaying a chromium chip labware --- spec/views/labware/show_chromium_chip_spec.rb | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 spec/views/labware/show_chromium_chip_spec.rb diff --git a/spec/views/labware/show_chromium_chip_spec.rb b/spec/views/labware/show_chromium_chip_spec.rb new file mode 100644 index 0000000000..a7f3b717ea --- /dev/null +++ b/spec/views/labware/show_chromium_chip_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'labware/show.html.erb' do + include AuthenticatedSystem + let(:user) { create :user } + + context 'when rendering a Chromium Chip 16-well plate' do + # We have a plate with a purpose that has an asset_shape of ChromiumChip + # and a size of 16. The plate has 16 wells, each with a sample. It has + # transfer requests into the plate in passed state, and a parent plate. + + let(:current_user) { user } + let(:purpose_name) { 'chromium-chip-purpose' } + let(:purpose) { create :chromium_chip_purpose, name: purpose_name } # AssetShape ChromiumChip, size 16 + let(:plate) { create :child_plate, well_factory: :passed_well, purpose: purpose, size: 16, sample_count: 16 } + let(:doc) { Nokogiri.HTML(rendered) } + + before do + assign(:asset, plate) + render + end + + it 'displays the barcode of the plate' do + expect(rendered).to have_css('tr', text: /Human barcode\s*#{plate.human_barcode}/) # Human Barcode + end + + it 'displays the purpose of the plate' do + expect(rendered).to have_css('tr', text: /Purpose\s*#{purpose_name}/) # Purpose chromium-chip-purpose + end + + it 'displays the plate wells' do + # The first column of the Samples table (Well) + expect(rendered).to have_table('plate-samples-table') + + table = doc.at('table#plate-samples-table') + column_texts = table.search('tr').filter_map { |tr| tr.at('td:first-child')&.text } + + expected = %w[A1 B1 A2 B2 A3 B3 A4 B4 A5 B5 A6 B6 A7 B7 A8 B8] # Full Chromium Chip 16-well plate + expect(column_texts).to eq(expected) + end + + it 'displays the plate samples' do + # The second column of the Samples table (Sample Name) + expect(rendered).to have_table('plate-samples-table') + + table = doc.at('table#plate-samples-table') + column_texts = table.search('tr').filter_map { |tr| tr.at('td:nth-child(2)')&.text } + + expected = plate.wells_in_column_order.map { |w| w.aliquots.first.sample.name } # Samples in wells in column order + expect(column_texts).to eq(expected) + end + + it 'displays the parent relationship' do + # The first row of the Relations table [Asset, Relationship type] + expect(rendered).to have_table('relations-table') + + table = doc.at('table#relations-table') + + asset_text = table.at('tbody tr').at('td')&.text + expect(asset_text).to eq("Plate: #{plate.parents.first.name}") # Plate + + relationship_type_text = table.at('tbody tr').at('td:nth-child(2)')&.text + expect(relationship_type_text).to eq('Parent') # Relationship + end + + it 'displays requests into the plate' do + expect(rendered).to have_table('target-requests-table') + + table = doc.at('table#target-requests-table') + number_of_rows = table.search('tbody tr').size + + expect(number_of_rows).to eq(plate.requests_as_target.size) # Number of requests + + column_texts = table.at('tbody tr').search('td').map { |td| td.text.strip } # First request + + expect(column_texts[0]).to eq(plate.wells.first.requests_as_target.first.id.to_s) # request id + expect(column_texts[1]).to eq(plate.wells.first.requests_as_target.first.request_type.name) # request type + expect(column_texts[2]).to eq("Study: #{plate.studies.first.name}") # study + expect(column_texts[3]).to eq('PASSED') # request status + end + end +end From c7a8e5cef51f82c23f9288fcfd905d116d637cb7 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 16 Jan 2024 02:06:21 +0000 Subject: [PATCH 25/37] Updated comment about use of purpose config options --- config/default_records/asset_shapes/default_records.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default_records/asset_shapes/default_records.yml b/config/default_records/asset_shapes/default_records.yml index 6c9ac47110..1c98a1e3b3 100644 --- a/config/default_records/asset_shapes/default_records.yml +++ b/config/default_records/asset_shapes/default_records.yml @@ -20,8 +20,8 @@ # specified in that configuration in order to use the correct labware. If not # specified, they will default to Standard and 96 respectively. For example, # LRC HT 5p Chip: -# asset_shape: ChromiumChip -# size: 16 +# :asset_shape: ChromiumChip +# :size: 16 # # The information in this file is duplicated in a couple of places in the # codebase. When the local development environment is "setup" or "reset", a From 425ddee2276a4beb7085d8a3cd2d4de896bbcb63 Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 17 Jan 2024 10:53:52 +0000 Subject: [PATCH 26/37] Added plate_purposes resource to routes --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 0756fd5404..96a764587e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,7 @@ jsonapi_resources :lot_types jsonapi_resources :lots jsonapi_resources :orders + jsonapi_resources :plate_purposes jsonapi_resources :plate_templates jsonapi_resources :plates jsonapi_resources :pre_capture_pools From b84dca795d72d5e97a2d5d5c00f9c516737d0d69 Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 17 Jan 2024 10:56:21 +0000 Subject: [PATCH 27/37] Added plate_purposes controller --- app/controllers/api/v2/plate_purposes_controller.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/controllers/api/v2/plate_purposes_controller.rb diff --git a/app/controllers/api/v2/plate_purposes_controller.rb b/app/controllers/api/v2/plate_purposes_controller.rb new file mode 100644 index 0000000000..82702603ff --- /dev/null +++ b/app/controllers/api/v2/plate_purposes_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API controller for receptacle + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation + class PlatePurposesController < JSONAPI::ResourceController + # By default JSONAPI::ResourceController provides most the standard + # behaviour, and in many cases this file may be left empty. + end + end +end From a24b146b29f5e87702c975df57e1bd025639d18c Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 17 Jan 2024 23:41:57 +0000 Subject: [PATCH 28/37] Added test for plate purpose resource --- .../api/v2/plate_purpose_resource_spec.rb | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 spec/resources/api/v2/plate_purpose_resource_spec.rb diff --git a/spec/resources/api/v2/plate_purpose_resource_spec.rb b/spec/resources/api/v2/plate_purpose_resource_spec.rb new file mode 100644 index 0000000000..0fc6fa9195 --- /dev/null +++ b/spec/resources/api/v2/plate_purpose_resource_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './app/resources/api/v2/plate_purpose_resource' + +RSpec.describe Api::V2::PlatePurposeResource, type: :resource do + # This test is different from the other resource tests because it will focus + # on payloads, the custom resource attribute 'asset_shape', and plate 'size'. + + let(:purpose) { PlatePurpose.new } # New instance of PlatePurpose model + let(:resource) { described_class.new(purpose, {}) } # Resource wrapping the instance + let(:receive) { resource.replace_fields(payload[:data]) } # Simulate receiving payload + let(:payload) { { data: { type: 'plate_purposes', attributes: attributes } } } # Payload to be received + let(:purpose_name) { 'LRC HT 5p Chip' } + let(:plate_size) { 16 } + let(:asset_shape_name) { 'ChromiumChip' } + let(:asset_shape) { AssetShape.find_by(name: asset_shape_name) } + + context 'when asset_shape and size are specified in payload' do + let(:attributes) { { name: purpose_name, size: plate_size, asset_shape: asset_shape_name } } + + # This creates a ChromiumChip 16-well plate purpose: + # LRC HT 5p Chip: + # size: 16 + # asset_shape: ChromiumChip + + it 'sets the specified asset_shape and size' do + receive + expect(purpose.name).to eq purpose_name + expect(purpose.asset_shape).to eq asset_shape # Association + expect(purpose.asset_shape_id).to eq asset_shape.id # Foreign key + expect(purpose.size).to eq plate_size + expect(purpose).to be_valid + end + end + + context 'when asset_shape and size are not specified in payload' do + let(:attributes) { { name: purpose_name } } + + # This creates a Standard 96-well plate purpose (default). + + it 'sets the default asset_shape and size' do + receive + expect(purpose.name).to eq purpose_name + expect(purpose.asset_shape).to eq AssetShape.default # Association + expect(purpose.asset_shape_id).to eq AssetShape.default.id # Foreign key + expect(purpose.size).to eq 96 # Default size + expect(purpose).to be_valid + end + end + + context 'when size is specified for Standard asset_shape' do + let(:attributes) { { name: purpose_name, size: 384 } } + + # This creates a Standard 384-well plate purpose. + + it 'sets the specified size' do + receive + expect(purpose.asset_shape).to eq AssetShape.default # Association + expect(purpose.asset_shape_id).to eq AssetShape.default.id # Foreign key + expect(purpose.size).to eq 384 + end + end + + context 'when asset_shape is specified' do + let(:attributes) { { name: purpose_name, asset_shape: asset_shape_name } } + + it 'sets the asset_shape' do + receive + expect(purpose.asset_shape).to eq asset_shape # Association + expect(purpose.asset_shape_id).to eq asset_shape.id # Foreign key + end + end + + context 'when asset_shape is not specified' do + let(:attributes) { { name: purpose_name } } + + it 'sets the default asset_shape' do + receive + expect(purpose.asset_shape).to eq AssetShape.default # Association + expect(purpose.asset_shape_id).to eq AssetShape.default.id # Foreign key + end + end + + context 'when asset_shape is specified as Standard' do + let(:attributes) { { name: purpose_name, asset_shape: 'Standard' } } + + it 'sets the default asset_shape' do + receive + expect(purpose.asset_shape).to eq AssetShape.default # Association + expect(purpose.asset_shape_id).to eq AssetShape.default.id # Foreign key + end + end + + context 'when the specified asset_shape is not found' do + let(:attributes) { { name: purpose_name, asset_shape: 'non-existing' } } + + it 'raises RecordNotFound error' do + # Note the curly braces to set up an expecation for the error. + expect { receive }.to raise_error ActiveRecord::RecordNotFound + end + end + + context 'when size is specified' do + let(:attributes) { { name: purpose_name, size: plate_size } } + + it 'sets the specified size' do + receive + expect(purpose.size).to eq plate_size + end + end + + context 'when size is not specified' do + let(:attributes) { { name: purpose_name } } + + it 'sets the default size' do + receive + expect(purpose.size).to eq 96 # Default size + end + end + + context 'when attributes are missing' do + let(:attributes) { { name: purpose_name } } + + it 'sets the defaults' do + receive + expect(purpose.stock_plate).to be false + expect(purpose.cherrypickable_target).to be true + expect(purpose.type).to eq 'PlatePurpose' # input_plate is false + end + end + + context 'when attributes are specified' do + let(:attributes) { { name: purpose_name, stock_plate: true, cherrypickable_target: false, input_plate: true } } + + it 'sets the specified attributes' do + receive + expect(purpose.stock_plate).to be true + expect(purpose.cherrypickable_target).to be false + expect(purpose.type).to eq 'PlatePurpose::Input' # input_plate is true + end + end +end From 237135ef62db68448712d9065493ca73f5b86ef1 Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 17 Jan 2024 23:44:27 +0000 Subject: [PATCH 29/37] Added resource to register new plate purposes via SS API v2 --- .../api/v2/plate_purpose_resource.rb | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/resources/api/v2/plate_purpose_resource.rb diff --git a/app/resources/api/v2/plate_purpose_resource.rb b/app/resources/api/v2/plate_purpose_resource.rb new file mode 100644 index 0000000000..33d28d3265 --- /dev/null +++ b/app/resources/api/v2/plate_purpose_resource.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API representation of Plate + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation + class PlatePurposeResource < BaseResource + model_name 'PlatePurpose' + + # This resource is similar to PurposeResource but it was created to + # migrate the registration of plate purposes done by the Limber rake + # task config:generate from API version 1 to API version 2. + + # The following options are sent by Limber for a new plate purpose. + + # @!attribute name + # @return [String] the name of the plate purpose + attribute :name + + # @!attribute stock_plate + # @return [Boolean] whether the plates of this purpose are stock plates + attribute :stock_plate + + # @!attribute cherrypickable_target + # @return [Boolean] whether the plates of this purpose are cherrypickable + attribute :cherrypickable_target + + # @!attribute input_plate + # @return [Boolean] whether the plates of this purpose are input plates + attribute :input_plate + + # @!attribute size + # @return [Integer] the size of the plates of this purpose + attribute :size + + # @!attribute asset_shape + # @return [String] the name of the shape of the plates of this purpose + attribute :asset_shape + + # Sets the asset shape of the plate purpose by name if given. + # 'asset_shape' can be given via the Limber purpose configuration and + # defaults to 'Standard' if not provided. If the name is given and not + # found, an error is raised. Note that the name is case-sensitive. + # + # @param name [String] the name of the asset shape + # @return [void] + def asset_shape=(name) + @model.asset_shape = (AssetShape.find_by!(name: name) if name.present?) || AssetShape.default + end + + # Returns the name of the asset shape of the plate purpose. + # The asset_shape association is not utilized in Limber. This method + # returns the name of the asset shape associated with the plate purpose. + # + # @return [String] the name of the asset shape + def asset_shape + @model.asset_shape.name + end + + # Returns the input_plate attribute from the type of the plate purpose. + # This method is the counterpart to the model's attribute writer for + # input_plate. It performs the inverse operation, determining the value + # of input_plate attribute based on the model's type. + # + # @return [Boolean] whether the plate purpose is an input plate + def input_plate + @model.type == 'PlatePurpose::Input' + end + + # Prevents updating existing plate purposes. + # + # @param _context [JSONAPI::Resource::Context] not used + # @return [Array] empty array + def updatable_fields(_context) + [] + end + end + end +end From 0d0525d0989b65fa945d90ebed5fee1f103608ba Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 18 Jan 2024 00:46:39 +0000 Subject: [PATCH 30/37] Added uuid attribute to plate purpose resource --- app/resources/api/v2/plate_purpose_resource.rb | 8 +++++++- spec/resources/api/v2/plate_purpose_resource_spec.rb | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/resources/api/v2/plate_purpose_resource.rb b/app/resources/api/v2/plate_purpose_resource.rb index 33d28d3265..3a256319c3 100644 --- a/app/resources/api/v2/plate_purpose_resource.rb +++ b/app/resources/api/v2/plate_purpose_resource.rb @@ -11,7 +11,7 @@ class PlatePurposeResource < BaseResource # migrate the registration of plate purposes done by the Limber rake # task config:generate from API version 1 to API version 2. - # The following options are sent by Limber for a new plate purpose. + # The following attributes are sent by Limber for a new plate purpose. # @!attribute name # @return [String] the name of the plate purpose @@ -37,6 +37,12 @@ class PlatePurposeResource < BaseResource # @return [String] the name of the shape of the plates of this purpose attribute :asset_shape + # The following attribute is required by Limber to store purposes. + + # @!attribute [r] uuid + # @return [String] the UUID of the plate purpose + attribute :uuid, readonly: true + # Sets the asset shape of the plate purpose by name if given. # 'asset_shape' can be given via the Limber purpose configuration and # defaults to 'Standard' if not provided. If the name is given and not diff --git a/spec/resources/api/v2/plate_purpose_resource_spec.rb b/spec/resources/api/v2/plate_purpose_resource_spec.rb index 0fc6fa9195..8a9933c82f 100644 --- a/spec/resources/api/v2/plate_purpose_resource_spec.rb +++ b/spec/resources/api/v2/plate_purpose_resource_spec.rb @@ -31,6 +31,7 @@ expect(purpose.asset_shape_id).to eq asset_shape.id # Foreign key expect(purpose.size).to eq plate_size expect(purpose).to be_valid + expect(purpose.uuid).to be_present end end @@ -46,6 +47,7 @@ expect(purpose.asset_shape_id).to eq AssetShape.default.id # Foreign key expect(purpose.size).to eq 96 # Default size expect(purpose).to be_valid + expect(purpose.uuid).to be_present end end @@ -59,6 +61,7 @@ expect(purpose.asset_shape).to eq AssetShape.default # Association expect(purpose.asset_shape_id).to eq AssetShape.default.id # Foreign key expect(purpose.size).to eq 384 + expect(purpose.uuid).to be_present end end From 6a7d0ba08215fb700937dacf30d03d79b5ae982b Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 19 Jan 2024 12:05:41 +0000 Subject: [PATCH 31/37] Update app/resources/api/v2/plate_purpose_resource.rb Co-authored-by: KatyTaylor --- app/resources/api/v2/plate_purpose_resource.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/resources/api/v2/plate_purpose_resource.rb b/app/resources/api/v2/plate_purpose_resource.rb index 3a256319c3..825792ca40 100644 --- a/app/resources/api/v2/plate_purpose_resource.rb +++ b/app/resources/api/v2/plate_purpose_resource.rb @@ -2,7 +2,7 @@ module Api module V2 - # Provides a JSON API representation of Plate + # Provides a JSON API representation of PlatePurpose # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation class PlatePurposeResource < BaseResource model_name 'PlatePurpose' From 798de85c83927335820c411c8b1c7df507649a5b Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 19 Jan 2024 13:08:17 +0000 Subject: [PATCH 32/37] Update config/default_records/asset_shapes/default_records.yml Co-authored-by: KatyTaylor --- config/default_records/asset_shapes/default_records.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/default_records/asset_shapes/default_records.yml b/config/default_records/asset_shapes/default_records.yml index 1c98a1e3b3..ff87d0352b 100644 --- a/config/default_records/asset_shapes/default_records.yml +++ b/config/default_records/asset_shapes/default_records.yml @@ -28,7 +28,7 @@ # database "seed" is executed. This results in using the maps hash in # PlateMapGeneration. The same hash is used when the RSpec before suite hook # calls PlateMapGeneration to create records in the local test environment. -# Plate sizes and number or rows and columns are also defined separately in +# Plate sizes and number of rows and columns are also defined separately in # a hash in the Map model, which is used by the nested Coordinate module. --- Standard: From f7677a29a0718f7296d1dd1bb4fd5e66578f494e Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 19 Jan 2024 11:15:41 +0000 Subject: [PATCH 33/37] Added comment for asset shape loader --- lib/record_loader/asset_shape_loader.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/record_loader/asset_shape_loader.rb b/lib/record_loader/asset_shape_loader.rb index 97bda6db42..7edacc11db 100644 --- a/lib/record_loader/asset_shape_loader.rb +++ b/lib/record_loader/asset_shape_loader.rb @@ -4,6 +4,7 @@ # across different environments # @see https://rubydoc.info/github/sanger/record_loader/ module RecordLoader + # Creates the specified asset shapes if they are not present class AssetShapeLoader < ApplicationRecordLoader config_folder 'asset_shapes' From 4ef17ca542b9d6cd0ec3fa684afbe4bd61b2cdc9 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 19 Jan 2024 13:13:51 +0000 Subject: [PATCH 34/37] Removed confusing comment about options --- config/default_records/asset_shapes/default_records.yml | 4 +--- lib/record_loader/asset_shape_loader.rb | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/config/default_records/asset_shapes/default_records.yml b/config/default_records/asset_shapes/default_records.yml index ff87d0352b..0cdfdda4f7 100644 --- a/config/default_records/asset_shapes/default_records.yml +++ b/config/default_records/asset_shapes/default_records.yml @@ -4,9 +4,7 @@ # their names. If the records already exist, they are not created. # AssetShape records are created in the "asset_shapes" table, and Map records # are created in the "maps" table. Each section in this file starts with the -# name of the AssetShape record. If the name is defined again in the options, -# it takes priority. The "horizontal_ratio" and "vertical_ratio" options -# define the shape of the plates. For example, a "Standard" 96-well plate has +# name of the AssetShape record. For example, a "Standard" 96-well plate has # a horizontal ratio of 3 and a vertical ratio of 2 because it has 12 columns # (1 to 12) and 8 rows (A to H). The "sizes" option defines different plate # sizes with that shape, for example, 96 and 384. Each size determines the diff --git a/lib/record_loader/asset_shape_loader.rb b/lib/record_loader/asset_shape_loader.rb index 7edacc11db..cb7289b5cc 100644 --- a/lib/record_loader/asset_shape_loader.rb +++ b/lib/record_loader/asset_shape_loader.rb @@ -15,7 +15,6 @@ class AssetShapeLoader < ApplicationRecordLoader # # @param name [String] the name of the AssetShape record # @param options [Hash] the options to be used for creating the record - # @param options [String] :name If provided, this will override the 'name' parameter # @option options [Integer] :horizontal_ratio The horizontal ratio of the plate # @option options [Integer] :vertical_ratio The vertical ratio of the plate # @option options [String] :description_strategy The strategy for describing the plate From f58c21a0dc6f1815716ecfdfa9e83b75337c30db Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 19 Jan 2024 13:52:47 +0000 Subject: [PATCH 35/37] Addded descriptions for horizontal_ratio and vertical_ratio --- .../asset_shapes/default_records.yml | 35 +++++++++++-------- lib/record_loader/asset_shape_loader.rb | 4 +-- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/config/default_records/asset_shapes/default_records.yml b/config/default_records/asset_shapes/default_records.yml index 0cdfdda4f7..dc95536256 100644 --- a/config/default_records/asset_shapes/default_records.yml +++ b/config/default_records/asset_shapes/default_records.yml @@ -1,18 +1,23 @@ -# This file contains the AssetShape and associated Map records to be created -# in deployment environments by RecordLoader. The loading is triggered by -# the "post_deploy" task. The records are checked against the database by -# their names. If the records already exist, they are not created. -# AssetShape records are created in the "asset_shapes" table, and Map records -# are created in the "maps" table. Each section in this file starts with the -# name of the AssetShape record. For example, a "Standard" 96-well plate has -# a horizontal ratio of 3 and a vertical ratio of 2 because it has 12 columns -# (1 to 12) and 8 rows (A to H). The "sizes" option defines different plate -# sizes with that shape, for example, 96 and 384. Each size determines the -# number of Map records to be created for that shape and size. -# The "description_strategy" option defines the nested module in the Map model -# that is used for handling positions, rows, columns, and wells in plate -# geometry. AssetShapeLoader uses PlateMapGeneration to create records in the -# database. Each "Well" on a "Plate" is associated with a "Map". +# This file contains the AssetShape and associated Map records to be created in +# deployment environments by RecordLoader. The loading is triggered by the +# "post_deploy" task. The records are checked against the database by their +# names. If the records already exist, they are not created. AssetShape records +# are created in the "asset_shapes" table, and Map records are created in the +# "maps" table. Each section in this file starts with the name of the AssetShape +# record. The "horizontal_ratio" and "vertical_ratio" options are used to define +# the plate's shape. These options represent the simplest form of the ratio +# between the number of columns and rows (i.e. width / height). Specifically, +# "horizontal_ratio" corresponds to the number of columns (numerator), and +# "vertical_ratio" corresponds to the number of rows (denominator). For +# instance, a "Standard" 96-well plate has a horizontal ratio of 3 and a +# vertical ratio of 2 because it has 12 columns (1 to 12) and 8 rows (A to H). +# The "sizes" option defines different plate sizes with that shape, for example, +# 96 and 384. Each size determines the number of Map records to be created for +# that shape and size. The "description_strategy" option defines the nested +# module in the Map model that is used for handling positions, rows, columns, +# and wells in plate geometry. AssetShapeLoader uses PlateMapGeneration to +# create records in the database. Each "Well" on a "Plate" is associated with a +# "Map". # # When configuring a "Purpose", both "asset_shape" and "size" need to be # specified in that configuration in order to use the correct labware. If not diff --git a/lib/record_loader/asset_shape_loader.rb b/lib/record_loader/asset_shape_loader.rb index cb7289b5cc..b10db6a3b5 100644 --- a/lib/record_loader/asset_shape_loader.rb +++ b/lib/record_loader/asset_shape_loader.rb @@ -15,8 +15,8 @@ class AssetShapeLoader < ApplicationRecordLoader # # @param name [String] the name of the AssetShape record # @param options [Hash] the options to be used for creating the record - # @option options [Integer] :horizontal_ratio The horizontal ratio of the plate - # @option options [Integer] :vertical_ratio The vertical ratio of the plate + # @option options [Integer] :horizontal_ratio the numerator in the simplest form of the plate width/height ratio + # @option options [Integer] :vertical_ratio the denominator in the simplest form of the plate width/height ratio # @option options [String] :description_strategy The strategy for describing the plate # @option options [Array] :sizes The sizes of the plates to generate Maps for def create_or_update!(name, options) From 449916a64fc61c58a8f5150c694e07f079716a9c Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 19 Jan 2024 14:13:29 +0000 Subject: [PATCH 36/37] Added comment about removing Map:: prefix --- lib/record_loader/asset_shape_loader.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/record_loader/asset_shape_loader.rb b/lib/record_loader/asset_shape_loader.rb index b10db6a3b5..c9b8f5e654 100644 --- a/lib/record_loader/asset_shape_loader.rb +++ b/lib/record_loader/asset_shape_loader.rb @@ -21,6 +21,10 @@ class AssetShapeLoader < ApplicationRecordLoader # @option options [Array] :sizes The sizes of the plates to generate Maps for def create_or_update!(name, options) config = { name: name }.merge(options.symbolize_keys) + # PlateMapGeneration expects a non-namespaced constant for + # description_strategy. It adds "Map::" prefix to refer to a nested + # module in the Map class. We remove this prefix in case it is given + # in the records file. config[:description_strategy] = config[:description_strategy].delete_prefix('Map::') PlateMapGeneration.new(**config).save! end From 112be8a5fc69943ec450d450ba7201e890af3758 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 19 Jan 2024 14:16:15 +0000 Subject: [PATCH 37/37] Prettier --- lib/record_loader/asset_shape_loader.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/record_loader/asset_shape_loader.rb b/lib/record_loader/asset_shape_loader.rb index c9b8f5e654..e1fe8a8449 100644 --- a/lib/record_loader/asset_shape_loader.rb +++ b/lib/record_loader/asset_shape_loader.rb @@ -21,6 +21,7 @@ class AssetShapeLoader < ApplicationRecordLoader # @option options [Array] :sizes The sizes of the plates to generate Maps for def create_or_update!(name, options) config = { name: name }.merge(options.symbolize_keys) + # PlateMapGeneration expects a non-namespaced constant for # description_strategy. It adds "Map::" prefix to refer to a nested # module in the Map class. We remove this prefix in case it is given