Class: Study

Overview

Note:

This is really quite convoluted, and couples together administrative organization alongside accessioning and data-access rules. It results in samples being tied to an EGAS/ERP far too early in their lifecycle, and as a result we often need to perform ‘sample moves’. Although we do need to know if samples are open(ENA) or managed(EGA) at the point of accessioning.

A Study is a collection of various samples and the work done on them. They are perhaps slightly overloaded, and provide: - A means of grouping together samples for administrative purposes - A means of generating EGAS/ERP study accession numbers at the ENA/EGA - @see Accessionable::Study - These accession numbers are used at data release to group samples together for publication - For managed/EGA studies, also ties the data to an Accessionable::Dac and Accessionable::Policy - A means of generating the aforementioned Accessionable::Dac and Accessionable::Policy @note These should DEFINITELY be separate entities - A means of tying data to internal data-release timings - A means to apply internal data access policies to released sequencing data - A means to tie interested parties to the samples and the work done on them - A way of specifying common ways of filtering/processing generated data. eg. filter human sequence - The service with which a Sample will be accessioned (eg. EGA/ENA)

When a Sample enters Sequencescape it will usually be associated with a single Study, usually determined by the Study associated with the SampleManifest. This study will be recorded on the Aliquot in the stock Receptacle, and additionally a StudySample will record this association.

When work is requested an Order will be created, specifying a list of receptacles and the Study for which this work is being performed. This will set initial study id on request and in turn will be recorded on any downstream aliquots. Critically, it is the study specified on the Aliquot in the Lane which will influence processes like data release and data access.

Defined Under Namespace

Classes: Metadata, PolyMetadataHandler

Constant Summary collapse

STOCK_PLATE_PURPOSES =

Constants

['Stock Plate', 'Stock RNA Plate'].freeze
YES =
'Yes'
NO =
'No'
YES_OR_NO =
[YES, NO].freeze
Other_type =
'Other'
STUDY_SRA_HOLDS =
%w[Hold Public].freeze
DATA_RELEASE_STRATEGY_OPEN =
'open'
DATA_RELEASE_STRATEGY_MANAGED =
'managed'
DATA_RELEASE_STRATEGY_NOT_APPLICABLE =
'not applicable'
DATA_RELEASE_STRATEGIES =
[
  DATA_RELEASE_STRATEGY_OPEN,
  DATA_RELEASE_STRATEGY_MANAGED,
  DATA_RELEASE_STRATEGY_NOT_APPLICABLE
].freeze
DATA_RELEASE_TIMING_STANDARD =
'standard'
DATA_RELEASE_TIMING_NEVER =
'never'
DATA_RELEASE_TIMING_DELAYED =
'delayed'
DATA_RELEASE_TIMING_IMMEDIATE =
'immediate'
DATA_RELEASE_TIMINGS =
[
  DATA_RELEASE_TIMING_STANDARD,
  DATA_RELEASE_TIMING_IMMEDIATE,
  DATA_RELEASE_TIMING_DELAYED
].freeze
DATA_RELEASE_PREVENTION_REASONS =
['data validity', 'legal', 'replication of data subset'].freeze
DATA_RELEASE_DELAY_FOR_OTHER =
'other'
DATA_RELEASE_DELAY_REASONS_STANDARD =
['phd study', DATA_RELEASE_DELAY_FOR_OTHER].freeze
DATA_RELEASE_DELAY_REASONS_ASSAY =
['phd study', 'assay of no other use', DATA_RELEASE_DELAY_FOR_OTHER].freeze
DATA_RELEASE_DELAY_PERIODS =
['3 months', '6 months', '9 months', '12 months', '18 months'].freeze

Constants included from Metadata

Metadata::SECTION_FIELDS

Constants included from StudyReport::StudyDetails

StudyReport::StudyDetails::BATCH_SIZE

Instance Attribute 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 Metadata

has_metadata

Methods included from Attributable::Association::Target

default, extended, for_select_association

Methods included from SampleManifest::Associations

included

Methods included from ReferenceGenome::Associations

included

Methods included from SharedBehaviour::Named

included

Methods included from Commentable

#after_comment_addition

Methods included from DataRelease

#accession_required?, #do_not_enforce_accessioning, #for_array_express?, #valid_data_release_properties?

Methods included from Uuid::Uuidable

included, #unsaved_uuid!, #uuid

Methods included from Api::StudyIo::Extensions

included, #render_class

Methods included from StudyReport::StudyDetails

#each_stock_well_id_in_study_in_batches, #progress_report_header, #progress_report_on_all_assets

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

#approvalObject

Returns the value of attribute approval.



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

def approval
  @approval
end

#run_countObject

Returns the value of attribute run_count.



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

def run_count
  @run_count
end

#total_priceObject

Returns the value of attribute total_price.



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

def total_price
  @total_price
end

Instance Method Details

#abbreviationObject



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

def abbreviation
  abbreviation = .study_name_abbreviation
  abbreviation.presence || "#{id}STDY"
end

#accession_all_samplesObject



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

def accession_all_samples
  samples.find_each(&:accession) if accession_number?
end

#accession_number?Boolean

Returns:

  • (Boolean)


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

def accession_number?
  ebi_accession_number.present?
end

#accession_serviceObject



525
526
527
528
529
530
531
532
533
534
# File 'app/models/study.rb', line 525

def accession_service
  case data_release_strategy
  when 'open'
    EnaAccessionService.new
  when 'managed'
    EgaAccessionService.new
  else
    NoAccessionService.new(self)
  end
end

#approved?Boolean

Returns:

  • (Boolean)


515
516
517
518
# File 'app/models/study.rb', line 515

def approved?
  # TODO: remove
  true
end

#asset_progress(assets = nil) {|initial_requests.asset_statistics(wheres)| ... } ⇒ Object

Yields information on the state of all assets in a convenient fashion for displaying in a table.

Yields:

  • (initial_requests.asset_statistics(wheres))


436
437
438
439
440
# File 'app/models/study.rb', line 436

def asset_progress(assets = nil)
  wheres = {}
  wheres = { asset_id: assets.map(&:id) } if assets.present?
  yield(initial_requests.asset_statistics(wheres))
end

#completedObject



421
422
423
424
425
426
427
# File 'app/models/study.rb', line 421

def completed
  counts = requests.standard.group('state').count
  total = counts.values.sum
  failed = counts['failed'] || 0
  cancelled = counts['cancelled'] || 0
  (total - failed - cancelled) > 0 ? (counts.fetch('passed', 0) * 100) / (total - failed - cancelled) : 0
end

#dac_accession_numberObject



490
491
492
# File 'app/models/study.rb', line 490

def dac_accession_number
  .ega_dac_accession_number
end

#dac_refnameObject



459
460
461
# File 'app/models/study.rb', line 459

def dac_refname
  "DAC for study - #{name} - ##{id}"
end

#dehumanise_abbreviated_nameObject



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

def dehumanise_abbreviated_name
  abbreviation.downcase.gsub(/ +/, '_')
end

#each_well_for_qc_report_in_batches(exclude_existing, product_criteria, plate_purposes = nil) ⇒ Object



386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'app/models/study.rb', line 386

def each_well_for_qc_report_in_batches(exclude_existing, product_criteria, plate_purposes = nil)
  # @note We include aliquots here, despite the fact they are only needed if we have to set a poor-quality flag
  #       as in some cases failures are not as rare as you may imagine, and it can cause major performance issues.
  base_scope =
    Well
      .on_plate_purpose_included(PlatePurpose.where(name: plate_purposes || STOCK_PLATE_PURPOSES))
      .for_study_through_aliquot(self)
      .without_blank_samples
      .includes(:well_attribute, :aliquots, :map, samples: :sample_metadata)
      .readonly(true)
  scope = exclude_existing ? base_scope.without_report(product_criteria) : base_scope
  scope.find_in_batches { |wells| yield wells }
end

#ebi_accession_numberObject



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

def ebi_accession_number
  .study_ebi_accession_number
end

#ethical_approval_required?Boolean

Returns:

  • (Boolean)


520
521
522
523
# File 'app/models/study.rb', line 520

def ethical_approval_required?
  .contains_human_dna == Study::YES && .contaminated_human_dna == Study::NO &&
    .commercially_available == Study::NO
end

#localeObject



482
483
484
# File 'app/models/study.rb', line 482

def locale
  funding_source
end

#mailing_list_of_managersObject



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

def mailing_list_of_managers
  configured_managers = managers.pluck(:email).compact.uniq
  configured_managers.empty? ? configatron.fetch(:ssr_emails, User.all_administrators_emails) : configured_managers
end

#mark_activeObject



413
414
415
# File 'app/models/study.rb', line 413

def mark_active
  logger.warn "Study activation failed! #{errors.map(&:to_s)}" unless active?
end

#mark_deactiveObject



409
410
411
# File 'app/models/study.rb', line 409

def mark_deactive
  logger.warn "Study deactivation failed! #{errors.map(&:to_s)}" unless inactive?
end

#ownerObject

Returns the study owner (user) if exists or nil TODO - Should be “owners” and return all owners or empty array - done TODO - Look into this is the person that created it really the owner? If so, then an owner should be created when a study is created.



478
479
480
# File 'app/models/study.rb', line 478

def owner
  owners.first
end

#policy_accession_numberObject



494
495
496
# File 'app/models/study.rb', line 494

def policy_accession_number
  .ega_policy_accession_number
end

#poly_metadatum_by_key(key) ⇒ PolyMetadatum?

Returns the PolyMetadatum object associated with the given key.

Examples:

study.poly_metadatum_by_key("sample_key")

Parameters:

  • key (String)

    The key of the PolyMetadatum to find.

Returns:

  • (PolyMetadatum, nil)

    The PolyMetadatum object with the given key, or nil if no such PolyMetadatum exists.



566
567
568
# File 'app/models/study.rb', line 566

def poly_metadatum_by_key(key)
  .find { |pm| pm.key == key.to_s }
end

#rebroadcastObject



553
554
555
# File 'app/models/study.rb', line 553

def rebroadcast
  broadcast
end

#request_progress {|@stats_cache ||= initial_requests.progress_statistics| ... } ⇒ Object

Yields information on the state of all request types in a convenient fashion for displaying in a table. Used initial requests, which won’t capture cross study sequencing requests.

Yields:

  • (@stats_cache ||= initial_requests.progress_statistics)


431
432
433
# File 'app/models/study.rb', line 431

def request_progress
  yield(@stats_cache ||= initial_requests.progress_statistics) if block_given?
end

#sample_progress(samples = nil) ⇒ Object

Yields information on the state of all samples in a convenient fashion for displaying in a table.



443
444
445
446
447
448
449
450
451
452
453
# File 'app/models/study.rb', line 443

def sample_progress(samples = nil)
  if samples.blank?
    requests.sample_statistics_new
  else
    # Rubocop suggests this changes as it allows MySQL to perform a single query, which is usually better
    # however in this case we've actually already loaded the samples. If we do try passing in the
    # samples themselves, then things top working as intended. (Performance tanks in some places, and
    # we generate invalid SQL in others)
    yield(requests.where(aliquots: { sample_id: samples.pluck(:id) }).sample_statistics_new)
  end
end

#send_samples_to_service?Boolean

Returns:

  • (Boolean)


536
537
538
# File 'app/models/study.rb', line 536

def send_samples_to_service?
  accession_service.no_study_accession_needed || (!.never_release? && accession_number?)
end

#studyObject

Used by EventfulMailer



469
470
471
# File 'app/models/study.rb', line 469

def study
  self
end

#study_statusObject



455
456
457
# File 'app/models/study.rb', line 455

def study_status
  inactive? ? 'closed' : 'open'
end

#subject_typeObject



549
550
551
# File 'app/models/study.rb', line 549

def subject_type
  'study'
end

#text_commentsObject



417
418
419
# File 'app/models/study.rb', line 417

def text_comments
  comments.each_with_object([]) { |c, array| array << c.description if c.description.present? }.join(', ')
end

#unprocessed_submissions?Boolean

Returns:

  • (Boolean)


463
464
465
466
# File 'app/models/study.rb', line 463

def unprocessed_submissions?
  # TODO[mb14] optimize if needed
  study.orders.any? { |o| o.submission.nil? || o.submission.unprocessed? }
end

#validate_ena_required_fields!Object



540
541
542
# File 'app/models/study.rb', line 540

def validate_ena_required_fields!
  valid?(:accession) or raise ActiveRecord::RecordInvalid, self
end

#validate_ethically_approvedObject

Instance methods



373
374
375
376
377
378
379
380
381
382
383
384
# File 'app/models/study.rb', line 373

def validate_ethically_approved
  return true if valid_ethically_approved?

  message =
    if ethical_approval_required?
      'should be either true or false for this study.'
    else
      'should be not applicable (null) not false.'
    end
  errors.add(:ethically_approved, message)
  false
end

#warningsObject



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

def warnings
  # These studies are now invalid, but the warning should remain until existing studies are fixed.
  if .managed? && .data_access_group.blank?
    # rubocop:todo Layout/LineLength
    'No user group specified for a managed study. Please specify a valid Unix user group to ensure study data is visible to the correct people.'
    # rubocop:enable Layout/LineLength
  end
end