Class: Batch
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 =
13
StandardNamedScopes::SORT_FIELDS, StandardNamedScopes::SORT_ORDERS
SequencingQcBatch::VALID_QC_STATES
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
-
#all_requests_are_ready? ⇒ Boolean
-
#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.
-
#assigned_user ⇒ Object
-
#control ⇒ Object
-
#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.
-
#displayed_status ⇒ Object
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.
-
#downstream_requests_needing_asset(request) {|next_requests_needing_asset| ... } ⇒ Object
-
#event_with_description(name) ⇒ Object
-
#eventful_studies ⇒ Object
-
#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.
-
#fail_requests(requests_to_fail, reason, comment, fail_but_charge = false) ⇒ Object
Fail specific requests on this batch.
-
#failed? ⇒ Boolean
-
#first_output_plate ⇒ Object
-
#flowcell ⇒ Object
-
#has_control? ⇒ Boolean
-
#has_event(event_name) ⇒ Object
Tests whether this Batch has any associated LabEvents.
-
#id_dup ⇒ Object
-
#input_labware_report ⇒ Labware::ActiveRecord_Relation
Returns a list of input labware including their barcodes, purposes, and a count of the number of requests associated with the batch.
-
#input_plate_group ⇒ Object
-
#npg_set_state ⇒ Object
-
#output_labware_report ⇒ Labware::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_plate_group ⇒ Object
-
#output_plate_purpose ⇒ Object
-
#output_plate_role ⇒ Object
-
#output_plates ⇒ Object
-
#pick_information? ⇒ Boolean
-
#plate_barcode(barcode) ⇒ Object
-
#plate_group_barcodes ⇒ Object
-
#plate_ids_in_study(study) ⇒ Object
rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity.
-
#rebroadcast ⇒ Object
-
#release_pending_requests ⇒ Object
rubocop:enable Metrics/MethodLength.
-
#remove_request_ids(request_ids, reason = nil, comment = nil) ⇒ Object
Remove the request from the batch and remove asset information.
-
#request_count ⇒ Object
-
#reset!(current_user) ⇒ Object
rubocop:todo Metrics/MethodLength.
-
#return_request_to_inbox(request, current_user = nil) ⇒ Object
-
#robot_id ⇒ Object
-
#robot_verified!(user_id) ⇒ Object
-
#set_position_based_on_asset_barcode ⇒ Object
Sets the position of the requests in the batch based on their asset barcodes.
-
#source_labware ⇒ Object
Source Labware returns the physical pieces of labware (ie. a plate for wells, but tubes for tubes).
-
#start_requests ⇒ Object
-
#subject_type ⇒ Object
-
#swap(current_user, batch_info = {}) ⇒ Object
rubocop:todo Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize.
-
#total_volume_to_cherrypick ⇒ Object
-
#underrun ⇒ Object
-
#update_batch_state(reason, comment) ⇒ Object
-
#verify_tube_layout(barcodes, user = nil) ⇒ Bool
Verifies that provided barcodes are in the correct locations according to the request organization within the batch.
has_many_events, has_many_lab_events, has_one_event_with_family
#complete_with_user!, #editable?, #finished?, included, #release_with_user!, #start_with_user!
#has_item_limit?, included, #last_completed_task
included
included, #unsaved_uuid!, #uuid
#after_comment_addition
adjacent_state_helper, included, #processing_in_manual_qc?, #qc_manual_in_progress, #qc_previous_state!, #qc_states, state_transition_helper
included
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_state ⇒ Object
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.
#qc_state ⇒ Object
Primarily for sequencing batches. See SequencingQcBatch. Holds the sequencing QC state
Class Method Details
.barcode_without_pick_number(code) ⇒ Object
499
500
501
|
# File 'app/models/batch.rb', line 499
def self.barcode_without_pick_number(code)
code.split('-').first
end
|
503
504
505
506
507
508
509
510
|
# File 'app/models/batch.rb', line 503
def self.(code)
split_code = code.split('-')
return Integer(split_code.last) if split_code.size > 1
1
end
|
.find_by_barcode(code) ⇒ Object
Also known as:
find_from_barcode
513
514
515
516
517
518
519
520
|
# File 'app/models/batch.rb', line 513
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
|
.prefix ⇒ Object
482
483
484
|
# File 'app/models/batch.rb', line 482
def self.prefix
'BA'
end
|
.valid_barcode?(code) ⇒ Boolean
486
487
488
489
490
491
492
493
494
495
496
497
|
# File 'app/models/batch.rb', line 486
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
116
117
118
119
120
|
# File 'app/models/batch.rb', line 116
def all_requests_are_ready?
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.
213
214
215
216
217
218
219
220
221
222
223
224
225
|
# File 'app/models/batch.rb', line 213
def assign_positions_to_requests!(request_ids_in_position_order)
request_ids_in_batch = batch_requests.map(&:request_id)
missing_requests = request_ids_in_batch.any? { |id| request_ids_in_position_order.exclude?(id) }
= 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 ||
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_user ⇒ Object
229
230
231
|
# File 'app/models/batch.rb', line 229
def assigned_user
assignee.try(:login) || ''
end
|
#control ⇒ Object
197
198
199
|
# File 'app/models/batch.rb', line 197
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.
353
354
355
356
357
358
359
360
|
# File 'app/models/batch.rb', line 353
def detach_request(request, current_user = nil)
ActiveRecord::Base.transaction do
unless current_user.nil?
request.("Used to belong to Batch #{id} removed at #{Time.zone.now}", current_user)
end
pipeline.detach_request_from_batch(self, request)
end
end
|
#displayed_status ⇒ Object
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.
555
556
557
|
# File 'app/models/batch.rb', line 555
def displayed_status
failed? ? 'failed' : state
end
|
#downstream_requests_needing_asset(request) {|next_requests_needing_asset| ... } ⇒ Object
536
537
538
539
|
# File 'app/models/batch.rb', line 536
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
185
186
187
|
# File 'app/models/batch.rb', line 185
def event_with_description(name)
lab_events.order(id: :desc).find_by(description: name)
end
|
#eventful_studies ⇒ Object
126
127
128
|
# File 'app/models/batch.rb', line 126
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
|
# File 'app/models/batch.rb', line 135
def fail(reason, , ignore_requests = false)
raise StandardError, 'Can not fail batch without failing requests' if ignore_requests
failures.create(reason: reason, comment: , notify_remote: false)
requests.each do |request|
request.failures.create(reason: reason, comment: , notify_remote: true)
EventSender.send_fail_event(request, reason, , 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
# File 'app/models/batch.rb', line 153
def fail_requests(requests_to_fail, reason, , fail_but_charge = false) 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: , notify_remote: true)
EventSender.send_fail_event(request, reason, , id)
end
update_batch_state(reason, )
end
end
|
#failed? ⇒ Boolean
176
177
178
|
# File 'app/models/batch.rb', line 176
def failed?
production_state == 'fail'
end
|
#first_output_plate ⇒ Object
272
273
274
|
# File 'app/models/batch.rb', line 272
def first_output_plate
Plate.output_by_batch(self).with_wells_and_requests.first
end
|
#flowcell ⇒ Object
130
131
132
|
# File 'app/models/batch.rb', line 130
def flowcell
self if sequencing?
end
|
#has_control? ⇒ Boolean
201
202
203
|
# File 'app/models/batch.rb', line 201
def has_control?
control.present?
end
|
#has_event(event_name) ⇒ Object
Tests whether this Batch has any associated LabEvents
181
182
183
|
# File 'app/models/batch.rb', line 181
def has_event(event_name)
lab_events.any? { |event| event_name.downcase == event.description.try(:downcase) }
end
|
#id_dup ⇒ Object
294
295
296
|
# File 'app/models/batch.rb', line 294
def id_dup
id
end
|
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
242
243
244
|
# File 'app/models/batch.rb', line 242
def input_labware_report
pipeline.input_labware requests
end
|
255
256
257
|
# File 'app/models/batch.rb', line 255
def input_plate_group
source_assets.group_by(&:plate)
end
|
#npg_set_state ⇒ Object
528
529
530
531
532
533
534
|
# File 'app/models/batch.rb', line 528
def npg_set_state
if all_requests_qced?
self.state = 'released'
qc_complete
save!
end
end
|
#output_labware_report ⇒ Labware::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
251
252
253
|
# File 'app/models/batch.rb', line 251
def output_labware_report
pipeline.output_labware requests.with_target
end
|
#output_plate_group ⇒ Object
This looks odd. Why would a request have the same asset as target asset? Why are we filtering them out here?
260
261
262
|
# File 'app/models/batch.rb', line 260
def output_plate_group
requests.select { |r| r.target_asset != r.asset }.map(&:target_asset).select(&:present?).group_by(&:plate)
end
|
#output_plate_purpose ⇒ Object
276
277
278
|
# File 'app/models/batch.rb', line 276
def output_plate_purpose
output_plates[0].plate_purpose unless output_plates[0].nil?
end
|
#output_plate_role ⇒ Object
280
281
282
|
# File 'app/models/batch.rb', line 280
def output_plate_role
requests.first.try(:role)
end
|
#output_plates ⇒ Object
264
265
266
267
268
269
270
|
# File 'app/models/batch.rb', line 264
def output_plates
return output_labware.sort_by(&:id) if output_labware.loaded?
output_labware.reorder(id: :asc)
end
|
545
546
547
|
# File 'app/models/batch.rb', line 545
def pick_information?
pipeline.pick_information?(self)
end
|
#plate_barcode(barcode) ⇒ Object
290
291
292
|
# File 'app/models/batch.rb', line 290
def plate_barcode(barcode)
barcode.presence || requests.first.target_asset.plate.human_barcode
end
|
#plate_group_barcodes ⇒ Object
284
285
286
287
288
|
# File 'app/models/batch.rb', line 284
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
459
460
461
|
# File 'app/models/batch.rb', line 459
def plate_ids_in_study(study)
Plate.plate_ids_from_requests(requests.for_studies(study))
end
|
#rebroadcast ⇒ Object
541
542
543
|
# File 'app/models/batch.rb', line 541
def rebroadcast
messengers.each(&:resend)
end
|
#release_pending_requests ⇒ Object
rubocop:enable Metrics/MethodLength
332
333
334
335
336
|
# File 'app/models/batch.rb', line 332
def release_pending_requests
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
339
340
341
342
343
344
345
346
347
348
349
|
# File 'app/models/batch.rb', line 339
def remove_request_ids(request_ids, reason = nil, = nil)
ActiveRecord::Base.transaction do
Request
.find(request_ids)
.each do |request|
request.failures.create(reason: reason, comment: , notify_remote: true)
detach_request(request)
end
update_batch_state(reason, )
end
end
|
#request_count ⇒ Object
524
525
526
|
# File 'app/models/batch.rb', line 524
def request_count
requests.count
end
|
#reset!(current_user) ⇒ Object
rubocop:todo Metrics/MethodLength
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
|
# File 'app/models/batch.rb', line 375
def reset!(current_user) 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
362
363
364
365
366
367
368
369
370
371
372
|
# File 'app/models/batch.rb', line 362
def return_request_to_inbox(request, current_user = nil)
ActiveRecord::Base.transaction do
unless current_user.nil?
request.(
"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_id ⇒ Object
189
190
191
|
# File 'app/models/batch.rb', line 189
def robot_id
event_with_description('Cherrypick Layout Set')&.descriptor_value('robot_id')
end
|
#robot_verified!(user_id) ⇒ Object
471
472
473
474
475
476
477
478
479
480
|
# File 'app/models/batch.rb', line 471
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_barcode ⇒ Object
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.
207
208
209
210
|
# File 'app/models/batch.rb', line 207
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_labware ⇒ Object
Source Labware returns the physical pieces of labware (ie. a plate for wells, but tubes for tubes)
299
300
301
|
# File 'app/models/batch.rb', line 299
def source_labware
input_labware
end
|
#start_requests ⇒ Object
233
234
235
|
# File 'app/models/batch.rb', line 233
def start_requests
requests.with_assets_for_starting_requests.not_failed.map(&:start!)
end
|
#subject_type ⇒ Object
122
123
124
|
# File 'app/models/batch.rb', line 122
def subject_type
sequencing? ? 'flowcell' : 'batch'
end
|
#swap(current_user, batch_info = {}) ⇒ Object
rubocop:todo Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
399
400
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
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
|
# File 'app/models/batch.rb', line 399
def swap(current_user, batch_info = {}) return false if batch_info.empty?
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
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
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
)
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_cherrypick ⇒ Object
463
464
465
466
467
468
469
|
# File 'app/models/batch.rb', line 463
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
|
#underrun ⇒ Object
193
194
195
|
# File 'app/models/batch.rb', line 193
def underrun
has_limit? ? (item_limit - batch_requests.size) : 0
end
|
#update_batch_state(reason, comment) ⇒ Object
168
169
170
171
172
173
174
|
# File 'app/models/batch.rb', line 168
def update_batch_state(reason, )
if requests.all?(&:terminated?)
failures.create(reason: reason, 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.
rubocop:todo Metrics/MethodLength
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 || barcode == request.asset.human_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)
true
else
false
end
end
|