Skip to content

Commit

Permalink
Merge pull request seek4science#1722 from ELIXIR-Belgium/implement_as…
Browse files Browse the repository at this point in the history
…say_streams_for_single_page

Implement assay streams for single page
  • Loading branch information
kdp-cloud authored Feb 1, 2024
2 parents f51826c + 6fdd31f commit 23c0daa
Show file tree
Hide file tree
Showing 35 changed files with 582 additions and 201 deletions.
6 changes: 3 additions & 3 deletions app/controllers/assays_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def new
@permitted_params = assay_params if params[:assay]

# jump straight to experimental if modelling analysis is disabled
@assay_class ||= 'experimental' unless Seek::Config.modelling_analysis_enabled
@assay_class ||= 'EXP' unless Seek::Config.modelling_analysis_enabled

@assay.assay_class = AssayClass.for_type(@assay_class) unless @assay_class.nil?

Expand All @@ -82,7 +82,7 @@ def edit
end

def create
params[:assay_class_id] ||= AssayClass.for_type('experimental').id
params[:assay_class_id] ||= AssayClass.experimental.id
@assay = Assay.new(assay_params)

update_assay_organisms @assay, params
Expand Down Expand Up @@ -117,7 +117,7 @@ def fix_assay_linkage
return unless is_single_page_assay?
return unless @assay.has_linked_child_assay?

previous_assay_linked_st_id = @assay.previous_linked_assay_sample_type&.id
previous_assay_linked_st_id = @assay.previous_linked_sample_type&.id

next_assay = Assay.all.detect do |a|
a.sample_type&.sample_attributes&.first&.linked_sample_type_id == @assay.sample_type_id
Expand Down
35 changes: 27 additions & 8 deletions app/controllers/isa_assays_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ class IsaAssaysController < ApplicationController
before_action :find_requested_item, only: %i[edit update]

def new
@isa_assay = IsaAssay.new
if params[:is_assay_stream]
@isa_assay = IsaAssay.new({ assay: { assay_class_id: AssayClass.assay_stream.id } })
else
@isa_assay = IsaAssay.new({ assay: { assay_class_id: AssayClass.experimental.id } })
end
end

def create
@isa_assay = IsaAssay.new(isa_assay_params)
update_sharing_policies @isa_assay.assay
@isa_assay.assay.contributor = current_person
@isa_assay.sample_type.contributor = User.current_user.person
@isa_assay.sample_type.contributor = User.current_user.person if isa_assay_params[:sample_type]
if @isa_assay.save
redirect_to single_page_path(id: @isa_assay.assay.projects.first, item_type: 'assay',
item_id: @isa_assay.assay, notice: 'The ISA assay was created successfully!')
Expand All @@ -27,7 +31,11 @@ def create

def edit
# let edit the assay if the sample_type is not authorized
@isa_assay.sample_type = nil unless requested_item_authorized?(@isa_assay.sample_type)
if @isa_assay.assay.is_assay_stream?
@isa_assay.sample_type = nil
else
@isa_assay.sample_type = nil unless requested_item_authorized?(@isa_assay.sample_type)
end

respond_to do |format|
format.html
Expand All @@ -38,9 +46,11 @@ def update
@isa_assay.assay.attributes = isa_assay_params[:assay]

# update the sample_type
if requested_item_authorized?(@isa_assay.sample_type)
@isa_assay.sample_type.update(isa_assay_params[:sample_type])
@isa_assay.sample_type.resolve_inconsistencies
unless @isa_assay.assay.is_assay_stream?
if requested_item_authorized?(@isa_assay.sample_type)
@isa_assay.sample_type.update(isa_assay_params[:sample_type])
@isa_assay.sample_type.resolve_inconsistencies
end
end

if @isa_assay.save
Expand Down Expand Up @@ -87,10 +97,12 @@ def assay_params
{ data_files_attributes: %i[asset_id direction relationship_type_id] },
{ publication_ids: [] },
{ extended_metadata_attributes: determine_extended_metadata_keys(:assay) },
{ discussion_links_attributes: %i[id url label _destroy] }]
{ discussion_links_attributes: %i[id url label _destroy] }, :assay_stream_id]
end

def sample_type_params(params)
return [] unless params[:sample_type]

attributes = params[:sample_type][:sample_attributes]
if attributes
params[:sample_type][:sample_attributes_attributes] = []
Expand Down Expand Up @@ -128,9 +140,16 @@ def set_up_instance_variable
end

def find_requested_item
@isa_assay = IsaAssay.new
if params[:is_assay_stream]
@isa_assay = IsaAssay.new({ assay: { assay_class_id: AssayClass.assay_stream.id } })
else
@isa_assay = IsaAssay.new({ assay: { assay_class_id: AssayClass.experimental.id } })
end
@isa_assay.populate(params[:id])

# Should not deal with sample type if assay has assay_class assay stream
return if @isa_assay.assay.is_assay_stream?

if @isa_assay.sample_type.nil? || !requested_item_authorized?(@isa_assay.assay)
flash[:error] = "You are not authorized to edit this #{t('isa_assay')}"
flash[:error] = 'Resource not found.' if @isa_assay.sample_type.nil?
Expand Down
30 changes: 19 additions & 11 deletions app/forms/isa_assay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,31 @@ class IsaAssay

attr_accessor :assay, :sample_type, :input_sample_type_id

validates_presence_of :assay, :sample_type, :input_sample_type_id
validates_presence_of :assay
validate :validate_objects

def initialize(params = {})
@assay = Assay.new(params[:assay] || {})
@sample_type = SampleType.new((params[:sample_type] || {}).merge({ project_ids: @assay.project_ids }))
@sample_type.sample_attributes.build(is_title: true, required: true) unless params[:sample_type]
@assay.sample_type = @sample_type
@assay.assay_class = AssayClass.for_type('experimental')
unless @assay.is_assay_stream?
@sample_type = SampleType.new((params[:sample_type] || {}).merge({ project_ids: @assay.project_ids }))
@sample_type.sample_attributes.build(is_title: true, required: true) unless params[:sample_type]
@assay.sample_type = @sample_type
end

@input_sample_type_id = params[:input_sample_type_id]
end

def save
if valid?
if @assay.new_record?
if @assay.new_record? && !@assay.is_assay_stream?
# connect the sample type multi link attribute to the last sample type of the assay's study
input_attribute = @sample_type.sample_attributes.detect(&:seek_sample_multi?)
input_attribute.linked_sample_type_id = @input_sample_type_id
title = SampleType.find(@input_sample_type_id).sample_attributes.detect(&:is_title).title
input_attribute.title = "Input (#{title})"
end
@sample_type.save unless @assay.is_assay_stream?
@assay.save
@sample_type.save
else
false
end
Expand Down Expand Up @@ -54,6 +56,12 @@ def populate(id)
def validate_objects
@assay.errors.each { |e| errors.add(:base, "[Assay]: #{e.full_message}") } unless @assay.valid?

return if @assay.is_assay_stream?

errors.add(:base, '[Assay]: The assay is missing a sample type.') if @sample_type.nil?

return unless @sample_type

@sample_type.errors.full_messages.each { |e| errors.add(:base, "[Sample type]: #{e}") } unless @sample_type.valid?

unless @sample_type.sample_attributes.any?(&:seek_sample_multi?)
Expand All @@ -66,22 +74,22 @@ def validate_objects

unless @sample_type.sample_attributes.select { |a| a.title.include?('Input') && a.isa_tag.nil? }.one?
errors.add(:base,
"[Sample type]: Should have exactly one attribute with the title 'Input' <u><b>and</b></u> no ISA tag".html_safe)
"[Sample type]: Should have exactly one attribute with the title 'Input' <u><b>and</b></u> no ISA tag".html_safe)
end

if @sample_type.sample_attributes.select { |a| !a.title.include?('Input') && a.isa_tag.nil? }.any?
errors.add(:base,
"[Sample type]: All attributes should have an ISA Tag except for the <em>'Input'</em> attribute (hidden)".html_safe)
"[Sample type]: All attributes should have an ISA Tag except for the <em>'Input'</em> attribute (hidden)".html_safe)
end

assay_sample_or_datafile_attributes = @sample_type.sample_attributes.select do |a|
a.isa_tag&.isa_other_material? || a.isa_tag&.isa_data_file?
end

unless assay_sample_or_datafile_attributes.one?
errors.add(:base,
"[Sample type]: Should have exactly one attribute with the 'data_file' <u><b>or</b></u> 'other_material' ISA tag selected".html_safe)
"[Sample type]: Should have exactly one attribute with the 'data_file' <u><b>or</b></u> 'other_material' ISA tag selected".html_safe)
end

errors.add(:base, '[Input Assay]: Input Assay is not provided') if @input_sample_type_id.blank?
end
end
4 changes: 2 additions & 2 deletions app/helpers/images_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ def append_size_parameter(url, size)
url
end

def delete_icon(model_item, user, confirm_msg='Are you sure?')
item_name = text_for_resource model_item
def delete_icon(model_item, user, confirm_msg='Are you sure?', alternative_item_name=nil)
item_name = alternative_item_name.nil? ? (text_for_resource model_item) : alternative_item_name
if model_item.can_delete?(user)
fullURL = url_for(model_item)

Expand Down
40 changes: 31 additions & 9 deletions app/models/assay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class Assay < ApplicationRecord

belongs_to :sample_type

has_many :child_assays, class_name: 'Assay', foreign_key: 'assay_stream_id', dependent: :destroy
belongs_to :assay_stream, class_name: 'Assay', optional: true

belongs_to :assay_class
has_many :assay_organisms, dependent: :destroy, inverse_of: :assay
has_many :organisms, through: :assay_organisms, inverse_of: :assays
Expand Down Expand Up @@ -65,19 +68,38 @@ class Assay < ApplicationRecord

enforce_authorization_on_association :study, :view

def previous_linked_assay_sample_type
sample_type.sample_attributes.detect { |sa| sa.isa_tag.nil? && sa.title.include?('Input') }&.linked_sample_type
def is_assay_stream?
assay_class&.is_assay_stream?
end

def previous_linked_sample_type
return unless is_isa_json_compliant?

if is_assay_stream?
study.sample_types.second
else
sample_type.sample_attributes.detect { |sa| sa.isa_tag.nil? && sa.title.include?('Input') }&.linked_sample_type
end
end

def has_linked_child_assay?
sample_type&.linked_sample_attributes&.any?
return false unless is_isa_json_compliant?

if is_assay_stream?
child_assays.any?
else
sample_type&.linked_sample_attributes&.any?
end
end

# Fetches the assay which is linked through linked_sample_attributes (Single Page specific method)
def linked_assay
sample_type.linked_sample_attributes
.select { |lsa| lsa.isa_tag.nil? && lsa.title.include?('Input') }
.first&.sample_type&.assays&.first
def next_linked_child_assay
return unless has_linked_child_assay?

if is_assay_stream?
previous_linked_sample_type&.linked_sample_attributes&.detect { |sa| sa.isa_tag.nil? && sa.title.include?('Input') }&.sample_type&.assays&.first
else
sample_type.linked_sample_attributes.detect { |sa| sa.isa_tag.nil? && sa.title.include?('Input') }&.sample_type&.assays&.first
end
end

def default_contributor
Expand All @@ -95,7 +117,7 @@ def state_allows_delete?(*args)
end

def is_isa_json_compliant?
investigation.is_isa_json_compliant? && !sample_type.nil?
investigation.is_isa_json_compliant? && (!sample_type.nil? || is_assay_stream?)
end

# returns true if this is a modelling class of assay
Expand Down
29 changes: 19 additions & 10 deletions app/models/assay_class.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
class AssayClass < ApplicationRecord

#this returns an instance of AssayClass according to one of the types "experimental" or "modelling"
#if there is not a match nil is returned
def self.for_type type
keys={"experimental"=>"EXP","modelling"=>"MODEL"}
return AssayClass.find_by(key: keys[type])
# this returns an instance of AssayClass according to one of the constants defined in seek/isa/assay_class.rb
# if there is not a match nil is returned
def self.for_type(type)
AssayClass.find_by(key: type)
end

def self.experimental
self.for_type('experimental')
for_type('EXP')
end

def self.modelling
self.for_type('modelling')
for_type('MODEL')

end

def self.assay_stream
for_type('STREAM')
end

def is_modelling?
key == "MODEL"
key == 'MODEL'
end

def is_experimental?
key == 'EXP'
end

def is_assay_stream?
key == 'STREAM'
end

LONG_KEYS = { 'EXP': 'Experimental Assay', 'MODEL': 'Modelling Analysis', 'STREAM': 'Assay Stream' }.freeze

# for cases where a longer more descriptive key is useful, but can't rely on the title
# which may have been changed over time
def long_key
{'EXP'=>'Experimental Assay','MODEL'=>'Modelling Analysis'}[key]
LONG_KEYS[key.to_sym]
end
end
4 changes: 4 additions & 0 deletions app/models/study.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class Study < ApplicationRecord
has_many "related_#{type.pluralize}".to_sym, -> { distinct }, through: :assays, source: type.pluralize.to_sym
end

def assay_streams
assays.select(&:is_assay_stream?)
end

def assets
related_data_files + related_sops + related_models + related_publications + related_documents
end
Expand Down
30 changes: 23 additions & 7 deletions app/views/assays/_buttons.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<% assay_word = item.is_modelling? ? t('assays.modelling_analysis') : t('assays.assay') %>
<% assay_word ||=
if item.is_assay_stream?
t('assays.assay_stream')
elsif Seek::Config.isa_json_compliance_enabled && item.is_isa_json_compliant?
t('isa_assay')
else
t("assays.#{item.assay_class.long_key.delete(' ').underscore}")
end
%>
<%= render :partial => "subscriptions/subscribe", :locals => {:object => item} %>
<% if Seek::Config.project_single_page_enabled %>
Expand All @@ -21,10 +29,14 @@
<% if item.can_edit? %>
<% if Seek::Config.isa_json_compliance_enabled && item.is_isa_json_compliant? %>
<% valid_study = item&.study&.sample_types.present? %>
<% valid_assay = item&.sample_type.present? %>
<% valid_study = item&.study&.is_isa_json_compliant? %>
<% valid_assay = item&.is_isa_json_compliant? %>
<% if valid_study && valid_assay %>
<%= button_link_to("Design the next #{t('assay')}", 'new', new_isa_assay_path(source_assay_id: item.id, study_id: item.study.id, single_page: params[:single_page])) %>
<% if item&.is_assay_stream? %>
<%= button_link_to("Design #{t('assay')}", 'new', new_isa_assay_path(source_assay_id: item.id, study_id: item.study.id, single_page: params[:single_page], assay_stream_id: item.id)) %>
<% else %>
<%= button_link_to("Design the next #{t('assay')}", 'new', new_isa_assay_path(source_assay_id: item.id, study_id: item.study.id, single_page: params[:single_page], assay_stream_id: item.assay_stream_id)) %>
<% end %>
<% end %>
<% else %>
<%= add_new_item_to_dropdown(item) %>
Expand All @@ -38,16 +50,20 @@
<%= item_actions_dropdown do %>
<% if item.can_edit? %>
<% if Seek::Config.isa_json_compliance_enabled && item.is_isa_json_compliant? %>
<li><%= image_tag_for_key('edit', edit_isa_assay_path(item, source_assay_id: item.id, study_id: item.study.id, single_page: params[:single_page]), "Edit #{t('isa_assay')}", nil, "Edit #{t('isa_assay')}") -%></li>
<% if item&.is_assay_stream? %>
<li><%= image_tag_for_key('edit', edit_isa_assay_path(item, source_assay_id: item.id, study_id: item.study.id, single_page: params[:single_page], is_assay_stream: true), "Edit #{assay_word}", nil, "Edit #{assay_word}") -%></li>
<% else %>
<li><%= image_tag_for_key('edit', edit_isa_assay_path(item, source_assay_id: item.id, study_id: item.study.id, single_page: params[:single_page], assay_stream_id: item.assay_stream_id), "Edit #{assay_word}", nil, "Edit #{assay_word}") -%></li>
<% end %>
<% else %>
<li><%= image_tag_for_key('edit', edit_assay_path(item), "Edit #{assay_word}", nil, "Edit #{assay_word}") -%></li>
<% end %>
<% end %>
<% if item.can_manage? -%>
<li><%= image_tag_for_key('manage', manage_assay_path(item), "Manage #{assay_word}", nil, "Manage #{assay_word}") -%></li>
<%= render :partial => 'snapshots/new_snapshot_link', :locals => {:item => item} %>
<%= render partial: 'snapshots/new_snapshot_link', locals: {item: item} %>
<% end -%>
<%= delete_icon(item, current_user) %>
<%= delete_icon(item, current_user, 'Are you sure?', assay_word) %>
<% end %>
Loading

0 comments on commit 23c0daa

Please sign in to comment.