Skip to content

Commit 43e4f06

Browse files
authored
Merge pull request #1019 from sanger/976-dpl-740-modify-the-pacbio-run-to-have-more-than-one-plate-for-multi-plate-support
976 dpl 740 modify the pacbio run to have more than one plate for multi plate support
2 parents b68f35c + cab62e2 commit 43e4f06

40 files changed

+1433
-753
lines changed

app/csv_generator/pacbio_sample_sheet.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def generate
3030
private
3131

3232
def wells
33-
run.plate.wells
33+
run.plates.first.wells
3434
end
3535

3636
# Returns a list of wells associated with the plate in column order

app/models/pacbio/plate.rb

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,14 @@ class Plate < ApplicationRecord
66
include Uuidable
77

88
belongs_to :run, foreign_key: :pacbio_run_id,
9-
inverse_of: :plate
9+
inverse_of: :plates
1010
has_many :wells, class_name: 'Pacbio::Well', foreign_key: :pacbio_plate_id,
11-
inverse_of: :plate, dependent: :destroy
11+
inverse_of: :plate, dependent: :destroy, autosave: true
1212

13-
accepts_nested_attributes_for :wells
13+
accepts_nested_attributes_for :wells, allow_destroy: true
1414

15-
validates :wells, length: {
16-
minimum: 1,
17-
message: :plate_min_wells
18-
}
19-
20-
def well_attributes=(well_options)
21-
# Delete wells if attributes are not given
22-
delete_removed_wells(well_options)
23-
24-
create_or_update_wells(well_options)
25-
end
26-
27-
def delete_removed_wells(well_options)
28-
# Here we need to map the json ids to integers to make sure the exclude comparison uses ints
29-
options_ids = well_options.pluck(:id).compact.map(&:to_i)
30-
31-
# This loop here preloads the wells
32-
# This ensures the wells.find(attributes[:id]) block
33-
# in well_attributes=
34-
# is successful because wells have been preloaded
35-
# Otherwise, access wells[i] before calling find
36-
wells.each do |well|
37-
wells.delete(well) if options_ids.exclude? well.id
38-
end
39-
end
40-
41-
def create_or_update_wells(well_options)
42-
well_options.map do |well_attributes|
43-
# Assuming attributes['pools'] and the given pool id's exists
44-
# If not, there is a problem and throw a 5**
45-
pools = well_attributes['pools'].map { |pool_id| Pacbio::Pool.find(pool_id) }
46-
well_attributes['pools'] = pools
47-
48-
if well_attributes[:id]
49-
wells.find(well_attributes[:id]).assign_attributes(well_attributes)
50-
else
51-
wells.build(well_attributes)
52-
end
53-
end
54-
end
15+
# we maybe still need this in case someone tries to create a
16+
# non sequel IIe or Revio run
17+
validates :sequencing_kit_box_barcode, :plate_number, presence: true
5518
end
5619
end

app/models/pacbio/run.rb

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ class Run < ApplicationRecord
1111
# Sequel II and Sequel I are now deprecated
1212
enum system_name: { 'Sequel II' => 0, 'Sequel I' => 1, 'Sequel IIe' => 2, 'Revio' => 3 }
1313

14-
delegate :wells, to: :plate, allow_nil: true
15-
1614
after_create :generate_name
1715

18-
has_one :plate, foreign_key: :pacbio_run_id,
19-
dependent: :destroy, inverse_of: :run
16+
has_many :plates, foreign_key: :pacbio_run_id,
17+
dependent: :destroy, inverse_of: :run, autosave: true
2018

2119
# This association creates the link to the SmrtLinkVersion. Run belongs
2220
# to a SmrtLinkVersion. We set the default SmrtLinkVersion for the run
@@ -27,29 +25,23 @@ class Run < ApplicationRecord
2725
inverse_of: :runs,
2826
default: -> { SmrtLinkVersion.default }
2927

30-
validates :sequencing_kit_box_barcode,
31-
:system_name, presence: true
32-
33-
# it would be sensible to move this to dependent validation as with wells
34-
# and SMRT Link. Something to ponder on ...
35-
validates :dna_control_complex_box_barcode, presence: true, unless: lambda {
36-
system_name == 'Revio'
37-
}
28+
validates :system_name, presence: true
3829

39-
# if it is a Revio run we need to check if the wells are in the correct positions
40-
validates_with WellPositionValidator, if: lambda {
41-
system_name == 'Revio'
42-
}
30+
validates_with InstrumentTypeValidator,
31+
instrument_types: Rails.configuration.pacbio_instrument_types,
32+
if: lambda {
33+
system_name.present?
34+
}
4335

4436
validates :name, uniqueness: { case_sensitive: false }
4537

4638
scope :active, -> { where(deactivated_at: nil) }
4739

48-
accepts_nested_attributes_for :plate
40+
accepts_nested_attributes_for :plates, allow_destroy: true
4941

5042
# This will return an empty list
51-
# If well data is required via the run, use ?include=plate.wells
52-
attr_reader :well_attributes
43+
# If plate/well data is required via the run, use ?include=plates.wells
44+
attr_reader :plates_attributes
5345

5446
# if comments are nil this blows up so add try.
5547
def comments
@@ -69,10 +61,9 @@ def instrument_name
6961
system_name
7062
end
7163

72-
def well_attributes=(well_options)
73-
self.plate = build_plate(run: self) unless plate
74-
75-
plate.well_attributes = well_options
64+
# This is needed to generate the comments
65+
def wells
66+
plates.collect(&:wells).flatten
7667
end
7768

7869
private

app/models/pacbio/well.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ class Well < ApplicationRecord
1111

1212
belongs_to :plate, class_name: 'Pacbio::Plate', foreign_key: :pacbio_plate_id,
1313
inverse_of: :wells
14-
1514
has_many :well_pools, class_name: 'Pacbio::WellPool', foreign_key: :pacbio_well_id,
16-
dependent: :destroy, inverse_of: :well
17-
has_many :pools, class_name: 'Pacbio::Pool', through: :well_pools, autosave: true
15+
dependent: :destroy, inverse_of: :well, autosave: true
16+
has_many :pools, class_name: 'Pacbio::Pool', through: :well_pools
1817

1918
has_many :libraries, through: :pools
2019
has_many :tag_sets, through: :libraries

app/resources/v1/pacbio/run_resource.rb

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ module Pacbio
66
class RunResource < JSONAPI::Resource
77
model_name 'Pacbio::Run'
88

9-
attributes :name, :sequencing_kit_box_barcode, :dna_control_complex_box_barcode,
9+
attributes :name, :dna_control_complex_box_barcode,
1010
:system_name, :created_at, :state, :comments,
11-
:pacbio_smrt_link_version_id, :well_attributes
12-
has_one :plate, foreign_key_on: :related, foreign_key: 'pacbio_run_id',
13-
class_name: 'Runs::Plate'
11+
:pacbio_smrt_link_version_id, :plates_attributes
12+
13+
has_many :plates, foreign_key_on: :related, foreign_key: 'pacbio_run_id',
14+
class_name: 'Runs::Plate'
1415

1516
has_one :smrt_link_version, foreign_key: 'pacbio_smrt_link_version_id'
1617

@@ -57,14 +58,25 @@ def self.resource_klass_for(type)
5758
end
5859

5960
def publish_messages
60-
Messages.publish(@model.plate, Pipelines.pacbio.message)
61+
Messages.publish(@model.plates.first, Pipelines.pacbio.message)
6162
end
6263

6364
private
6465

65-
def well_attributes=(wells_parameters)
66-
@model.well_attributes = wells_parameters.map do |well|
67-
well.permit(PERMITTED_WELL_PARAMETERS, pools: [])
66+
def plates_attributes=(plates_parameters)
67+
@model.plates_attributes = plates_parameters.map do |plate|
68+
plate.permit(
69+
:id,
70+
:sequencing_kit_box_barcode,
71+
:plate_number,
72+
wells_attributes: [
73+
# the following is needed to allow the _destroy parameter which
74+
# is used to mark wells for destruction
75+
:_destroy,
76+
PERMITTED_WELL_PARAMETERS,
77+
{ pool_ids: [] }
78+
]
79+
)
6880
end
6981
end
7082
end

app/resources/v1/pacbio/runs/plate_resource.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module Runs
77
class PlateResource < JSONAPI::Resource
88
model_name 'Pacbio::Plate'
99

10-
attributes :pacbio_run_id
10+
attributes :pacbio_run_id, :plate_number, :sequencing_kit_box_barcode
1111

1212
has_many :wells, class_name: 'Well'
1313
end

app/validators/has_filters.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
# Concern for filtering records
4+
# currently will filter out records which are marked for destruction
5+
# could be extended to filter out other records
6+
module HasFilters
7+
extend ActiveSupport::Concern
8+
9+
# @return [Boolean]
10+
# records which are marked for destruction should be excluded from the validation
11+
def exclude_marked_for_destruction
12+
@exclude_marked_for_destruction ||= options[:exclude_marked_for_destruction] || false
13+
end
14+
15+
private
16+
17+
# @param [ActiveRecord::Base] record
18+
# @return [Array]
19+
# filter out records which are marked for destruction
20+
def filtered(records)
21+
return records unless exclude_marked_for_destruction
22+
23+
records.filter { |record| !record.marked_for_destruction? }
24+
end
25+
end
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# frozen_string_literal: true
2+
3+
# Validator for InstrumentType
4+
# Validates the InstrumentType and its associated Plates and Wells
5+
# Validates the presence of required attributes
6+
# Validates the number of Plates and Wells
7+
# Validates the positions of Wells
8+
# Validates the combinations of Wells
9+
class InstrumentTypeValidator < ActiveModel::Validator
10+
attr_reader :instrument_types, :instrument_type
11+
12+
# @param [Hash] options
13+
# @option options [Hash] :instrument_types
14+
def initialize(options)
15+
super
16+
@instrument_types = options[:instrument_types].with_indifferent_access
17+
end
18+
19+
# @param [Run] record
20+
# validates the InstrumentType and its associated Plates and Wells
21+
def validate(record)
22+
self.instrument_type = record
23+
24+
return if instrument_type.blank?
25+
26+
# e.g. if record is a Run, then root is :run
27+
validate_model(record.model_name.element.to_sym, record, instrument_type['models'])
28+
29+
bubble_errors(record)
30+
end
31+
32+
private
33+
34+
# @param [Symbol] root - the root of the model e.g. :run
35+
# @param [Model] record - the model to validate
36+
# @param [Hash] models - the validations
37+
# validates the model and its children
38+
def validate_model(root, record, models)
39+
model = models[root]
40+
41+
return if model[:validations].blank?
42+
43+
# if the model is a has_many relationship
44+
if model[:validate_each]
45+
run_child_validations(record, model, models)
46+
else
47+
run_validations(record, model[:validations])
48+
# recursively validate the children
49+
if model[:children].present?
50+
validate_model(model[:children], record.send(model[:children]),
51+
models)
52+
end
53+
end
54+
end
55+
56+
# @param [Model] record - the model to validate
57+
# @param [Hash] validations - the validations
58+
# run each validation by calling the corresponding validator
59+
def run_validations(record, validations)
60+
validations.each do |key, validation|
61+
# e.g. my_simple_validator = MySimpleValidator.new(validation[:options])
62+
validator = "#{key.camelize}Validator".constantize
63+
instance = validator.new(validation[:options])
64+
instance.validate(record)
65+
end
66+
end
67+
68+
# @param [Model] record - the model to validate
69+
# @param [Hash] model - the specific validations for the record
70+
# @param [Hash] models - all of the validations
71+
# run each validation by calling the corresponding validator
72+
def run_child_validations(record, model, models)
73+
record.each do |child|
74+
run_validations(child, model[:validations])
75+
validate_model(model[:children], child, models) if model[:children].present?
76+
end
77+
end
78+
79+
# @param [Run] record
80+
# adds the errors from the Plates and Wells to the Run
81+
# It would be better to use the configuration to make this more flexible
82+
# but I need a bit more time to get it right
83+
# if we don't do this the run could be marked as valid
84+
# because it will not recognise nested errors
85+
def bubble_errors(record)
86+
record.plates.each do |plate|
87+
next if plate.errors.empty?
88+
89+
plate.errors.each do |error|
90+
record.errors.add(:plates,
91+
"plate #{plate.plate_number} #{error.attribute} #{error.message}")
92+
end
93+
end
94+
end
95+
96+
# @param [Run] record
97+
# sets the instrument_type
98+
# @return [Hash]
99+
def instrument_type=(record)
100+
@instrument_type = instrument_types.select do |_key, value|
101+
value['name'] == record.system_name
102+
end.values.first
103+
end
104+
end

0 commit comments

Comments
 (0)