Class: Batch

Inherits:
ApplicationRecord show all
Extended by:
EventfulRecord
Includes:
AASM, Api::BatchIO::Extensions, Api::Messages::FlowcellIO::Extensions, PipelineBehaviour, StateMachineBehaviour, TecanBehaviour, Commentable, SequencingQcBatch, StandardNamedScopes, 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, TecanBehaviour Classes: RequestFailAndRemover

Constant Summary collapse

DEFAULT_VOLUME =
13

Constants included from SequencingQcBatch

SequencingQcBatch::VALID_QC_STATES

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 TecanBehaviour

#generate_tecan_data, #tecan_gwl_file_as_text, #tecan_layout_plate_barcodes

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

convert_labware_to_receptacle_for, find_by_id_or_name, find_by_id_or_name!

Methods included from Warren::BroadcastMessages

#broadcast, included, #queue_associated_for_broadcast, #queue_for_broadcast, #warren

Class Method Details

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


471
472
473
474
475
476
477
# File 'app/models/batch.rb', line 471

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

  batch
end

.prefixObject


452
453
454
# File 'app/models/batch.rb', line 452

def self.prefix
  'BA'
end

.valid_barcode?(code) ⇒ Boolean

Returns:

  • (Boolean)

456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'app/models/batch.rb', line 456

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

  if find_from_barcode(code).nil?
    return false
  end

  true
end

Instance Method Details

#all_requests_are_ready?Boolean

Returns:

  • (Boolean)

89
90
91
92
93
94
# File 'app/models/batch.rb', line 89

def all_requests_are_ready?
  # Checks that SequencingRequests have at least one LibraryCreationRequest in passed status before being processed (as refered by #75102998)
  unless requests.all?(&:ready?)
    errors.add :base, 'All requests must be ready to be added to a batch'
  end
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 array.

Raises:

  • (StandardError)

193
194
195
196
197
198
199
200
201
202
# File 'app/models/batch.rb', line 193

def assign_positions_to_requests!(request_ids_in_position_order)
  disparate_ids = batch_requests.map(&:request_id) - request_ids_in_position_order
  raise StandardError, 'Can only sort all requests at once' unless disparate_ids.empty?

  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


219
220
221
# File 'app/models/batch.rb', line 219

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

#batch_meets_minimum_sizeObject


108
109
110
111
112
# File 'app/models/batch.rb', line 108

def batch_meets_minimum_size
  if min_size && (requests.size < min_size)
    errors.add :base, "You must create batches of at least #{min_size} requests in the pipeline #{pipeline.name}"
  end
end

#controlObject


184
185
186
# File 'app/models/batch.rb', line 184

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.


355
356
357
358
359
360
# File 'app/models/batch.rb', line 355

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

#display_tags?Boolean

Returns:

  • (Boolean)

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

def display_tags?
  multiplexed?
end

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

Yields:

  • (next_requests_needing_asset)

502
503
504
505
# File 'app/models/batch.rb', line 502

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

#eventful_studiesObject


100
101
102
# File 'app/models/batch.rb', line 100

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)

121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/models/batch.rb', line 121

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)
    unless request.asset && request.asset.resource?
      EventSender.send_fail_event(request.id, reason, comment, id)
    end
  end

  self.production_state = 'fail'
  save!
end

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

Fail specific items on this batch


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'app/models/batch.rb', line 141

def fail_batch_items(requests_to_fail, reason, comment, fail_but_charge = false)
  checkpoint = true

  requests_to_fail.each do |key, value|
    if value == 'on'
      logger.debug "SENDING FAIL FOR REQUEST #{key}, BATCH #{id}, WITH REASON #{reason}"
      unless key == 'control'
        ActiveRecord::Base.transaction do
          request = requests.find(key)
          request.customer_accepts_responsibility! if fail_but_charge
          request.failures.create(reason: reason, comment: comment, notify_remote: true)
          EventSender.send_fail_event(request.id, reason, comment, id)
        end
      end
    else
      checkpoint = false
    end
  end

  update_batch_state(reason, comment) if checkpoint
end

#failed?Boolean

Returns:

  • (Boolean)

171
172
173
# File 'app/models/batch.rb', line 171

def failed?
  production_state == 'fail'
end

#first_output_plateObject


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

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

#flowcellObject


104
105
106
# File 'app/models/batch.rb', line 104

def flowcell
  self if sequencing?
end

#has_control?Boolean

Returns:

  • (Boolean)

188
189
190
# File 'app/models/batch.rb', line 188

def has_control?
  control.present?
end

#has_event(event_name) ⇒ Object

Tests whether this Batch has any associated LabEvents


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

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

#input_labware_groupObject


227
228
229
# File 'app/models/batch.rb', line 227

def input_labware_group
  pipeline.input_labware requests
end

#input_plate_groupObject


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

def input_plate_group
  source_assets.group_by(&:plate)
end

#mpx_library_nameObject


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

def mpx_library_name
  return '' unless multiplexed? && requests.any?

  mpx_library_tube = requests.first.target_asset.children.first
  mpx_library_tube&.name || ''
end

#multiplexed_items_with_unique_library_idsObject


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

def multiplexed_items_with_unique_library_ids
  requests.map { |r| r.target_asset.children }.flatten.uniq
end

#npg_set_stateObject


490
491
492
493
494
495
496
# File 'app/models/batch.rb', line 490

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

#output_labware_groupObject


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

def output_labware_group
  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?


240
241
242
# File 'app/models/batch.rb', line 240

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

#output_plate_purposeObject


266
267
268
# File 'app/models/batch.rb', line 266

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

#output_plate_roleObject


270
271
272
# File 'app/models/batch.rb', line 270

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

#parent_of_purpose(name) ⇒ Object


392
393
394
395
396
397
398
399
# File 'app/models/batch.rb', line 392

def parent_of_purpose(name)
  return nil if requests.empty?

  requests.first.asset.ancestors.joins(
    "INNER JOIN plate_purposes ON #{Plate.table_name}.plate_purpose_id = plate_purposes.id"
  )
          .find_by(plate_purposes: { name: name })
end

#plate_barcode(barcode) ⇒ Object


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

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

#plate_group_barcodesObject


274
275
276
277
278
# File 'app/models/batch.rb', line 274

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


429
430
431
# File 'app/models/batch.rb', line 429

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

#rebroadcastObject


507
508
509
# File 'app/models/batch.rb', line 507

def rebroadcast
  messengers.each(&:queue_for_broadcast)
end

#release_pending_requestsObject


330
331
332
333
334
335
336
# File 'app/models/batch.rb', line 330

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

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

def remove_link(request)
  request.batch = nil
end

#remove_request_ids(request_ids, reason = nil, comment = nil) ⇒ Object Also known as: recycle_request_ids

Remove the request from the batch and remove asset information


339
340
341
342
343
344
345
346
347
348
349
350
# File 'app/models/batch.rb', line 339

def remove_request_ids(request_ids, reason = nil, comment = nil)
  ActiveRecord::Base.transaction do
    request_ids.each do |request_id|
      request = Request.find(request_id)
      next if request.nil?

      request.failures.create(reason: reason, comment: comment, notify_remote: true)
      detach_request(request)
    end
    update_batch_state(reason, comment)
  end
end

#request_countObject


481
482
483
# File 'app/models/batch.rb', line 481

def request_count
  requests.count
end

#requests_have_same_read_lengthObject


114
115
116
117
118
# File 'app/models/batch.rb', line 114

def requests_have_same_read_length
  unless pipeline.is_read_length_consistent_for_batch?(self)
    errors.add :base, "The selected requests must have the same values in their 'Read length' field."
  end
end

#reset!(current_user) ⇒ Object


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'app/models/batch.rb', line 373

def reset!(current_user)
  ActiveRecord::Base.transaction do
    discard!

    requests.each do |request|
      remove_link(request) # Remove link in all types of pipelines
      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


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

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

#robot_verified!(user_id) ⇒ Object


445
446
447
448
449
450
# File 'app/models/batch.rb', line 445

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

#shift_item_positions(position, number) ⇒ Object


206
207
208
209
210
211
212
213
214
215
216
217
# File 'app/models/batch.rb', line 206

def shift_item_positions(position, number)
  return unless number

  BatchRequest.transaction do
    batch_requests.each do |batch_request|
      next unless batch_request.position >= position
      next if batch_request.request.asset.try(:resource?)

      batch_request.move_to_position!(batch_request.position + number)
    end
  end
end

#show_actions?Boolean

Returns:

  • (Boolean)

485
486
487
488
# File 'app/models/batch.rb', line 485

def show_actions?
  released? == false or
    pipeline.class.const_get(:ALWAYS_SHOW_RELEASE_ACTIONS)
end

#show_fail_link?Boolean

Returns:

  • (Boolean)

498
499
500
# File 'app/models/batch.rb', line 498

def show_fail_link?
  released? && sequencing?
end

#source_labwareObject

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


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

def source_labware
  requests.map(&:asset).map(&:labware).uniq
end

#space_leftObject


433
434
435
# File 'app/models/batch.rb', line 433

def space_left
  [item_limit - batch_requests.count, 0].max
end

#start_requestsObject


223
224
225
# File 'app/models/batch.rb', line 223

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

#subject_typeObject


96
97
98
# File 'app/models/batch.rb', line 96

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

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


401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'app/models/batch.rb', line 401

def swap(current_user, batch_info = {})
  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? and 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  { |event| event.update!(batch_id: batch_request_right.batch_id) if event.batch_id == batch_request_left.batch_id  }
    batch_request_right.request.lab_events.each { |event| event.update!(batch_id: batch_request_left.batch_id)  if event.batch_id == batch_request_right.batch_id }

    # 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', message: "Lane #{batch_request_right.position} moved to #{batch_request_left.batch_id} lane #{batch_request_left.position}", user_id: current_user.id)
    batch_request_right.batch.lab_events.create!(description: 'Lane swap', message: "Lane #{batch_request_left.position} moved to #{batch_request_right.batch_id} lane #{batch_request_right.position}", user_id: current_user.id)
  end

  true
end

#total_volume_to_cherrypickObject


437
438
439
440
441
442
443
# File 'app/models/batch.rb', line 437

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

#underrunObject


180
181
182
# File 'app/models/batch.rb', line 180

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

#update_batch_state(reason, comment) ⇒ Object


163
164
165
166
167
168
169
# File 'app/models/batch.rb', line 163

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_tube_layout(barcodes, user = nil) ⇒ Bool

Verifies that provided barcodes are in the correct locations according to the request organization within the batch. Either returns true, and logs the event or returns false.

Parameters:

  • barcodes (Array<Integer>)

    An array of 1-7 digit long barcodes

  • user (User) (defaults to: nil)

    The user validating the barcode layout

Returns:

  • (Bool)

    true if the layout is correct, false otherwise


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

def verify_tube_layout(barcodes, user = nil)
  requests.each do |request|
    barcode = barcodes[request.position - 1]
    unless barcode == request.asset.machine_barcode
      expected_barcode = request.asset.human_barcode
      errors.add(:base, "The tube at position #{request.position} is incorrect: expected #{expected_barcode}.")
    end
  end
  if errors.empty?
    lab_events.create(description: 'Tube layout verified', user: user)
    return true
  else
    return false
  end
end