diff --git a/app/controllers/assays_controller.rb b/app/controllers/assays_controller.rb index 0c88fabba4..007e3f7da9 100644 --- a/app/controllers/assays_controller.rb +++ b/app/controllers/assays_controller.rb @@ -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? @@ -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 @@ -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 diff --git a/app/controllers/isa_assays_controller.rb b/app/controllers/isa_assays_controller.rb index df0643ba43..cc0c8128ed 100644 --- a/app/controllers/isa_assays_controller.rb +++ b/app/controllers/isa_assays_controller.rb @@ -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!') @@ -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 @@ -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 @@ -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] = [] @@ -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? diff --git a/app/forms/isa_assay.rb b/app/forms/isa_assay.rb index d5817e0051..a586c75851 100644 --- a/app/forms/isa_assay.rb +++ b/app/forms/isa_assay.rb @@ -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 @@ -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?) @@ -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' and no ISA tag".html_safe) + "[Sample type]: Should have exactly one attribute with the title 'Input' and 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 'Input' attribute (hidden)".html_safe) + "[Sample type]: All attributes should have an ISA Tag except for the 'Input' 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' or 'other_material' ISA tag selected".html_safe) + "[Sample type]: Should have exactly one attribute with the 'data_file' or '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 diff --git a/app/helpers/images_helper.rb b/app/helpers/images_helper.rb index a126c4a054..bc23201ea8 100644 --- a/app/helpers/images_helper.rb +++ b/app/helpers/images_helper.rb @@ -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) diff --git a/app/models/assay.rb b/app/models/assay.rb index e2fe2430fa..ab9e5b95e6 100644 --- a/app/models/assay.rb +++ b/app/models/assay.rb @@ -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 @@ -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 @@ -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 diff --git a/app/models/assay_class.rb b/app/models/assay_class.rb index a7a72dc0fa..5e8940f9ee 100644 --- a/app/models/assay_class.rb +++ b/app/models/assay_class.rb @@ -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 diff --git a/app/models/study.rb b/app/models/study.rb index f7b6371c73..467a550083 100644 --- a/app/models/study.rb +++ b/app/models/study.rb @@ -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 diff --git a/app/views/assays/_buttons.html.erb b/app/views/assays/_buttons.html.erb index ac5b58e309..72c59241fe 100644 --- a/app/views/assays/_buttons.html.erb +++ b/app/views/assays/_buttons.html.erb @@ -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 %> @@ -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) %> @@ -38,7 +50,11 @@ <%= item_actions_dropdown do %> <% if item.can_edit? %> <% if Seek::Config.isa_json_compliance_enabled && item.is_isa_json_compliant? %> -
  • <%= 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')}") -%>
  • + <% if item&.is_assay_stream? %> +
  • <%= 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}") -%>
  • + <% else %> +
  • <%= 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}") -%>
  • + <% end %> <% else %>
  • <%= image_tag_for_key('edit', edit_assay_path(item), "Edit #{assay_word}", nil, "Edit #{assay_word}") -%>
  • <% end %> @@ -46,8 +62,8 @@ <% if item.can_manage? -%>
  • <%= image_tag_for_key('manage', manage_assay_path(item), "Manage #{assay_word}", nil, "Manage #{assay_word}") -%>
  • - <%= 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 %> diff --git a/app/views/assays/show.html.erb b/app/views/assays/show.html.erb index 6f2c697b1b..9f85fc1b39 100644 --- a/app/views/assays/show.html.erb +++ b/app/views/assays/show.html.erb @@ -47,16 +47,18 @@ <%= "Assay position" %>: <%= @assay.position %>

    -

    - <%= assay_type_text -%>: - <%= link_to_assay_type(@assay) -%> -

    - <% unless @assay.is_modelling? -%> -

    - Technology type: - <%= link_to_technology_type(@assay) -%> + <% unless @assay.is_isa_json_compliant? %> +

    + <%= assay_type_text -%>: + <%= link_to_assay_type(@assay) -%>

    - <% end -%> + <% unless @assay.is_modelling? -%> +

    + Technology type: + <%= link_to_technology_type(@assay) -%> +

    + <% end -%> + <% end %> <% if Seek::Config.organisms_enabled %> <%= list_assay_organisms("Organisms", @assay.assay_organisms, { :id => "organism" }) %> @@ -68,12 +70,34 @@ <% if Seek::Config.isa_json_compliance_enabled %>

    - <%= "Is ISA-JSON compliant" %>: + Is ISA-JSON compliant: <%= @assay.is_isa_json_compliant? %>

    <%= render partial: 'isa_studies/applied_templates', locals: { resource: @assay } -%> <% end %> + <% if @assay.is_isa_json_compliant? %> + <% if @assay.is_assay_stream? %> +

    + Child assays: +

    +

    + <% else %> +

    + <%= t('assays.assay_stream') %>: + <%= link_to @assay.assay_stream.title, @assay.assay_stream %> +

    + <% end %> + <% end %> + diff --git a/app/views/isa_assays/_form.html.erb b/app/views/isa_assays/_form.html.erb index 35d5b38d2c..cc707ccf20 100644 --- a/app/views/isa_assays/_form.html.erb +++ b/app/views/isa_assays/_form.html.erb @@ -1,35 +1,40 @@ <% assay = params[:isa_assay][:assay] if params.dig(:isa_assay, :assay) %> <% study = Study.find(params[:study_id] || assay[:study_id]) %> <% -input_sample_type_id = params[:isa_assay][:input_sample_type_id] if params.dig(:isa_assay, :input_sample_type_id) -source_assay = Assay.find(params[:source_assay_id]) if params[:source_assay_id] - -if @isa_assay.assay.new_record? - if params[:source_assay_id] - assay_position = source_assay.position + 1 - else - assay_position = 0 - end -else - assay_position = @isa_assay.assay.position -end - -# assay_position = params[:source_assay_id] ? source_assay.position + 1 : 0 -input_sample_type_id ||= - if params[:is_source] - study.sample_types.second.id + source_assay = Assay.find(params[:source_assay_id]) if params[:source_assay_id] + assay_stream_id = params[:assay_stream_id] if params[:assay_stream_id] + + if @isa_assay.assay.new_record? + if params[:is_assay_stream] + assay_position = 0 + assay_class_id = AssayClass.assay_stream.id + is_assay_stream = true + else + assay_position = params[:source_assay_id].nil? ? 1 : source_assay.position + 1 + assay_class_id = AssayClass.experimental.id + is_assay_stream = false + end else - source_assay.sample_type.id if params[:source_assay_id] + assay_position = @isa_assay.assay.position + assay_class_id = @isa_assay.assay.assay_class_id + is_assay_stream = @isa_assay.assay.is_assay_stream? end -show_extended_metadata = - if params[:is_source] - true - elsif source_assay&.position&.zero? && !@isa_assay.assay.new_record? - true # Custom metadata should be shown in edit as well if assay position is 0. - else - false - end + input_sample_type_id ||= + if is_assay_stream || source_assay&.assay_class&.is_assay_stream? + study.sample_types.second.id + else + source_assay.sample_type.id if source_assay + end + + show_extended_metadata = + if is_assay_stream + true + elsif source_assay&.position&.zero? && !@isa_assay.assay.new_record? + true # Custom metadata should be shown in edit as well if assay position is 0. + else + false + end %> <%= error_messages_for :isa_assay %> @@ -60,22 +65,25 @@ show_extended_metadata = <%= assay_fields.number_field :position, value: assay_position || study.assays.length -%> - <%= assay_fields.hidden_field :assay_class_id -%> + <%= assay_fields.hidden_field :assay_stream_id, value: assay_stream_id -%> + <%= assay_fields.hidden_field :assay_class_id, value: assay_class_id -%> - <% if User.current_user -%> - <%= render partial: 'assets/manage_specific_attributes', locals:{f:assay_fields} if show_form_manage_specific_attributes? %> - <%= assay_fields.fancy_multiselect(:sops, other_projects_checkbox: true, name: "isa_assay[assay][sop_ids]")%> - <%= assay_fields.fancy_multiselect :publications, { other_projects_checkbox: true, name: "isa_assay[assay][publication_ids]" } %> - <%= assay_fields.fancy_multiselect :documents, { other_projects_checkbox: true, name: "isa_assay[assay][document_ids]" } %> - <% end -%> + <% unless is_assay_stream %> + <% if User.current_user -%> + <%= render partial: 'assets/manage_specific_attributes', locals:{f:assay_fields} if show_form_manage_specific_attributes? %> + <%= assay_fields.fancy_multiselect(:sops, other_projects_checkbox: true, name: "isa_assay[assay][sop_ids]")%> + <%= assay_fields.fancy_multiselect :publications, { other_projects_checkbox: true, name: "isa_assay[assay][publication_ids]" } %> + <%= assay_fields.fancy_multiselect :documents, { other_projects_checkbox: true, name: "isa_assay[assay][document_ids]" } %> + <% end -%> - <%= render :partial=> "assets/discussion_links_form", :locals=>{:resource => @isa_assay.assay} -%> + <%= render partial: "assets/discussion_links_form", locals: { resource: @isa_assay.assay } -%> + <% end -%> <% end -%> <%= f.hidden_field :input_sample_type_id, value: input_sample_type_id -%> -<% if @isa_assay.sample_type %> +<% unless is_assay_stream %> <%= folding_panel("Define #{t(:sample_type)} for #{t(:assay)}") do %> <%= render partial: 'isa_studies/sample_types_form', locals: {f: f, sample_type: @isa_assay.sample_type, id_suffix: "_sample_type", isa_element: "assay", action: action} %> <% end %> diff --git a/app/views/isa_assays/edit.erb b/app/views/isa_assays/edit.erb index 6afac60429..476b164dcd 100644 --- a/app/views/isa_assays/edit.erb +++ b/app/views/isa_assays/edit.erb @@ -1,5 +1,8 @@ -

    Edit <%= t('isa_assay') %>

    - +<% if @isa_assay.assay.is_assay_stream? %> +

    Edit <%= t('assays.assay_stream') %>

    +<% else %> +

    Edit <%= t('isa_assay') %>

    +<% end %> <%= render partial: "templates/template_modal" -%>
    <%= form_for @isa_assay, url: { controller: "isa_assays", action: "update", id: params[:id] }, method: :put do |f| %> diff --git a/app/views/isa_assays/new.html.erb b/app/views/isa_assays/new.html.erb index d2a46bab7b..07da9cf1e0 100644 --- a/app/views/isa_assays/new.html.erb +++ b/app/views/isa_assays/new.html.erb @@ -1,5 +1,12 @@
    -

    New <%=t("isa_assay")%>

    + + <%= sample_controlled_vocab_model_dialog('cv-modal') %> + + <% if params[:is_assay_stream] %> +

    New <%=t("assays.assay_stream")%>

    + <% else %> +

    New <%=t("isa_assay")%>

    + <% end %>
    <%= render partial: "templates/template_modal" -%> diff --git a/app/views/isa_studies/_sample_types_form.html.erb b/app/views/isa_studies/_sample_types_form.html.erb index 1e78428601..b2c7605af6 100644 --- a/app/views/isa_studies/_sample_types_form.html.erb +++ b/app/views/isa_studies/_sample_types_form.html.erb @@ -5,9 +5,6 @@ <% main_field_name = id_suffix[1..-1] %> <% isa_element ||= "study" %> - - <%= sample_controlled_vocab_model_dialog('cv-modal') %> - <%= f.fields_for main_field_name, sample_type do |field| %> <% unless action == :edit %> diff --git a/app/views/isa_studies/new.html.erb b/app/views/isa_studies/new.html.erb index 38f08dc616..021e3e17f4 100644 --- a/app/views/isa_studies/new.html.erb +++ b/app/views/isa_studies/new.html.erb @@ -1,6 +1,10 @@ <% if Investigation.authorized_for('view').none? %> <%= button_link_to("New #{t('investigation')}", 'arrow_right', new_investigation_path) -%> <% else %> + + <%= sample_controlled_vocab_model_dialog('cv-modal') %> + +

    New <%=t('isa_study')%>

    <%= render :partial => "templates/template_modal" -%> diff --git a/app/views/studies/_buttons.html.erb b/app/views/studies/_buttons.html.erb index b27edc214f..a308561224 100644 --- a/app/views/studies/_buttons.html.erb +++ b/app/views/studies/_buttons.html.erb @@ -21,7 +21,7 @@ <% if item.can_edit? -%> <% if Seek::Config.isa_json_compliance_enabled && item.is_isa_json_compliant? %> <% if item&.sample_types.present? %> - <%= button_link_to("Design #{t('assay')}", 'new', new_isa_assay_path(study_id: item.id, single_page: params[:single_page], is_source: true)) %> + <%= button_link_to("Design #{t('assays.assay_stream')}", 'new', new_isa_assay_path(study_id: item.id, single_page: params[:single_page], is_assay_stream: true)) %> <% end -%> <% else -%> <%= add_new_item_to_dropdown(item) %> @@ -38,7 +38,12 @@ <% end %> <% if item.can_manage? -%> -
  • <%= image_tag_for_key('manage', manage_study_path(item), "Manage #{t('study')}", nil, "Manage #{t('study')}") -%>
  • + <% if Seek::Config.isa_json_compliance_enabled && item.is_isa_json_compliant? %> +
  • <%= image_tag_for_key('manage', manage_study_path(item), "Manage #{t('study')}", nil, "Manage #{t('isa_study')}") -%>
  • + <% else %> +
  • <%= image_tag_for_key('manage', manage_study_path(item), "Manage #{t('study')}", nil, "Manage #{t('study')}") -%>
  • + <% end %> + <%= render :partial => 'snapshots/new_snapshot_link', :locals => { :item => item } %> <% end -%> diff --git a/config/default_data/assay_classes.yml b/config/default_data/assay_classes.yml index 94a08a2a99..8b01bc6ad3 100644 --- a/config/default_data/assay_classes.yml +++ b/config/default_data/assay_classes.yml @@ -6,4 +6,9 @@ experimental_assay_class: modelling_assay_class: id: 2 title: <%= I18n.t('assays.modelling_analysis') %> - key: MODEL \ No newline at end of file + key: MODEL + +assay_stream_class: + id: 3 + title: <%= I18n.t('assays.assay_stream') %> + key: STREAM diff --git a/config/locales/en.yml b/config/locales/en.yml index ced43e1a21..8880ff79e8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,6 +16,7 @@ en: assay: "Assay" experimental_assay: "Experimental assay" modelling_analysis: "Modelling analysis" + assay_stream: "Assay Stream" isa_study: "ISA Study" study_design: "Study Design" diff --git a/config/routes.rb b/config/routes.rb index e632e6f4f0..49bdcba035 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -744,7 +744,7 @@ post :template_attributes end collection do -post :filter_isa_tags_by_level + post :filter_isa_tags_by_level get :task_status get :default_templates post :populate_template diff --git a/db/migrate/20240112141513_add_assay_stream_id_to_assay.rb b/db/migrate/20240112141513_add_assay_stream_id_to_assay.rb new file mode 100644 index 0000000000..6220df0185 --- /dev/null +++ b/db/migrate/20240112141513_add_assay_stream_id_to_assay.rb @@ -0,0 +1,6 @@ +class AddAssayStreamIdToAssay < ActiveRecord::Migration[6.1] + def change + add_column :assays, :assay_stream_id, :integer + add_index :assays, :assay_stream_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 22e12632eb..2f51e08778 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_12_18_133053) do +ActiveRecord::Schema.define(version: 2024_01_12_141513) do create_table "activity_logs", id: :integer, force: :cascade do |t| t.string "action" @@ -189,6 +189,8 @@ t.string "deleted_contributor" t.integer "sample_type_id" t.integer "position" + t.integer "assay_stream_id" + t.index ["assay_stream_id"], name: "index_assays_on_assay_stream_id" t.index ["sample_type_id"], name: "index_assays_on_sample_type_id" end diff --git a/lib/isa_exporter.rb b/lib/isa_exporter.rb index 108144752a..5d5a3ff288 100644 --- a/lib/isa_exporter.rb +++ b/lib/isa_exporter.rb @@ -117,7 +117,7 @@ def convert_study(study) # raise "The Study with the title '#{study.title}' does not have any SOP" if study.sops.blank? protocols << convert_protocol(study.sops, study.id, with_tag_protocol_study, with_tag_parameter_value_study) - study.assays.each do |a| + study.assay_streams.map(&:child_assays).flatten.each do |a| # There should be only one attribute with isa_tag == protocol protocol_attribute = a.sample_type.sample_attributes.detect { |sa| sa.isa_tag&.isa_protocol? } with_tag_parameter_value = a.sample_type.sample_attributes.select { |sa| sa.isa_tag&.isa_parameter_value? } @@ -134,20 +134,7 @@ def convert_study(study) raise "All assays in study `#{study.title}` should be ISA-JSON compliant." end - assay_streams = study.assays.map { |assay| [assay] if assay&.position&.zero? } - .compact - .map do |assay_stream| - last_assay = assay_stream.first - until last_assay.linked_assay.nil? - linked_assay = last_assay.linked_assay - assay_stream.push(linked_assay) - last_assay = linked_assay - end - assay_stream - end - - isa_study[:assays] = assay_streams.map { |assay_stream| convert_assays(assay_stream) }.compact - + isa_study[:assays] = study.assay_streams.map { |assay_stream| convert_assays(assay_stream) }.compact isa_study[:factors] = [] isa_study[:unitCategories] = [] @@ -163,28 +150,25 @@ def convert_annotation(term_uri) isa_annotation end - def convert_assay_comments(assays) + def convert_assay_comments(assay_stream) assay_comments = [] - assay_streams = assays.select { |a| a.position.zero? } - assay_stream_id = assays.pluck(:id).join('_') - - linked_assays = assays.map { |assay| { 'id': assay.id, 'title': assay.title } }.to_json - - assay_streams.map do |assay| - study_id = assay.study_id - next if assay.extended_metadata.nil? - - json = JSON.parse(assay.extended_metadata&.json_metadata) - cm_attributes = assay.extended_metadata.extended_metadata_attributes - cm_id = assay.extended_metadata&.id - json.map do |key, val| - cma_id = cm_attributes.detect { |cma| cma.title == key }&.id - assay_comments.push({ - '@id': "#assay_comment/#{[study_id, assay_stream_id, cm_id, cma_id].join('_')}", - 'name': key, - 'value': val - }) - end + assay_stream_id = assay_stream&.id + + linked_assays = assay_stream.child_assays.map { |assay| { 'id': assay.id, 'title': assay.title } }.to_json + + study_id = assay_stream.study_id + return [] if assay_stream.extended_metadata.nil? + + json = JSON.parse(assay_stream.extended_metadata&.json_metadata) + cm_attributes = assay_stream.extended_metadata.extended_metadata_attributes + cm_id = assay_stream.extended_metadata&.id + json.map do |key, val| + cma_id = cm_attributes.detect { |cma| cma.title == key }&.id + assay_comments.push({ + '@id': "#assay_comment/#{[study_id, assay_stream_id, cm_id, cma_id].join('_')}", + 'name': key, + 'value': val + }) end assay_comments.push({ @@ -195,35 +179,43 @@ def convert_assay_comments(assays) assay_comments.compact end - def convert_assays(assays) - return unless assays.all? { |a| a.can_view?(@current_user) } + def convert_assays(assay_stream) + child_assays = assay_stream.child_assays + return unless assay_stream.can_view?(@current_user) + return unless child_assays.all? { |a| a.can_view?(@current_user) } - all_sample_types = assays.map(&:sample_type) - first_assay = assays.detect { |s| s.position.zero? } - raise 'No assay could be found!' unless first_assay + child_assays.map do |ca| + unless ca.sample_type.present? + raise "No Sample type was found in Assay '#{ca.id} - #{ca.title}'," \ + " part of Assay Stream '#{assay_stream.id - assay_stream.title}'" + end + end - stream_name = "assays_#{assays.pluck(:id).join('_')}" - assay_comments = convert_assay_comments(assays) + all_sample_types = child_assays.map(&:sample_type).compact + + # stream_name = "assays_#{child_assays.pluck(:id).join('_')}" + stream_name = "#{ assay_stream.title }_#{assay_stream.id}_#{child_assays.pluck(:id).join('_')}" + assay_comments = convert_assay_comments(assay_stream) # Retrieve assay_stream if - stream_name_comment = assay_comments.detect { |ac| ac[:name] == 'assay_stream' } - stream_name = stream_name_comment[:value] unless stream_name_comment.nil? + # stream_name_comment = assay_comments.detect { |ac| ac[:name] == 'assay_stream' } + # stream_name = stream_name_comment[:value] unless stream_name_comment.nil? isa_assay = {} - isa_assay['@id'] = "#assay/#{assays.pluck(:id).join('_')}" + isa_assay['@id'] = "#assay/#{child_assays.pluck(:id).join('_')}" isa_assay[:filename] = "a_#{stream_name.downcase.tr(" ", "_")}.txt" isa_assay[:measurementType] = { annotationValue: '', termSource: '', termAccession: '' } isa_assay[:technologyType] = { annotationValue: '', termSource: '', termAccession: '' } isa_assay[:comments] = assay_comments isa_assay[:technologyPlatform] = '' - isa_assay[:characteristicCategories] = convert_characteristic_categories(nil, assays) + isa_assay[:characteristicCategories] = convert_characteristic_categories(nil, child_assays) isa_assay[:materials] = { # Here, the first assay's samples will be enough - samples: assay_samples(first_assay), # the samples from study level that are referenced in this assay's samples, + samples: assay_samples(child_assays.first), # the samples from study level that are referenced in this assay's samples, otherMaterials: convert_other_materials(all_sample_types) } isa_assay[:processSequence] = - assays.map { |a| convert_process_sequence(a.sample_type, a.sops.map(&:id).join("_"), a.id) }.flatten + child_assays.map { |a| convert_process_sequence(a.sample_type, a.sops.map(&:id).join("_"), a.id) }.flatten isa_assay[:dataFiles] = convert_data_files(all_sample_types) isa_assay[:unitCategories] = [] isa_assay @@ -278,7 +270,14 @@ def convert_publication(publication) def convert_ontologies source_ontologies = [] - sample_types = @investigation.studies.map(&:sample_types) + @investigation.assays.map(&:sample_type) + sample_types = @investigation.studies.map(&:sample_types) + @investigation.assays + .select(&:is_assay_stream?) + .map(&:child_assays) + .compact + .flatten + .map(&:sample_type) + .compact + sample_types.flatten.each do |sa| sa.sample_attributes.each do |atr| source_ontologies << atr.sample_controlled_vocab.source_ontology if atr.ontology_based? @@ -752,7 +751,7 @@ def random_string(len) end def get_derived_from_type(sample_type) - raise 'There is no sample!' if sample_type.samples.length == 0 + raise "There are no samples in '#{sample_type.title}'!" if sample_type.samples.blank? prev_sample_type = sample_type.samples[0]&.linked_samples[0]&.sample_type return nil if prev_sample_type.blank? diff --git a/lib/seek/ontologies/synchronize.rb b/lib/seek/ontologies/synchronize.rb index e15f0119e8..cfce103be3 100644 --- a/lib/seek/ontologies/synchronize.rb +++ b/lib/seek/ontologies/synchronize.rb @@ -99,7 +99,7 @@ def assay_changes_class?(assays, ontology_uri) def determine_assay_class_from_uri(uri) ontology_class = Seek::Ontologies::AssayTypeReader.instance.class_for_uri(uri) - ontology_class.nil? ? AssayClass.for_type('modelling') : AssayClass.for_type('experimental') + ontology_class.nil? ? AssayClass.modelling : AssayClass.experimental end def get_suggested_types_found_in_ontology(type) diff --git a/lib/seek/openbis/seek_util.rb b/lib/seek/openbis/seek_util.rb index 6476676d54..a99b15949e 100644 --- a/lib/seek/openbis/seek_util.rb +++ b/lib/seek/openbis/seek_util.rb @@ -27,7 +27,7 @@ def createObisAssay(assay_params, creator, obis_asset) zample = obis_asset.content openbis_endpoint = obis_asset.seek_service - assay_params[:assay_class_id] ||= AssayClass.for_type('experimental').id + assay_params[:assay_class_id] ||= AssayClass.experimental.id assay_params[:title] ||= extract_title(zample) ## "OpenBIS #{zample.perm_id}" assay = Assay.new(assay_params) @@ -88,7 +88,7 @@ def fake_file_assay(study) assay = study.assays.where(title: FAKE_FILE_ASSAY_NAME).first return assay if assay - assay_params = {assay_class_id: AssayClass.for_type('experimental').id, + assay_params = {assay_class_id: AssayClass.experimental.id, title: FAKE_FILE_ASSAY_NAME, description: 'Automatically generated assay to host openbis files that are linked to the original OpenBIS experiment. Its content and linked data files will be updated by the system @@ -446,7 +446,7 @@ def study_types(openbis_endpoint) Seek::Openbis::EntityType.ExperimentType(openbis_endpoint).find_by_codes(study_codes) end - + end end end diff --git a/lib/seek/projects/population.rb b/lib/seek/projects/population.rb index 2ab008c1e4..faeb03241b 100644 --- a/lib/seek/projects/population.rb +++ b/lib/seek/projects/population.rb @@ -21,7 +21,7 @@ def populate_from_spreadsheet_impl r = sheet.rows[1] if r.cell(1).value.blank? - flash[:error]= "Unable to find header cells in #{datafile.title}" + flash[:error]= "Unable to find header cells in #{datafile.title}" return end @@ -78,7 +78,7 @@ def populate_from_spreadsheet_impl assay_position = 1 investigation.save! end - + if r.cell(study_index)&.value.present? if investigation.blank? flash[:error]= "Study specified without Investigation in #{datafile.title} at row #{r.index}" @@ -111,7 +111,7 @@ def populate_from_spreadsheet_impl set_description(assay, r, description_index) assay.position = assay_position assay_position += 1 - assay.assay_class = AssayClass.for_type('experimental') + assay.assay_class = AssayClass.experimental set_assignees(assay, r, assignee_indices) @@ -134,7 +134,7 @@ def set_description(object, r, description_index) end object.description = description end - + def set_assignees(assay, r, assignee_indices) assignees = [] assignee_indices.each do |x| @@ -154,7 +154,7 @@ def set_assignees(assay, r, assignee_indices) end assay.creators = known_creators assay.other_creators = other_creators.join(';') - end + end def set_protocol(assay, r, protocol_index) protocol_string = r.cell(protocol_index)&.value&.to_s.strip diff --git a/lib/tasks/seek_upgrades.rake b/lib/tasks/seek_upgrades.rake index fb779e184a..e9b1ae3c15 100644 --- a/lib/tasks/seek_upgrades.rake +++ b/lib/tasks/seek_upgrades.rake @@ -15,7 +15,9 @@ namespace :seek do rename_registered_sample_multiple_attribute_type remove_ontology_attribute_type db:seed:007_sample_attribute_types + db:seed:001_create_controlled_vocabs recognise_isa_json_compliant_items + implement_assay_streams_for_isa_assays ] # these are the tasks that are executes for each upgrade as standard, and rarely change @@ -176,6 +178,48 @@ namespace :seek do puts "...Updated #{investigations_updated.to_s} investigations" end + task(implement_assay_streams_for_isa_assays: [:environment]) do + puts '... Organising isa json compliant assays in assay streams' + assay_streams_created = 0 + disable_authorization_checks do + # find assays linked to a study through their sample_types + # Should be isa json compliant + # Shouldn't already have an assay stream (don't update assays that have been updated already) + # Previous ST should be second ST of study + first_assays_in_stream = Assay.joins(:sample_type, study: :investigation) + .where(assay_stream_id: nil, investigation: { is_isa_json_compliant: true }) + .select { |a| a.previous_linked_sample_type == a.study.sample_types.second } + + first_assays_in_stream.map do |fas| + stream_name = "Assay Stream - #{UUID.generate}" + assay_stream = Assay.create(title: stream_name, + study_id: fas.study_id, + assay_class_id: AssayClass.assay_stream.id, + contributor: fas.contributor, + position: 0) + + # Transfer extended metadata from first assay to newly created assay stream + unless fas.extended_metadata.nil? + em = ExtendedMetadata.find_by(item_id: fas.id) + em.update_column(:item_id, assay_stream.id) + end + + assay_position = 1 + current_assay = fas + while current_assay + current_assay.update_column(:position, assay_position) + current_assay.update_column(:assay_stream_id, assay_stream.id) + + assay_position += 1 + current_assay = current_assay.next_linked_child_assay + end + assay_streams_created += 1 + end + end + + puts "...Created #{assay_streams_created} new assay streams" + end + private ## diff --git a/lib/treeview_builder.rb b/lib/treeview_builder.rb index d64e6f4217..6f89ddf2d3 100644 --- a/lib/treeview_builder.rb +++ b/lib/treeview_builder.rb @@ -14,9 +14,22 @@ def build_tree_data investigation_items = [] @project.investigations.map do |investigation| - investigation.studies.map do |study| - assay_items = study.assays.map { |assay| build_assay_item(assay) } - study_items << build_study_item(study, assay_items) + if investigation.is_isa_json_compliant? + investigation.studies.map do |study| + assay_stream_items = study.assay_streams.map do |assay_stream| + assay_items = assay_stream.child_assays.map do |child_assay| + build_assay_item(child_assay) + end + build_assay_stream_item(assay_stream, assay_items) + end + + study_items << build_study_item(study, assay_stream_items) + end + else + investigation.studies.map do |study| + assay_items = study.assays.map { |assay| build_assay_item(assay) } + study_items << build_study_item(study, assay_items) + end end investigation_items << build_investigation_item(investigation, study_items) study_items = [] @@ -125,6 +138,11 @@ def build_study_item(study, assay_items) children: isa_study_elements(study) + assay_items, resource: study }) end + def build_assay_stream_item(assay_stream, child_assays) + create_node({ text: assay_stream.title, _type: 'assay', label: 'Assay Stream', _id: assay_stream.id, a_attr: BOLD, + children: isa_assay_elements(assay_stream) + child_assays, resource: assay_stream }) + end + def build_assay_item(assay) create_node({ text: assay.title, _type: 'assay', label: 'Assay', _id: assay.id, a_attr: BOLD, children: isa_assay_elements(assay), resource: assay }) diff --git a/test/factories/assays.rb b/test/factories/assays.rb index c0ab8efcf5..24f95f1ae0 100644 --- a/test/factories/assays.rb +++ b/test/factories/assays.rb @@ -13,6 +13,12 @@ description { "An experimental assay class description" } end + factory(:assay_stream_class, class: AssayClass) do + title { I18n.t('assays.assay_stream') } + key { 'STREAM' } + description { "An assay stream class description" } + end + # SuggestedTechnologyType factory(:suggested_technology_type) do sequence(:label) { | n | "A TechnologyType#{n}" } @@ -99,11 +105,20 @@ title { 'ISA JSON compliant assay' } description { 'An assay linked to an ISA JSON compliant study and a sample type' } after(:build) do |assay| - assay.study = FactoryBot.create(:isa_json_compliant_study) + assay.study ||= FactoryBot.create(:isa_json_compliant_study) assay.sample_type = FactoryBot.create(:isa_assay_material_sample_type, linked_sample_type: assay.study.sample_types.last) end end + factory(:assay_stream, parent: :assay_base) do + title { 'Assay Stream' } + description { 'A holder assay holding multiple child assays' } + association :assay_class, factory: :assay_stream_class + after(:build) do |assay| + assay.study ||= FactoryBot.create(:isa_json_compliant_study, contributor: assay.contributor) + end + end + # AssayAsset factory :assay_asset do association :assay diff --git a/test/fixtures/assay_classes.yml b/test/fixtures/assay_classes.yml index 6811c4bdb0..cf87a8f7d1 100644 --- a/test/fixtures/assay_classes.yml +++ b/test/fixtures/assay_classes.yml @@ -1,7 +1,11 @@ experimental_assay_class: title: <%= I18n.t('assays.experimental_assay') %> key: EXP - -modelling_assay_class: + +modelling_assay_class: title: <%= I18n.t('assays.modelling_analysis') %> - key: MODEL \ No newline at end of file + key: MODEL + +assay_stream_class: + title: <%= I18n.t('assays.assay_stream') %> + key: STREAM diff --git a/test/functional/assays_controller_test.rb b/test/functional/assays_controller_test.rb index 77253f7191..1996d36918 100644 --- a/test/functional/assays_controller_test.rb +++ b/test/functional/assays_controller_test.rb @@ -587,14 +587,14 @@ def test_title test 'get new with class doesnt present options for class' do login_as(:model_owner) - get :new, params: { class: 'experimental' } + get :new, params: { class: 'EXP' } assert_response :success assert_select 'a[href=?]', new_assay_path(class: :experimental), count: 0 assert_select 'a', text: /An #{I18n.t('assays.experimental_assay')}/i, count: 0 assert_select 'a[href=?]', new_assay_path(class: :modelling), count: 0 assert_select 'a', text: /A #{I18n.t('assays.modelling_analysis')}/i, count: 0 - get :new, params: { class: 'modelling' } + get :new, params: { class: 'MODEL' } assert_response :success assert_select 'a[href=?]', new_assay_path(class: :experimental), count: 0 assert_select 'a', text: /An #{I18n.t('assays.experimental_assay')}/i, count: 0 @@ -1065,7 +1065,7 @@ def check_fixtures_for_authorization_of_sops_and_datafiles_links end test 'new should include tags element' do - get :new, params: { class: :experimental } + get :new, params: { class: 'EXP' } assert_response :success assert_select 'div.panel-heading', text: /Tags/, count: 1 assert_select 'select#tag_list', count: 1 @@ -1121,7 +1121,7 @@ def check_fixtures_for_authorization_of_sops_and_datafiles_links end test 'should show experimental assay types for new experimental assay' do - get :new, params: { class: :experimental } + get :new, params: { class: 'EXP' } assert_response :success assert_select 'label', text: /assay type/i assert_select 'select#assay_assay_type_uri' do @@ -1131,7 +1131,7 @@ def check_fixtures_for_authorization_of_sops_and_datafiles_links end test 'should show modelling assay types for new modelling assay' do - get :new, params: { class: :modelling } + get :new, params: { class: 'MODEL' } assert_response :success assert_select 'label', text: /Biological problem addressed/i assert_select 'select#assay_assay_type_uri' do @@ -1947,7 +1947,7 @@ def check_fixtures_for_authorization_of_sops_and_datafiles_links # person = User.current_user.person person = FactoryBot.create(:person) project = person.projects.first - investigation = FactoryBot.create(:investigation, projects: [project]) + investigation = FactoryBot.create(:investigation, projects: [project], is_isa_json_compliant: true) source_st = FactoryBot.create(:isa_source_sample_type, contributor: person, projects: [project]) sample_collection_st = FactoryBot.create(:isa_sample_collection_sample_type, contributor: person, projects: [project], @@ -1984,7 +1984,7 @@ def check_fixtures_for_authorization_of_sops_and_datafiles_links assay3.reload - assert_equal(assay3.previous_linked_assay_sample_type&.id, assay1.sample_type&.id) + assert_equal(assay3.previous_linked_sample_type&.id, assay1.sample_type&.id) end test 'do not get index if feature disabled' do @@ -2012,9 +2012,24 @@ def check_fixtures_for_authorization_of_sops_and_datafiles_links with_config_value(:isa_json_compliance_enabled, true) do current_user = FactoryBot.create(:user) login_as(current_user) - assay = FactoryBot.create(:isa_json_compliant_assay, contributor: current_user.person) + study = FactoryBot.create(:isa_json_compliant_study) + assay_stream = FactoryBot.create(:assay_stream, study: , contributor: current_user.person) + assay1 = FactoryBot.create(:isa_json_compliant_assay, contributor: current_user.person, study: , assay_stream:) + assay2 = FactoryBot.create(:isa_json_compliant_assay, contributor: current_user.person, study: , assay_stream:) - get :show, params: { id: assay } + assert_equal assay_stream.study, assay1.study + + get :show, params: { id: assay_stream } + assert_response :success + + assert_select 'a', text: /Design #{I18n.t('assay')}/i, count: 1 + + get :show, params: { id: assay1 } + assert_response :success + + assert_select 'a', text: /Design the next #{I18n.t('assay')}/i, count: 1 + + get :show, params: { id: assay2 } assert_response :success assert_select 'a', text: /Design the next #{I18n.t('assay')}/i, count: 1 diff --git a/test/functional/investigations_controller_test.rb b/test/functional/investigations_controller_test.rb index a564eb29a4..b38571a183 100644 --- a/test/functional/investigations_controller_test.rb +++ b/test/functional/investigations_controller_test.rb @@ -911,16 +911,22 @@ def test_title contributor: other_user.person) # Create a 'private' assay in an assay stream - assay_1_stream_1_sample_type = FactoryBot.create(:isa_assay_material_sample_type, linked_sample_type: sample_collection_sample_type, template_id: FactoryBot.create(:isa_assay_material_template).id) - assay_1_stream_1 = FactoryBot.create(:assay, position: 0, sample_type: assay_1_stream_1_sample_type, study: accessible_study, contributor: current_user.person) - assay_2_stream_1_sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, linked_sample_type: assay_1_stream_1_sample_type, template_id: FactoryBot.create(:isa_assay_data_file_template).id) - assay_2_stream_1 = FactoryBot.create(:assay, position:1, sample_type: assay_2_stream_1_sample_type, study: accessible_study, contributor: other_user.person) + stream_1 = FactoryBot.create(:assay_stream, title: 'Assay Stream 1', study: accessible_study, contributor: other_user.person) + assert_equal(stream_1.study, accessible_study) + assert(stream_1.is_assay_stream?) + assay_1_stream_1_sample_type = FactoryBot.create(:isa_assay_material_sample_type, contributor: other_user.person, linked_sample_type: sample_collection_sample_type, template_id: FactoryBot.create(:isa_assay_material_template).id) + assay_1_stream_1 = FactoryBot.create(:assay, position: 1, sample_type: assay_1_stream_1_sample_type, study: accessible_study, contributor: other_user.person, assay_stream_id: stream_1.id) + assay_2_stream_1_sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, contributor: other_user.person, linked_sample_type: assay_1_stream_1_sample_type, template_id: FactoryBot.create(:isa_assay_data_file_template).id) + assay_2_stream_1 = FactoryBot.create(:assay, position: 2, sample_type: assay_2_stream_1_sample_type, study: accessible_study, contributor: other_user.person, assay_stream_id: stream_1.id) # Create an assay stream with all assays visible - assay_1_stream_2_sample_type = FactoryBot.create(:isa_assay_material_sample_type, linked_sample_type: sample_collection_sample_type, template_id: FactoryBot.create(:isa_assay_material_template).id) - assay_1_stream_2 = FactoryBot.create(:assay, position: 0, sample_type: assay_1_stream_2_sample_type, study: accessible_study, contributor: current_user.person) - assay_2_stream_2_sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, linked_sample_type: assay_1_stream_2_sample_type, template_id: FactoryBot.create(:isa_assay_data_file_template).id) - assay_2_stream_2 = FactoryBot.create(:assay, position:1, sample_type: assay_2_stream_2_sample_type, study: accessible_study, contributor: current_user.person) + stream_2 = FactoryBot.create(:assay_stream, title: 'Assay Stream 2', study: accessible_study, contributor: current_user.person) + assert_equal(stream_2.study, accessible_study) + assert(stream_2.is_assay_stream?) + assay_1_stream_2_sample_type = FactoryBot.create(:isa_assay_material_sample_type, contributor: current_user.person, linked_sample_type: sample_collection_sample_type, template_id: FactoryBot.create(:isa_assay_material_template).id) + assay_1_stream_2 = FactoryBot.create(:assay, position: 1, sample_type: assay_1_stream_2_sample_type, study: accessible_study, contributor: current_user.person, assay_stream_id: stream_2.id) + assay_2_stream_2_sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, contributor: current_user.person, linked_sample_type: assay_1_stream_2_sample_type, template_id: FactoryBot.create(:isa_assay_data_file_template).id) + assay_2_stream_2 = FactoryBot.create(:assay, position: 2, sample_type: assay_2_stream_2_sample_type, study: accessible_study, contributor: current_user.person, assay_stream_id: stream_2.id) # create samples in second assay stream with viewing permission @@ -1096,8 +1102,13 @@ def test_title assert json_investigation['studies'].map { |s| s['title'] }.include? accessible_study.title study_json = json_investigation['studies'].first + # Total assays + assert_equal accessible_study.assays.count, 6 + # Assay_streams + assert_equal accessible_study.assay_streams.count, 2 + # Child assays + assert_equal accessible_study.assay_streams.map(&:child_assays).compact.flatten.count, 4 # Only one assay should end up in 1 assay stream in the ISA JSON - assert_equal accessible_study.assays.count, 4 assert_equal study_json['assays'].count, 1 sample_ids = study_json['materials']['samples'].map { |sample| sample['@id'] } diff --git a/test/functional/isa_assays_controller_test.rb b/test/functional/isa_assays_controller_test.rb index f6b436f227..1212dd21d1 100644 --- a/test/functional/isa_assays_controller_test.rb +++ b/test/functional/isa_assays_controller_test.rb @@ -45,7 +45,8 @@ def setup sop_ids: [FactoryBot.create(:sop, policy: FactoryBot.create(:public_policy)).id], creator_ids: [this_person.id, other_creator.id], other_creators: 'other collaborators', - position: 0, assay_class_id: 1, policy_attributes: }, + assay_class_id: AssayClass.experimental.id, + position: 0, policy_attributes: }, input_sample_type_id: sample_collection_sample_type.id, sample_type: { title: 'assay sample_type', project_ids: [projects.first.id], template_id: 1, sample_attributes_attributes: { @@ -188,7 +189,7 @@ def setup sop_ids: [FactoryBot.create(:sop, policy: FactoryBot.create(:public_policy)).id], creator_ids: [this_person.id, other_creator.id], other_creators: 'other collaborators', - position: 0, assay_class_id: 1, policy_attributes: } + position: 0, assay_class_id: AssayClass.experimental.id, policy_attributes: } isa_assay_attributes = { assay: assay_attributes.merge(emt_attributes), input_sample_type_id: sample_collection_sample_type.id, @@ -224,4 +225,60 @@ def setup end end end + + test 'hide sops, publications, documents, and discussion channels if assay stream' do + person = FactoryBot.create(:person) + study = FactoryBot.create(:isa_json_compliant_study, contributor: person) + assay_stream = FactoryBot.create(:assay_stream, study: , contributor: person) + + get :new, params: {study_id: study.id, is_assay_stream: true} + assert_response :success + + assert_select 'div#add_sops_form', text: /SOPs/i, count: 0 + assert_select 'div#add_publications_form', text: /Publications/i, count: 0 + assert_select 'div#add_documents_form', text: /Documents/i, count: 0 + assert_select 'div.panel-heading', text: /Discussion Channels/i, count: 0 + assert_select 'div.panel-heading', text: /Define Sample type for Assay/i, count: 0 + + get :edit, params: { id: assay_stream.id, study_id: study.id, source_assay_id: assay_stream.id, is_assay_stream: true } + assert_response :success + + assert_select 'div#add_sops_form', text: /SOPs/i, count: 0 + assert_select 'div#add_publications_form', text: /Publications/i, count: 0 + assert_select 'div#add_documents_form', text: /Documents/i, count: 0 + assert_select 'div.panel-heading', text: /Discussion Channels/i, count: 0 + assert_select 'div.panel-heading', text: /Define Sample type for Assay/i, count: 0 +end + + test 'show sops, publications, documents, and discussion channels if experimental assay' do + person = FactoryBot.create(:person) + project = person.projects.first + investigation = FactoryBot.create(:investigation, is_isa_json_compliant: true, contributor: person, projects: [project]) + study = FactoryBot.create(:isa_json_compliant_study, contributor: person, investigation: ) + assay_stream = FactoryBot.create(:assay_stream, study: , contributor: person, position: 0) + + login_as(person) + + get :new, params: {study_id: study.id, assay_stream_id: assay_stream.id, source_assay_id: assay_stream.id} + assert_response :success + + assert_select 'div#add_sops_form', text: /SOPs/i, count: 1 + assert_select 'div#add_publications_form', text: /Publications/i, count: 1 + assert_select 'div#add_documents_form', text: /Documents/i, count:1 + assert_select 'div.panel-heading', text: /Discussion Channels/i, count: 1 + assert_select 'div.panel-heading', text: /Define Sample type for Assay/i, count: 1 + + first_assay_st = FactoryBot.create(:isa_assay_material_sample_type, contributor: person, projects: [project], linked_sample_type: study.sample_types.second) + first_assay = FactoryBot.create(:assay, contributor: person, study: , assay_stream: , position: 1, sample_type: first_assay_st) + assert_equal assay_stream, first_assay.assay_stream + + get :edit, params: { id: first_assay.id, assay_stream_id: assay_stream.id, source_assay_id: first_assay.id, study_id: study.id } + assert_response :success + + assert_select 'div#add_sops_form', text: /SOPs/i, count: 1 + assert_select 'div#add_publications_form', text: /Publications/i, count: 1 + assert_select 'div#add_documents_form', text: /Documents/i, count: 1 + assert_select 'div.panel-heading', text: /Discussion Channels/i, count: 1 + assert_select 'div.panel-heading', text: /Define Sample type for Assay/i, count: 1 + end end diff --git a/test/functional/studies_controller_test.rb b/test/functional/studies_controller_test.rb index 69a5ef4419..a29d1c8891 100644 --- a/test/functional/studies_controller_test.rb +++ b/test/functional/studies_controller_test.rb @@ -2022,7 +2022,7 @@ def test_should_show_investigation_tab get :show, params: { id: study } assert_response :success - assert_select 'a', text: /Design #{I18n.t('assay')}/i, count: 1 + assert_select 'a', text: /Design #{I18n.t('assay')} Stream/i, count: 1 assert_select 'a', text: /Add new #{I18n.t('assay')}/i, count: 0 end end diff --git a/test/unit/assay_class_test.rb b/test/unit/assay_class_test.rb index 3a3d002f6d..3ef0593df8 100644 --- a/test/unit/assay_class_test.rb +++ b/test/unit/assay_class_test.rb @@ -4,12 +4,12 @@ class AssayClassTest < ActiveSupport::TestCase # Replace this with your real tests. fixtures :assay_classes test 'for_type' do - assert_equal 'EXP', AssayClass.for_type('experimental').key - assert_equal 'MODEL', AssayClass.for_type('modelling').key + assert_equal 'EXP', AssayClass.experimental.key + assert_equal 'MODEL', AssayClass.modelling.key end test 'is_modelling?' do - assert AssayClass.for_type('modelling').is_modelling? - refute AssayClass.for_type('experimental').is_modelling? + assert AssayClass.modelling.is_modelling? + refute AssayClass.experimental.is_modelling? end end diff --git a/test/unit/assay_test.rb b/test/unit/assay_test.rb index a8a9199528..f72a1e7d50 100644 --- a/test/unit/assay_test.rb +++ b/test/unit/assay_test.rb @@ -754,10 +754,88 @@ def new_valid_assay test 'isa json compliance' do isa_json_compliant_study = FactoryBot.create(:isa_json_compliant_study) assert isa_json_compliant_study.is_isa_json_compliant? + default_assay = FactoryBot.create(:assay, study: isa_json_compliant_study) refute default_assay.is_isa_json_compliant? - isa_json_compliant_assay = FactoryBot.create(:isa_json_compliant_assay) + assay_stream = FactoryBot.create(:assay_stream, study: isa_json_compliant_study) + assert assay_stream.is_isa_json_compliant? + + isa_json_compliant_assay = FactoryBot.create(:isa_json_compliant_assay, study: isa_json_compliant_study) assert isa_json_compliant_assay.is_isa_json_compliant? end + + test 'is assay stream' do + isa_json_compliant_study = FactoryBot.create(:isa_json_compliant_study) + assay_stream = FactoryBot.create(:assay_stream, study: isa_json_compliant_study) + assert assay_stream.is_assay_stream? + + default_assay = FactoryBot.create(:assay) + refute default_assay.is_assay_stream? + end + + test 'previous linked sample type' do + isa_study = FactoryBot.create(:isa_json_compliant_study) + def_study = FactoryBot.create(:study) + + assay_stream = FactoryBot.create(:assay_stream, study: isa_study) + assert_equal assay_stream.previous_linked_sample_type, isa_study.sample_types.second + + def_assay = FactoryBot.create(:assay, study:def_study) + assert_nil def_assay.previous_linked_sample_type + + first_isa_assay = FactoryBot.create(:isa_json_compliant_assay, + assay_stream: , + study: isa_study) + assert_equal first_isa_assay.previous_linked_sample_type, isa_study.sample_types.second + + data_file_sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, + linked_sample_type: first_isa_assay.sample_type) + second_isa_assay = FactoryBot.create(:assay, + study: isa_study, + assay_stream: , + sample_type: data_file_sample_type) + + assert_equal second_isa_assay.previous_linked_sample_type, first_isa_assay.sample_type + end + + test 'has_linked_child_assay?' do + isa_study = FactoryBot.create(:isa_json_compliant_study) + def_study = FactoryBot.create(:study) + def_assay = FactoryBot.create(:assay, study:def_study) + + assay_stream = FactoryBot.create(:assay_stream, study: isa_study) + first_isa_assay = FactoryBot.create(:isa_json_compliant_assay, study: isa_study) + data_file_sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, + linked_sample_type: first_isa_assay.sample_type) + second_isa_assay = FactoryBot.create(:assay, + study: isa_study, + assay_stream: , + sample_type: data_file_sample_type) + + assert assay_stream.has_linked_child_assay? + refute def_assay.has_linked_child_assay? + assert first_isa_assay.has_linked_child_assay? + refute second_isa_assay.has_linked_child_assay? + end + + test 'next_linked_child_assay' do + isa_study = FactoryBot.create(:isa_json_compliant_study) + def_study = FactoryBot.create(:study) + def_assay = FactoryBot.create(:assay, study:def_study) + + assay_stream = FactoryBot.create(:assay_stream, study: isa_study) + first_isa_assay = FactoryBot.create(:isa_json_compliant_assay, study: isa_study) + data_file_sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, + linked_sample_type: first_isa_assay.sample_type) + second_isa_assay = FactoryBot.create(:assay, + study: isa_study, + assay_stream: , + sample_type: data_file_sample_type) + + assert_equal assay_stream.next_linked_child_assay, first_isa_assay + assert_nil def_assay.next_linked_child_assay + assert_equal first_isa_assay.next_linked_child_assay, second_isa_assay + assert_nil second_isa_assay.next_linked_child_assay + end end diff --git a/test/unit/ontologies/ontology_synchronization_test.rb b/test/unit/ontologies/ontology_synchronization_test.rb index 50aeb1c446..06ba1d31df 100644 --- a/test/unit/ontologies/ontology_synchronization_test.rb +++ b/test/unit/ontologies/ontology_synchronization_test.rb @@ -3,10 +3,10 @@ class OntologySynchronizationTest < ActiveSupport::TestCase def setup - unless AssayClass.for_type('modelling') + unless AssayClass.modelling FactoryBot.create(:modelling_assay_class) end - unless AssayClass.for_type('experimental') + unless AssayClass.experimental FactoryBot.create(:experimental_assay_class) end end