Class: Sample

Inherits:
ApplicationRecord show all
Extended by:
EventfulRecord, IncludeTag, Metadata, ValidationStateGuard
Includes:
Aliquot::Aliquotable, Api::SampleIo::Extensions, Commentable, ModelExtensions::Sample, Role::Authorized, 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. - Heron: Heron samples get registered via the Api::V2::Heron::PlatesController - Special samples: Samples such as PhiX are generated internally

Defined Under Namespace

Classes: Current, 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 =

rubocop:enable Layout/LineLength

'\d+(?:\.\d+)?\s+\w+(?:\/\w+)?|Not Applicable|N/A|To be provided'
REMAPPED_ATTRIBUTES =

The spreadsheets that people upload contain various fields that could be mistyped. Here we ensure that the capitalisation of these is correct.

{
  gc_content: GC_CONTENTS,
  gender: GENDERS,
  dna_source: DNA_SOURCES,
  sample_sra_hold: SRA_HOLD_VALUES
}.transform_values { |v| v.index_by { |b| b.downcase } }

Constants included from Metadata

Metadata::SECTION_FIELDS

Constants included from StandardNamedScopes

StandardNamedScopes::SORT_FIELDS, StandardNamedScopes::SORT_ORDERS

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 ValidationStateGuard

validation_guard, validation_guarded_by

Methods included from Metadata

has_metadata

Methods included from IncludeTag

include_tag

Methods included from Commentable

#after_comment_addition

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

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

#current_userObject

For attributing accessioning changes recorded in the SS events table



289
290
291
# File 'app/models/sample.rb', line 289

def current_user
  @current_user
end

#empty_supplier_sample_nameBoolean

Deprecated.

Only set on older samples where samples were created at manifest generation, rather than upload

Returns true if the customer didn't fill in the supplier_sample_name. Indicating that there is actually no sample in the well.

Returns:

  • (Boolean)

    Returns true if the customer didn't fill in the supplier_sample_name. Indicating that there is actually no sample in the well.



# File 'app/models/sample.rb', line 77

Class Method Details

.tagsObject



217
218
219
# File 'app/models/sample.rb', line 217

def self.tags
  @tags ||= []
end

Instance Method Details

#accession_and_handle_validation_errorsObject

NOTE: this does not check whether the current user is permitted to accession the sample, nor if accessioning is enabled, as these belong in a controller or library, rather than the model.



553
554
555
556
557
558
559
560
561
562
563
# File 'app/models/sample.rb', line 553

def accession_and_handle_validation_errors
  event_user = current_user # the event_user for this sample must be set from the calling controller
  Accession.accession_sample(self, event_user, perform_now: true)

# Save error messages for later feedback to the user in a flash message
rescue Accession::InternalValidationError
  # validation errors have already been added to the sample in Accession::Sample.validate!
rescue Accession::Error, Faraday::Error => e
  message = Accession.user_error_message(e)
  errors.add(:base, message)
end

#accession_number?Boolean

Returns:

  • (Boolean)


496
497
498
# File 'app/models/sample.rb', line 496

def accession_number?
  ebi_accession_number.present?
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)


628
629
630
# File 'app/models/sample.rb', line 628

def can_be_included_in_submission?
  registered_through_manifest? ? .supplier_name.present? : true
end

#control_formattedObject



632
633
634
635
636
637
638
639
# File 'app/models/sample.rb', line 632

def control_formatted
  return nil if control.nil?

  return 'No' if control == false

  type_text = control_type || 'type unspecified'
  "Yes (#{type_text})"
end

#current_accession_statusObject



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

def current_accession_status
  accession_sample_statuses.last
end

#ebi_accession_numberObject



492
493
494
# File 'app/models/sample.rb', line 492

def ebi_accession_number
  .sample_ebi_accession_number
end

#ena_studyObject



569
570
571
# File 'app/models/sample.rb', line 569

def ena_study
  studies.first
end

#errorObject



500
501
502
# File 'app/models/sample.rb', line 500

def error
  'Default error message'
end

#friendly_nameObject



608
609
610
# File 'app/models/sample.rb', line 608

def friendly_name
  sanger_sample_id || name
end

#handle_update_event(user) ⇒ Object



565
566
567
# File 'app/models/sample.rb', line 565

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

#name_unchangedObject



612
613
614
615
# File 'app/models/sample.rb', line 612

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, historically through studies/:id/sample_registration or via external services like Heron

Returns:

  • (Boolean)


620
621
622
# File 'app/models/sample.rb', line 620

def registered_through_manifest?
  sample_manifest.present?
end

#rename_to!(new_name) ⇒ Object

this method has to be before validation_guarded_by



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

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

#sample_empty?(supplier_sample_name = name) ⇒ Boolean

Returns:

  • (Boolean)


504
505
506
507
508
# File 'app/models/sample.rb', line 504

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



591
592
593
594
595
596
# File 'app/models/sample.rb', line 591

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)


510
511
512
513
# File 'app/models/sample.rb', line 510

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_sample_id, but the feature was incorrectly implemented, and would have thrown an exception.



479
480
481
482
483
484
485
486
487
488
489
490
# File 'app/models/sample.rb', line 479

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

#should_be_accessioned?Boolean

Criteria for whether a sample should be accessioned. A sample should be accessioned if: - it is part of a single accessionable study - that study is active - that study is set to open or managed - that study is set to be released - that study requires accessioning - that study has an accession number

Returns:

  • (Boolean)

    true if the sample should be accessioned, false otherwise



536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'app/models/sample.rb', line 536

def should_be_accessioned?
  # If updating this method, please also update app/views/samples/_studies.html.erb
  accessioning_criteria = [
    studies_for_accessioning.size == 1
  ]
  return true if accessioning_criteria.all?

  Rails.logger.debug do
    "Sample '#{name}' should not be accessioned as it " \
      "belongs to #{studies_for_accessioning.size} accessionable studies."
  end

  false
end

#studies_for_accessioningArray<Study>

Returns an array of studies linked to this sample that are eligible for accessioning A study is eligible for accessioning if: - it is active - it is set to open or managed - it is not set to never release - it requires accessioning - it has an accession number

Returns:

  • (Array<Study>)

    the studies linked to this sample that are eligible for accessioning



523
524
525
# File 'app/models/sample.rb', line 523

def studies_for_accessioning
  studies.select(&:samples_accessionable?)
end

#study_for_accessioningObject



573
574
575
576
# File 'app/models/sample.rb', line 573

def study_for_accessioning
  # There should only be one study for accessioning, if we want to accession, so we return the only one
  studies_for_accessioning.first
end

#subject_typeObject



598
599
600
# File 'app/models/sample.rb', line 598

def subject_type
  'sample'
end

#validate_sample_for_accessioning!Object

Validates that the sample and it's study are valid for ALL accessioning services accessioning



579
580
581
582
583
584
585
# File 'app/models/sample.rb', line 579

def validate_sample_for_accessioning!
  accession_service = AccessionService.select_for_sample(self)
  (valid?(:accession) && valid?(accession_service.provider)) || raise(ActiveRecord::RecordInvalid, self)
rescue ActiveRecord::RecordInvalid => e
  ena_study.errors.full_messages.each { |message| errors.add(:base, "#{message} on study") } unless ena_study.nil?
  raise e
end