Class: Sample

Inherits:
ApplicationRecord show all
Extended by:
EventfulRecord, Metadata, ValidationStateGuard
Includes:
Aliquot::Aliquotable, Api::SampleIO::Extensions, ModelExtensions::Sample, SharedBehaviour::Named, StandardNamedScopes, Uuid::Uuidable
Defined in:
app/models/sample.rb

Overview

A Sample is an abstract concept, with represents the life of a sample of DNA/RNA as it moves through our processes. As a result, a sample may exist in multiple receptacles at the same time, in the form of an Aliquot. As a result Sample is mainly concerned with dealing with aspects which are always true, such as tracking where it originally came from.

An individual sample may be subject to library creation and sequencing multiple different times. These processes may be different each time.

## Sample Creation Samples can enter Sequencescape via a number of different routes. Such as:

  • SampleManifest: Large spreadsheets of sample information are generated.

    When uploaded samples are created in the corresponding
    {Receptacle}.
    
  • SampleRegistrar: Largely superseded by SampleManifest provides a web form

    and spreadsheet upload providing similar functionality to
    SampleManifest
    
  • S2 Bridge: A handful of samples are injected directly into the database via

    the S2 bridge application.
    
  • Aker::Factories::Material: Samples imported from Aker

  • Special samples: Samples such as PhiX are generated internally

Defined Under Namespace

Classes: Metadata

Constant Summary collapse

GC_CONTENTS =
['Neutral', 'High AT', 'High GC'].freeze
GENDERS =
['Male', 'Female', 'Mixed', 'Hermaphrodite', 'Unknown', 'Not Applicable'].freeze
DNA_SOURCES =
['Genomic', 'Whole Genome Amplified', 'Blood', 'Cell Line', 'Saliva', 'Brain', 'FFPE',
'Amniocentesis Uncultured', 'Amniocentesis Cultured', 'CVS Uncultured', 'CVS Cultured', 'Fetal Blood', 'Tissue'].freeze
SRA_HOLD_VALUES =
%w[Hold Public Protect].freeze
AGE_REGEXP =
'\d+(?:\.\d+|\-\d+|\.\d+\-\d+\.\d+|\.\d+\-\d+\.\d+)?\s+(?:second|minute|day|week|month|year)s?|Not Applicable|N/A|To be provided'
DOSE_REGEXP =
'\d+(?:\.\d+)?\s+\w+(?:\/\w+)?|Not Applicable|N/A|To be provided'

Constants included from Metadata

Metadata::SECTION_FIELDS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Metadata

has_metadata, required_tags

Methods included from ValidationStateGuard

validation_guard, validation_guarded_by

Methods included from EventfulRecord

has_many_events, has_many_lab_events, has_one_event_with_family

Methods included from Aliquot::Aliquotable

included

Methods included from SharedBehaviour::Named

included

Methods included from StandardNamedScopes

included

Methods included from Uuid::Uuidable

included, #unsaved_uuid!, #uuid

Methods included from Api::SampleIO::Extensions

included, #json_root

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

Instance Attribute Details

#ena_studyObject (readonly)

Returns the value of attribute ena_study


357
358
359
# File 'app/models/sample.rb', line 357

def ena_study
  @ena_study
end

Instance Method Details

#accessionObject


345
346
347
348
349
350
351
# File 'app/models/sample.rb', line 345

def accession
  return unless configatron.accession_samples

  accessionable = Accession::Sample.new(Accession.configuration.tags, self)
  # Accessioning jobs are lower priority (higher number) than submissions and reports
  Delayed::Job.enqueue(SampleAccessioningJob.new(accessionable), priority: 200) if accessionable.valid?
end

#accession_number?Boolean

Returns:

  • (Boolean)

315
316
317
# File 'app/models/sample.rb', line 315

def accession_number?
  ebi_accession_number.present?
end

#accession_serviceObject

Return the highest priority accession service


334
335
336
337
338
339
340
341
342
343
# File 'app/models/sample.rb', line 334

def accession_service
  services = studies.group_by { |s| s.accession_service.priority }
  return UnsuitableAccessionService.new([]) if services.empty?

  highest_priority = services.keys.max
  suitable_study = services[highest_priority].detect(&:send_samples_to_service?)
  return suitable_study.accession_service if suitable_study

  UnsuitableAccessionService.new(services[highest_priority])
end

#can_be_included_in_submission?Boolean

if sample is registered through sample manifest it should have supplier sample name (without it the row is considered empty) if sample was registered directly, only sample name is a required field, so supplier sample name can be empty but it is reasonably safe to assume that required metadata was provided

Returns:

  • (Boolean)

418
419
420
421
422
423
424
# File 'app/models/sample.rb', line 418

def can_be_included_in_submission?
  if registered_through_manifest?
    .supplier_name.present?
  else
    true
  end
end

#ebi_accession_numberObject


311
312
313
# File 'app/models/sample.rb', line 311

def ebi_accession_number
  .sample_ebi_accession_number
end

#errorObject


319
320
321
# File 'app/models/sample.rb', line 319

def error
  'Default error message'
end

#friendly_nameObject


399
400
401
# File 'app/models/sample.rb', line 399

def friendly_name
  sanger_sample_id || name
end

#handle_update_event(user) ⇒ Object


353
354
355
# File 'app/models/sample.rb', line 353

def handle_update_event(user)
  events.updated_using_sample_manifest!(user)
end

#name_unchangedObject


403
404
405
406
# File 'app/models/sample.rb', line 403

def name_unchanged
  errors.add(:name, 'cannot be changed') unless can_rename_sample
  can_rename_sample
end

#registered_through_manifest?Boolean

sample can either be registered through sample manifest or through studies/:id/sample_registration

Returns:

  • (Boolean)

410
411
412
# File 'app/models/sample.rb', line 410

def registered_through_manifest?
  sample_manifest.present?
end

#rename_to!(new_name) ⇒ Object

this method has to be before validation_guarded_by


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

def rename_to!(new_name)
  update!(name: new_name)
end

#sample_empty?(supplier_sample_name = name) ⇒ Boolean

Returns:

  • (Boolean)

323
324
325
326
327
# File 'app/models/sample.rb', line 323

def sample_empty?(supplier_sample_name = name)
  return true if empty_supplier_sample_name

  sample_supplier_name_empty?(supplier_sample_name)
end

#sample_reference_genomeObject


384
385
386
387
388
389
# File 'app/models/sample.rb', line 384

def sample_reference_genome
  return .reference_genome if .reference_genome.try(:name).present?
  return study_reference_genome if study_reference_genome.try(:name).present?

  nil
end

#sample_supplier_name_empty?(supplier_sample_name) ⇒ Boolean

Returns:

  • (Boolean)

329
330
331
# File 'app/models/sample.rb', line 329

def sample_supplier_name_empty?(supplier_sample_name)
  supplier_sample_name.blank? || ['empty', 'blank', 'water', 'no supplier name available', 'none'].include?(supplier_sample_name.downcase)
end

#shorten_sanger_sample_idObject

Note:

This appears to be set up to handle legacy data. All currently generated Sanger sample ids will be meet criteria 1 or 2.

Truncates the sanger_sample_id for display on labels

  • Returns the sanger_sample_id AS IS if it is nil or less than 10 characters

  • Tries to truncate it to the last 7 digits, and returns that

  • If it cannot extract 7 digits, the full sanger_sample_id is returned

Earlier implementations were supposed to fall back to the name in the absence of a sanger_smaple_id, but the feature was incorrectly implemented, and would have thrown an exception.


301
302
303
304
305
306
307
308
309
# File 'app/models/sample.rb', line 301

def shorten_sanger_sample_id
  case sanger_sample_id
  when nil then sanger_sample_id
  when sanger_sample_id.size < 10 then sanger_sample_id
  when /([\d]{7})$/ then Regexp.last_match(1)
  else
    sanger_sample_id
  end
end

#subject_typeObject


395
396
397
# File 'app/models/sample.rb', line 395

def subject_type
  'sample'
end

#validate_ena_required_fields!Object


366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'app/models/sample.rb', line 366

def validate_ena_required_fields!
  # Do not alter the order of this line, otherwise @ena_study won't be set correctly!
  @ena_study = studies.first
  self.validating_ena_required_fields = true
  valid? || raise(ActiveRecord::RecordInvalid, self)
rescue ActiveRecord::RecordInvalid => e
  unless @ena_study.nil?
    @ena_study.errors.full_messages.each do |message|
      errors.add(:base, "#{message} on study")
    end
  end
  raise e
ensure
  # Do not alter the order of this line, otherwise the @ena_study won't be reset!
  self.validating_ena_required_fields = false
  @ena_study = nil
end

#validating_ena_required_fields_with_first_study=(state) ⇒ Object Also known as: validating_ena_required_fields=


359
360
361
362
# File 'app/models/sample.rb', line 359

def validating_ena_required_fields_with_first_study=(state)
  self.validating_ena_required_fields_without_first_study = state
  @ena_study.try(:validating_ena_required_fields=, state)
end

391
392
393
# File 'app/models/sample.rb', line 391

def withdraw_consent
  update!(consent_withdrawn: true)
end