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

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_TIMING_PUBLICATION =
'delay until publication'
ALL_DATA_RELEASE_TIMINGS =

The list of all possible data release timings

[
  DATA_RELEASE_TIMING_STANDARD,
  DATA_RELEASE_TIMING_NEVER,
  DATA_RELEASE_TIMING_DELAYED,
  DATA_RELEASE_TIMING_IMMEDIATE,
  DATA_RELEASE_TIMING_PUBLICATION
].freeze
DATA_RELEASE_TIMINGS_FOR_OPEN_RELEASE =

Release timings for open studies

[
  DATA_RELEASE_TIMING_STANDARD,
  DATA_RELEASE_TIMING_IMMEDIATE,
  DATA_RELEASE_TIMING_DELAYED,
  DATA_RELEASE_TIMING_PUBLICATION
].freeze
DATA_RELEASE_TIMINGS_FOR_MANAGED_RELEASE =

Release timings for managed studies

[
  DATA_RELEASE_TIMING_STANDARD,
  DATA_RELEASE_TIMING_IMMEDIATE,
  DATA_RELEASE_TIMING_DELAYED
].freeze
OLD_DATA_RELEASE_PREVENTION_REASONS =
['data validity', 'legal', 'replication of data subset'].freeze
DATA_RELEASE_PREVENTION_REASON_OTHER =
'Other (please specify)'
DATA_RELEASE_PREVENTION_REASONS =
[
  'Pilot or validation studies - DAC approval not required',
  'Collaborators will share data in a research repository - DAC approval not required',
  'Prevent harm (e.g sensitive studies or biosecurity) - DAC approval required',
  'Protecting IP - DAC approval required',
  DATA_RELEASE_PREVENTION_REASON_OTHER
].freeze
OLD_DATA_RELEASE_DELAY_FOR_OTHER =
'other'
DATA_RELEASE_DELAY_FOR_OTHER =
'Other (please specify below)'
OLD_DATA_RELEASE_DELAY_REASONS =
['other', 'phd study'].freeze
DATA_RELEASE_DELAY_REASONS_STANDARD =
[
  'PhD study',
  'Capacity building',
  'Intellectual property protection',
  'Additional time to make data FAIR',
  DATA_RELEASE_DELAY_FOR_OTHER
].freeze
DATA_RELEASE_DELAY_REASONS_ASSAY =
['assay of no other use'].freeze
DATA_RELEASE_DELAY_PERIODS =
['3 months', '6 months', '9 months', '12 months', '18 months'].freeze
EBI_LIBRARY_STRATEGY_OPTIONS =
EBI_LIBRARY_SOURCE_OPTIONS =
EBI_LIBRARY_SELECTION_OPTIONS =
REMAPPED_ATTRIBUTES =
{
  contaminated_human_dna: YES_OR_NO,
  remove_x_and_autosomes: YES_OR_NO,
  study_sra_hold: STUDY_SRA_HOLDS,
  contains_human_dna: YES_OR_NO,
  commercially_available: YES_OR_NO
}.transform_values { |v| v.index_by { |b| b.downcase } }

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

Methods included from Warren::Callback::ExceptExternallyManaged

#broadcast_except_externally_managed, included

Instance Attribute Details

#approvalObject

Returns the value of attribute approval.



143
144
145
# File 'app/models/study.rb', line 143

def approval
  @approval
end

#run_countObject

Returns the value of attribute run_count.



143
144
145
# File 'app/models/study.rb', line 143

def run_count
  @run_count
end

#skip_externally_managed_restrictionObject

Flag set by Integration Hub requests to lift the externally managed study edit lock.



146
147
148
# File 'app/models/study.rb', line 146

def skip_externally_managed_restriction
  @skip_externally_managed_restriction
end

#total_priceObject

Returns the value of attribute total_price.



143
144
145
# File 'app/models/study.rb', line 143

def total_price
  @total_price
end

Instance Method Details

#abbreviationObject



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

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

#accession_number?Boolean

Returns:

  • (Boolean)


560
561
562
# File 'app/models/study.rb', line 560

def accession_number?
  ebi_accession_number.present?
end

#approved?Boolean

Returns:

  • (Boolean)


595
596
597
598
# File 'app/models/study.rb', line 595

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))


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

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

#completedObject



483
484
485
486
487
488
489
# File 'app/models/study.rb', line 483

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



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

def dac_accession_number
  .ega_dac_accession_number
end

#dac_refnameObject



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

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

#data_release_delay_options(assay_option: false) ⇒ Array<String>

Helper method for edit dropdowns to support backwards compatibility with old options.

Parameters:

  • assay_option (Boolean) (defaults to: false)
    • whether to include assay-specific options

Returns:

  • (Array<String>)

    the list of options for the data release delay reason dropdown



642
643
644
645
646
647
648
649
650
651
# File 'app/models/study.rb', line 642

def data_release_delay_options(assay_option: false)
  # If the current value is an old one, then we need to include it in the list of options
  additional_options = []
  if OLD_DATA_RELEASE_DELAY_REASONS.include? .data_release_delay_reason
    additional_options << .data_release_delay_reason
  end

  additional_options.concat(DATA_RELEASE_DELAY_REASONS_ASSAY) if assay_option
  DATA_RELEASE_DELAY_REASONS_STANDARD + additional_options
end

#data_release_prevention_optionsArray<String>

Helper method for edit dropdowns to support backwards compatibility with old options.

Returns:

  • (Array<String>)

    the list of options for the data release prevention reason dropdown



629
630
631
632
633
634
635
636
# File 'app/models/study.rb', line 629

def data_release_prevention_options
  additional_options = []
  if OLD_DATA_RELEASE_PREVENTION_REASONS.include? .data_release_prevention_reason
    additional_options << .data_release_prevention_reason
  end

  DATA_RELEASE_PREVENTION_REASONS + additional_options
end

#dehumanise_abbreviated_nameObject



591
592
593
# File 'app/models/study.rb', line 591

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

#ebi_accession_numberObject



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

def ebi_accession_number
  .study_ebi_accession_number
end

#ethical_approval_required?Boolean

Returns:

  • (Boolean)


600
601
602
603
# File 'app/models/study.rb', line 600

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

#externally_managed_restrictions_enabled?Boolean

Instance methods

Returns:

  • (Boolean)


440
441
442
# File 'app/models/study.rb', line 440

def externally_managed_restrictions_enabled?
  Flipper.enabled?(:y26_172_enable_externally_managed_study_restrictions)
end

#localeObject



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

def locale
  funding_source
end

#mailing_list_of_managersObject



613
614
615
616
# File 'app/models/study.rb', line 613

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



475
476
477
# File 'app/models/study.rb', line 475

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

#mark_deactiveObject



471
472
473
# File 'app/models/study.rb', line 471

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.



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

def owner
  owners.first
end

#policy_accession_numberObject



556
557
558
# File 'app/models/study.rb', line 556

def policy_accession_number
  .ega_policy_accession_number
end

#rebroadcastObject



622
623
624
# File 'app/models/study.rb', line 622

def rebroadcast
  broadcast_except_externally_managed
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)


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

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.



505
506
507
508
509
510
511
512
513
514
515
# File 'app/models/study.rb', line 505

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

#samples_accessionable?Boolean

Returns true if the samples in this study are eligible for accessioning

A study's samples are eligible for accessioning if:

  • the study is active
  • the study's data release strategy open or managed
  • the study is not set to never release
  • the study requires accessioning
  • the study has an accession number

Returns:

  • (Boolean)

    true if the samples in this study are eligible for accessioning, false otherwise



574
575
576
577
578
579
580
581
582
583
584
# File 'app/models/study.rb', line 574

def samples_accessionable?
  # If updating this method, please also update app/views/studies/information/_study_accession_status.html.erb
  [
    active?,
    !ui_locked?,
    !.strategy_not_applicable?,
    !.never_release?,
    accession_required?,
    accession_number?
  ].all?
end

#studyObject

Used by EventfulMailer



531
532
533
# File 'app/models/study.rb', line 531

def study
  self
end

#study_statusObject



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

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

#subject_typeObject



618
619
620
# File 'app/models/study.rb', line 618

def subject_type
  'study'
end

#text_commentsObject



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

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

#ui_locked?Boolean

Returns true if the study is externally_managed and the feature flag is enabled

Returns:

  • (Boolean)


445
446
447
# File 'app/models/study.rb', line 445

def ui_locked?
  externally_managed_restrictions_enabled? && externally_managed?
end

#unprocessed_submissions?Boolean

Returns:

  • (Boolean)


525
526
527
528
# File 'app/models/study.rb', line 525

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

#validate_ethically_approvedObject



449
450
451
452
453
454
455
456
457
458
459
460
# File 'app/models/study.rb', line 449

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

#validate_study_for_accessioning!Object



605
606
607
608
609
610
611
# File 'app/models/study.rb', line 605

def validate_study_for_accessioning!
  if ui_locked?
    errors.add(:base, I18n.t('studies.externally_managed.accession_not_allowed'))
    raise ActiveRecord::RecordInvalid, self
  end
  valid?(:accession) or raise ActiveRecord::RecordInvalid, self
end

#warningsObject



462
463
464
465
466
467
468
469
# File 'app/models/study.rb', line 462

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