diff --git a/app/assets/javascripts/single_page/dynamic_table.js.erb b/app/assets/javascripts/single_page/dynamic_table.js.erb
index 63627432bc..619c7713ea 100644
--- a/app/assets/javascripts/single_page/dynamic_table.js.erb
+++ b/app/assets/javascripts/single_page/dynamic_table.js.erb
@@ -53,6 +53,9 @@ const objectInputTemp = ' {
$j(e).parents("table").DataTable().row(e.closest("tr")).data()[0] = e.is(":checked")
@@ -72,27 +75,53 @@ const handleSelect = (e) => {
};
$j.dynamicTable.prototype = {
init: function(rows, columns, options = {}) {
- columns.forEach((c) => {
+ const studyId = options.studyId;
+ const assayId = options.assayId;
+
+ columns.forEach((c) => {
let linkedSamplesUrl;
let cvUrl;
- if (c.linked_sample_type) {
+ let registeredSopUrl;
+
+ let isRegisteredSample = false;
+ let isCVList = false;
+ let isRegisteredSop = false;
+ let isRegisteredDataFile = false;
+ let isRegisteredStrain = false;
+ if (c.attribute_type) {
+ isRegisteredSample = c.attribute_type.base_type.includes("SeekSample");
+ isCVList = c.attribute_type.base_type === "CVList";
+ isRegisteredSop = c.attribute_type.base_type === "SeekSop";
+ isRegisteredDataFile = c.attribute_type.base_type === "SeekDataFile";
+ isRegisteredStrain = c.attribute_type.base_type === "SeekStrain";
+ }
+
+ if (isRegisteredSample) {
linkedSamplesUrl = typeaheadSamplesUrl.replace("_LINKED_", c.linked_sample_type);
const linkedSamples = retrieveLinkedSamples(linkedSamplesUrl);
c.linkedSampleIds = linkedSamples.map((ls) => ls.id);
}
- if(c.is_cv_list && 'cv_id' in c){
+ if(isCVList && 'cv_id' in c){
cvUrl = typeaheadCVUrl.replace("_CVID_", c.cv_id);
}
+ if (isRegisteredSop) {
+ registeredSopUrl = typeaheadSopsUrl.replace('__STUDY_ID__', options.studyId).replace('__ASSAY_ID__', options.assayId);
+ }
+
c["render"] = function(data_, type, full, meta) {
let sanitizedData = sanitizeData(data_);
let data;
- if(c.linked_sample_type){
+ if(isRegisteredSample){
data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData];
data = data[0]?.id ? data : [];
return registeredSamplesObjectsInput(c, data, options, linkedSamplesUrl);
- } else if(c.is_cv_list && sanitizedData !== "#HIDDEN"){
+ } else if (isRegisteredSop) {
+ data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData];
+ data = data[0]?.id ? data : [];
+ return simpleObjectsInput(c, data, options, registeredSopUrl);
+ } else if(isCVList && sanitizedData !== "#HIDDEN"){
data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData];
data = data.map((e) => {
if (e?.id){
@@ -102,6 +131,14 @@ const handleSelect = (e) => {
}
});
return cvListObjectsInput(c, data, options, cvUrl);
+ } else if (isRegisteredDataFile) {
+ data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData];
+ data = data[0]?.id ? data : [];
+ return simpleObjectsInput(c, data, options, typeaheadDataFilesUrl);
+ } else if (isRegisteredStrain) {
+ data = sanitizedData && Array.isArray(sanitizedData) ? sanitizedData : [sanitizedData];
+ data = data[0]?.id ? data : [];
+ return simpleObjectsInput(c, data, options, typeaheadStrainsUrl);
} else if (sanitizedData === "#HIDDEN") {
return "Hidden";
} else {
@@ -630,10 +667,34 @@ function cvListObjectsInput(column, data, options, url){
.replace('_EXTRACLASS_', extraClass)
.replace('_TITLE_', titleText)
.replace('_LIMIT?_', '')
- .replace('_ALLOW_FREE_TEXT_', allowNewItems);
+ .replace('_ALLOW_FREE_TEXT_', allowNewItems.toString());
}
}
+function simpleObjectsInput(column, data, options, url) {
+ const existingOptions = data.map((e) => ``);
+ if (options.readonly) {
+ return data.map((e) => `${sanitizeData(e)}`).join(" ");
+ } else {
+ const typeaheadTemplate = 'typeahead/single_pages_samples';
+ const objectInputName = data.map((e) => sanitizeData(e)).join('-') + '-' + crypto.randomUUID();
+ const extraClass = '';
+ const titleText = '';
+ const allowNewItems = false;
+ setTimeout(ObjectsInput.init);
+
+ return objectInputTemp
+ .replace(/_NAME_/g, objectInputName)
+ .replace('_TYPEHEAD_', typeaheadTemplate)
+ .replace('_URL_', url)
+ .replace('_OPTIONS_', existingOptions)
+ .replace('_EXTRACLASS_', extraClass)
+ .replace('_TITLE_', titleText)
+ .replace('_LIMIT?_', '1')
+ .replace('_ALLOW_FREE_TEXT_', allowNewItems.toString());
+ }
+}
+
const handleFailure = (table, res) => {
const errors = new Set();
errors.add("The operation can not be performed for one or some samples. The red cells indicate unacceptable values.");
diff --git a/app/controllers/sops_controller.rb b/app/controllers/sops_controller.rb
index e6e0e803c5..244adf892b 100644
--- a/app/controllers/sops_controller.rb
+++ b/app/controllers/sops_controller.rb
@@ -56,6 +56,26 @@ def update
end
end
+ def dynamic_table_typeahead
+ return if params[:study_id].blank? && params[:assay_id].blank?
+
+ query = params[:query] || ''
+ asset = if params[:study_id].present?
+ Study.authorized_for('view').detect { |study| study.id.to_s == params[:study_id] }
+ else
+ Assay.authorized_for('view').detect { |assay| assay.id.to_s == params[:assay_id] }
+ end
+
+ sops = asset&.sops || []
+ filtered_sops = sops.select { |sop| sop.title&.downcase&.include?(query.downcase) }
+ items = filtered_sops.collect { |sop| { id: sop.id, text: sop.title } }
+
+ respond_to do |format|
+ format.json { render json: { results: items }.to_json }
+ end
+ end
+
+
private
def sop_params
diff --git a/app/helpers/dynamic_table_helper.rb b/app/helpers/dynamic_table_helper.rb
index 47ec75dc4a..32ed5b903f 100644
--- a/app/helpers/dynamic_table_helper.rb
+++ b/app/helpers/dynamic_table_helper.rb
@@ -99,15 +99,17 @@ def transform_registered_sample_single(json_metadata, input_key)
def dt_cols(sample_type)
attribs = sample_type.sample_attributes.map do |a|
attribute = { title: a.title, name: sample_type.id.to_s, required: a.required, description: a.description,
- is_title: a.is_title }
- attribute.merge!({ cv_id: a.sample_controlled_vocab_id }) unless a.sample_controlled_vocab_id.blank?
- is_seek_sample = a.sample_attribute_type.seek_sample?
- is_seek_multi_sample = a.sample_attribute_type.seek_sample_multi?
- is_cv_list = a.sample_attribute_type.seek_cv_list?
- cv_allows_free_text = a.allow_cv_free_text
- attribute.merge!({ multi_link: true, linked_sample_type: a.linked_sample_type_id }) if is_seek_multi_sample
- attribute.merge!({ multi_link: false, linked_sample_type: a.linked_sample_type_id }) if is_seek_sample
- attribute.merge!({ is_cv_list: true, cv_allows_free_text:}) if is_cv_list
+ is_title: a.is_title, attribute_type: a.sample_attribute_type }
+
+ if a.sample_attribute_type&.controlled_vocab?
+ cv_allows_free_text = a.allow_cv_free_text
+ attribute.merge!({ cv_allows_free_text: cv_allows_free_text, cv_id: a.sample_controlled_vocab_id })
+ end
+
+ if a.sample_attribute_type&.seek_sample_multi? || a.sample_attribute_type&.seek_sample?
+ attribute.merge!({ multi_link: a.sample_attribute_type&.seek_sample_multi?, linked_sample_type: a.linked_sample_type_id })
+ end
+
attribute
end
(dt_default_cols(sample_type.id.to_s) + attribs).flatten
diff --git a/app/views/isa_studies/_study_samples.html.erb b/app/views/isa_studies/_study_samples.html.erb
index 299ec76edd..fd1d3a5827 100644
--- a/app/views/isa_studies/_study_samples.html.erb
+++ b/app/views/isa_studies/_study_samples.html.erb
@@ -24,6 +24,7 @@
window.sampleDynamicTable = new $j.dynamicTable('#study-samples-table')
const elem = $j("#btn_save_sample")
const options = {
+ studyId: <%= study&.id %>,
ajax:{
url: dynamicTableDataPath,
data: function(d) {
diff --git a/config/routes.rb b/config/routes.rb
index 179c4a7514..e92f618b19 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -532,6 +532,9 @@
end
resources :sops, concerns: [:has_content_blobs, :publishable, :has_doi, :has_versions, :asset, :explorable_spreadsheet] do
+ collection do
+ get :dynamic_table_typeahead
+ end
resources :people, :programmes, :projects, :investigations, :assays, :samples, :studies, :publications, :events, :workflows, :collections, only: [:index]
end
diff --git a/lib/isa_exporter.rb b/lib/isa_exporter.rb
index 853cb4f489..70fb45faad 100644
--- a/lib/isa_exporter.rb
+++ b/lib/isa_exporter.rb
@@ -103,44 +103,69 @@ def convert_study(study)
isa_study[:people] = people
isa_study[:studyDesignDescriptors] = []
isa_study[:characteristicCategories] = convert_characteristic_categories(study)
+ protocols_maps = fetch_study_protocols(study)
+ isa_study[:protocols] = protocols_maps.map { |p| p[:protocols] }.compact.flatten
isa_study[:materials] = {
sources: convert_materials_sources(study.sample_types.first),
samples: convert_materials_samples(study.sample_types.second)
}
- protocols = []
- with_tag_protocol_study = study.sample_types.second.sample_attributes.detect { |sa| sa.isa_tag&.isa_protocol? }
- with_tag_parameter_value_study =
- study.sample_types.second.sample_attributes.select { |sa| sa.isa_tag&.isa_parameter_value? }
- raise "Protocol ISA tag not found in #{t(:study)} #{study.id}" if with_tag_protocol_study.blank?
- # 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.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? }
- raise "Protocol ISA tag not found in #{t(:assay)} #{a.id}" if protocol_attribute.blank?
-
- # raise "The #{t(:study)} with the title '#{study.title}' does not have an SOP" if a.sops.blank?
- protocols << convert_protocol(a.sops, a.id, protocol_attribute, with_tag_parameter_value)
- end
- isa_study[:protocols] = protocols
-
- isa_study[:processSequence] = convert_process_sequence(study.sample_types.second, study.sops.map(&:id).join("_"), study.id)
+ study_protocols_map = protocols_maps.detect { |pm| pm[:sample_type_id] == study.sample_types.second.id }
+ isa_study[:processSequence] = convert_process_sequence(study.sample_types.second, study_protocols_map)
unless study.assays.all?(&:is_isa_json_compliant?)
raise "All assays in study `#{study.title}` should be ISA-JSON compliant."
end
- isa_study[:assays] = study.assay_streams.map { |assay_stream| convert_assays(assay_stream) }.compact
+ isa_study[:assays] = study.assay_streams.map { |assay_stream| convert_assays(assay_stream, protocols_maps) }.compact
isa_study[:factors] = []
isa_study[:unitCategories] = []
isa_study
end
+ def fetch_study_protocols(study)
+ protocols_maps = []
+
+ # Get all sample types from study and its assays
+ sample_type_objcts = [{ sample_type: study.sample_types.second, isa: 'study', isa_id: study.id }]
+ sample_type_objcts += study.assay_streams.map(&:child_assays).flatten.map(&:sample_type).map do |sample_type|
+ { sample_type: sample_type, isa: 'assay', isa_id: sample_type.assays.first.id }
+ end
+
+ sample_type_objcts.each do |sample_type_object|
+ sample_type, isa, isa_id = sample_type_object[:sample_type], sample_type_object[:isa], sample_type_object[:isa_id]
+ protocol_attribute = sample_type.sample_attributes.detect { |sa| sa.isa_tag&.isa_protocol? }
+ parameter_attributes = sample_type.sample_attributes.select { |sa| sa.isa_tag&.isa_parameter_value? }.compact
+ next if protocol_attribute.blank?
+
+ is_registered_sop = protocol_attribute.sample_attribute_type.seek_sop?
+ # Get used SOPs
+ if is_registered_sop
+ used_sops = sample_type.samples.map do |sample|
+ sop = sample.get_attribute_value(protocol_attribute)
+ raise "Sample {#{sample.id}: #{sample.title}} has no registered SOP as protocol" if sop.blank?
+ Sop.find_by_id(sop['id'])
+ end.uniq
+ else
+ used_sops = sample_type.samples.map do |sample|
+ protocol_title = sample.get_attribute_value(protocol_attribute)
+ raise "Sample {#{sample.id}: #{sample.title}} has no protocol" if protocol_title.blank?
+ { id: protocol_attribute.id, title: protocol_title, description: '' }
+ end.uniq
+ end
+
+ # generate & append to protcols
+ protocols_maps.append({ protocols: used_sops.map do |sop|
+ id = "#protocol/#{isa}_#{isa_id}_#{sop[:id]}"
+ convert_protocol(sop, id, protocol_attribute, parameter_attributes)
+ end, isa: isa, isa_id: isa_id, sample_type_id: sample_type.id })
+ end
+
+ protocols_maps
+ end
+
def convert_annotation(term_uri)
isa_annotation = {}
term = term_uri.split('#')[1]
@@ -179,7 +204,7 @@ def convert_assay_comments(assay_stream)
assay_comments.compact
end
- def convert_assays(assay_stream)
+ def convert_assays(assay_stream, protocols_maps)
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) }
@@ -215,7 +240,10 @@ def convert_assays(assay_stream)
otherMaterials: convert_other_materials(all_sample_types)
}
isa_assay[:processSequence] =
- child_assays.map { |a| convert_process_sequence(a.sample_type, a.sops.map(&:id).join("_"), a.id) }.flatten
+ child_assays.map do |assay|
+ assay_protocols_map = protocols_maps.detect { |pm| pm[:sample_type_id] == assay.sample_type.id }
+ convert_process_sequence(assay.sample_type,assay_protocols_map)
+ end.flatten
isa_assay[:dataFiles] = convert_data_files(all_sample_types)
isa_assay[:unitCategories] = []
isa_assay
@@ -286,34 +314,34 @@ def convert_ontologies
source_ontologies.uniq.map { |s| { name: s, file: '', version: '', description: '' } }
end
- def convert_protocol(sops, id, protocol, parameter_values)
+ def convert_protocol(sop, id, protocol_attrbute, parameter_attributes)
isa_protocol = {}
- isa_protocol['@id'] = "#protocol/#{sops.map(&:id).join("-")}_#{id}"
- isa_protocol[:name] = protocol.title # sop.title
+ isa_protocol['@id'] = id
+ isa_protocol[:name] = sop[:title]
- ontology = get_ontology_details(protocol, protocol.title, false)
+ ontology = get_ontology_details(protocol_attrbute, protocol_attrbute.title, false)
isa_protocol[:protocolType] = {
- annotationValue: protocol.title,
+ annotationValue: protocol_attrbute.title,
termAccession: ontology[:termAccession],
termSource: ontology[:termSource]
}
- isa_protocol[:description] = sops&.first&.description || ''
+ isa_protocol[:description] = sop[:description] || ''
isa_protocol[:uri] = ontology[:termAccession]
isa_protocol[:version] = ''
isa_protocol[:parameters] =
- parameter_values.map do |parameter_value|
+ parameter_attributes.map do |parameter|
parameter_value_ontology =
- if parameter_value.pid.present?
- get_ontology_details(parameter_value, parameter_value.title, false)
+ if parameter.pid.present?
+ get_ontology_details(parameter, parameter.title, false)
else
{ termAccession: '', termSource: '' }
end
{
- '@id': "#parameter/#{parameter_value.id}",
+ '@id': "#parameter/#{parameter.id}",
parameterName: {
- annotationValue: parameter_value.title,
+ annotationValue: parameter.title,
termAccession: parameter_value_ontology[:termAccession],
termSource: parameter_value_ontology[:termSource]
}
@@ -352,13 +380,14 @@ def convert_materials_samples(sample_type)
with_tag_sample = sample_type.sample_attributes.detect { |sa| sa.isa_tag&.isa_sample? }
with_tag_sample_characteristic =
sample_type.sample_attributes.select { |sa| sa.isa_tag&.isa_sample_characteristic? }
- seek_sample_multi_attribute = sample_type.sample_attributes.detect(&:seek_sample_multi?)
+ input_attribute = sample_type.sample_attributes.detect(&:input_attribute?)
sample_type.samples.map do |s|
+ # To Do: FactorValues is empty. This relates to # 1869: https://github.com/seek4science/seek/issues/1869
if s.can_view?(@current_user)
{
'@id': "#sample/#{s.id}",
name: s.get_attribute_value(with_tag_sample),
- derivesFrom: extract_sample_ids(s.get_attribute_value(seek_sample_multi_attribute), 'source'),
+ derivesFrom: extract_sample_ids(s.get_attribute_value(input_attribute), 'source'),
characteristics: convert_characteristics(s, with_tag_sample_characteristic),
factorValues: [
{
@@ -378,7 +407,7 @@ def convert_materials_samples(sample_type)
{
'@id': "#sample/HIDDEN",
name: '',
- derivesFrom: extract_sample_ids(s.get_attribute_value(seek_sample_multi_attribute), 'source'),
+ derivesFrom: extract_sample_ids(s.get_attribute_value(input_attribute), 'source'),
characteristics: [],
factorValues: [
{
@@ -399,12 +428,12 @@ def convert_materials_samples(sample_type)
end
def convert_characteristics(sample, attributes)
- attributes.map do |c|
- value = sample.can_view?(@current_user) ? (sample.get_attribute_value(c) || '') : ''
- ontology = get_ontology_details(c, value, true)
+ attributes.map do |attribute|
+ value = convert_attribute_value(sample, attribute)
+ ontology = get_ontology_details(attribute, value, true)
{
category: {
- '@id': normalize_id("#characteristic_category/#{c.title}_#{c.id}")
+ '@id': normalize_id("#characteristic_category/#{attribute.title}_#{attribute.id}")
},
value: {
annotationValue: value,
@@ -416,6 +445,18 @@ def convert_characteristics(sample, attributes)
end
end
+ def convert_attribute_value(sample, attribute)
+ return '' unless sample.can_view?(@current_user)
+
+ if attribute.sample_attribute_type.seek_sample? || attribute.sample_attribute_type.seek_sample_multi? || attribute.sample_attribute_type.seek_strain? || attribute.sample_attribute_type.seek_data_file? || attribute.sample_attribute_type.seek_sop?
+ sample.get_attribute_value(attribute).to_json || ''
+ elsif attribute.sample_attribute_type.base_type == Seek::Samples::BaseType::CV_LIST
+ sample.get_attribute_value(attribute).join(', ')
+ else
+ sample.get_attribute_value(attribute) || ''
+ end
+ end
+
def convert_characteristic_categories(study = nil, assays = nil)
attributes = []
if study
@@ -440,7 +481,7 @@ def convert_characteristic_categories(study = nil, assays = nil)
end
end
- def convert_process_sequence(sample_type, sop_ids, id)
+ def convert_process_sequence(sample_type, protocols_map)
# This method is meant to be used for both Studies and Assays
return [] unless sample_type.samples.any?
@@ -456,25 +497,28 @@ def convert_process_sequence(sample_type, sop_ids, id)
# should be in a different process in the processSequence
samples_grouped_by_input_and_parameter_value = group_samples_by_input_and_parameter_value(sample_type)
result = []
- samples_grouped_by_input_and_parameter_value.map do |input_ids, samples_group|
- output_ids = samples_group.pluck(:id).join('_')
+ samples_grouped_by_input_and_parameter_value.map do |sample_group|
+ # It's fine to take the first output to get the protocol name since the outputs are grouped by protocol as well.
+ protocols, isa, isa_id, _sample_type_id = protocols_map.values_at(:protocols, :isa, :isa_id, :sample_type_id)
+ executed_protocol = protocols_map[:protocols].detect { |p| p[:name] == sample_group[:executed_protocol][:title] }
+ output_ids = sample_group[:outputs].map { |s| s[:id] }.join('_')
process = {
'@id': normalize_id("#process/#{protocol_attribute.title}/#{output_ids}"),
name: '',
executesProtocol: {
- '@id': "#protocol/#{sop_ids}_#{id}"
+ '@id': executed_protocol["@id"]
},
- parameterValues: convert_parameter_values(samples_group, isa_parameter_value_attributes),
+ parameterValues: convert_parameter_values(sample_group[:outputs], isa_parameter_value_attributes),
performer: '',
date: '',
- inputs: process_sequence_input(input_ids.first, type),
- outputs: process_sequence_output(samples_group)
+ inputs: process_sequence_input(sample_group[:inputs], type),
+ outputs: process_sequence_output(sample_group[:outputs])
}
# Study processes don't have a previousProcess and nextProcess
unless type == 'source'
process.merge!({
- previousProcess: previous_process(samples_group),
- nextProcess: next_process(samples_group)
+ previousProcess: previous_process(sample_group[:outputs]),
+ nextProcess: next_process(sample_group[:outputs])
})
end
result.push(process)
@@ -567,6 +611,15 @@ def export
private
+ def detect_material(sample_type)
+ sample_type.sample_attributes.detect do |sa|
+ [
+ Seek::ISA::TagType::SOURCE,
+ Seek::ISA::TagType::SAMPLE,
+ Seek::ISA::TagType::OTHER_MATERIAL
+ ].include? sa.isa_tag&.title
+ end
+ end
def detect_sample(sample_type)
sample_type.sample_attributes.detect { |sa| sa.isa_tag&.isa_sample? }
end
@@ -595,8 +648,8 @@ def detect_other_material(sample_type)
sample_type.sample_attributes.detect { |sa| sa.isa_tag&.isa_other_material? }
end
- def detect_sample_multi(sample_type)
- sample_type.sample_attributes.detect(&:seek_sample_multi?)
+ def detect_input_attribute(sample_type)
+ sample_type.sample_attributes.detect(&:input_attribute?)
end
def next_process(samples_hash)
@@ -642,12 +695,27 @@ def group_samples_by_input_and_parameter_value(sample_type)
}.merge(metadata).transform_keys!(&:to_sym)
end
- input_attribute = detect_sample_multi(sample_type)&.title&.to_sym
- parameter_value_attributes = select_parameter_values(sample_type).map(&:title).map(&:to_sym)
- group_attributes = parameter_value_attributes.unshift(input_attribute)
+ input_attribute_name = detect_input_attribute(sample_type)&.title&.to_sym
+ parameter_value_attribute_names = select_parameter_values(sample_type).map(&:title).map(&:to_sym)
+ protocol_attribute_name = detect_protocol(sample_type)&.title&.to_sym
+ material_attribute_name = detect_material(sample_type)&.title&.to_sym
+ group_attribute_names = parameter_value_attribute_names.unshift(protocol_attribute_name).unshift(input_attribute_name)
+
+ # grouped_samples = samples_metadata.group_by { |smd| group_attributes.map { |attr| smd[attr] } }
+ sample_groups = samples_metadata.group_by { |smd| group_attribute_names.map { |attr| smd[attr] }.flatten }.map { |_key, val| val }
- grouped_samples = samples_metadata.group_by { |smd| group_attributes.map { |attr| smd[attr] } }
- grouped_samples.transform_keys { |key| group_id(key) }
+ sample_groups.map do |sample_group|
+ inputs = sample_group.first[input_attribute_name]
+ executed_protocol = detect_protocol(sample_type).sample_attribute_type.seek_sop? ? sample_group.first[protocol_attribute_name] : { id: detect_protocol(sample_type)&.id, title: sample_group.first[protocol_attribute_name] }
+ parameter_values = sample_group.first.slice(*parameter_value_attribute_names)
+ outputs = sample_group.map { |sample| {id: sample[:id], title: sample[material_attribute_name] } }
+ {
+ inputs: inputs.map { |input| input.transform_keys!(&:to_sym) },
+ executed_protocol: executed_protocol.transform_keys!(&:to_sym),
+ parameter_values: parameter_values.transform_keys!(&:to_sym),
+ outputs: outputs.map { |input| input.transform_keys!(&:to_sym) }
+ }
+ end
end
def process_sequence_input(inputs, type)
@@ -688,7 +756,7 @@ def convert_parameter_values(sample_group_hash, isa_parameter_value_attributes)
# So retrieving the first one in the group should be fine.
sample = Sample.find(sample_group_hash.first[:id])
isa_parameter_value_attributes.map do |p|
- value = sample.get_attribute_value(p) || ''
+ value = convert_attribute_value(sample, p)
ontology = get_ontology_details(p, value, true)
{
category: {
diff --git a/test/factories/assays.rb b/test/factories/assays.rb
index 24f95f1ae0..e932c7b282 100644
--- a/test/factories/assays.rb
+++ b/test/factories/assays.rb
@@ -101,21 +101,51 @@
assets_creators { [AssetsCreator.new(affiliation: 'University of Somewhere', creator: FactoryBot.create(:person, first_name: 'Some', last_name: 'One'))] }
end
- factory(:isa_json_compliant_assay, parent: :assay) do
- title { 'ISA JSON compliant assay' }
- description { 'An assay linked to an ISA JSON compliant study and a sample type' }
+ factory(:assay_stream, parent: :assay_base) do
+ sequence(:title) { |n| "Assay Stream #{n}" }
+ 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
+
+ factory(:isa_json_compliant_material_assay, parent: :assay) do
+ transient do
+ linked_sample_type { nil }
+ end
+ sequence(:title) { |n| "ISA JSON compliant material assay #{n}" }
+ description { 'An assay linked to an ISA JSON compliant study and a sample type' }
+ after(:build) do |assay, eval|
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)
+ assay.sample_type = FactoryBot.create(:isa_assay_material_sample_type, linked_sample_type: eval.linked_sample_type)
end
end
- factory(:assay_stream, parent: :assay_base) do
- title { 'Assay Stream' }
- description { 'A holder assay holding multiple child assays' }
+ factory(:isa_json_compliant_data_file_assay, parent: :assay) do
+ transient do
+ linked_sample_type { nil }
+ end
+ sequence(:title) { |n| "ISA JSON compliant data file assay #{n}" }
+ description { 'An assay linked to an ISA JSON compliant study and a sample type' }
+ after(:build) do |assay, eval|
+ assay.study ||= FactoryBot.create(:isa_json_compliant_study)
+ assay.sample_type = FactoryBot.create(:isa_assay_data_file_sample_type, linked_sample_type: eval.linked_sample_type)
+ end
+ end
+
+ factory(:complete_assay_stream, parent: :assay_stream) do
+ transient do
+ sample_collection_sample_type { nil }
+ end
+ sequence(:title) { |n| "Complete Assay Stream #{n}" }
+ description { 'An assay stream populated with assays' }
association :assay_class, factory: :assay_stream_class
- after(:build) do |assay|
- assay.study ||= FactoryBot.create(:isa_json_compliant_study, contributor: assay.contributor)
+ after(:build) do |assay_stream, eval|
+ first_material_assay = FactoryBot.create(:isa_json_compliant_material_assay, title: 'Pre-treatment', linked_sample_type: eval.sample_collection_sample_type, study: assay_stream.study, contributor: assay_stream.contributor)
+ second_material_assay = FactoryBot.create(:isa_json_compliant_material_assay, title: 'Extraction', linked_sample_type: first_material_assay.sample_type, study: assay_stream.study, contributor: assay_stream.contributor)
+ data_file_assay = FactoryBot.create(:isa_json_compliant_data_file_assay, title: 'Measurement', linked_sample_type: second_material_assay.sample_type, study: assay_stream.study, contributor: assay_stream.contributor)
+ assay_stream.child_assays = [first_material_assay, second_material_assay, data_file_assay]
end
end
diff --git a/test/functional/sops_controller_test.rb b/test/functional/sops_controller_test.rb
index 829146d0bc..f7e884f9bb 100644
--- a/test/functional/sops_controller_test.rb
+++ b/test/functional/sops_controller_test.rb
@@ -2140,6 +2140,52 @@ def test_editing_doesnt_change_contributor
assert_equal 0, policy.permissions.count
end
+ test 'dynamic table typeahead should return sops only linked to studies or assays' do
+ person = FactoryBot.create(:person)
+ project = person.projects.first
+ study_sops = (1..10).collect.each { |i| FactoryBot.create(:sop, contributor: person, projects: [project], title: "My study level protocol nr. #{i}") }
+
+ # 5 sops unrelated to study
+ (11..15).collect.each { |i| FactoryBot.create(:sop, contributor: person, projects: [project], title: "My protocol nr. #{i}") }
+
+ # Project has a total of 15 sops
+ assert_equal project.sops.count, 15
+
+ investigation = FactoryBot.create(:investigation, contributor: person, projects: [project])
+ study = FactoryBot.create(:study, contributor: person, investigation: investigation, sops: study_sops)
+ login_as(person)
+
+ # No query
+ # Should only return 10 sops linked to study
+ get :dynamic_table_typeahead, params: { study_id: study.id }, format: :json
+ results = JSON.parse(response.body)['results']
+ assert_equal results.count, 10
+
+ # Query '1'
+ # Should return 2 sops linked to study: nr. 1 and 10
+ get :dynamic_table_typeahead, params: { study_id: study.id, query: '1' }, format: :json
+ results = JSON.parse(response.body)['results']
+ assert_equal results.count, 2
+
+ assay_sops = (16..20).collect.each { |i| FactoryBot.create(:sop, contributor: person, projects: [project], title: "My assay level protocol nr. #{i}") }
+ assay = FactoryBot.create(:assay, contributor: person, study: study, sops: assay_sops)
+
+ # Project has now a total of 20 sops
+ assert_equal project.sops.count, 20
+
+ # Query 'assay'
+ # Should return 5 sops linked to assay: nr. 16 to 20
+ get :dynamic_table_typeahead, params: { assay_id: assay.id, query: 'assay' }, format: :json
+ results = JSON.parse(response.body)['results']
+ assert_equal results.count, 5
+
+ # Query '12'
+ # Should return 0 sops linked to assay
+ get :dynamic_table_typeahead, params: { assay_id: assay.id, query: '12' }, format: :json
+ results = JSON.parse(response.body)['results']
+ assert_equal results.count, 0
+ end
+
private
def doi_citation_mock
diff --git a/test/unit/assay_test.rb b/test/unit/assay_test.rb
index 1938706e20..6adb8e37f6 100644
--- a/test/unit/assay_test.rb
+++ b/test/unit/assay_test.rb
@@ -765,7 +765,7 @@ def new_valid_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)
+ isa_json_compliant_assay = FactoryBot.create(:isa_json_compliant_material_assay, study: isa_json_compliant_study, linked_sample_type: isa_json_compliant_study.sample_types.second)
assert isa_json_compliant_assay.is_isa_json_compliant?
end
@@ -791,9 +791,10 @@ def new_valid_assay
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,
+ first_isa_assay = FactoryBot.create(:isa_json_compliant_material_assay,
assay_stream: assay_stream ,
- study: isa_study)
+ study: isa_study,
+ linked_sample_type: isa_study.sample_types.second)
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,
@@ -814,7 +815,7 @@ def new_valid_assay
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)
+ first_isa_assay = FactoryBot.create(:isa_json_compliant_material_assay, study: isa_study, linked_sample_type: isa_study.sample_types.second, assay_stream: assay_stream)
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,
@@ -835,7 +836,7 @@ def new_valid_assay
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, assay_stream: assay_stream )
+ first_isa_assay = FactoryBot.create(:isa_json_compliant_material_assay, study: isa_study, assay_stream: assay_stream, 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,
diff --git a/test/unit/isa_exporter_test.rb b/test/unit/isa_exporter_test.rb
index c4cd891778..7467640e3d 100644
--- a/test/unit/isa_exporter_test.rb
+++ b/test/unit/isa_exporter_test.rb
@@ -1,6 +1,10 @@
require 'test_helper'
class IsaExporterTest < ActionController::TestCase
+ def setup
+ @seek_sop_type = SampleAttributeType.find_by(base_type: Seek::Samples::BaseType::SEEK_SOP) || FactoryBot.create(:sop_sample_attribute_type)
+ end
+
test 'find sample origin' do
current_user = FactoryBot.create(:user)
controller = IsaExporter::Exporter.new(FactoryBot.create(:investigation), current_user)
@@ -83,4 +87,75 @@ class IsaExporterTest < ActionController::TestCase
child_3.reload
assert_equal [child_1.id, child_2_another_parent.id], controller.send(:find_sample_origin, [child_3], 1)
end
+
+ test 'should export registered sops correctly' do
+ person = FactoryBot.create(:person)
+ investigation = FactoryBot.create(:investigation, contributor: person, projects: [person.projects.first], is_isa_json_compliant: true)
+ sample_collection_sop = FactoryBot.create(:sop, contributor: person, title: 'Sample collection protocol', projects: [investigation.projects.first])
+ study = FactoryBot.create(:isa_json_compliant_study, contributor: person, investigation: investigation, sops: [sample_collection_sop])
+
+ # Create sources
+ source_sample_type = study.sample_types.first
+ (0..10).each do |i|
+ FactoryBot.create(:sample, title: "Source #{i}", contributor: person, sample_type: source_sample_type, project_ids: [study.projects.first.id],
+ data: { 'Source Name': "Source #{i}", 'Source Characteristic 1': 'source 1 characteristic 1', 'Source Characteristic 2': 'Bramley' })
+ end
+
+ sample_collection_sample_type = study.sample_types.last
+ sample_collection_sample_type.sample_attributes.detect { |sa| sa.isa_tag&.isa_protocol? }.update_column('sample_attribute_type_id', @seek_sop_type.id)
+ (0..10).each do |i|
+ FactoryBot.create(:sample, title: "Sample #{i}", contributor: person, sample_type: sample_collection_sample_type, project_ids: [study.projects.first.id],
+ data: { 'Sample Name': "Sample #{i}", 'sample collection': sample_collection_sop, Input: "Source #{i}", 'sample characteristic 1': 'value sample 1', 'sample collection parameter value 1': 'value 1' })
+ end
+
+ assay_stream = FactoryBot.create(:complete_assay_stream, study: study, contributor: person, sample_collection_sample_type: sample_collection_sample_type)
+
+ pre_treatment_assay = assay_stream.child_assays.detect { |assay| assay.title.include? "Pre-treatment" }
+ pre_treatment_sample_type = pre_treatment_assay.sample_type
+ 0.upto(10) do |i|
+ FactoryBot.create(:sample, title: "Pre-treatment #{i}", contributor: person, sample_type: pre_treatment_sample_type, project_ids: [study.projects.first.id],
+ data: { 'Extract Name': "Pre-treatment #{i}", Input: "Sample #{i}", 'Protocol Assay 1': "Pre-treatment protocol", 'other material characteristic 1': "Pre-treatment #{i} characteristic 1", 'Assay 1 parameter value 1': "Pre-treatment #{i} Parameter value 1" })
+ end
+
+ extraction_assay = assay_stream.child_assays.detect { |assay| assay.title.include? "Extraction" }
+ extraction_assay_sample_type = extraction_assay.sample_type
+ 0.upto(10) do |i|
+ FactoryBot.create(:sample, title: "Extraction #{i}", contributor: person, sample_type: extraction_assay_sample_type, project_ids: [study.projects.first.id],
+ data: { 'Extract Name': "Extract #{i}", Input: "Pre-treatment #{i}", 'Protocol Assay 1': "Extraction protocol", 'other material characteristic 1': "Extract #{i} characteristic 1", 'Assay 1 parameter value 1': "Extract #{i} Parameter value 1" })
+ end
+
+ measurement_assay = assay_stream.child_assays.detect { |assay| assay.title.include? "Measurement" }
+ measurement_assay_sample_type = measurement_assay.sample_type
+ 0.upto(10) do |i|
+ protocol_name = i%3 == 0 ? "Measurement protocol 1" : "Measurement protocol 2"
+ FactoryBot.create(:sample, title: "Measurement #{i}", contributor: person, sample_type: measurement_assay_sample_type, project_ids: [study.projects.first.id],
+ data: { 'File Name': "Measurement #{i}", Input: "Extract #{i}", 'Protocol Assay 2': protocol_name, 'Assay 2 parameter value 1': "Measurement #{i} Parameter 1", 'Data file comment 1': "Measurement #{i} comment 1" })
+ end
+
+ isa = JSON.parse(IsaExporter::Exporter.new(investigation, person.user).export)
+ assert_not_nil isa
+
+ # Check the number of studies
+ assert_equal isa['studies'].count, 1
+
+ # Check the number of assay streams
+ assert_equal isa['studies'][0]['assays'].count, 1
+
+ # check the study protocols
+ # Study holds a registered SOP as protocol. All samples use the "Sample collection protocol".
+ # The pre-treatment assay has free text to describe the protocol. All samples use the "Pre-treatment protocol".
+ # The extraction assay has free text to describe the protocol. All samples use the "Extraction protocol".
+ # The measurement assay has free text to describe the protocol. Some samples use the "Measurement protocol 1", while others use "Measurement protocol 2".
+ # This gives a total of 5 protocols.
+ protocols = isa['studies'][0]['protocols']
+ assert_equal protocols.count, 5
+
+ # check the parameters
+ parameter_attributes = [sample_collection_sample_type, pre_treatment_sample_type, extraction_assay_sample_type, measurement_assay_sample_type].map do |sample_type|
+ sample_type.sample_attributes.select { |sa| sa.isa_tag&.isa_parameter_value? }.map(&:title)
+ end.flatten.compact.uniq
+ isa_parameters = protocols.map { |protocol| protocol['parameters'] }.flatten.compact.map { |parameter| parameter['parameterName']['annotationValue'] }.compact.uniq
+ assert_equal isa_parameters.count, parameter_attributes.count
+ assert parameter_attributes.all? { |title| isa_parameters.include? title }
+ end
end