Class: Batch

Inherits:
ApplicationRecord show all
Extended by:
EventfulRecord
Includes:
AASM, Api::BatchIo::Extensions, Api::Messages::FlowcellIo::Extensions, PipelineBehaviour, StateMachineBehaviour, Commentable, SequencingQcBatch, StandardNamedScopes, UnderRepWellCommentsToBroadcast, Uuid::Uuidable
Defined in:
app/models/batch.rb

Overview

A Batch groups 1 or more requests together to enable processing in a Pipeline. All requests in a batch get usually processed together, although it is possible for requests to get removed from a batch in a handful of cases.

Defined Under Namespace

Modules: PipelineBehaviour, RequestBehaviour, StateMachineBehaviour Classes: RequestFailAndRemover

Constant Summary collapse

DEFAULT_VOLUME =

The three states of Batch Also @see SequencingQcBatch

13

Constants included from UnderRepWellCommentsToBroadcast

UnderRepWellCommentsToBroadcast::UNDER_REPRESENTED_KEY

Constants included from StandardNamedScopes

StandardNamedScopes::SORT_FIELDS, StandardNamedScopes::SORT_ORDERS

Constants included from SequencingQcBatch

SequencingQcBatch::VALID_QC_STATES

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from EventfulRecord

has_many_events, has_many_lab_events, has_one_event_with_family

Methods included from UnderRepWellCommentsToBroadcast

#comments, #request_with_under_represented_wells, #under_represented_well_comments

Methods included from StateMachineBehaviour

#complete_with_user!, #editable?, #finished?, included, #release_with_user!, #start_with_user!

Methods included from PipelineBehaviour

#has_item_limit?, included, #last_completed_task

Methods included from StandardNamedScopes

included

Methods included from Uuid::Uuidable

included, #unsaved_uuid!, #uuid

Methods included from Commentable

#after_comment_addition

Methods included from SequencingQcBatch

adjacent_state_helper, included, #processing_in_manual_qc?, #qc_manual_in_progress, #qc_previous_state!, #qc_states, state_transition_helper

Methods included from Api::BatchIo::Extensions

included

Methods inherited from ApplicationRecord

alias_association, convert_labware_to_receptacle_for, find_by_id_or_name, find_by_id_or_name!

Methods included from Squishify

extended

Instance Attribute Details

#production_stateObject

Also referenced in StateMachineBehaviour. Either nil, or fail. This is updated in Batch#fail_requests and Batch#fail. The former is used via BatchesController#fail_items, the latter seems to be unused. Is intended to take precedence over both other states to track failures in-spite of QC results.



31
# File 'app/models/batch.rb', line 31

DEFAULT_VOLUME = 13

#qc_stateObject

Primarily for sequencing batches. See SequencingQcBatch. Holds the sequencing QC state



31
# File 'app/models/batch.rb', line 31

DEFAULT_VOLUME = 13

#stateObject

The main state machine, used to track the batch through the pipeline. Handled by StateMachineBehaviour



31
# File 'app/models/batch.rb', line 31

DEFAULT_VOLUME = 13

Class Method Details

.barcode_without_pick_number(code) ⇒ Object



545
546
547
# File 'app/models/batch.rb', line 545

def self.barcode_without_pick_number(code)
  code.split('-').first
end

.extract_pick_number(code) ⇒ Object



549
550
551
552
553
554
555
556
# File 'app/models/batch.rb', line 549

def self.extract_pick_number(code)
  # expecting format 550000555760-1 with pick number at end
  split_code = code.split('-')
  return Integer(split_code.last) if split_code.size > 1

  # default to 1 if the pick number is not present
  1
end

.find_by_barcode(code) ⇒ Object Also known as: find_from_barcode



559
560
561
562
563
564
565
566
# File 'app/models/batch.rb', line 559

def find_by_barcode(code)
  split_code = barcode_without_pick_number(code)
  human_batch_barcode = Barcode.number_to_human(split_code)
  batch = Batch.find_by(barcode: human_batch_barcode)
  batch ||= Batch.find_by(id: human_batch_barcode)

  batch
end

.prefixObject



528
529
530
# File 'app/models/batch.rb', line 528

def self.prefix
  'BA'
end

.valid_barcode?(code) ⇒ Boolean

Returns:

  • (Boolean)


532
533
534
535
536
537
538
539
540
541
542
543
# File 'app/models/batch.rb', line 532

def self.valid_barcode?(code)
  begin
    split_code = barcode_without_pick_number(code)
    Barcode.barcode_to_human!(split_code, prefix)
  rescue StandardError
    return false
  end

  return false if find_from_barcode(code).nil?

  true
end

Instance Method Details

#all_requests_are_ready?Boolean

Returns:

  • (Boolean)


117
118
119
120
121
# File 'app/models/batch.rb', line 117

def all_requests_are_ready?
  # Checks that SequencingRequests have at least one LibraryCreationRequest in passed status before being processed
  # (as referred by #75102998)
  errors.add :base, 'All requests must be ready to be added to a batch' unless requests.all?(&:ready?)
end

#assign_positions_to_requests!(request_ids_in_position_order) ⇒ Object

Sets the position of the requests in the batch to their index in the supplied array.

Raises:

  • (StandardError)


214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'app/models/batch.rb', line 214

def assign_positions_to_requests!(request_ids_in_position_order)
  request_ids_in_batch = batch_requests.map(&:request_id)
  # checking for both missing and extra requests
  missing_requests = request_ids_in_batch.any? { |id| request_ids_in_position_order.exclude?(id) }
  extra_requests = request_ids_in_position_order.any? { |id| request_ids_in_batch.exclude?(id) }
  raise StandardError, 'Can only sort all the requests in the batch at once' if missing_requests || extra_requests

  BatchRequest.transaction do
    batch_requests.each do |batch_request|
      batch_request.move_to_position!(request_ids_in_position_order.index(batch_request.request_id) + 1)
    end
  end
end

#assigned_userObject



230
231
232
# File 'app/models/batch.rb', line 230

def assigned_user
  assignee.try(:login) || ''
end

#batch_id_matches(scanned_batch_id) ⇒ Object



374
375
376
# File 'app/models/batch.rb', line 374

def batch_id_matches(scanned_batch_id)
  scanned_batch_id == id.to_s
end

#controlObject



198
199
200
# File 'app/models/batch.rb', line 198

def control
  requests.detect { |request| request.try(:asset).try(:resource?) }
end

#detach_request(request, current_user = nil) ⇒ Object

Remove a request from the batch and reset it to a point where it can be put back into the pending queue.



399
400
401
402
403
404
405
406
# File 'app/models/batch.rb', line 399

def detach_request(request, current_user = nil)
  ActiveRecord::Base.transaction do
    unless current_user.nil?
      request.add_comment("Used to belong to Batch #{id} removed at #{Time.zone.now}", current_user)
    end
    pipeline.detach_request_from_batch(self, request)
  end
end

#displayed_statusObject

Summarise the state encapsulated by state and production_state Essentially a 'fail' production_state over-rides the 'state' We don't use production_state directly as it it 'fail' rather than ' failed' qc_state it kept separate as its a fairly distinct concept and is summarised elsewhere in the interface.



601
602
603
# File 'app/models/batch.rb', line 601

def displayed_status
  failed? ? 'failed' : state
end

#downstream_requests_needing_asset(request) {|next_requests_needing_asset| ... } ⇒ Object

Yields:

  • (next_requests_needing_asset)


582
583
584
585
# File 'app/models/batch.rb', line 582

def downstream_requests_needing_asset(request)
  next_requests_needing_asset = request.next_requests.select { |r| r.asset_id.blank? }
  yield(next_requests_needing_asset) if next_requests_needing_asset.present?
end

#event_with_description(name) ⇒ Object



186
187
188
# File 'app/models/batch.rb', line 186

def event_with_description(name)
  lab_events.order(id: :desc).find_by(description: name)
end

#eventful_studiesObject



127
128
129
# File 'app/models/batch.rb', line 127

def eventful_studies
  requests.reduce([]) { |studies, request| studies.concat(request.eventful_studies) }.uniq
end

#fail(reason, comment, ignore_requests = false) ⇒ Object

Fail was removed from State Machine (as a state) to allow the addition of qc_state column and features

Raises:

  • (StandardError)


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'app/models/batch.rb', line 136

def fail(reason, comment, ignore_requests = false)
  # We've deprecated the ability to fail a batch but not its requests.
  # Keep this check here until we're sure we haven't missed anything.
  raise StandardError, 'Can not fail batch without failing requests' if ignore_requests

  # create failures
  failures.create(reason: reason, comment: comment, notify_remote: false)

  requests.each do |request|
    request.failures.create(reason: reason, comment: comment, notify_remote: true)
    EventSender.send_fail_event(request, reason, comment, id) unless request.asset && request.asset.resource?
  end

  self.production_state = 'fail'
  save!
end

#fail_requests(requests_to_fail, reason, comment, fail_but_charge = false) ⇒ Object

Fail specific requests on this batch



154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'app/models/batch.rb', line 154

def fail_requests(requests_to_fail, reason, comment, fail_but_charge = false) # rubocop:todo Metrics/MethodLength
  ActiveRecord::Base.transaction do
    requests
      .find(requests_to_fail)
      .each do |request|
        logger.debug "SENDING FAIL FOR REQUEST #{request.id}, BATCH #{id}, WITH REASON #{reason}"

        request.customer_accepts_responsibility! if fail_but_charge
        request.failures.create(reason: reason, comment: comment, notify_remote: true)
        EventSender.send_fail_event(request, reason, comment, id)
      end
    update_batch_state(reason, comment)
  end
end

#failed?Boolean

Returns:

  • (Boolean)


177
178
179
# File 'app/models/batch.rb', line 177

def failed?
  production_state == 'fail'
end

#first_output_plateObject



273
274
275
# File 'app/models/batch.rb', line 273

def first_output_plate
  Plate.output_by_batch(self).with_wells_and_requests.first
end

#flowcellObject



131
132
133
# File 'app/models/batch.rb', line 131

def flowcell
  self if sequencing?
end

#has_control?Boolean

Returns:

  • (Boolean)


202
203
204
# File 'app/models/batch.rb', line 202

def has_control?
  control.present?
end

#has_event(event_name) ⇒ Object

Tests whether this Batch has any associated LabEvents



182
183
184
# File 'app/models/batch.rb', line 182

def has_event(event_name)
  lab_events.any? { |event| event_name.downcase == event.description.try(:downcase) }
end

#id_dupObject



295
296
297
# File 'app/models/batch.rb', line 295

def id_dup
  id
end

#input_labware_reportLabware::ActiveRecord_Relation

Returns a list of input labware including their barcodes, purposes, and a count of the number of requests associated with the batch. Output depends on Pipeline. Some pipelines return an empty relationship

Returns:

  • (Labware::ActiveRecord_Relation)

    The associated labware



243
244
245
# File 'app/models/batch.rb', line 243

def input_labware_report
  pipeline.input_labware requests
end

#input_plate_groupObject



256
257
258
# File 'app/models/batch.rb', line 256

def input_plate_group
  source_assets.group_by(&:plate)
end

#npg_set_stateObject



574
575
576
577
578
579
580
# File 'app/models/batch.rb', line 574

def npg_set_state
  if all_requests_qced?
    self.state = 'released'
    qc_complete
    save!
  end
end

#output_labware_reportLabware::ActiveRecord_Relation

Returns a list of output labware including their barcodes, purposes, and a count of the number of requests associated with the batch. Output depends on Pipeline. Some pipelines return an empty relationship

Returns:

  • (Labware::ActiveRecord_Relation)

    The associated labware



252
253
254
# File 'app/models/batch.rb', line 252

def output_labware_report
  pipeline.output_labware requests.with_target
end

#output_plate_groupObject

This looks odd. Why would a request have the same asset as target asset? Why are we filtering them out here?



261
262
263
# File 'app/models/batch.rb', line 261

def output_plate_group
  requests.select { |r| r.target_asset != r.asset }.map(&:target_asset).select(&:present?).group_by(&:plate)
end

#output_plate_purposeObject



277
278
279
# File 'app/models/batch.rb', line 277

def output_plate_purpose
  output_plates[0].plate_purpose unless output_plates[0].nil?
end

#output_plate_roleObject



281
282
283
# File 'app/models/batch.rb', line 281

def output_plate_role
  requests.first.try(:role)
end

#output_platesObject



265
266
267
268
269
270
271
# File 'app/models/batch.rb', line 265

def output_plates
  # We use re-order here as batch_requests applies a default sort order to
  # the relationship, which takes preference, even though we're has_many throughing
  return output_labware.sort_by(&:id) if output_labware.loaded?

  output_labware.reorder(id: :asc)
end

#pick_information?Boolean

Returns:

  • (Boolean)


591
592
593
# File 'app/models/batch.rb', line 591

def pick_information?
  pipeline.pick_information?(self)
end

#plate_barcode(barcode) ⇒ Object



291
292
293
# File 'app/models/batch.rb', line 291

def plate_barcode(barcode)
  barcode.presence || requests.first.target_asset.plate.human_barcode
end

#plate_group_barcodesObject



285
286
287
288
289
# File 'app/models/batch.rb', line 285

def plate_group_barcodes
  return nil unless pipeline.group_by_parent || requests.first.target_asset.is_a?(Well)

  output_plate_group.presence || input_plate_group
end

#plate_ids_in_study(study) ⇒ Object

rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity



505
506
507
# File 'app/models/batch.rb', line 505

def plate_ids_in_study(study)
  Plate.plate_ids_from_requests(requests.for_studies(study))
end

#rebroadcastObject



587
588
589
# File 'app/models/batch.rb', line 587

def rebroadcast
  messengers.each(&:resend)
end

#release_pending_requestsObject



378
379
380
381
382
# File 'app/models/batch.rb', line 378

def release_pending_requests
  # We set the unused requests to pending.
  # this is to allow unused well to be cherry-picked again
  requests.each { |request| detach_request(request) if request.started? }
end

#remove_request_ids(request_ids, reason = nil, comment = nil) ⇒ Object

Remove the request from the batch and remove asset information



385
386
387
388
389
390
391
392
393
394
395
# File 'app/models/batch.rb', line 385

def remove_request_ids(request_ids, reason = nil, comment = nil)
  ActiveRecord::Base.transaction do
    Request
      .find(request_ids)
      .each do |request|
        request.failures.create(reason: reason, comment: comment, notify_remote: true)
        detach_request(request)
      end
    update_batch_state(reason, comment)
  end
end

#request_countObject



570
571
572
# File 'app/models/batch.rb', line 570

def request_count
  requests.count
end

#reset!(current_user) ⇒ Object

rubocop:todo Metrics/MethodLength



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'app/models/batch.rb', line 421

def reset!(current_user) # rubocop:todo Metrics/AbcSize
  ActiveRecord::Base.transaction do
    discard!

    requests.each do |request|
      request.batch = nil
      return_request_to_inbox(request, current_user)
    end

    if requests.last.submission_id.present?
      Request
        .where(submission_id: requests.last.submission_id, state: 'pending')
        .where.not(request_type_id: pipeline.request_type_ids)
        .find_each do |request|
          request.asset_id = nil
          request.save!
        end
    end
  end
end

#return_request_to_inbox(request, current_user = nil) ⇒ Object



408
409
410
411
412
413
414
415
416
417
418
# File 'app/models/batch.rb', line 408

def return_request_to_inbox(request, current_user = nil)
  ActiveRecord::Base.transaction do
    unless current_user.nil?
      request.add_comment(
        "Used to belong to Batch #{id} returned to inbox unstarted at #{Time.zone.now}",
        current_user
      )
    end
    request.return_pending_to_inbox!
  end
end

#robot_idObject



190
191
192
# File 'app/models/batch.rb', line 190

def robot_id
  event_with_description('Cherrypick Layout Set')&.descriptor_value('robot_id')
end

#robot_verified!(user_id) ⇒ Object



517
518
519
520
521
522
523
524
525
526
# File 'app/models/batch.rb', line 517

def robot_verified!(user_id)
  return if has_event('robot verified')

  pipeline.robot_verified!(self)
  lab_events.create(
    description: 'Robot verified',
    message: 'Robot verification completed and source volumes updated.',
    user_id: user_id
  )
end

#set_position_based_on_asset_barcodeObject

Sets the position of the requests in the batch based on their asset barcodes. This was done at Lab request to make it easier to order the tubes in the batch.



208
209
210
211
# File 'app/models/batch.rb', line 208

def set_position_based_on_asset_barcode
  request_ids_in_position_order = requests.sort_by { |r| r.asset.human_barcode }.map(&:id)
  assign_positions_to_requests!(request_ids_in_position_order)
end

#source_labwareObject

Source Labware returns the physical pieces of labware (ie. a plate for wells, but tubes for tubes)



300
301
302
# File 'app/models/batch.rb', line 300

def source_labware
  input_labware
end

#start_requestsObject



234
235
236
# File 'app/models/batch.rb', line 234

def start_requests
  requests.with_assets_for_starting_requests.not_failed.map(&:start!)
end

#subject_typeObject



123
124
125
# File 'app/models/batch.rb', line 123

def subject_type
  sequencing? ? 'flowcell' : 'batch'
end

#swap(current_user, batch_info = {}) ⇒ Object

rubocop:todo Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize



445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'app/models/batch.rb', line 445

def swap(current_user, batch_info = {}) # rubocop:todo Metrics/CyclomaticComplexity
  return false if batch_info.empty?

  # Find the two lanes that are to be swapped
  batch_request_left =
    BatchRequest.find_by(batch_id: batch_info['batch_1']['id'], position: batch_info['batch_1']['lane']) or
    errors.add('Swap: ', 'The first lane cannot be found')
  batch_request_right =
    BatchRequest.find_by(batch_id: batch_info['batch_2']['id'], position: batch_info['batch_2']['lane']) or
    errors.add('Swap: ', 'The second lane cannot be found')
  return unless batch_request_left.present? && batch_request_right.present?

  ActiveRecord::Base.transaction do
    # Update the lab events for the request so that they reference the batch that the request is moving to
    batch_request_left.request.lab_events.each do |event|
      event.update!(batch_id: batch_request_right.batch_id) if event.batch_id == batch_request_left.batch_id
    end
    batch_request_right.request.lab_events.each do |event|
      event.update!(batch_id: batch_request_left.batch_id) if event.batch_id == batch_request_right.batch_id
    end

    # Swap the two batch requests so that they are correct.  This involves swapping both the batch and the lane but
    # ensuring that the two requests don't clash on position by removing one of them.
    original_left_batch_id, original_left_position, original_right_request_id =
      batch_request_left.batch_id,
      batch_request_left.position,
      batch_request_right.request_id
    batch_request_right.destroy
    batch_request_left.update!(batch_id: batch_request_right.batch_id, position: batch_request_right.position)
    batch_request_right =
      BatchRequest.create!(
        batch_id: original_left_batch_id,
        position: original_left_position,
        request_id: original_right_request_id
      )

    # 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
      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
      user_id: current_user.id
    )
  end

  true
end

#total_volume_to_cherrypickObject



509
510
511
512
513
514
515
# File 'app/models/batch.rb', line 509

def total_volume_to_cherrypick
  request = requests.first
  return DEFAULT_VOLUME unless request.asset.is_a?(Well)
  return DEFAULT_VOLUME unless request.target_asset.is_a?(Well)

  request.target_asset.get_requested_volume
end

#tube_barcode_matches(request, scanned_barcode) ⇒ Object



370
371
372
# File 'app/models/batch.rb', line 370

def tube_barcode_matches(request, scanned_barcode)
  scanned_barcode == request.asset.machine_barcode || scanned_barcode == request.asset.human_barcode
end

#underrunObject



194
195
196
# File 'app/models/batch.rb', line 194

def underrun
  has_limit? ? (item_limit - batch_requests.size) : 0
end

#update_batch_state(reason, comment) ⇒ Object



169
170
171
172
173
174
175
# File 'app/models/batch.rb', line 169

def update_batch_state(reason, comment)
  if requests.all?(&:terminated?)
    failures.create(reason: reason, comment: comment, notify_remote: false)
    self.production_state = 'fail'
    save!
  end
end

#verify_amp_plate_layout(barcodes, user = nil) ⇒ Bool

Used in the Ultima sequencing pipelines to check AMP plates are in the correct position.

Verifies that provided barcodes are in the correct locations according to the request 'position' within the batch. Logs an event if the layout is correct.

Parameters:

  • barcodes (Array<Integer>)

    An array of AMP plate barcodes, assumed ordered by position

  • user (User) (defaults to: nil)

    The user validating the barcode layout

Returns:

  • (Bool)

    true if the layout is correct, false otherwise



348
349
350
351
352
353
354
355
356
357
# File 'app/models/batch.rb', line 348

def verify_amp_plate_layout(barcodes, user = nil)
  requests.each { |request| verify_amp_plate_position(request, barcodes) }

  if errors.empty?
    lab_events.create(description: 'AMP plate layout verified', user: user)
    true
  else
    false
  end
end

#verify_amp_plate_position(request, barcodes) ⇒ Object



359
360
361
362
363
364
365
366
367
368
# File 'app/models/batch.rb', line 359

def verify_amp_plate_position(request, barcodes)
  divider = '-'
  scanned_barcode = barcodes[request.position - 1]
  scanned_batch_id, tube_barcode = scanned_barcode.split(divider)

  unless batch_id_matches(scanned_batch_id) && tube_barcode_matches(request, tube_barcode)
    expected_barcode = "#{id}#{divider}#{request.asset.human_barcode}"
    errors.add(:base, "The barcode at position #{request.position} is incorrect: expected #{expected_barcode}.")
  end
end

#verify_tube_layout(barcodes, user = nil) ⇒ Bool

Used in Sequencing pipelines to check tubes are in the correct position on the flowcell.

Verifies that provided barcodes are in the correct locations according to the request 'position' within the batch. Logs an event if the layout is correct.

Parameters:

  • barcodes (Array<Integer>)

    An array of 1-7 digit long barcodes, assumed ordered by position

  • user (User) (defaults to: nil)

    The user validating the barcode layout

Returns:

  • (Bool)

    true if the layout is correct, false otherwise



316
317
318
319
320
321
322
323
324
325
# File 'app/models/batch.rb', line 316

def verify_tube_layout(barcodes, user = nil)
  requests.each { |request| verify_tube_position(request, barcodes) }

  if errors.empty?
    lab_events.create(description: 'Tube layout verified', user: user)
    true
  else
    false
  end
end

#verify_tube_position(request, barcodes) ⇒ Object



327
328
329
330
331
332
333
334
# File 'app/models/batch.rb', line 327

def verify_tube_position(request, barcodes)
  scanned_barcode = barcodes[request.position - 1]

  unless tube_barcode_matches(request, scanned_barcode)
    expected_barcode = request.asset.human_barcode
    errors.add(:base, "The tube at position #{request.position} is incorrect: expected #{expected_barcode}.")
  end
end