diff --git a/app/controllers/single_pages_controller.rb b/app/controllers/single_pages_controller.rb index 070e071064..87fc47f230 100644 --- a/app/controllers/single_pages_controller.rb +++ b/app/controllers/single_pages_controller.rb @@ -49,7 +49,10 @@ def dynamic_table_data end def download_samples_excel - sample_ids, sample_type_id, study_id, assay_id = Rails.cache.read(params[:uuid]).values_at(:sample_ids, :sample_type_id, + cached_asset_ids = Rails.cache.read(params[:uuid]) + raise "Request took too long or was interrupted." if cached_asset_ids.nil? + + sample_ids, sample_type_id, study_id, assay_id = cached_asset_ids.values_at(:sample_ids, :sample_type_id, :study_id, :assay_id) @study = Study.find(study_id) @@ -65,31 +68,7 @@ def download_samples_excel raise "Could not retrieve #{assay_id.nil? ? 'Study' : 'Assay'} Sample Type! Do you have at least viewing permissions?" unless @sample_type.can_view? @template = Template.find(@sample_type.template_id) - - sample_attributes = @sample_type.sample_attributes.map do |sa| - is_cv_list = sa.sample_attribute_type.base_type == Seek::Samples::BaseType::CV_LIST - obj = if sa.sample_controlled_vocab_id.nil? - { sa_cv_title: sa.title, sa_cv_id: nil } - else - { sa_cv_title: sa.title, sa_cv_id: sa.sample_controlled_vocab_id, allows_custom_input: sa.allow_cv_free_text } - end - obj.merge({ required: sa.required, is_cv_list: }) - end - - @sa_cv_terms = [{ name: 'id', has_cv: false, data: nil, allows_custom_input: nil, required: nil, is_cv_list: nil }, - { name: 'uuid', has_cv: false, data: nil, allows_custom_input: nil, required: nil, is_cv_list: nil }] - - sample_attributes.map do |sa| - if sa[:sa_cv_id].nil? - @sa_cv_terms.push({ name: sa[:sa_cv_title], has_cv: false, data: nil, - allows_custom_input: nil, required: sa[:required], is_cv_list: nil }) - else - sa_terms = SampleControlledVocabTerm.where(sample_controlled_vocab_id: sa[:sa_cv_id]).map(&:label) - @sa_cv_terms.push({ name: sa[:sa_cv_title], has_cv: true, data: sa_terms, - allows_custom_input: sa[:allows_custom_input], required: sa[:required], is_cv_list: sa[:is_cv_list] }) - end - end - + spreadsheet_name = @sample_type.title&.concat(".xlsx") notice_message << '' @@ -132,7 +111,7 @@ def upload_samples when 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' spreadsheet_xml = spreadsheet_to_xml(uploaded_file.path, Seek::Config.jvm_memory_allocation) wb = parse_spreadsheet_xml(spreadsheet_xml) - metadata_sheet = wb.sheet('Metadata') + metadata_sheet = wb.sheet('Sample Type Metadata') samples_sheet = wb.sheet('Samples') else raise "Please upload a valid spreadsheet file with extension '.xlsx'" @@ -145,10 +124,10 @@ def upload_samples end # Extract Samples metadata from spreadsheet - study_id = metadata_sheet.cell(2, 2).value.to_i + sample_type_id_spreadsheet = metadata_sheet.cell(2, 2).value.to_i + @sample_type = SampleType.find(sample_type_id_spreadsheet) + study_id = metadata_sheet.cell(10, 2).value.to_i @study = Study.find(study_id) - sample_type_id = metadata_sheet.cell(5, 2).value.to_i - @sample_type = SampleType.find(sample_type_id) is_assay = @sample_type.assays.any? @assay = @sample_type.assays.first @@ -163,17 +142,29 @@ def upload_samples raise "Sample Type '#{@sample_type.id}' doesn't belong to Assay #{@assay.id}. Sample Upload aborted." end - @multiple_input_fields = @sample_type.sample_attributes.map do |sa_attr| - sa_attr.title if sa_attr.sample_attribute_type.base_type == Seek::Samples::BaseType::SEEK_SAMPLE_MULTI - end + @registered_sample_multi_fields = @sample_type.sample_attributes.select do |sa_attr| + sa_attr.sample_attribute_type.seek_sample_multi? + end.map(&:title) - @registered_sample_fields = @sample_type.sample_attributes.map do |sa_attr| - sa_attr.title if sa_attr.sample_attribute_type.base_type == Seek::Samples::BaseType::SEEK_SAMPLE - end + @registered_sample_fields = @sample_type.sample_attributes.select do |sa_attr| + sa_attr.sample_attribute_type.seek_sample? + end.map(&:title) - @cv_list_fields = @sample_type.sample_attributes.map do |sa_attr| - sa_attr.title if sa_attr.sample_attribute_type.base_type == Seek::Samples::BaseType::CV_LIST - end + @registered_data_file_fields = @sample_type.sample_attributes.select do |sa_attr| + sa_attr.sample_attribute_type.seek_data_file? + end.map(&:title) + + @registered_strain_fields = @sample_type.sample_attributes.select do |sa_attr| + sa_attr.sample_attribute_type.seek_strain? + end.map(&:title) + + @registered_sops = @sample_type.sample_attributes.select do |sa_attr| + sa_attr.sample_attribute_type.seek_sop? + end.map(&:title) + + @cv_list_fields = @sample_type.sample_attributes.select do |sa_attr| + sa_attr.sample_attribute_type.base_type == Seek::Samples::BaseType::CV_LIST + end.map(&:title) sample_fields, samples_data = get_spreadsheet_data(samples_sheet) @@ -198,12 +189,12 @@ def upload_samples # Construct Samples objects from Excel data excel_samples = generate_excel_samples(samples_data, sample_fields, sample_type_attributes) - existing_excel_samples = excel_samples.map { |sample| sample unless sample['id'].nil? }.compact - new_excel_samples = excel_samples.map { |sample| sample if sample['id'].nil? }.compact + existing_excel_samples = excel_samples.select { |sample| !sample['id'].nil? } + new_excel_samples = excel_samples.select { |sample| sample['id'].nil? } # Retrieve all samples of the Sample Type, also the unauthorized ones @db_samples = sample_type_samples(@sample_type) - # Retrieve the Sample Types samples wich are authorized for editing + # Retrieve the Sample Types samples which are authorized for editing @authorized_db_samples = sample_type_samples(@sample_type, :edit) # Determine whether samples have been modified or not, @@ -261,12 +252,14 @@ def generate_excel_samples(samples_data, sample_fields, sample_type_attributes) samples_data.map do |excel_sample| obj = {} (0..sample_fields.size - 1).map do |i| + cell_value = excel_sample[i] current_sample_attribute = sample_type_attributes.detect { |sa| sa[:title] == sample_fields[i] } validate_cv_terms = cv_sample_attributes.map{ |cv_sa| cv_sa[:title] }.include?(sample_fields[i]) - validate_cv_terms &&= current_sample_attribute[:required] && !excel_sample[i].blank? + validate_cv_terms &&= current_sample_attribute[:required] && !cell_value.blank? attr_terms = validate_cv_terms ? cv_sample_attributes.detect { |sa| sa[:title] == sample_fields[i] }[:cv_terms] : [] - if @multiple_input_fields.include?(sample_fields[i]) - parsed_excel_input_samples = JSON.parse(excel_sample[i].gsub(/"=>/x, '":')).map do |subsample| + if @registered_sample_multi_fields.include?(sample_fields[i]) + parsed_json = cell_value.nil? ? [] : JSON.parse(cell_value.gsub(/"=>/x, '":')) + parsed_excel_input_samples = parsed_json.map do |subsample| # Uploader should at least have viewing permissions for the inputs he's using unless Sample.find(subsample['id'])&.authorized_for_view? raise "Unauthorized Sample was detected in spreadsheet: #{subsample.inspect}" @@ -275,14 +268,32 @@ def generate_excel_samples(samples_data, sample_fields, sample_type_attributes) subsample end obj.merge!(sample_fields[i] => parsed_excel_input_samples) - elsif @registered_sample_fields.include?(sample_fields[i]) - parsed_excel_registered_sample = JSON.parse(excel_sample[i].gsub(/"=>/x, '":')) - unless Sample.find(parsed_excel_registered_sample['id'])&.authorized_for_view? - raise "Unauthorized Sample was detected in spreadsheet: #{parsed_excel_registered_sample.inspect}" + elsif [@registered_sample_fields, @registered_sops, @registered_data_file_fields, @registered_strain_fields].any? { |reg_asset| reg_asset.include?(sample_fields[i]) } + unless cell_value.nil? + parsed_excel_registered_asset = JSON.parse(cell_value.gsub(/"=>/x, '":')) + if @registered_sample_fields.include?(sample_fields[i]) + unless Sample.find(parsed_excel_registered_asset['id'])&.authorized_for_view? + raise "Unauthorized Sample was detected in spreadsheet: #{parsed_excel_registered_asset.inspect}" + end + elsif @registered_sops.include?(sample_fields[i]) + unless Sop.find(parsed_excel_registered_asset['id'])&.authorized_for_view? + raise "Unauthorized Sop was detected in spreadsheet: #{parsed_excel_registered_asset.inspect}" + end + elsif @registered_data_file_fields.include?(sample_fields[i]) + unless DataFile.find(parsed_excel_registered_asset['id'])&.authorized_for_view? + raise "Unauthorized Data File was detected in spreadsheet: #{parsed_excel_registered_asset.inspect}" + end + elsif @registered_strain_fields.include?(sample_fields[i]) + unless Strain.find(parsed_excel_registered_asset['id'])&.authorized_for_view? + raise "Unauthorized Strain was detected in spreadsheet: #{parsed_excel_registered_asset.inspect}" + end + else + raise "\"#{parsed_excel_registered_asset["type"]}\" is not a supported type of registered asset." + end end - obj.merge!(sample_fields[i] => parsed_excel_registered_sample) + obj.merge!(sample_fields[i] => parsed_excel_registered_asset) elsif @cv_list_fields.include?(sample_fields[i]) - parsed_cv_terms = JSON.parse(excel_sample[i]) + parsed_cv_terms = JSON.parse(cell_value) # CV validation for CV_LIST attributes parsed_cv_terms.map do |term| if !attr_terms.include?(term) && validate_cv_terms @@ -291,18 +302,18 @@ def generate_excel_samples(samples_data, sample_fields, sample_type_attributes) end obj.merge!(sample_fields[i] => parsed_cv_terms) elsif sample_fields[i] == 'id' - if excel_sample[i].blank? + if cell_value.blank? obj.merge!(sample_fields[i] => nil) else - obj.merge!(sample_fields[i] => excel_sample[i]&.to_i) + obj.merge!(sample_fields[i] => cell_value&.to_i) end else if validate_cv_terms - unless attr_terms.include?(excel_sample[i]) - raise "Invalid Controlled vocabulary term detected '#{excel_sample[i]}' in sample ID #{excel_sample[0]}: { #{sample_fields[i]}: #{excel_sample[i]} }" + unless attr_terms.include?(cell_value) + raise "Invalid Controlled vocabulary term detected '#{cell_value}' in sample ID #{excel_sample[0]}: { #{sample_fields[i]}: #{cell_value} }" end end - obj.merge!(sample_fields[i] => excel_sample[i]) + obj.merge!(sample_fields[i] => cell_value) end end obj @@ -386,9 +397,9 @@ def separate_possible_duplicates(new_excel_samples, db_samples) end def valid_workbook?(workbook) - !((workbook.sheet_names.map do |sheet| - %w[Metadata Samples cv_ontology].include? sheet - end.include? false) && (workbook.sheets.size != 3)) + ["Sample Type Metadata", "Samples", "Controlled Vocabularies"].all? do |expected_sheet| + workbook.sheet_names.include? expected_sheet + end end def set_up_instance_variable diff --git a/app/helpers/single_pages_helper.rb b/app/helpers/single_pages_helper.rb new file mode 100644 index 0000000000..30ef13e7f4 --- /dev/null +++ b/app/helpers/single_pages_helper.rb @@ -0,0 +1,74 @@ +module SinglePagesHelper + ## Function to get a fixed size matrix + def transposed_filled_arrays(arrays) + raise 'Input is no array' unless arrays.is_a?(Array) + + size = 0 + arrays.map { |array| size = array.size if array.size > size } + + filled_arrays = [] + arrays.map { |array| filled_arrays.push(Array.new(size) { |i| array[i] }) } + + filled_arrays.transpose + end + + ATTRIBUTES_WITH_DATA_VALIDATION = [ + Seek::Samples::BaseType::CV, + Seek::Samples::BaseType::SEEK_SAMPLE, + Seek::Samples::BaseType::SEEK_DATA_FILE, + Seek::Samples::BaseType::SEEK_STRAIN, + Seek::Samples::BaseType::SEEK_SOP, + ] + + def requires_data_validation?(sample_attribute) + ATTRIBUTES_WITH_DATA_VALIDATION.include?(sample_attribute.sample_attribute_type.base_type) + end + + def get_values_for_attribute(attribute) + type = attribute.sample_attribute_type + if type.controlled_vocab? + get_values_for_cv(attribute) + elsif type.seek_sample? || type.seek_sample_multi? + get_values_for_registered_samples(attribute) + elsif type.seek_data_file? + get_values_for_datafiles(attribute) + elsif type.seek_strain? + get_values_for_strains(attribute) + elsif type.seek_sop? + get_values_for_sops(attribute) + else + [] + end + end + + def get_values_for_cv(sample_attribute) + sample_attribute.sample_controlled_vocab.labels + end + + def get_values_for_registered_samples(sample_attribute) + sample_attribute.linked_sample_type.samples.map do |sample| + { id: sample.id, type: 'Sample', title: sample.title }.to_json + end + end + + def get_values_for_sops(sample_attribute) + is_assay_attribute = sample_attribute.sample_type.assays.any? + if is_assay_attribute + sops = sample_attribute.sample_type.assays.first.sops + else + sops = sample_attribute.sample_type.studies.first.sops + end + + sops.map { |sop| { id: sop.id, type: 'Sop', title: sop.title }.to_json } + end + + def get_values_for_datafiles(sample_attribute) + data_files = sample_attribute.sample_type.projects.first.data_files + data_files.map { |data_file| { id: data_file.id, type: 'DataFile', title: data_file.title }.to_json } + end + + def get_values_for_strains(sample_attribute) + strains = sample_attribute.sample_type.projects.first.strains + strains.map { |strain| { id: strain.id, type: 'Strain', title: strain.title }.to_json } + end +end \ No newline at end of file diff --git a/app/views/single_pages/_duplicate_samples_panel.html.erb b/app/views/single_pages/_duplicate_samples_panel.html.erb index 6a623a4286..e18b122919 100644 --- a/app/views/single_pages/_duplicate_samples_panel.html.erb +++ b/app/views/single_pages/_duplicate_samples_panel.html.erb @@ -23,10 +23,10 @@ <% dupl_sample.map do |key, val| %> <% val = '' if key == 'id' %> <% unless %w[uuid duplicate].include?(key) %> - <% if @multiple_input_fields.include?(key) %> + <% if @registered_sample_multi_fields.include?(key) %> <% val.each do |sub_sample| %> - ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> + ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> <% end %> <% elsif @cv_list_fields.include?(key) %> @@ -37,7 +37,19 @@ <% elsif @registered_sample_fields.include?(key) %> - ' data-attr_type="seek-sample"><%= val['title'] %> + ' data-attr_type="seek-sample"><%= val['title'] %> + + <% elsif @registered_data_file_fields.include?(key) %> + + ' data-attr_type="seek-data-file"><%= val['title'] %> + + <% elsif @registered_strain_fields.include?(key) %> + + ' data-attr_type="seek-strain"><%= val['title'] %> + + <% elsif @registered_sops.include?(key) %> + + ' data-attr_type="seek-sop"><%= val['title'] %> <% else %> '><%= val %> @@ -47,20 +59,26 @@ ' class="danger"> <% dupl_sample['duplicate'].map do |key, val| %> <% unless %w[uuid duplicate].include?(key) %> - <% if @multiple_input_fields.include?(key) %> + <% if @registered_sample_multi_fields.include?(key) %> <% val.each do |sub_sample| %> - '><%= sub_sample['title'] %> + '><%= sub_sample['title'] %> <% end %> <% elsif @cv_list_fields.include?(key) %> <% val.each do |cv_term| %> - <%= cv_term %> + <%= cv_term %> <% end %> <% elsif @registered_sample_fields.include?(key) %> - '><%= val['title'] %> + '><%= val['title'] %> + <% elsif @registered_data_file_fields.include?(key) %> + '><%= val['title'] %> + <% elsif @registered_strain_fields.include?(key) %> + '><%= val['title'] %> + <% elsif @registered_sops.include?(key) %> + '><%= val['title'] %> <% else %> <%= val %> <% end %> diff --git a/app/views/single_pages/_new_samples_panel.html.erb b/app/views/single_pages/_new_samples_panel.html.erb index 4fd18ae27b..9844a268b7 100644 --- a/app/views/single_pages/_new_samples_panel.html.erb +++ b/app/views/single_pages/_new_samples_panel.html.erb @@ -20,25 +20,38 @@ + onclick=<%= "removeSample('new-sample-#{new_sample_id}')" %>> + <% new_sample.map do |key, val| %> <% val = '' if key == 'id' %> <% unless key == 'uuid' %> - <% if @multiple_input_fields.include?(key) %> + <% if @registered_sample_multi_fields.include?(key) %> <% val.each do |sub_sample| %> - ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> + <%= sub_sample['title'] %> <% end %> <% elsif @cv_list_fields.include?(key) %> <% val.each do |cv_term| %> - <%= cv_term %> + <%= cv_term %> <% end %> <% elsif @registered_sample_fields.include?(key) %> - ' data-attr_type="seek-sample"><%= val['title'] %> + <%= val['title'] %> + + <% elsif @registered_data_file_fields.include?(key) %> + + <%= val['title'] %> + + <% elsif @registered_strain_fields.include?(key) %> + + <%= val['title'] %> + + <% elsif @registered_sops.include?(key) %> + + <%= val['title'] %> <% else %> <%= val %> diff --git a/app/views/single_pages/_unauthorized_samples_panel.html.erb b/app/views/single_pages/_unauthorized_samples_panel.html.erb index 4d69e2a741..89a448ee46 100644 --- a/app/views/single_pages/_unauthorized_samples_panel.html.erb +++ b/app/views/single_pages/_unauthorized_samples_panel.html.erb @@ -25,12 +25,20 @@ ' class=""> <% unauthorized_sample.map do |key, val| %> <% unless key == 'uuid' %> - <% if @multiple_input_fields.include?(key) %> + <% if @registered_sample_multi_fields.include?(key) %> <% val.each do |sub_sample| %> - '><%= sub_sample['title'] %> + <%= sub_sample['title'] %> <% end %> + <% elsif @registered_sample_fields.include?(key) %> + <%= val['title'] %> + <% elsif @registered_data_file_fields.include?(key) %> + <%= val['title'] %> + <% elsif @registered_strain_fields.include?(key) %> + <%= val['title'] %> + <% elsif @registered_sops.include?(key) %> + <%= val['title'] %> <% else %> <%= val %> <% end %> diff --git a/app/views/single_pages/_update_samples_panel.html.erb b/app/views/single_pages/_update_samples_panel.html.erb index 1d339a2bba..35c0522d8e 100644 --- a/app/views/single_pages/_update_samples_panel.html.erb +++ b/app/views/single_pages/_update_samples_panel.html.erb @@ -18,12 +18,15 @@ '> <% db_sample = @authorized_db_samples.select { |s| s['id'] == update_sample['id'] }.first %> - + + <% update_sample.map do |key, val| %> <% unless key == 'uuid' %> - <% if @multiple_input_fields.include?(key) %> + <% if @registered_sample_multi_fields.include?(key) %> '> <% val.each do |sub_sample| %> ' data-attr_type="seek-sample-multi"><%= sub_sample['title'] %> @@ -39,6 +42,18 @@ '> ' data-attr_type="seek-sample"><%= val['title'] %> + <% elsif @registered_data_file_fields.include?(key) %> + '> + ' data-attr_type="seek-data-file"><%= val['title'] %> + + <% elsif @registered_strain_fields.include?(key) %> + '> + ' data-attr_type="seek-strain"><%= val['title'] %> + + <% elsif @registered_sops.include?(key) %> + '> + ' data-attr_type="seek-sop"><%= val['title'] %> + <% else %> '><%= val %> <% end %> @@ -48,7 +63,7 @@ '> <% db_sample.map do |key, val| %> <% unless key == 'uuid' %> - <% if @multiple_input_fields.include?(key) %> + <% if @registered_sample_multi_fields.include?(key) %> <% val.each do |sub_sample| %> '><%= sub_sample['title'] %> @@ -62,6 +77,12 @@ <% elsif @registered_sample_fields.include?(key) %> '><%= val['title'] %> + <% elsif @registered_data_file_fields.include?(key) %> + '><%= val['title'] %> + <% elsif @registered_strain_fields.include?(key) %> + '><%= val['title'] %> + <% elsif @registered_sops.include?(key) %> + '><%= val['title'] %> <% else %> <%= val %> <% end %> diff --git a/app/views/single_pages/download_samples_excel.axlsx b/app/views/single_pages/download_samples_excel.axlsx index 76d266b90d..1f89d67bf7 100644 --- a/app/views/single_pages/download_samples_excel.axlsx +++ b/app/views/single_pages/download_samples_excel.axlsx @@ -1,17 +1,16 @@ -require 'caxlsx' -require 'uuid' - ##################################### debug = false ##################################### if debug - secret_pwd = 'florakevinrafael' + secret_pwd = 'seek' else secret_pwd = UUID.new.generate end +xlsx_package = Axlsx::Package.new workbook = xlsx_package.workbook + # Prevents formula injections Axlsx.escape_formulas = true @@ -23,136 +22,189 @@ end # Colors # TODO: Make the colors depend on the type of branding instead of hard-coded colors # REQUIREMENT: branding configuration in seek -datahub_blue = '486273' -datahub_yellow = 'F2b035' +header_fg_color = '486273' +header_bg_color = 'F2b035' # Cell Styles -unlocked = workbook.styles.add_style locked: false -locked = workbook.styles.add_style locked: true -title_style = workbook.styles.add_style(bg_color: datahub_yellow, - fg_color: datahub_blue, - b: true, - u: true, - sz: 18) - -workbook.add_worksheet(name: 'Metadata') do |sheet| - sheet.add_row ['Study:'], style: title_style - sheet.add_row ['Fairdom ID:', @study.id] - sheet.add_row ['UUID:', @study.uuid] - - sheet.add_row ['Sample Type:'], style: title_style - sheet.add_row ['Fairdom ID:', @sample_type.id] - sheet.add_row ['UUID:', @sample_type.uuid] - - sheet.add_row ['Template:'], style: title_style - sheet.add_row ['Fairdom ID:', @template.id] - sheet.add_row ['UUID:', @template.uuid] +unlocked_style = workbook.styles.add_style locked: false +locked_style = workbook.styles.add_style locked: true +header_one_style = workbook.styles.add_style( + bg_color: header_bg_color, + fg_color: header_fg_color, + b: true, + u: true, + sz: 18, + locked: true +) + +header_two_style = workbook.styles.add_style(b:true, u: true, locked: true) + +######################################################################################################################## +# Adding the Sample Type metadata sheet +######################################################################################################################## + +workbook.add_worksheet(name: 'Sample Type Metadata') do |sheet| + sheet.add_row ['Sample Type:'], style: header_one_style + sheet.add_row ['Fairdom ID:', @sample_type.id], style: [header_two_style, nil] + sheet.add_row ['Title:', @sample_type.title], style: [header_two_style, nil] + sheet.add_row ['UUID:', @sample_type.uuid], style: [header_two_style, nil] + + sheet.add_row ['Template:'], style: header_one_style + sheet.add_row ['Fairdom ID:', @template.id], style: [header_two_style, nil] + sheet.add_row ['Title:', @template.title], style: [header_two_style, nil] + sheet.add_row ['UUID:', @template.uuid], style: [header_two_style, nil] + + sheet.add_row ['Study:'], style: header_one_style + sheet.add_row ['Fairdom ID:', @study.id], style: [header_two_style, nil] + sheet.add_row ['Title:', @study.title], style: [header_two_style, nil] + sheet.add_row ['UUID:', @study.uuid], style: [header_two_style, nil] if @assay - sheet.add_row ['Assay:'], style: title_style - sheet.add_row ['Fairdom ID:', @assay.id] - sheet.add_row ['UUID:', @assay.uuid] + sheet.add_row ['Assay:'], style: header_one_style + sheet.add_row ['Fairdom ID:', @assay&.id], style: [header_two_style, nil] + sheet.add_row ['Title:', @assay&.title], style: [header_two_style, nil] + sheet.add_row ['UUID:', @assay&.uuid], style: [header_two_style, nil] end sheet.sheet_protection.password = secret_pwd end -# CV / Ontologies sheet -## Function to get a fixed size matrix -def transposed_filled_arrays(arrays) - raise 'Input is no array' unless arrays.is_a?(Array) - - size = 0 - arrays.map { |array| size = array.size if array.size > size } - - filled_arrays = [] - arrays.map { |array| filled_arrays.push(Array.new(size) { |i| array[i] }) } - - filled_arrays.transpose +sample_attributes = @sample_type.sample_attributes +sample_attribute_value_map = {} +sample_attributes.each do |sample_attribute| + attribute_values = get_values_for_attribute(sample_attribute) + sample_attribute_value_map[sample_attribute.title] = attribute_values end -## Populate and protect CV / Ontologies sheet -workbook.add_worksheet name: 'cv_ontology', state: :hidden do |sheet| - rows = [] - @sa_cv_terms.map do |cv| - row = [cv[:name]] - - if cv[:has_cv] - cv[:data]&.map do |val| - row.push val - end - end - rows.push(row) - end - tfs_rows = transposed_filled_arrays(rows) - tfs_rows.map do |tfs_row| - sheet.add_row tfs_row - end - sheet.sheet_protection.password = secret_pwd -end +######################################################################################################################## +# Adding the Sample sheet +######################################################################################################################## -# Sample sheet workbook.add_worksheet(name: 'Samples') do |sheet| ## Adding the header cells - header_row = @sa_cv_terms.map do |sa_cv_term| - sa_cv_term[:required] ? "#{sa_cv_term[:name]} *" : sa_cv_term[:name] - end - sheet.add_row header_row + header_row = %w[id uuid] + header_row.concat( + sample_attributes.map do |attribute| + attribute.required ? "#{attribute.title} *" : attribute.title + end + ) + sheet.add_row header_row, style: header_one_style ## populating the sheet with the data unless sample_data.none? sample_data.each do |item| row = item.collect { |_key, val| val } - sheet.add_row row, style: unlocked + + # Lock only first two cells => id and uuid + current_row_style = row.each_with_index.map do |_cell, i| + i < 2 ? locked_style : unlocked_style + end + + # Add styled row + sheet.add_row row, style: current_row_style end end - ## Adding extra empty rows so new samples can be added to the table - 1000.times do - sheet.add_row Array::new(header_row.size), style: unlocked + ## Adding 10000 extra empty rows with correct styling + 10000.times do + cell_values = Array.new(header_row.length, "") + current_row_style = (0..header_row.length - 1).map do |cell_idx| + cell_idx < 2 ? locked_style : unlocked_style + end + sheet.add_row cell_values, style: current_row_style end - ## styling - [0, 1].each { |col_index| sheet.col_style(col_index, locked) } - sheet.row_style(0, title_style) - ## filtering sheet.auto_filter = "#{sheet.cells.first.r}:#{sheet.cells.last.r}" +end + +######################################################################################################################## +# Adding Controlled vocabularies sheet for registered assets +######################################################################################################################## +workbook.add_worksheet name: 'Controlled Vocabularies' do |sheet| + + # Get the rows for the CV sheet + # [, *] + rows = sample_attribute_value_map.map do |key, values| + [key].concat(values) + end.unshift(["id"], ["uuid"]) # Adds the ID and UUID column + + # Transpose the rows into columns + tfs_rows = transposed_filled_arrays(rows) + + # Add rows to sheet and lock the rows + tfs_rows.each_with_index do |tfs_row, i| + if i.zero? + sheet.add_row tfs_row, style: header_one_style + else + sheet.add_row tfs_row, style: locked_style + end + end + + # Add sheet protection + sheet.sheet_protection.password = secret_pwd +end - ## Data Validation - # https://github.com/caxlsx/caxlsx/blob/master/examples/list_validation_example.md - attribute_size = @sa_cv_terms.size - 1 - (0..attribute_size).map do |col_nr| +######################################################################################################################## +# Apply Data Validation for the attributes that require data validation in the 'Samples' sheet +# https://github.com/caxlsx/caxlsx/blob/master/examples/list_validation_example.md +######################################################################################################################## - # If the has_cv field is false, it should skip this iteration and not apply the data validation - next unless @sa_cv_terms[col_nr][:has_cv] +workbook.worksheets.find { |sheet| sheet.name == 'Samples' }.tap do |sheet| + sample_attributes.each_with_index do |attribute, i| + + # Columns 1 and 2 are ID and UUID columns + col_nr = i + 2 # Get sa_cv_terms_length - sa_cv_terms_size = @sa_cv_terms[col_nr][:data].size + terms = sample_attribute_value_map[attribute.title] + sa_terms_size = terms.size + + # Prepare a different prompt text for these types of attributes: + # - List attributes + # - Attributes that require data validation in the spreadsheet + # - Other attributes + # All prompts will have the description of the attribute + prompt_text = attribute.description.blank? ? "" : attribute.description.concat("\n\r") + + if attribute.sample_attribute_type.seek_cv_list? || attribute.sample_attribute_type.seek_sample_multi? + prompt_text << "Any valid combination of the terms on sheet 'Controlled Vocabularies'#{attribute.allow_cv_free_text ? ', as well as free text,' : ''} are accepted.\n\r" + end + + # if attribute.sample_attribute_type.seek_cv_list? || attribute.sample_attribute_type.seek_sample_multi? + # prompt_text << "Any valid combination of the terms on sheet 'Controlled Vocabularies'#{attribute.allow_cv_free_text ? ', as well as free text,' : ''} are accepted. E.g. [#{terms.first}, #{terms.last}#{attribute.allow_cv_free_text ? ', My custom term' : ''}].\n\r" + # end + # + if requires_data_validation?(attribute) + prompt_text << "Choose a valid option from the 'Controlled Vocabularies' sheet #{attribute.allow_cv_free_text ? 'or add free text' : ''}.\n\r" + end - if @sa_cv_terms[col_nr][:is_cv_list] - terms = @sa_cv_terms[col_nr][:data] - prompt_text = "Any combination of these terms between are accepted: #{terms.join(', ')}. E.g. [''#{terms.first}'', ''#{terms.last}''].\n\r#{(@sa_cv_terms[col_nr][:required] ? 'This field is REQUIRED!' : 'This field is optional.')}" + if attribute.required + prompt_text << "This field is REQUIRED!" else - prompt_text = "Choose a valid option. #{(@sa_cv_terms[col_nr][:required] ? 'This field is REQUIRED!' : 'This field is optional.')}" + prompt_text << "This field is optional." end + # Apply the Data Validation on the attribute from row 2 until 100000 col_ref = Axlsx.cell_r(col_nr, 1).gsub(/\d+/, '') dv_range = "#{col_ref}2:#{col_ref}1000000" sheet.add_data_validation(dv_range, type: :list, - formula1: "'cv_ontology'!$#{col_ref}$2:$#{col_ref}$#{sa_cv_terms_size + 1}", - hideDropDown: @sa_cv_terms[col_nr][:is_cv_list], # CV lists should not have dropdown - showErrorMessage: !@sa_cv_terms[col_nr][:allows_custom_input] && !@sa_cv_terms[col_nr][:is_cv_list], # CV Lists must have free text input + formula1: "'Controlled Vocabularies'!$#{col_ref}$2:$#{col_ref}$#{sa_terms_size + 1}", + # Field that do not require data validation should not have dropdown + hideDropDown: !requires_data_validation?(attribute), + # If the attribute requires data validation, show Error message + # If attribute does not allow free text for CVs, Error message + showErrorMessage: !attribute.allow_cv_free_text && requires_data_validation?(attribute), errorTitle: 'Input Error!', error: 'Please select one of the available options', errorStyle: :stop, # options here are: 'information', 'stop', 'warning' showInputMessage: true, - promptTitle: @sa_cv_terms[col_nr][:name], + promptTitle: attribute.title, prompt: prompt_text) end - ## Password protections Samples sheet + # Password protections Samples sheet sheet.sheet_protection.password = secret_pwd sheet.sheet_protection.format_cells = false sheet.sheet_protection.format_columns = false diff --git a/app/views/single_pages/sample_upload_content.html.erb b/app/views/single_pages/sample_upload_content.html.erb index b8b1f51dc2..dfe8abf03f 100644 --- a/app/views/single_pages/sample_upload_content.html.erb +++ b/app/views/single_pages/sample_upload_content.html.erb @@ -134,18 +134,27 @@ cells.map(function(cell){ const val = cell.textContent; const key = cell.id.match(/\[.*\]/)[0].replace('[', "").replace("]", ''); - const multiInputfields = $j(cell).find('span[data-attr_type="seek-sample-multi"]').toArray(); + const seekSampleMultiFields = $j(cell).find('span[data-attr_type="seek-sample-multi"]').toArray(); const cvListFields = $j(cell).find('span[data-attr_type="cv-list"]').toArray(); - const seekSample = $j(cell).find('span[data-attr_type="seek-sample"]'); + const seekSampleFields = $j(cell).find('span[data-attr_type="seek-sample"]'); + const seekStrainFields = $j(cell).find('span[data-attr_type="seek-strain"]'); + const seekDataFileFields = $j(cell).find('span[data-attr_type="seek-data-file"]'); + const seekSopFields = $j(cell).find('span[data-attr_type="seek-sop"]'); - if (multiInputfields.length > 0 ){ - const inputIds = multiInputfields.map(is => is.title.split(" ").pop()).join(','); + if (seekSampleMultiFields.length > 0 ){ + const inputIds = seekSampleMultiFields.map(is => is.title.split(" ").pop()).join(','); samplesObj[key] = inputIds; } else if (cvListFields.length > 0){ let cvTerms = cvListFields.map(cvt => cvt.title) samplesObj[key] = cvTerms; - } else if (seekSample.length > 0) { - samplesObj[key] = seekSample[0].title + } else if (seekSampleFields.length > 0) { + samplesObj[key] = seekSampleFields[0].title + } else if (seekStrainFields.length > 0) { + samplesObj[key] = seekStrainFields[0].title + } else if (seekDataFileFields.length > 0) { + samplesObj[key] = seekDataFileFields[0].title + } else if (seekSopFields.length > 0) { + samplesObj[key] = seekSopFields[0].title } else { samplesObj[key] = val; } diff --git a/test/fixtures/files/upload_single_page/00_wrong_format_spreadsheet.ods b/test/fixtures/files/upload_single_page/00_wrong_format_spreadsheet.ods index 1fbacaba08..2ca320acf9 100644 Binary files a/test/fixtures/files/upload_single_page/00_wrong_format_spreadsheet.ods and b/test/fixtures/files/upload_single_page/00_wrong_format_spreadsheet.ods differ diff --git a/test/fixtures/files/upload_single_page/01_combo_update_sources_spreadsheet.xlsx b/test/fixtures/files/upload_single_page/01_combo_update_sources_spreadsheet.xlsx index faba1182a3..fce39a01f9 100644 Binary files a/test/fixtures/files/upload_single_page/01_combo_update_sources_spreadsheet.xlsx and b/test/fixtures/files/upload_single_page/01_combo_update_sources_spreadsheet.xlsx differ diff --git a/test/fixtures/files/upload_single_page/02_invalid_workbook.xlsx b/test/fixtures/files/upload_single_page/02_invalid_workbook.xlsx index e05a7d4c6d..8b9a322614 100644 Binary files a/test/fixtures/files/upload_single_page/02_invalid_workbook.xlsx and b/test/fixtures/files/upload_single_page/02_invalid_workbook.xlsx differ diff --git a/test/fixtures/files/upload_single_page/03_combo_update_samples_spreadsheet.xlsx b/test/fixtures/files/upload_single_page/03_combo_update_samples_spreadsheet.xlsx index 18e3038622..eeba66af34 100644 Binary files a/test/fixtures/files/upload_single_page/03_combo_update_samples_spreadsheet.xlsx and b/test/fixtures/files/upload_single_page/03_combo_update_samples_spreadsheet.xlsx differ diff --git a/test/fixtures/files/upload_single_page/04_combo_update_assay_samples_spreadsheet.xlsx b/test/fixtures/files/upload_single_page/04_combo_update_assay_samples_spreadsheet.xlsx index 2e29324ac6..d27e2b4d10 100644 Binary files a/test/fixtures/files/upload_single_page/04_combo_update_assay_samples_spreadsheet.xlsx and b/test/fixtures/files/upload_single_page/04_combo_update_assay_samples_spreadsheet.xlsx differ diff --git a/test/fixtures/files/upload_single_page/05_combo_update_assay_samples_with_registered_assets_spreadsheet.xlsx b/test/fixtures/files/upload_single_page/05_combo_update_assay_samples_with_registered_assets_spreadsheet.xlsx new file mode 100644 index 0000000000..ccffb298a2 Binary files /dev/null and b/test/fixtures/files/upload_single_page/05_combo_update_assay_samples_with_registered_assets_spreadsheet.xlsx differ diff --git a/test/functional/single_pages_controller_test.rb b/test/functional/single_pages_controller_test.rb index 5b9805f17d..a563a60903 100644 --- a/test/functional/single_pages_controller_test.rb +++ b/test/functional/single_pages_controller_test.rb @@ -434,6 +434,41 @@ def teardown end end + test 'Should link registered assets to the sample metadata' do + project, assay_sample_type = setup_file_upload.values_at( + :project, :assay_sample_type + ) + car_catalogue = car_catalogue(project, @member.person) + flower_based_names_catalogue = flower_names(project, @member.person) + _strains = bacteria_strains(project, @member.person) + _data_files = create_data_files(project, @member.person) + _sops = create_sops(project, @member.person) + + assay_sample_type.sample_attributes << [ + FactoryBot.create(:data_file_sample_attribute, required: false, is_title: false, sample_type: assay_sample_type, title: "Registered Data File"), + FactoryBot.create(:sop_sample_attribute, required: false, is_title: false, sample_type: assay_sample_type, title: "Registered SOP"), + FactoryBot.create(:strain_sample_attribute, required: false, is_title: false, sample_type: assay_sample_type, title: "Registered Strain"), + FactoryBot.create(:sample_sample_attribute, required: false, is_title: false, sample_type: assay_sample_type, title: "Registered Sample", linked_sample_type: car_catalogue), + FactoryBot.create(:sample_multi_sample_attribute, required: false, is_title: false, sample_type: assay_sample_type, title: "Registered Sample List", linked_sample_type: flower_based_names_catalogue), + ] + file_path = 'upload_single_page/05_combo_update_assay_samples_with_registered_assets_spreadsheet.xlsx' + file = fixture_file_upload(file_path, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + + post :upload_samples, as: :json, params: { file:, project_id:project.id, sample_type_id: assay_sample_type.id } + + assert_response :success + response_data = JSON.parse(response.body)['uploadData'] + updated_samples = response_data['updateSamples'] + unauthorized_samples = response_data['unauthorized_samples'] + new_samples = response_data['newSamples'] + possible_duplicates = response_data['possibleDuplicates'] + + assert_equal updated_samples.size, 2 + assert_equal unauthorized_samples.size, 0 + assert_equal new_samples.size, 1 + assert_equal possible_duplicates.size, 1 + end + private def setup_file_upload @@ -546,4 +581,129 @@ def setup_file_upload "assay_samples": assay_samples } end + + def car_catalogue(project, person) + sample_catalogue_cars = FactoryBot.build(:sample_type, + title: "Sample Catalogue Cars", + projects: [project], + contributor: person + ) + sample_catalogue_cars.sample_attributes << [ + FactoryBot.create(:any_string_sample_attribute, title: "Car name", sample_type: sample_catalogue_cars, is_title: true), + FactoryBot.create(:any_string_sample_attribute, title: "Brand", sample_type: sample_catalogue_cars), + FactoryBot.create(:any_string_sample_attribute, title: "Model", sample_type: sample_catalogue_cars), + ] + sample_catalogue_cars.save + names = [ + "Herbie", + "Ecto-1", + "K.I.T.T.", + "General Lee", + "DeLorean Time Machine" + ] + brands = [ + "Volkswagen", + "Cadillac", + "Pontiac", + "Dodge", + "DeLorean Motor Company" + ] + models = [ + "Beetle", + "Miller-Meteor", + "Firebird Trans Am", + "Charger", + "DMC-12" + ] + _cars = (1..5).map do |n| + FactoryBot.create(:sample, + id: 10_040 + n, + title: names[n-1], + sample_type: sample_catalogue_cars, + project_ids: [project.id], + contributor: person, + data: { + 'Car name': names[n-1], + Brand: brands[n-1], + Model: models[n-1] + } + ) + end + sample_catalogue_cars.reload + end + + def flower_names(project, person) + sample_catalogue_flower_names = FactoryBot.build(:sample_type, + title: "Sample Catalogue Flowers", + projects: [project], + contributor: person + ) + + sample_catalogue_flower_names.sample_attributes << [ + FactoryBot.create(:any_string_sample_attribute, title: "Human name", sample_type: sample_catalogue_flower_names, is_title: true), + FactoryBot.create(:any_string_sample_attribute, title: "Scientific Name", sample_type: sample_catalogue_flower_names), + FactoryBot.create(:any_string_sample_attribute, title: "Trivial Name", sample_type: sample_catalogue_flower_names), + ] + sample_catalogue_flower_names.save + human_names = %w[Rosalind Sonny Daisy Lavanda Daffy] + scientific_names = ["Rosa indica", "Helianthus annuus", "Bellis perennis", "Lavandula", "Narcissus pseudonarcissus"] + trivial_names = ["Rose", "Sunflower", "English Daisy", "Lavender", "Wild Daffodil"] + _flowers = (1..5).map do |n| + FactoryBot.create(:sample, + id: 10_050 + n, + title: human_names[n - 1], + sample_type: sample_catalogue_flower_names, + project_ids: [project.id], + contributor: person, + data: { + 'Human name': human_names[n - 1], + 'Scientific Name': scientific_names[n-1], + 'Trivial Name': trivial_names[n-1] + } + ) + end + sample_catalogue_flower_names.reload + end + + def bacteria_strains(project, person) + organism = FactoryBot.create(:organism, title: "Bacteriaceae", projects: [project]) + bacteria_names = [ + "Escherichia coli", + "Streptococcus pyogenes", + "Staphylococcus aureus", + "Streptococcus pneumoniae", + "Clostridioides difficile" + ] + + (1..5).map do |n| + FactoryBot.create(:strain, id: 10_060 + n, title: bacteria_names[n-1], organism: organism, projects: [project], contributor: person) + end + end + + def create_data_files(project, person) + file_types = [ + "Comma-Separated Values", + "JavaScript Object Notation", + "Extensible Markup Language", + "Apache Parquet", + "Portable Document Format" + ] + (1..5).map do |n| + FactoryBot.create(:min_data_file, id: 10_070 + n, title: "My #{file_types[n-1]} file", projects: [project], contributor: person) + end + end + + def create_sops(project, person) + lab_protocols = [ + "Standard Operating Procedure for High-Performance Liquid Chromatography (HPLC) Analysis", + "Protocol for DNA Isolation and Purification Using the CTAB Method", + "Polymerase Chain Reaction (PCR) Program for Target Sequence Amplification", + "Protocol for Protein Extraction and SDS-PAGE Analysis", + "Standard Procedure for Chemical Spill Response and Hazardous Waste Disposal" + ] + + (1..5).map do |n| + FactoryBot.create(:sop, id: 10_080 + n, title: lab_protocols[n-1], projects: [project], contributor: person) + end + end end diff --git a/test/unit/helpers/single_pages_helper_test.rb b/test/unit/helpers/single_pages_helper_test.rb new file mode 100644 index 0000000000..25089b0f2c --- /dev/null +++ b/test/unit/helpers/single_pages_helper_test.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'test_helper' + +class SinglePagesHelperTest < ActiveSupport::TestCase + include AuthenticatedTestHelper + include SinglePagesHelper + + def setup + @person = FactoryBot.create(:person) + @project = @person.projects.first + @investigation = FactoryBot.create(:investigation, projects: [@project], is_isa_json_compliant: true, contributor: @person) + @study = FactoryBot.create(:isa_json_compliant_study, investigation: @investigation, contributor: @person) + + string_attr_type = FactoryBot.create(:string_sample_attribute_type) + integer_attr_type = FactoryBot.create(:integer_sample_attribute_type) + reg_sample_attr_type = FactoryBot.create(:sample_sample_attribute_type) + reg_sample_mutli_attr_type = FactoryBot.create(:sample_multi_sample_attribute_type) + reg_data_file_attr_type = FactoryBot.create(:data_file_sample_attribute_type) + cv_attr_type = FactoryBot.create(:controlled_vocab_attribute_type) + cv_list_attr_type = FactoryBot.create(:cv_list_attribute_type) + strain_attr_type = FactoryBot.create(:strain_sample_attribute_type) + sop_sample_attr_type = FactoryBot.create(:sop_sample_attribute_type) + + @cars_sample_type = FactoryBot.create( + :sample_type, + title: 'Cars', + projects: [@project], + contributor: @person, + sample_attributes: [ + FactoryBot.build(:sample_attribute, title: 'name', sample_attribute_type: string_attr_type, required: true, is_title: true), + FactoryBot.build(:sample_attribute, title: 'brand', sample_attribute_type: string_attr_type), + FactoryBot.build(:sample_attribute, title: 'model', sample_attribute_type: string_attr_type), + FactoryBot.build(:sample_attribute, title: 'race_number', sample_attribute_type: integer_attr_type), + ] + ) + + @drivers_sample_type = FactoryBot.create( + :sample_type, + title: 'Drivers', + projects: [@project], + contributor: @person, + sample_attributes: [ + FactoryBot.build(:sample_attribute, title: 'name', sample_attribute_type: string_attr_type, required: true, is_title: true), + FactoryBot.build(:sample_attribute, title: 'team', sample_attribute_type: string_attr_type), + FactoryBot.build(:sample_attribute, title: 'victories', sample_attribute_type: integer_attr_type), + FactoryBot.build(:sample_attribute, title: 'crashes', sample_attribute_type: integer_attr_type), + FactoryBot.build(:sample_attribute, title: 'ranking', sample_attribute_type: integer_attr_type), + ] + ) + + race_cars.each do |car| + sample = FactoryBot.build(:sample, title: car[:name], sample_type: @cars_sample_type, projects: [@project], contributor: @person) + sample.set_attribute_value(:name, car[:name]) + sample.set_attribute_value(:brand, car[:brand]) + sample.set_attribute_value(:model, car[:model]) + sample.set_attribute_value(:race_number, car[:race_number]) + sample.save + end + + drivers.each do |driver| + sample = FactoryBot.build(:sample, title: driver[:name], sample_type: @drivers_sample_type, projects: [@project], contributor: @person) + sample.set_attribute_value(:name, driver[:name]) + sample.set_attribute_value(:team, driver[:team]) + sample.set_attribute_value(:victories, driver[:victories]) + sample.set_attribute_value(:crashes, driver[:crashes]) + sample.set_attribute_value(:ranking, driver[:ranking]) + sample.save + end + + sample_attributes = [ + FactoryBot.build(:sample_attribute, title: 'Title', sample_attribute_type: string_attr_type, required: true, is_title: true), + FactoryBot.build(:sample_attribute, title: 'Driver', sample_attribute_type: reg_sample_attr_type, required: false, is_title: false, linked_sample_type: @drivers_sample_type), + FactoryBot.build(:sample_attribute, title: 'Cars', sample_attribute_type: reg_sample_mutli_attr_type, required: false, is_title: false, linked_sample_type: @cars_sample_type), + FactoryBot.build(:sample_attribute, title: 'Registered Data File', sample_attribute_type: reg_data_file_attr_type, required: false, is_title: false), + FactoryBot.build(:sample_attribute, title: 'Apples Controlled Vocab', sample_attribute_type: cv_attr_type, required: false, is_title: false, sample_controlled_vocab: FactoryBot.create(:apples_sample_controlled_vocab, title: 'apples cv', key: 'apple')), + FactoryBot.build(:sample_attribute, title: 'Topics Controlled Vocab List', sample_attribute_type: cv_list_attr_type, required: false, is_title: false, sample_controlled_vocab: FactoryBot.create(:topics_controlled_vocab, title: 'topics cv list', key: 'top')), + FactoryBot.build(:sample_attribute, title: 'Registered Strain', sample_attribute_type: strain_attr_type, required: false, is_title: false), + FactoryBot.build(:sample_attribute, title: 'Registered SOP', sample_attribute_type: sop_sample_attr_type, required: false, is_title: false), + ] + @assay_sample_type = FactoryBot.create(:sample_type, title: "Assay Sample Type", projects: [@project], contributor: @person, sample_attributes: sample_attributes) + @assay = FactoryBot.create(:assay, study: @study, contributor: @person, sample_type: @assay_sample_type) + + organism = FactoryBot.create(:organism, title: "E. coli", projects: [@project]) + (1..3).each do |i| + FactoryBot.create(:min_sop, title: "Assay SOP #{i}", projects: [@project], contributor: @person) + FactoryBot.create(:data_file, title: "Data File #{i}", projects: [@project], contributor: @person) + FactoryBot.create(:strain, title: "E. coli strain #{i}", organism: organism, projects: [@project], contributor: @person) + end + end + + test 'should require (excel) data validation' do + # Title: String attributes do not require data validation + refute requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Title" }) + + # Driver: Seek Sample attributes require data validation + assert requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Driver" }) + + # Cars: Seek Sample Multi attributes do not require data validation + refute requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Cars" }) + + # Registered Data File: Seek Data File attributes require data validation + assert requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Registered Data File" }) + + # Apples Controlled Vocab: CV attributes require data validation + assert requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Apples Controlled Vocab" }) + + # Topics Controlled Vocab List: CV List attributes do not require data validation + refute requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Topics Controlled Vocab List" }) + + # Registered Strain: Seek Strain attributes require data validation + assert requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Registered Strain" }) + + # Registered SOP: Seek SOP attributes require data validation + assert requires_data_validation?(@assay_sample_type.sample_attributes.detect { |sa| sa.title == "Registered SOP" }) + end + + test 'should ge sample values for CV attributes' do + # Apples Controlled Vocab is of type CV + cv_attr = @assay_sample_type.sample_attributes.detect { |sa| sa.title == "Apples Controlled Vocab" } + apple_labels = SampleControlledVocab.find_by(title: 'apples cv').labels + apple_values = get_values_for_cv(cv_attr) + assert apple_values.all? { |av| apple_labels.include? av } + end + + test 'should get sample values for Seek Sample attributes' do + # Driver attribute is of type Seek Sample + reg_sample_attr = @assay_sample_type.sample_attributes.detect { |sa| sa.title == "Driver" } + reg_sample_values = get_values_for_registered_samples(reg_sample_attr) + driver_ids = @drivers_sample_type.samples.pluck(:id) + assert reg_sample_values.all? { |rsv| driver_ids.include?(rsv[:id]) } + + # Cars attribute is of type Seek Sample Multi + reg_sample_mult_attr = @assay_sample_type.sample_attributes.detect { |sa| sa.title == "Cars" } + reg_sample_multi_values = get_values_for_registered_samples(reg_sample_mult_attr) + car_ids = @cars_sample_type.samples.pluck(:id) + assert reg_sample_multi_values.all? { |rsmv| car_ids.include?(rsmv[:id]) } + end + + test 'should ge sample values for Seek Data File attributes' do + # Registered Data File attribute is of type Seek Data File + reg_data_file_attr = @assay_sample_type.sample_attributes.detect { |sa| sa.title == "Registered Data File" } + reg_data_file_values = get_values_for_datafiles(reg_data_file_attr) + data_file_ids = @project.data_files.pluck(:id) + assert reg_data_file_values.all? { |data_file| data_file_ids.include?(data_file[:id]) } + end + + test 'should ge sample values for Seek Strain attributes' do + # Registered Strain attribute is of type Seek Strain + strain_attr = @assay_sample_type.sample_attributes.detect { |sa| sa.title == "Registered Strain" } + strain_values = get_values_for_strains(strain_attr) + strain_ids = @project.strains.pluck(:id) + assert strain_values.all? { |sv| strain_ids.include?(sv[:id]) } + end + + test 'should ge sample values for Seek SOP attributes' do + # Registered SOP attribute is of type Seek SOP + sop_attr = @assay_sample_type.sample_attributes.detect { |sa| sa.title == "Registered SOP" } + sop_values = get_values_for_sops(sop_attr) + sop_ids = @assay.sops.pluck(:id) + assert sop_values.all? { |sv| sop_ids.include?(sv[:id]) } + end + + private + + def race_cars + [ + { model: "R8 LMS GT3", brand: "Audi", name: "Silver Arrow", race_number: 12 }, + { model: "488 GT3 Evo", brand: "Ferrari", name: "Red Fury", race_number: 27 }, + { model: "AMG GT3", brand: "Mercedes-Benz", name: "Black Panther", race_number: 44 }, + { model: "911 GT3 R", brand: "Porsche", name: "White Lightning", race_number: 91 }, + { model: "Huracán GT3 EVO", brand: "Lamborghini", name: "Green Beast", race_number: 63 }, + { model: "Supra GT500", brand: "Toyota", name: "Samurai Speed", race_number: 37 }, + { model: "M6 GT3", brand: "BMW", name: "Blue Thunder", race_number: 99 }, + { model: "Vantage GT3", brand: "Aston Martin", name: "British Bullet", race_number: 7 } + ] + end + + def drivers + [ + { name: "Alex Hunter", team: "Audi Sport", victories: 15, crashes: 3, ranking: 1 }, + { name: "Marco Rossi", team: "Ferrari Racing", victories: 12, crashes: 5, ranking: 2 }, + { name: "Liam Carter", team: "Mercedes-AMG", victories: 10, crashes: 2, ranking: 3 }, + { name: "Sven Müller", team: "Porsche Motorsport", victories: 8, crashes: 4, ranking: 4 }, + { name: "Diego Alvarez", team: "Lamborghini Squadra Corse", victories: 7, crashes: 6, ranking: 5 }, + { name: "Hiro Tanaka", team: "Toyota Gazoo Racing", victories: 6, crashes: 3, ranking: 6 }, + { name: "Max Bauer", team: "BMW Motorsport", victories: 5, crashes: 7, ranking: 7 }, + { name: "Oliver Grant", team: "Aston Martin Racing", victories: 4, crashes: 2, ranking: 8 } + ] + end + +end