diff --git a/app/api/chemotion/chemical_api.rb b/app/api/chemotion/chemical_api.rb index c26cc9d9ef..7aa80525a4 100644 --- a/app/api/chemotion/chemical_api.rb +++ b/app/api/chemotion/chemical_api.rb @@ -58,7 +58,7 @@ class ChemicalAPI < Grape::API get do Chemotion::ChemicalsService.handle_exceptions do data = params[:data] - molecule = Molecule.find(params[:id]) + molecule = Molecule.find(params[:id]) if params[:id] != 'null' vendor = data[:vendor] language = data[:language] case data[:option] diff --git a/app/api/chemotion/report_api.rb b/app/api/chemotion/report_api.rb index 4386b72fc8..9d2b736a3d 100644 --- a/app/api/chemotion/report_api.rb +++ b/app/api/chemotion/report_api.rb @@ -56,34 +56,24 @@ def is_int? end c_id = params[:uiState][:currentCollection] c_id = SyncCollectionsUser.find(c_id)&.collection_id if params[:uiState][:isSync] - %i[sample reaction wellplate].each do |table| - next unless (p_t = params[:uiState][table]) - ids = p_t[:checkedAll] ? p_t[:uncheckedIds] : p_t[:checkedIds] - next unless p_t[:checkedAll] || ids.present? + table_params = { + ui_state: params[:uiState], + c_id: c_id, + } - column_query = build_column_query(filter_column_selection(table), current_user.id) - sql_query = send("build_sql_#{table}_sample", column_query, c_id, ids, p_t[:checkedAll]) - next unless sql_query - - result = db_exec_query(sql_query) - export.generate_sheet_with_samples(table, result) + if params[:columns][:chemicals].blank? + generate_sheets_for_tables(%i[sample reaction wellplate], table_params, export) end if params[:exportType] == 1 && params[:columns][:analyses].present? - %i[sample].each do |table| - next unless (p_t = params[:uiState][table]) - - ids = p_t[:checkedAll] ? p_t[:uncheckedIds] : p_t[:checkedIds] - next unless p_t[:checkedAll] || ids - - column_query = build_column_query(filter_column_selection("#{table}_analyses".to_sym), current_user.id) - sql_query = send("build_sql_#{table}_analyses", column_query, c_id, ids, p_t[:checkedAll]) - next unless sql_query + generate_sheets_for_tables(%i[sample], table_params, export, params[:columns][:analyses], :analyses) + end - result = db_exec_query(sql_query) - export.generate_analyses_sheet_with_samples("#{table}_analyses".to_sym, result, params[:columns][:analyses]) - end + if params[:exportType] == 1 && params[:columns][:chemicals].present? + generate_sheets_for_tables(%i[sample], table_params, export, params[:columns][:chemicals], + :chemicals) + generate_sheets_for_tables(%i[reaction wellplate], table_params, export) end case export.file_extension diff --git a/app/api/chemotion/sample_api.rb b/app/api/chemotion/sample_api.rb index 22a4611b63..437505908c 100644 --- a/app/api/chemotion/sample_api.rb +++ b/app/api/chemotion/sample_api.rb @@ -96,7 +96,7 @@ class SampleAPI < Grape::API if file_size < 25_000 import = Import::ImportSamples.new( params[:file][:tempfile].path, - params[:currentCollectionId], current_user.id, file['filename'] + params[:currentCollectionId], current_user.id, file['filename'], params[:import_type] ) import_result = import.process if import_result[:status] == 'ok' || import_result[:status] == 'warning' @@ -117,6 +117,7 @@ class SampleAPI < Grape::API user_id: current_user.id, file_name: file['filename'], file_path: tmp_file_path, + import_type: params[:import_type], } ImportSamplesJob.perform_later(parameters) { status: 'in progress', message: 'Importing samples in background' } diff --git a/app/api/helpers/report_helpers.rb b/app/api/helpers/report_helpers.rb index 63657df7a5..4a4cac2133 100644 --- a/app/api/helpers/report_helpers.rb +++ b/app/api/helpers/report_helpers.rb @@ -192,6 +192,76 @@ def build_sql(table, columns, c_id, ids, checkedAll = false) # from collections sync_colls assigned to user # - selected columns from samples, molecules table # + + def generate_sheet(table, result, columns_params, export, type) + case type + when :analyses + sheet_name = "#{table}_analyses".to_sym + export.generate_analyses_sheet_with_samples(sheet_name, result, columns_params) + when :chemicals + sheet_name = "#{table}_chemicals" + format_result = Export::ExportChemicals.format_chemical_results(result) + export.generate_sheet_with_samples(sheet_name, format_result, columns_params) + else + export.generate_sheet_with_samples(table, result) + end + end + + def build_sql_query(table, current_user, sql_params, type) + tables = %i[sample reaction wellplate] + filter_parameter = if tables.include?(table) && type.nil? + table + else + "#{table}_#{type}".to_sym + end + type ||= :sample + filter_selections = filter_column_selection(filter_parameter) + column_query = build_column_query(filter_selections, current_user.id) + send("build_sql_#{table}_#{type}", column_query, sql_params[:c_id], sql_params[:ids], sql_params[:checked_all]) + end + + def generate_sheets_for_tables(tables, table_params, export, columns_params = nil, type = nil) + tables.each do |table| + next unless (p_t = table_params[:ui_state][table]) + + checked_all = p_t[:checkedAll] + + ids = checked_all ? p_t[:uncheckedIds] : p_t[:checkedIds] + next unless checked_all || ids.present? + + sql_params = { + c_id: table_params[:c_id], ids: ids, checked_all: checked_all + } + sql_query = build_sql_query(table, current_user, sql_params, type) + next unless sql_query + + result = db_exec_query(sql_query) + generate_sheet(table, result, columns_params, export, type) + end + end + + def sample_details_subquery(u_ids, selection) + # Extract sample details subquery + <<~SQL.squish + select + s.id as s_id + , s.is_top_secret as ts + , min(co.id) as co_id + , min(scu.id) as scu_id + , bool_and(co.is_shared) as shared_sync + , max(GREATEST(co.permission_level, scu.permission_level)) as pl + , max(GREATEST(co.sample_detail_level,scu.sample_detail_level)) dl_s + from samples s + inner join collections_samples c_s on s.id = c_s.sample_id and c_s.deleted_at is null + left join collections co on (co.id = c_s.collection_id and co.user_id in (#{u_ids})) + left join collections sco on (sco.id = c_s.collection_id and sco.user_id not in (#{u_ids})) + left join sync_collections_users scu on (sco.id = scu.collection_id and scu.user_id in (#{u_ids})) + where #{selection} s.deleted_at isnull and c_s.deleted_at isnull + and (co.id is not null or scu.id is not null) + group by s_id + SQL + end + def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) s_ids = [ids].flatten.join(',') u_ids = [user_ids].flatten.join(',') @@ -200,6 +270,7 @@ def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) if checkedAll return unless c_id + collection_join = " inner join collections_samples c_s on s_id = c_s.sample_id and c_s.deleted_at is null and c_s.collection_id = #{c_id} " order = 's_id asc' selection = s_ids.empty? && '' || "s.id not in (#{s_ids}) and" @@ -208,35 +279,49 @@ def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) selection = "s.id in (#{s_ids}) and" end - <<~SQL + rest_of_selections = if columns[0].is_a?(Array) + columns[0][0] + else + columns + end + s_subquery = sample_details_subquery(u_ids, selection) + + <<~SQL.squish select s_id, ts, co_id, scu_id, shared_sync, pl, dl_s , res.residue_type, s.molfile_version, s.decoupled, s.molecular_mass as "molecular mass (decoupled)", s.sum_formula as "sum formula (decoupled)" , s.stereo->>'abs' as "stereo_abs", s.stereo->>'rel' as "stereo_rel" - , #{columns} - from ( - select - s.id as s_id - , s.is_top_secret as ts - , min(co.id) as co_id - , min(scu.id) as scu_id - , bool_and(co.is_shared) as shared_sync - , max(GREATEST(co.permission_level, scu.permission_level)) as pl - , max(GREATEST(co.sample_detail_level,scu.sample_detail_level)) dl_s - from samples s - inner join collections_samples c_s on s.id = c_s.sample_id and c_s.deleted_at is null - left join collections co on (co.id = c_s.collection_id and co.user_id in (#{u_ids})) - left join collections sco on (sco.id = c_s.collection_id and sco.user_id not in (#{u_ids})) - left join sync_collections_users scu on (sco.id = scu.collection_id and scu.user_id in (#{u_ids})) - where #{selection} s.deleted_at isnull and c_s.deleted_at isnull - and (co.id is not null or scu.id is not null) - group by s_id - ) as s_dl + , #{rest_of_selections} + from (#{s_subquery}) as s_dl inner join samples s on s_dl.s_id = s.id #{collection_join} left join molecules m on s.molecule_id = m.id left join molecule_names mn on s.molecule_name_id = mn.id left join residues res on res.sample_id = s.id - order by #{order}; + order by #{order} + SQL + end + + def chemical_query(chemical_columns, sample_ids) + individual_queries = sample_ids.map do |s_id| + <<~SQL.squish + SELECT #{s_id} AS chemical_sample_id, #{chemical_columns} + FROM chemicals c + WHERE c.sample_id = #{s_id} + SQL + end + individual_queries.join(' UNION ALL ') + end + + def build_sql_sample_chemicals(columns, c_id, ids, checked_all) + sample_query = build_sql_sample_sample(columns[0].join(','), c_id, ids, checked_all) + return nil if sample_query.blank? + + chemical_query = chemical_query(columns[1].join(','), ids) + <<~SQL.squish + SELECT * + FROM (#{sample_query}) AS sample_results + JOIN (#{chemical_query}) AS chemical_results + ON sample_results.s_id = chemical_results.chemical_sample_id SQL end @@ -245,6 +330,7 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) u_ids = [user_ids].flatten.join(',') return if columns.empty? || u_ids.empty? return if !checkedAll && s_ids.empty? + t = 's' # table samples cont_type = 'Sample' # containable_type if checkedAll @@ -256,8 +342,9 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) order = "position(','||s_id::text||',' in '(,#{s_ids},)')" selection = "s.id in (#{s_ids}) and" end + s_subquery = sample_details_subquery(u_ids, selection) - <<~SQL + <<~SQL.squish select s_id, ts, co_id, scu_id, shared_sync, pl, dl_s , #{columns} @@ -292,24 +379,7 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) where cont.containable_type = '#{cont_type}' and cont.containable_id = #{t}.id ) analysis ) as analyses - from ( - select - s.id as s_id - , s.is_top_secret as ts - , min(co.id) as co_id - , min(scu.id) as scu_id - , bool_and(co.is_shared) as shared_sync - , max(GREATEST(co.permission_level, scu.permission_level)) as pl - , max(GREATEST(co.sample_detail_level,scu.sample_detail_level)) dl_s - from samples s - inner join collections_samples c_s on s.id = c_s.sample_id and c_s.deleted_at is null - left join collections co on (co.id = c_s.collection_id and co.user_id in (#{u_ids})) - left join collections sco on (sco.id = c_s.collection_id and sco.user_id not in (#{u_ids})) - left join sync_collections_users scu on (sco.id = scu.collection_id and scu.user_id in (#{u_ids})) - where #{selection} s.deleted_at isnull and c_s.deleted_at isnull - and (co.id is not null or scu.id is not null) - group by s_id - ) as s_dl + from (#{s_subquery}) as s_dl inner join samples s on s_dl.s_id = s.id #{collection_join} order by #{order}; SQL @@ -460,17 +530,17 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) sample: { external_label: ['s.external_label', '"sample external label"', 0], name: ['s."name"', '"sample name"', 0], - cas: ['s.xref', nil, 0], + cas: ['s.xref', '"cas"', 0], target_amount_value: ['s.target_amount_value', '"target amount"', 0], target_amount_unit: ['s.target_amount_unit', '"target unit"', 0], real_amount_value: ['s.real_amount_value', '"real amount"', 0], real_amount_unit: ['s.real_amount_unit', '"real unit"', 0], - description: ['s.description', nil, 0], + description: ['s.description', '"description"', 0], molfile: ["encode(s.molfile, 'escape')", 'molfile', 1], - purity: ['s.purity', nil, 0], - solvent: ['s.solvent', nil, 0], + purity: ['s.purity', '"purity"', 0], + solvent: ['s.solvent', '"solvent"', 0], # impurities: ['s.impurities', nil, 0], - location: ['s.location', nil, 0], + location: ['s.location', '"location"', 0], is_top_secret: ['s.is_top_secret', '"secret"', 10], # ancestry: ['s.ancestry', nil, 10], short_label: ['s.short_label', '"short label"', 0], @@ -478,11 +548,11 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) sample_svg_file: ['s.sample_svg_file', 'image', 1], molecule_svg_file: ['m.molecule_svg_file', 'm_image', 1], identifier: ['s.identifier', nil, 1], - density: ['s.density', nil, 0], + density: ['s.density', '"density"', 0], melting_point: ['s.melting_point', '"melting pt"', 0], boiling_point: ['s.boiling_point', '"boiling pt"', 0], - created_at: ['s.created_at', nil, 0], - updated_at: ['s.updated_at', nil, 0], + created_at: ['s.created_at', '"created at"', 0], + updated_at: ['s.updated_at', '"updated_at"', 0], # deleted_at: ['wp.deleted_at', nil, 10], molecule_name: ['mn."name"', '"molecule name"', 1], molarity_value: ['s."molarity_value"', '"molarity_value"', 0], @@ -492,7 +562,7 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) external_label: ['s.external_label', '"sample external label"', 0], name: ['s."name"', '"sample name"', 0], short_label: ['s.short_label', '"short label"', 0], - #molecule_name: ['mn."name"', '"molecule name"', 1] + # molecule_name: ['mn."name"', '"molecule name"', 1] }, molecule: { cano_smiles: ['m.cano_smiles', '"canonical smiles"', 10], @@ -563,27 +633,34 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) }, }.freeze - # desc: concatenate columns to be queried + def custom_column_query(table, col, selection, user_id, attrs) + if col == 'user_labels' + selection << "labels_by_user_sample(#{user_id}, s_id) as user_labels" + elsif col == 'literature' + selection << "literatures_by_element('Sample', s_id) as literatures" + elsif col == 'cas' + selection << "s.xref->>'cas' as cas" + elsif (s = attrs[table][col.to_sym]) + selection << ("#{s[1] && s[0]} as #{s[1] || s[0]}") + end + end + def build_column_query(sel, user_id = 0, attrs = EXP_MAP_ATTR) selection = [] - attrs.keys.each do |table| + attrs.each_key do |table| sel.symbolize_keys.fetch(table, []).each do |col| - if col == 'user_labels' - selection << "labels_by_user_sample(#{user_id}, s_id) as user_labels" - elsif col == 'literature' - selection << "literatures_by_element('Sample', s_id) as literatures" - elsif col == 'cas' - selection << "s.xref->>'cas' as cas" - elsif (s = attrs[table][col.to_sym]) - selection << (s[1] && s[0] + ' as ' + s[1] || s[0]) - end + custom_column_query(table, col, selection, user_id, attrs) end end - selection.join(', ') + selection = if sel[:chemicals].present? + Export::ExportChemicals.build_chemical_column_query(selection, sel) + else + selection.join(',') + end end - def filter_column_selection(type, columns = params[:columns]) - case type.to_sym + def filter_column_selection(table, columns = params[:columns]) + case table.to_sym when :sample columns.slice(:sample, :molecule) when :reaction @@ -591,11 +668,13 @@ def filter_column_selection(type, columns = params[:columns]) when :wellplate columns.slice(:sample, :molecule, :wellplate) when :sample_analyses - # FIXME: slice analyses + process properly + # FIXME: slice analyses + process properly columns.slice(:analyses).merge(sample_id: params[:columns][:sample]) # TODO: reaction analyses data # when :reaction_analyses # columns.slice(:analysis).merge(reaction_id: params[:columns][:reaction]) + when :sample_chemicals + columns.slice(:chemicals, :sample, :molecule) else {} end @@ -670,6 +749,7 @@ def force_molfile_selection def default_columns_reaction DEFAULT_COLUMNS_REACTION end + def default_columns_wellplate DEFAULT_COLUMNS_WELLPLATE end diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index 40a2817dfc..421ac68e2a 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -193,7 +193,7 @@ $font-icons-research_plan: "\f0f6"; .exportModal { width: 60%; - height: auto; + height: 100%; } .importChemDrawModal { diff --git a/app/jobs/import_samples_job.rb b/app/jobs/import_samples_job.rb index f058ffd3b4..a0b1d53fdf 100644 --- a/app/jobs/import_samples_job.rb +++ b/app/jobs/import_samples_job.rb @@ -25,7 +25,12 @@ def perform(params) begin case file_format when '.xlsx' - import = Import::ImportSamples.new(file_path, params[:collection_id], @user_id, params[:file_name]) + import = Import::ImportSamples.new( + file_path, + params[:collection_id], + @user_id, params[:file_name], + params[:import_type] + ) @result = import.process when '.sdf' sdf_import = Import::ImportSdf.new( diff --git a/app/packs/src/components/ChemicalTab.js b/app/packs/src/components/ChemicalTab.js index 102971adae..c37cd87ebb 100644 --- a/app/packs/src/components/ChemicalTab.js +++ b/app/packs/src/components/ChemicalTab.js @@ -228,8 +228,8 @@ export default class ChemicalTab extends React.Component { precautionaryPhrases.push(st); } - const pictogramsArray = str.pictograms ? str.pictograms.map((i) => (i !== null - ? : null)) : []; + const pictogramsArray = str.pictograms?.map((i) => ( + i !== null ? : null)); return (
diff --git a/app/packs/src/components/contextActions/ExportImportButton.js b/app/packs/src/components/contextActions/ExportImportButton.js index 107ce4ac64..813b1fb53e 100644 --- a/app/packs/src/components/contextActions/ExportImportButton.js +++ b/app/packs/src/components/contextActions/ExportImportButton.js @@ -11,58 +11,82 @@ import ModalReactionExport from 'src/components/contextActions/ModalReactionExpo import ModalExportCollection from 'src/components/contextActions/ModalExportCollection'; import ModalExportRadarCollection from 'src/components/contextActions/ModalExportRadarCollection'; import ModalImportCollection from 'src/components/contextActions/ModalImportCollection'; -import { elementShowOrNew } from 'src/utilities/routesUtils.js' +import { elementShowOrNew } from 'src/utilities/routesUtils.js'; -const ExportImportButton = ({ isDisabled, updateModalProps, customClass }) => { - const showRadar = UIStore.getState().hasRadar? ( +function ExportImportButton({ isDisabled, updateModalProps, customClass }) { + const showRadar = UIStore.getState().hasRadar ? ( <> - editMetadataFunction()} - disabled={isDisabled} - title='Edit metadata'> + editMetadataFunction()} + disabled={isDisabled} + title="Edit metadata" + > Edit collection metadata - exportCollectionToRadarFunction(updateModalProps)} disabled={isDisabled} - title='Export to RADAR'> - Publish current collection via RADAR - + exportCollectionToRadarFunction(updateModalProps)} + disabled={isDisabled} + title="Export to RADAR" + > + Publish current collection via RADAR + - ): ; + ) : ; return ( - + - + + - exportFunction(updateModalProps)} - title='Export to spreadsheet'> + exportFunction(updateModalProps)} + title="Export to spreadsheet" + > Export samples from selection - exportReactionFunction(updateModalProps)} - title='Export reaction smiles to csv'> + exportReactionFunction(updateModalProps)} + title="Export reaction smiles to csv" + > Export reactions from selection - importSampleFunction(updateModalProps)} disabled={isDisabled} - title='Import from spreadsheet or sdf'> + importSampleFunction(updateModalProps, false)} + disabled={isDisabled} + title="Import from spreadsheet or sdf" + > Import samples to collection + importSampleFunction(updateModalProps, true)} + disabled={isDisabled} + title="Import chemicals from spreadsheet" + > + Import chemicals to collection + - exportCollectionFunction(updateModalProps)} - title='Export as ZIP archive'> + exportCollectionFunction(updateModalProps)} + title="Export as ZIP archive" + > Export collections - importCollectionFunction(updateModalProps)} - title='Import collections from ZIP archive'> + importCollectionFunction(updateModalProps)} + title="Import collections from ZIP archive" + > Import collections {showRadar} - ) -}; + ); +} ExportImportButton.propTypes = { isDisabled: PropTypes.bool, @@ -73,9 +97,8 @@ ExportImportButton.defaultProps = { isDisabled: false, customClass: null, }; - -const importSampleFunction = (updateModalProps) => { - const title = "Import Samples from File"; +const importSampleFunction = (updateModalProps, importAsChemical) => { + const title = importAsChemical ? 'Import Chemicals from File' : 'Import Samples from File'; const component = ModalImport; const action = ElementActions.importSamplesFromFile; const listSharedCollections = false; @@ -90,30 +113,30 @@ const importSampleFunction = (updateModalProps) => { }; const exportFunction = (updateModalProps) => { - const title = "Select Data to Export"; + const title = 'Select Data to Export'; const component = ModalExport; const modalProps = { show: true, title, component, - customModal: "exportModal" + customModal: 'exportModal' }; updateModalProps(modalProps); -} +}; const exportReactionFunction = (updateModalProps) => { const component = ModalReactionExport; const modalProps = { show: true, - title: "Reaction Smiles Export", + title: 'Reaction Smiles Export', component, - customModal: "exportModal" + customModal: 'exportModal' }; updateModalProps(modalProps); -} +}; const exportCollectionFunction = (updateModalProps) => { - const title = "Export Collections as ZIP archive"; + const title = 'Export Collections as ZIP archive'; const component = ModalExportCollection; const action = CollectionActions.exportCollectionsToFile; const full = false; @@ -129,10 +152,10 @@ const exportCollectionFunction = (updateModalProps) => { }; updateModalProps(modalProps); -} +}; const importCollectionFunction = (updateModalProps) => { - const title = "Import Collections from ZIP archive"; + const title = 'Import Collections from ZIP archive'; const component = ModalImportCollection; const action = CollectionActions.importCollectionsFromFile; const listSharedCollections = false; @@ -149,17 +172,17 @@ const importCollectionFunction = (updateModalProps) => { }; const editMetadataFunction = () => { - const { currentCollection, isSync } = UIStore.getState(); - const uri = isSync - ? `/scollection/${currentCollection.id}/metadata` - : `/collection/${currentCollection.id}/metadata`; - Aviator.navigate(uri, { silent: true} ); - - elementShowOrNew({ - type: 'metadata', - params: { collectionID: currentCollection.id } - }); -} + const { currentCollection, isSync } = UIStore.getState(); + const uri = isSync + ? `/scollection/${currentCollection.id}/metadata` + : `/collection/${currentCollection.id}/metadata`; + Aviator.navigate(uri, { silent: true }); + + elementShowOrNew({ + type: 'metadata', + params: { collectionID: currentCollection.id } + }); +}; const exportCollectionToRadarFunction = (updateModalProps) => { const title = "Publish current collection via RADAR"; @@ -177,4 +200,4 @@ const exportCollectionToRadarFunction = (updateModalProps) => { updateModalProps(modalProps); }; -export default ExportImportButton +export default ExportImportButton; diff --git a/app/packs/src/components/contextActions/ModalExport.js b/app/packs/src/components/contextActions/ModalExport.js index 8c2cd39c62..9272bb2550 100644 --- a/app/packs/src/components/contextActions/ModalExport.js +++ b/app/packs/src/components/contextActions/ModalExport.js @@ -1,5 +1,7 @@ import React from 'react'; -import { Button, ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap'; +import { + Button, ButtonToolbar, DropdownButton, MenuItem +} from 'react-bootstrap'; import CheckBoxs from 'src/components/common/CheckBoxs'; import UIStore from 'src/stores/alt/stores/UIStore'; import UserStore from 'src/stores/alt/stores/UserStore'; @@ -40,63 +42,91 @@ export default class ModalExport extends React.Component { this.state = { columns: { sample: [ - { value: "cas", text: "cas", checked: true }, - { value: ["sample_svg_file", "molecule_svg_file"], text: "image", checked: false }, - { value: "name", text: "name", checked: true }, - { value: "molecule_name", text: "molecule name", checked: false }, - { value: "external_label", text: "external label", checked: true }, - { value: "short_label", text: "short label", checked: false }, - { value: "description", text: "description", checked: false }, - { value: ["real_amount_value", "real_amount_unit"], text: "real amount", checked: true }, - { value: ["target_amount_value", "target_amount_unit"], text: "target amount", checked: false }, - { value: ["molarity_value", "molarity_unit"], text: "molarity", checked: false }, - { value: "density", text: "density", checked: false }, - { value: "molfile", text: "molfile", checked: false }, + { value: 'cas', text: 'cas', checked: true }, + { value: ['sample_svg_file', 'molecule_svg_file'], text: 'image', checked: false }, + { value: 'name', text: 'name', checked: true }, + { value: 'molecule_name', text: 'molecule name', checked: false }, + { value: 'external_label', text: 'external label', checked: true }, + { value: 'short_label', text: 'short label', checked: false }, + { value: 'description', text: 'description', checked: false }, + { value: ['real_amount_value', 'real_amount_unit'], text: 'real amount', checked: true }, + { value: ['target_amount_value', 'target_amount_unit'], text: 'target amount', checked: false }, + { value: ['molarity_value', 'molarity_unit'], text: 'molarity', checked: false }, + { value: 'density', text: 'density', checked: false }, + { value: 'molfile', text: 'molfile', checked: false }, // {value: "purity", text: "purity", checked: false}, { value: "solvent", text: "solvent", checked: false }, { value: "location", text: "location", checked: false }, { value: "is_top_secret", text: "is top secret?", checked: false }, { value: "dry_solvent", text: "dry solvent", checked: false }, // {value: "ancestry", text: "ancestry", checked: false}, - { value: "imported_readout", text: "imported readout", checked: false }, + { value: 'imported_readout', text: 'imported readout', checked: false }, // {value: "identifier", text: "identifier", checked: false}, - { value: "melting_point", text: "melting point", checked: false }, - { value: "boiling_point", text: "boiling point", checked: false }, - { value: "created_at", text: "created at", checked: true }, - { value: "updated_at", text: "updated at", checked: false }, - { value: "user_labels", text: "user labels", checked: false }, - { value: "literature", text: "literature", checked: false }, + { value: 'melting_point', text: 'melting point', checked: false }, + { value: 'boiling_point', text: 'boiling point', checked: false }, + { value: 'created_at', text: 'created at', checked: true }, + { value: 'updated_at', text: 'updated at', checked: false }, + { value: 'user_labels', text: 'user labels', checked: false }, + { value: 'literature', text: 'literature', checked: false }, ], molecule: [ - { value: "cano_smiles", text: "canonical smiles", checked: true }, - { value: "inchistring", text: "InChIstring", checked: false }, - { value: "inchikey", text: "InChIkey", checked: false }, - { value: "sum_formular", text: "sum formula", checked: false }, - { value: "molecular_weight", text: "molecular weight", checked: false }, + { value: 'cano_smiles', text: 'canonical smiles', checked: true }, + { value: 'inchistring', text: 'InChIstring', checked: false }, + { value: 'inchikey', text: 'InChIkey', checked: false }, + { value: 'sum_formular', text: 'sum formula', checked: false }, + { value: 'molecular_weight', text: 'molecular weight', checked: false }, ], reaction: [ - { value: "name", text: "reaction name", checked: true }, - { value: "short_label", text: "r short label", checked: true }, - { value: "reference", text: "reference", checked: false }, - { value: "equivalent", text: "equivalent", checked: false }, + { value: 'name', text: 'reaction name', checked: true }, + { value: 'short_label', text: 'r short label', checked: true }, + { value: 'reference', text: 'reference', checked: false }, + { value: 'equivalent', text: 'equivalent', checked: false }, ], wellplate: [ - { value: "name", text: "wellplate name", checked: false }, - { value: "position_x", text: "well x", checked: false }, - { value: "position_y", text: "well y", checked: false }, + { value: 'name', text: 'wellplate name', checked: false }, + { value: 'position_x', text: 'well x', checked: false }, + { value: 'position_y', text: 'well y', checked: false }, ], analyses: [ - { value: "name", text: "analysis name", checked: false }, - { value: "description", text: "analysis description", checked: false }, - { value: "kind", text: "analysis type", checked: false }, - { value: "content", text: "analysis content", checked: false }, - { value: "status", text: "analysis status", checked: false }, - { value: "uuid", text: "uuid", checked: false }, - { value: "dataset name", text: "dataset name", checked: false }, - { value: "dataset description", text: "dataset description", checked: false }, - { value: "instrument", text: "instrument", checked: false }, - { value: "filename", text: "file name", checked: false }, - { value: "checksum", text: "file checksum", checked: false }, + { value: 'name', text: 'analysis name', checked: false }, + { value: 'description', text: 'analysis description', checked: false }, + { value: 'kind', text: 'analysis type', checked: false }, + { value: 'content', text: 'analysis content', checked: false }, + { value: 'status', text: 'analysis status', checked: false }, + { value: 'uuid', text: 'uuid', checked: false }, + { value: 'dataset name', text: 'dataset name', checked: false }, + { value: 'dataset description', text: 'dataset description', checked: false }, + { value: 'instrument', text: 'instrument', checked: false }, + { value: 'filename', text: 'file name', checked: false }, + { value: 'checksum', text: 'file checksum', checked: false }, + ], + chemicals: [ + { value: 'status', text: 'status', checked: false }, + { value: 'vendor', text: 'vendor', checked: false }, + { value: 'order_number', text: 'order number', checked: false }, + { value: 'amount', text: 'amount', checked: false }, + { value: 'price', text: 'price', checked: false }, + { value: 'person', text: 'person', checked: false }, + { value: 'required_date', text: 'required date', checked: false }, + { value: 'ordered_date', text: 'ordered date', checked: false }, + { value: 'required_by', text: 'required by', checked: false }, + { value: ['safety_sheet_link_merck', 'safety_sheet_link_thermofischer'], text: 'safety sheet link', checked: false }, + { value: ['product_link_merck', 'product_link_thermofischer'], text: 'product link', checked: false }, + { value: 'pictograms', text: 'pictograms', checked: false }, + { value: 'h_statements', text: 'h statements', checked: false }, + { value: 'p_statements', text: 'p statements', checked: false }, + { value: 'host_building', text: 'host building', checked: false }, + { value: 'host_room', text: 'host room', checked: false }, + { value: 'host_cabinet', text: 'host cabinet', checked: false }, + { value: 'host_group', text: 'host group', checked: false }, + { value: 'owner', text: 'owner', checked: false }, + { value: 'current_building', text: 'current building', checked: false }, + { value: 'current_room', text: 'current room', checked: false }, + { value: 'current_cabinet', text: 'current cabinet', checked: false }, + { value: 'current_group', text: 'current group', checked: false }, + { value: 'borrowed_by', text: 'borrowed by', checked: false }, + { value: 'disposal_info', text: 'disposal info', checked: false }, + { value: 'important_notes', text: 'important notes', checked: false }, ], }, checkedAllColumns: { @@ -107,7 +137,7 @@ export default class ModalExport extends React.Component { analysis: false, }, }; - this.handleClick = this.handleClick.bind(this) + this.handleClick = this.handleClick.bind(this); } toggleColumns(text, checked, section) { @@ -143,6 +173,10 @@ export default class ModalExport extends React.Component { this.toggleColumns(text, checked, 'analyses'); } + toggleColumnsChemicals(text, checked) { + this.toggleColumns(text, checked, 'chemicals'); + } + toggleColumnsAll(section) { this.setState((prevState) => { const { columns, checkedAllColumns } = prevState; @@ -174,22 +208,33 @@ export default class ModalExport extends React.Component { this.toggleColumnsAll('analyses'); } + toggleColumnsAllChemicals(text, checked) { + this.toggleColumnsAll('chemicals'); + } + buttonBar() { const { onHide } = this.props; + const chemicalColumns = this.filteredColumns(); + const sdfChemicalExport = chemicalColumns.chemicals.length !== 0; return (
- + XLSX Export - SDF Export + SDF Export
- ) + ); } handleClick(e) { @@ -255,9 +300,15 @@ export default class ModalExport extends React.Component { toggleCheckAll={this.toggleColumnsAllAnalyses.bind(this)} checkedAll={this.state.checkedAllColumns.analyses} /> +

Chemicals

+
{this.buttonBar()} - ) + ); } } diff --git a/app/packs/src/components/contextActions/ModalImport.js b/app/packs/src/components/contextActions/ModalImport.js index 104e19e914..495885a6ab 100644 --- a/app/packs/src/components/contextActions/ModalImport.js +++ b/app/packs/src/components/contextActions/ModalImport.js @@ -16,28 +16,30 @@ export default class ModalImport extends React.Component { handleClick() { const { onHide, action } = this.props; const { file } = this.state; - let ui_state = UIStore.getState(); - let params = { - file: file, - currentCollectionId: ui_state.currentCollection.id - } + const uiState = UIStore.getState(); + const importSampleAs = uiState.modalParams.title === 'Import Chemicals from File' ? 'chemical' : 'sample'; + const params = { + file, + currentCollectionId: uiState.currentCollection.id, + type: importSampleAs + }; action(params); onHide(); - let notification = { - title: "Uploading", - message: "The file is being processed. Please wait...", - level: "warning", + const notification = { + title: 'Uploading', + message: 'The file is being processed. Please wait...', + level: 'warning', dismissible: false, - uid: "import_samples_upload", - position: "bl" - } + uid: 'import_samples_upload', + position: 'bl' + }; NotificationActions.add(notification); } - handleFileDrop(attachment_file) { - this.setState({ file: attachment_file[0] }); + handleFileDrop(attachmentFile) { + this.setState({ file: attachmentFile[0] }); } handleAttachmentRemove() { @@ -71,7 +73,7 @@ export default class ModalImport extends React.Component { isDisabled() { const { file } = this.state; - return file == null + return file == null; } render() { @@ -85,6 +87,6 @@ export default class ModalImport extends React.Component { - ) + ); } } diff --git a/app/packs/src/components/navigation/Navigation.js b/app/packs/src/components/navigation/Navigation.js index a857c5b818..f5a81a2240 100644 --- a/app/packs/src/components/navigation/Navigation.js +++ b/app/packs/src/components/navigation/Navigation.js @@ -96,9 +96,8 @@ export default class Navigation extends React.Component { } updateModalProps(modalProps) { - this.setState({ - modalProps: modalProps - }); + this.setState({ modalProps }); + UIActions.updateModalProps(modalProps); } advancedSearch(filters) { diff --git a/app/packs/src/fetchers/SamplesFetcher.js b/app/packs/src/fetchers/SamplesFetcher.js index ff45999681..c41b364e3c 100644 --- a/app/packs/src/fetchers/SamplesFetcher.js +++ b/app/packs/src/fetchers/SamplesFetcher.js @@ -156,6 +156,7 @@ export default class SamplesFetcher { const data = new FormData(); data.append('file', params.file); data.append('currentCollectionId', params.currentCollectionId); + data.append('import_type', params.type); const promise = fetch('/api/v1/samples/import/', { credentials: 'same-origin', diff --git a/lib/chemotion/chemicals_service.rb b/lib/chemotion/chemicals_service.rb index dd9084b849..3f40df9e1b 100644 --- a/lib/chemotion/chemicals_service.rb +++ b/lib/chemotion/chemicals_service.rb @@ -91,19 +91,22 @@ def self.health_section(product_number) .children[1].children[1].children[1] end - def self.construct_h_statements(h_phrases) - h_phrases_hash = JSON.parse(File.read('./public/json/hazardPhrases.json')) + def self.construct_h_statements(h_phrases, vendor = nil) h_statements = {} - h_phrases.each do |element| + h_phrases_hash = JSON.parse(File.read('./public/json/hazardPhrases.json')) + + h_array = vendor == 'merck' ? h_phrases[1].split(/\s[+-]\s/) : h_phrases + h_array.each do |element| h_phrases_hash.map { |k, v| k == element ? h_statements[k] = " #{v}" : nil } end h_statements end - def self.construct_p_statements(p_phrases) - p_phrases_hash = JSON.parse(File.read('./public/json/precautionaryPhrases.json')) + def self.construct_p_statements(p_phrases, vendor = nil) p_statements = {} - p_phrases.each do |element| + p_phrases_hash = JSON.parse(File.read('./public/json/precautionaryPhrases.json')) + p_array = vendor == 'merck' ? p_phrases[2].split(/\s[+-]\s/) : p_phrases + p_array.each do |element| p_phrases_hash.map { |k, v| k == element ? p_statements[k] = " #{v}" : nil } end p_statements @@ -136,32 +139,12 @@ def self.safety_section(product_link) .xpath("//*[contains(@class, '#{search_string}')]") end - def self.construct_h_statements_merck(safety_array) - h_statements = {} - h_array = safety_array[1].split(/\s[+-]\s/) - h_phrases_hash = JSON.parse(File.read('./public/json/hazardPhrases.json')) - h_array.each do |element| - h_phrases_hash.map { |k, v| k == element ? h_statements[k] = " #{v}" : nil } - end - h_statements - end - - def self.construct_p_statements_merck(safety_array) - p_statements = {} - p_phrases_hash = JSON.parse(File.read('./public/json/precautionaryPhrases.json')) - p_array = safety_array[2].split(/\s[+-]\s/) - p_array.each do |element| - p_phrases_hash.map { |k, v| k == element ? p_statements[k] = " #{v}" : nil } - end - p_statements - end - def self.safety_phrases_merck(product_link) safety_section = safety_section(product_link) safety_array = safety_section.children.reject { |i| i.text.empty? }.map(&:text) pictograms = safety_array[0].split(',') - { 'h_statements' => construct_h_statements_merck(safety_array), - 'p_statements' => construct_p_statements_merck(safety_array), + { 'h_statements' => construct_h_statements(safety_array, 'merck'), + 'p_statements' => construct_p_statements(safety_array, 'merck'), 'pictograms' => pictograms } rescue StandardError 'Could not find H and P phrases' diff --git a/lib/export/export_chemicals.rb b/lib/export/export_chemicals.rb new file mode 100644 index 0000000000..8efd6514de --- /dev/null +++ b/lib/export/export_chemicals.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +module Export + class ExportChemicals + CHEMICAL_FIELDS = %w[ + chemical_sample_id cas status vendor order_number amount price person required_date + ordered_date required_by pictograms h_statements p_statements safety_sheet_link_merck + safety_sheet_link_thermofischer product_link_merck product_link_thermofischer + host_building host_room host_cabinet host_group owner borrowed_by current_building + current_room current_cabinet current_group disposal_info important_notes + ].freeze + MERCK_SDS_LINK = 'c."chemical_data"->0->\'merckProductInfo\'->\'sdsLink\'' + ALFA_SDS_LINK = 'c."chemical_data"->0->\'alfaProductInfo\'->\'sdsLink\'' + MERCK_PRODUCT_LINK = 'c."chemical_data"->0->\'merckProductInfo\'->\'productLink\'' + ALFA_PRODUCT_LINK = 'c."chemical_data"->0->\'alfaProductInfo\'->\'productLink\'' + SAFETY_SHEET_INFO = %w[safety_sheet_link product_link].freeze + CHEMICAL_QUERIES = { + status: ['c."chemical_data"->0->\'status\'', '"status"', nil], + vendor: ['c."chemical_data"->0->\'vendor\'', '"vendor"', nil], + person: ['c."chemical_data"->0->\'person\'', '"person"', nil], + price: ['c."chemical_data"->0->\'price\'', '"price"', nil], + amount: ['c."chemical_data"->0->\'amount\'', '"amount"', nil], + order_number: ['c."chemical_data"->0->\'order_number\'', '"order_number"', nil], + required_date: ['c."chemical_data"->0->\'required_date\'', '"required_date"', nil], + required_by: ['c."chemical_data"->0->\'required_by\'', '"required_by"', nil], + safety_sheet_link_merck: [MERCK_SDS_LINK, '"safety_sheet_link_merck"', nil], + safety_sheet_link_thermofischer: [ALFA_SDS_LINK, '"safety_sheet_link_thermofischer"', nil], + product_link_merck: [MERCK_PRODUCT_LINK, '"product_link_merck"', nil], + product_link_thermofischer: [ALFA_PRODUCT_LINK, '"product_link_thermofischer"', nil], + ordered_date: ['c."chemical_data"->0->\'ordered_date\'', '"ordered_date"', nil], + h_statements: ['c."chemical_data"->0->\'safetyPhrases\'->\'h_statements\'', '"h_statements"', nil], + p_statements: ['c."chemical_data"->0->\'safetyPhrases\'->\'p_statements\'', '"p_statements"', nil], + pictograms: ['c."chemical_data"->0->\'safetyPhrases\'->\'pictograms\'', '"pictograms"', nil], + host_building: ['c."chemical_data"->0->\'host_building\'', '"host_building"', nil], + host_room: ['c."chemical_data"->0->\'host_room\'', '"host_room"', nil], + host_cabinet: ['c."chemical_data"->0->\'host_cabinet\'', '"host_cabinet"', nil], + host_group: ['c."chemical_data"->0->\'host_group\'', '"host_group"', nil], + owner: ['c."chemical_data"->0->\'host_owner\'', '"owner"', nil], + current_building: ['c."chemical_data"->0->\'current_building\'', '"current_building"', nil], + current_room: ['c."chemical_data"->0->\'current_room\'', '"current_room"', nil], + current_cabinet: ['c."chemical_data"->0->\'current_cabinet\'', '"current_cabinet"', nil], + current_group: ['c."chemical_data"->0->\'current_group\'', '"current_group"', nil], + borrowed_by: ['c."chemical_data"->0->\'borrowed_by\'', '"borrowed_by"', nil], + disposal_info: ['c."chemical_data"->0->\'disposal_info\'', '"disposal_info"', nil], + important_notes: ['c."chemical_data"->0->\'important_notes\'', '"important_notes"', nil], + }.freeze + + def self.build_chemical_column_query(selection, sel) + chemical_selections = [] + sel[:chemicals].each do |col| + query = CHEMICAL_QUERIES[col.to_sym] + chemical_selections << ("#{query[2]} as #{query[1]}") if SAFETY_SHEET_INFO.include?(col) + chemical_selections << ("#{query[0]} as #{query[1]}") + end + gathered_selections = [] + gathered_selections << selection + gathered_selections << chemical_selections + end + + def self.format_chemical_results(result) + columns_index = { 'safety_sheet_link' => [], 'product_link' => [] } + result.columns.map.with_index do |column_name, index| + column_name, columns_index = construct_column_name(column_name, index, columns_index) + result.columns[index] = column_name # Replace the value in the array + end + format_chemical_results_row(result, columns_index) + end + + def self.construct_column_name(column_name, index, columns_index) + format_chemical_column = ['p statements', 'h statements', 'amount', 'safety sheet link thermofischer', + 'safety sheet link merck', 'product link thermofischer', 'product link merck'].freeze + if column_name.is_a?(String) && CHEMICAL_FIELDS.include?(column_name) + column_name = column_name.tr('_', ' ') + construct_column_name_hash(columns_index, column_name, index) if format_chemical_column.include?(column_name) + else + column_name + end + [column_name, columns_index] + end + + def self.construct_column_name_hash(columns_index, column_name, index) + case column_name + when 'p statements' + columns_index['p_statements'] = index + when 'h statements' + columns_index['h_statements'] = index + when 'amount' + columns_index['amount'] = index + when 'safety sheet link merck', 'safety sheet link thermofischer' + columns_index['safety_sheet_link'].push(index) + when 'product link merck', 'product link thermofischer' + columns_index['product_link'].push(index) + end + end + + def self.format_chemical_results_row(result, columns_index) + indexes_to_delete = [] + result.rows.map! do |row| + format_row(row, columns_index, indexes_to_delete) + end + return result if indexes_to_delete.empty? + + merge_safety_sheets_columns_rows(result, indexes_to_delete, columns_index) + end + + def self.format_row(row, columns_index, indexes_to_delete) + row.map.with_index do |value, index| + next value unless value.is_a?(String) + + case index + when columns_index['p_statements'], columns_index['h_statements'] + value = format_p_and_h_statements(value) + when columns_index['amount'] + value = format_chemical_amount(value) + when columns_index['safety_sheet_link'][0] + value = format_link(value, row, columns_index['safety_sheet_link'][1], indexes_to_delete) + when columns_index['product_link'][0] + value = format_link(value, row, columns_index['product_link'][1], indexes_to_delete) + end + value.gsub(/[\[\]"]/, '') + end + end + + def self.format_p_and_h_statements(value) + keys = JSON.parse(value).keys + keys.join('-') + end + + def self.format_chemical_amount(value) + amount_value_unit = JSON.parse(value).values + sorted = amount_value_unit.sort_by { |element| [element.is_a?(Integer) || element.is_a?(Float) ? 0 : 1, element] } + sorted.join + end + + def self.format_link(value, row, next_index, indexes_to_delete) + # binding.pry + if next_index && row[next_index].present? + value += "-#{row[next_index]}" + indexes_to_delete.push(next_index) + end + value + end + + def self.merge_safety_sheets_columns_rows(result, indexes_to_delete, columns_index) + process_to_delete_indexes(result, indexes_to_delete) + process_merged_columns(result, columns_index) if indexes_to_delete.empty? + result + end + + def self.process_to_delete_indexes(result, indexes_to_delete) + indexes_to_delete.sort.reverse_each do |index| + result.columns.delete_at(index) + result.rows.each { |row| row.delete_at(index) } + format_columns_name(result, index - 1) + end + end + + def self.process_merged_columns(result, columns_index) + format_columns_name(result, columns_index['safety_sheet_link'][0], columns_index['product_link'][0]) + delete_columns(result, columns_index['safety_sheet_link'][1], columns_index['product_link'][1]) + end + + def self.format_columns_name(result, *indexes) + indexes.sort.reverse_each do |index| + result.columns[index] = result.columns[index].sub(/\s+\S+\z/, '') + end + end + + def self.delete_columns(result, *indexes) + indexes.sort.reverse_each do |index| + result.columns.delete_at(index) + result.rows.each { |row| row.delete_at(index) } + end + end + end +end diff --git a/lib/export/export_excel.rb b/lib/export/export_excel.rb index 904ad1babf..49f23536b4 100644 --- a/lib/export/export_excel.rb +++ b/lib/export/export_excel.rb @@ -11,11 +11,11 @@ def initialize(**args) @xfile.workbook.styles.fonts.first.name = 'Calibri' end - def generate_sheet_with_samples(table, samples = nil) + def generate_sheet_with_samples(table, samples = nil, selected_columns = nil) @samples = samples return if samples.nil? # || samples.count.zero? - generate_headers(table) + generate_headers(table, [], selected_columns) sheet = @xfile.workbook.add_worksheet(name: table.to_s) # do |sheet| grey = sheet.styles.add_style(sz: 12, border: { style: :thick, color: 'FF777777', edges: [:bottom] }) sheet.add_row(@headers, style: grey) # Add header @@ -39,7 +39,7 @@ def generate_sheet_with_samples(table, samples = nil) image_width = row_image_width if row_image_width > image_width # 3/4 -> The misterious ratio! if filtered_sample[decouple_idx].present? - filtered_sample[decouple_idx] = filtered_sample[decouple_idx].presence == 't' ? 'Yes' : 'No' + filtered_sample[decouple_idx] = filtered_sample[decouple_idx].presence == true ? 'yes' : 'No' end size = sheet.styles.add_style :sz => 12 @@ -114,7 +114,7 @@ def literatures_info(ids) end def filter_with_permission_and_detail_level(sample) - # return all data if sample in own collection + # return all data if sample/chemical in own collection if sample['shared_sync'] == 'f' || sample['shared_sync'] == false headers = @headers reference_values = ['melting pt', 'boiling pt'] @@ -143,7 +143,6 @@ def filter_with_permission_and_detail_level(sample) data = headers.map { |column| column ? sample[column] : nil } data[@image_index] = svg_path(sample) if headers.include?('image') end - data end diff --git a/lib/export/export_table.rb b/lib/export/export_table.rb index d657f1ecf1..7f7239e500 100644 --- a/lib/export/export_table.rb +++ b/lib/export/export_table.rb @@ -77,7 +77,7 @@ def generate_headers(table, excluded_columns = [], selected_columns = []) when :sample_analyses generate_headers_sample_id add_analyses_header(selected_columns) - when :sample + when :sample, :sample_chemicals generate_headers_sample else generate_headers_sample_id end @@ -110,6 +110,13 @@ def generate_headers_reaction } end + def format_headers(headers) + headers.map! do |header| + header.tr('_', ' ') + end + headers + end + def generate_headers_sample @headers00 = @headers.map { |column| HEADERS_SAMPLE_0.include?(column) ? column : nil @@ -117,6 +124,7 @@ def generate_headers_sample @headers100 = @headers.map { |column| HEADERS_SAMPLE_10.include?(column) ? column : nil } + @headers = format_headers(@headers) end def generate_headers_sample_id diff --git a/lib/import/import_chemicals.rb b/lib/import/import_chemicals.rb new file mode 100644 index 0000000000..03b2f66571 --- /dev/null +++ b/lib/import/import_chemicals.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +module Import + class ImportChemicals + SAFETY_PHRASES = %w[pictograms h_statements p_statements].freeze + AMOUNT = %w[amount].freeze + SAFETY_SHEET = %w[safety_sheet_link_merck product_link_merck].freeze + KEYS_TO_EXCLUDE = SAFETY_SHEET + %w[cas].freeze + SIGMA_ALDRICH_PATTERN = /(sigmaaldrich|merck)/.freeze + THERMOFISCHER_PATTERN = /(thermofischer|alfa)/.freeze + SAFETY_SHEET_PATH = '/safety_sheets/' + VENDOR_MAP = { + SIGMA_ALDRICH_PATTERN => 'Merck', + THERMOFISCHER_PATTERN => 'Alfa', + }.freeze + CHEMICAL_FIELDS = [ + 'cas', 'status', 'vendor', 'order number', 'amount', 'price', 'person', 'required date', 'ordered date', + 'required by', 'pictograms', 'h statements', 'p statements', 'safety sheet link', 'product link', 'host building', + 'host room', 'host cabinet', 'host group', 'owner', 'borrowed by', 'current building', 'current room', + 'current cabinet', 'current group', 'disposal info', 'important notes' + ].freeze + GHS_VALUES = %w[GHS01 GHS02 GHS03 GHS04 GHS05 GHS06 GHS07 GHS08 GHS09].freeze + AMOUNT_UNITS = %w[g mg μg].freeze + + def self.build_chemical(row, header) + chemical = Chemical.new + chemical.chemical_data = [{}] + import_data(chemical, row, header) + end + + def self.import_data(chemical, row, header) + header.each do |column_header| + value = row[column_header] + next if skip_import?(value, column_header) + + if column_header == 'cas' + chemical.cas = value + else + process_column(chemical, column_header, value) + end + end + chemical + end + + def self.skip_import?(value, column_header) + value.blank? || column_header.nil? + end + + def self.process_column(chemical, column_header, value) + map_column = CHEMICAL_FIELDS.find { |e| e == column_header.downcase.rstrip } + key = to_snake_case(column_header) + format_value = value.strip + if map_column.present? && should_process_key(key) + chemical['chemical_data'][0][key] = format_value + elsif SAFETY_SHEET.include?(key) + set_safety_sheet(chemical, key, format_value) + elsif SAFETY_PHRASES.include?(key) + set_safety_phrases(chemical, key, format_value) + elsif AMOUNT.include?(key) + set_amount(chemical, format_value) + end + end + + def self.to_snake_case(column_header) + key = column_header.downcase.rstrip.gsub(/\s+/, '_') + key == 'owner' ? 'host_owner' : key + end + + def self.should_process_key(key) + KEYS_TO_EXCLUDE.exclude?(key) && (AMOUNT + SAFETY_PHRASES).exclude?(key) + end + + def self.set_safety_sheet(chemical, key, value) + vendor = detect_vendor(value) + return unless vendor + + product_info = handle_safety_sheet(key, vendor, value, chemical) + product_info_key = "#{vendor.downcase}ProductInfo" + chemical['chemical_data'][0][product_info_key] ||= {} + chemical['chemical_data'][0][product_info_key].merge!(product_info || {}) + rescue StandardError => e + raise "Error setting safety sheet info for chemical: #{e.message}" + end + + def self.detect_vendor(value) + VENDOR_MAP.each do |pattern, vendor| + return vendor if value.present? && value.match?(pattern) + end + nil + end + + def self.handle_safety_sheet(key, vendor, value, chemical) + case key + when 'safety_sheet_link_merck' + product_number = extract_product_number(value) + create_safety_sheet_path(vendor.downcase, value, product_number, chemical) if product_number.present? + set_safety_sheet_link(vendor, product_number, value) if product_number.present? + when 'product_link_merck' + { 'productLink' => value } + end + end + + def self.extract_product_number(url) + match = url.match(/productNumber=(\d+)/) || url.match(/sku=(\w+)/) + if match + match[1] + else + path = url.split('/') + path.last + end + end + + def self.create_safety_sheet_path(vendor, value, product_number, chemical) + file_path = "#{product_number}_#{vendor.capitalize}.pdf" + chemical['chemical_data'][0]['safetySheetPath'] ||= [] + sheet_path = { "#{vendor}_link" => "#{SAFETY_SHEET_PATH}#{file_path}" } + is_created = Chemotion::ChemicalsService.create_sds_file(file_path, value) + result = [true, 'file is already saved'].include?(is_created) + chemical['chemical_data'][0]['safetySheetPath'] << sheet_path if result + chemical + end + + def self.set_safety_sheet_link(vendor, product_number, value) + { + 'vendor' => vendor, + 'productNumber' => product_number, + 'sdsLink' => value, + } + end + + def self.check_available_ghs_values(values) + ghs_values_to_set = [] + values.each do |value| + format_value = value.strip + ghs_values_to_set.push(format_value) if GHS_VALUES.include?(format_value) + end + ghs_values_to_set + end + + def self.assign_phrases(key, values, phrases) + case key + when 'pictograms' + value = check_available_ghs_values(values) + phrases[key] = value + when 'h_statements' + value = Chemotion::ChemicalsService.construct_h_statements(values) + phrases[key] = value + when 'p_statements' + value = Chemotion::ChemicalsService.construct_p_statements(values) + phrases[key] = value + end + end + + def self.set_safety_phrases(chemical, key, value) + phrases = chemical['chemical_data'][0]['safetyPhrases'] ||= {} + values = value.split(/,|-/) + assign_phrases(key, values, phrases) + end + + def self.set_amount(chemical, value) + chemical['chemical_data'][0]['amount'] = {} if chemical['chemical_data'][0]['amount'].nil? + quantity = value.to_f + unit = value.gsub(/\d+(\.\d+)?/, '') + return chemical unless AMOUNT_UNITS.include?(unit) + + chemical['chemical_data'][0]['amount']['value'] = quantity + chemical['chemical_data'][0]['amount']['unit'] = unit + end + end +end diff --git a/lib/import/import_samples.rb b/lib/import/import_samples.rb index 1ecf8fe166..bb0618e704 100644 --- a/lib/import/import_samples.rb +++ b/lib/import/import_samples.rb @@ -8,7 +8,7 @@ class ImportSamples attr_reader :xlsx, :sheet, :header, :mandatory_check, :rows, :unprocessable, :processed, :file_path, :collection_id, :current_user_id, :file_name - def initialize(file_path, collection_id, user_id, file_name) + def initialize(file_path, collection_id, user_id, file_name, import_type) @rows = [] @unprocessable = [] @processed = [] @@ -16,6 +16,7 @@ def initialize(file_path, collection_id, user_id, file_name) @collection_id = collection_id @current_user_id = user_id @file_name = file_name + @import_type = import_type end def process @@ -46,7 +47,7 @@ def check_required_fields @sheet = xlsx.sheet(0) @header = sheet.row(1) @mandatory_check = {} - ['molfile', 'smiles', 'cano_smiles', 'canonical smiles'].each do |check| + ['molfile', 'smiles', 'cano_smiles', 'canonical smiles', 'decoupled'].each do |check| @mandatory_check[check] = true if header.find { |e| /^\s*#{check}?/i =~ e } end @@ -55,11 +56,7 @@ def check_required_fields end def extract_molfile_and_molecule(row) - # If molfile and smiles (Canonical smiles) is both present - # Double check the rows - if molfile?(row) && smiles?(row) - get_data_from_molfile_and_smiles(row) - elsif molfile?(row) + if molfile?(row) get_data_from_molfile(row) elsif smiles?(row) get_data_from_smiles(row) @@ -68,19 +65,20 @@ def extract_molfile_and_molecule(row) def process_row(data) row = [header, xlsx.row(data)].transpose.to_h - - return unless structure?(row) || row['decoupled'] == 'Yes' + is_decoupled = row['decoupled'].casecmp('yes').zero? if row['decoupled'].present? + return unless structure?(row) || is_decoupled rows << row.each_pair { |k, v| v && row[k] = v.to_s } end def process_row_data(row) - return Molecule.find_or_create_dummy if row['decoupled'] == 'Yes' && !structure?(row) + is_decoupled = row['decoupled'].casecmp('yes').zero? if row['decoupled'].present? + return Molecule.find_or_create_dummy if is_decoupled && !structure?(row) - molfile, molecule = extract_molfile_and_molecule(row) + molecule, molfile = extract_molfile_and_molecule(row) return if molfile.nil? || molecule.nil? - [molfile, molecule] + [molecule, molfile] end def molecule_not_exist(molecule) @@ -93,13 +91,13 @@ def write_to_db begin ActiveRecord::Base.transaction do rows.map.with_index do |row, i| - molfile, molecule = process_row_data(row) + molecule, molfile = process_row_data(row) if molecule_not_exist(molecule) unprocessable_count += 1 next end sample_save(row, molfile, molecule) - rescue StandardError + rescue StandardError => _e unprocessable_count += 1 @unprocessable << { row: row, index: i } end @@ -135,7 +133,7 @@ def get_data_from_molfile_and_smiles(row) @unprocessable << { row: row, index: i } go_to_next = true end - [molfile, go_to_next] + [go_to_next, molfile] end def get_data_from_molfile(row) @@ -143,7 +141,7 @@ def get_data_from_molfile(row) babel_info = Chemotion::OpenBabelService.molecule_info_from_molfile(molfile) inchikey = babel_info[:inchikey] molecule = Molecule.find_or_create_by_molfile(molfile, babel_info) if inchikey.presence - [molfile, molecule] + [molecule, molfile] end def assign_molecule_data(molfile_coord, babel_info, inchikey, row) @@ -158,7 +156,7 @@ def assign_molecule_data(molfile_coord, babel_info, inchikey, row) molecul.assign_molecule_data babel_info, pubchem_info end end - [molfile_coord, molecule, go_to_next] + [molecule, molfile_coord, go_to_next] end def get_data_from_smiles(row) @@ -211,18 +209,30 @@ def format_to_interval_syntax(row_field) "[#{lower_bound}, #{upper_bound}]" end + def handle_sample_fields(sample, db_column, value) + if db_column == 'cas' + sample['xref']['cas'] = value + else + sample[db_column] = value || '' + end + end + def process_sample_fields(sample, db_column, field, row) - return unless included_fields.include?(db_column) + return unless included_fields.include?(db_column) || db_column == 'cas' excluded_column = %w[description solvent location external_label].freeze comparison_values = %w[melting_point boiling_point].freeze value = row[field] value = format_to_interval_syntax(value) if comparison_values.include?(db_column) - - sample[db_column] = value || '' + handle_sample_fields(sample, db_column, value) sample[db_column] = '' if excluded_column.include?(db_column) && row[field].nil? - sample[db_column] = row[field] == 'Yes' if %w[decoupled].include?(db_column) + sample[db_column] = row[field].casecmp('yes').zero? if %w[decoupled].include?(db_column) + end + + def save_chemical(chemical, sample) + chemical.sample_id = sample.id + chemical.save! end def validate_sample_and_save(sample, stereo, row) @@ -230,23 +240,33 @@ def validate_sample_and_save(sample, stereo, row) sample.validate_stereo(stereo) sample.collections << Collection.find(collection_id) sample.collections << Collection.get_all_collection_for_user(current_user_id) + sample.inventory_sample = true if @import_type == 'chemical' + chemical = ImportChemicals.build_chemical(row, header) if @import_type == 'chemical' sample.save! + save_chemical(chemical, sample) if @import_type == 'chemical' processed.push(sample) end - def sample_save(row, molfile, molecule) + def create_sample_and_assign_molecule(current_user_id, molfile, molecule) sample = Sample.new(created_by: current_user_id) sample.molfile = molfile sample.molecule = molecule + sample + end + + # rubocop:disable Style/StringLiterals + def sample_save(row, molfile, molecule) + sample = create_sample_and_assign_molecule(current_user_id, molfile, molecule) stereo = {} header.each do |field| stereo[Regexp.last_match(1)] = row[field] if field.to_s.strip =~ /^stereo_(abs|rel)$/ map_column = ReportHelpers::EXP_MAP_ATTR[:sample].values.find { |e| e[1] == "\"#{field}\"" } - db_column = map_column.nil? ? field : map_column[0].sub('s.', '').delete!('"') + db_column = map_column.nil? || map_column[1] == "\"cas\"" ? field : map_column[0].sub('s.', '').delete!('"') process_sample_fields(sample, db_column, field, row) end validate_sample_and_save(sample, stereo, row) end + # rubocop:enable Style/StringLiterals def process_all_rows (2..xlsx.last_row).each do |data| @@ -296,7 +316,7 @@ def excluded_fields # 'melting_point', # 'boiling_point', 'fingerprint_id', - 'xref', + # 'xref', # 'molarity_value', # 'molarity_unit', 'molecule_name_id', diff --git a/spec/lib/chemotion/chemicals_service_spec.rb b/spec/lib/chemotion/chemicals_service_spec.rb index 9cb124fc8e..8be85b6de8 100644 --- a/spec/lib/chemotion/chemicals_service_spec.rb +++ b/spec/lib/chemotion/chemicals_service_spec.rb @@ -133,30 +133,6 @@ end end - context 'when construct_h_statements_merck is called' do - it 'constructs hazard statements from the health section' do - safety_array = ['GHS02', 'H226 - H301', 'P201 - P210'] - h_statements = described_class.construct_h_statements_merck(safety_array) - expect(h_statements).to be_a(Hash) - expect(h_statements.keys).to contain_exactly('H226', 'H301') - expect(h_statements.values).to contain_exactly(' Flammable liquid and vapour', ' Toxic if swallowed') - end - end - - context 'when construct_p_statements_merck is called' do - it 'constructs precautionary statements for merck vendor' do - safety_array = ['GHS02', 'H226 - H301', 'P201 - P102 + P103'] - p_statements = described_class.construct_p_statements_merck(safety_array) - expect(p_statements).to be_a(Hash) - expect(p_statements.keys).to contain_exactly('P201', 'P102', 'P103') - expect(p_statements.values).to contain_exactly( - ' Obtain special instructions before use.', - ' Keep out of reach of children.', - ' Read label before use.', - ) - end - end - context 'when chem_properties_alfa is called' do it 'constructs chemical properties hash for alfa vendor' do properties = [ diff --git a/spec/lib/export/export_chemicals_spec.rb b/spec/lib/export/export_chemicals_spec.rb new file mode 100644 index 0000000000..4d8c102eff --- /dev/null +++ b/spec/lib/export/export_chemicals_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# rubocop: disable Style/OpenStructUse + +describe Export::ExportChemicals do + describe '.format_chemical_amount' do + it 'formats chemical amount correctly' do + input_value = '{"value": 50, "unit": "mg"}' + formatted_amount = described_class.format_chemical_amount(input_value) + expect(formatted_amount).to eq('50mg') + end + end + + describe '.format_columns_name' do + it 'format columns name' do + result = OpenStruct.new(columns: ['safety sheet link merck', 'product link thermofischer']) + indexes = [0, 1] + described_class.format_columns_name(result, *indexes) + expect(result.columns).to eq(['safety sheet link', 'product link']) + end + end + + describe '.delete_columns' do + it 'delete specific columns and rows' do + result = OpenStruct.new(columns: ['safety sheet link', 'safety sheet link thermofischer', + 'product link', 'product link thermofischer'], + rows: [['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', '', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376', nil]]) + indexes = [1, 3] + described_class.delete_columns(result, *indexes) + expect(result.columns).to eq(['safety sheet link', 'product link']) + expect(result.rows[0]).to eq(['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376']) + end + end + + describe '.process_merged_columns' do + it 'process merged columns' do + columns_index = { + 'safety_sheet_link' => [0, 1], + 'product_link' => [2, 3], + } + result = OpenStruct.new(columns: ['safety sheet link merck', 'safety sheet link thermofischer', + 'product link merck', 'product link thermofischer'], + rows: [['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', '', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376', nil]]) + described_class.process_merged_columns(result, columns_index) + expect(result.columns).to eq(['safety sheet link', 'product link']) + expect(result.rows[0]).to eq(['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376']) + end + end + + describe '.process_to_delete_indexes' do + it 'process to delete indexes' do + result = OpenStruct.new(columns: ['safety sheet link merck', 'safety sheet link thermofischer', + 'product link merck', 'product link thermofischer'], + rows: [['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', '', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376', nil]]) + indexes_to_delete = [1, 3] + described_class.process_to_delete_indexes(result, indexes_to_delete) + expect(result.columns).to eq(['safety sheet link', 'product link']) + expect(result.rows[0]).to eq(['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376']) + end + end + + describe '.merge_safety_sheets_columns_rows' do + it 'merge safety sheets columns rows' do + columns_index = { + 'safety_sheet_link' => [0, 1], + 'product_link' => [2, 3], + } + result = OpenStruct.new(columns: ['safety sheet link merck', 'safety sheet link thermofischer', + 'product link merck', 'product link thermofischer'], + rows: [['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', '', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376', nil]]) + indexes_to_delete = [1, 3] + described_class.merge_safety_sheets_columns_rows(result, indexes_to_delete, columns_index) + expect(result.columns).to eq(['safety sheet link', 'product link']) + expect(result.rows[0]).to eq(['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376']) + end + end + + describe '.format_p_and_h_statements' do + it 'formats p and h statements' do + value = '{"key1": "value1", "key2": "value2"}' + expect(described_class.format_p_and_h_statements(value)).to eq('key1-key2') + end + end + + describe '.format_link' do + it 'formats link value' do + row = ['', 'next_value'] + next_index = 1 + indexes_to_delete = [] + value = 'current_value' + expect(described_class.format_link(value, row, next_index, indexes_to_delete)) + .to eq('current_value-next_value') + expect(indexes_to_delete).to eq([next_index]) + end + end + + describe '.format_row' do + it 'formats the row' do + columns_index = { + 'p_statements' => 1, + 'h_statements' => 2, + 'amount' => 3, + 'safety_sheet_link' => [4, 5], + 'product_link' => [6, 7], + } + indexes_to_delete = [] + + row = [ + 'value1', + '{"key1": "value1", "key2": "value2"}', + '{"key3": "value3", "key4": "value4"}', + '{"unit":"g", "value": "300"}', + 'safety_link_value', + 'safety_link_value2', + 'product_link_value', + 'product_next_value', + ] + + formatted_row = described_class.format_row(row, columns_index, indexes_to_delete) + + expect(indexes_to_delete).to eq([5, 7]) + expect(formatted_row).to eq(%w[value1 key1-key2 key3-key4 300g + safety_link_value-safety_link_value2 safety_link_value2 + product_link_value-product_next_value product_next_value]) + end + + describe '.construct_column_name' do + it 'constructs column name (h statements)' do + columns_index = { 'safety_sheet_link' => [], 'product_link' => [] } + result = described_class.construct_column_name('h_statements', 2, columns_index) + resulting_columns_index = ['h statements', { 'h_statements' => 2, 'safety_sheet_link' => [], + 'product_link' => [] }] + expect(result).to eq(resulting_columns_index) + end + + it 'constructs column name (amount)' do + columns_index = { 'safety_sheet_link' => [], 'product_link' => [] } + result = described_class.construct_column_name('amount', 2, columns_index) + resulting_columns_index = ['amount', { 'amount' => 2, 'safety_sheet_link' => [], + 'product_link' => [] }] + expect(result).to eq(resulting_columns_index) + end + + it 'constructs column name (p statements)' do + columns_index = { 'safety_sheet_link' => [], 'product_link' => [] } + result = described_class.construct_column_name('p_statements', 2, columns_index) + resulting_columns_index = ['p statements', { 'p_statements' => 2, 'safety_sheet_link' => [], + 'product_link' => [] }] + expect(result).to eq(resulting_columns_index) + end + end + + describe '.format_chemical_results' do + it 'format chemical results' do + result = OpenStruct.new(columns: %w[safety_sheet_link_merck safety_sheet_link_thermofischer + product_link_merck product_link_thermofischer], + rows: [['https://www.sigmaaldrich.com/DE/en/sds/sigma/a5376', '', + 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376', nil]]) + + result = described_class.format_chemical_results(result) + + expect(result.columns).to eq(['safety sheet link merck', 'safety sheet link thermofischer', + 'product link merck', 'product link thermofischer']) + end + end + + describe '.build_chemical_column_query' do + it 'builds chemical column query' do + selection = 'SELECT something FROM some_table' + sel = { + chemicals: %w[status safety_sheet_link_merck p_statements], + } + + expected_chemical_selections = [ + 'c."chemical_data"->0->\'status\' as "status"', + 'c."chemical_data"->0->\'merckProductInfo\'->\'sdsLink\' as "safety_sheet_link_merck"', + 'c."chemical_data"->0->\'safetyPhrases\'->\'p_statements\' as "p_statements"', + ] + + expected_gathered_selections = [selection, expected_chemical_selections] + + expect(described_class.build_chemical_column_query(selection, sel)).to eq(expected_gathered_selections) + end + end + end +end +# rubocop: enable Style/OpenStructUse diff --git a/spec/lib/import/import_chemicals_spec.rb b/spec/lib/import/import_chemicals_spec.rb new file mode 100644 index 0000000000..15a25507c7 --- /dev/null +++ b/spec/lib/import/import_chemicals_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Import::ImportChemicals do + describe '.extract_product_number' do + it 'extracts a product number from a Sigma-Aldrich URL' do + url = 'http://www.sigmaaldrich.com/MSDS/MSDS/DisplayMSDSPage.do?country=DE&language=DE&productNumber=131377&brand=ALDRICH' + expect(described_class.extract_product_number(url)).to eq('131377') + url2 = 'https://www.sigmaaldrich.com/US/en/product/sigma/a5376' + expect(described_class.extract_product_number(url2)).to eq('a5376') + end + end + + describe '.set_safety_phrases' do + let(:chemical) { { 'chemical_data' => [{}] } } + + it 'sets pictogram to safety phrases' do + described_class.set_safety_phrases(chemical, 'pictograms', 'GHS02, GHS07') + actual_phrases = chemical['chemical_data'][0]['safetyPhrases']['pictograms'].map(&:strip) + expect(actual_phrases).to eq(%w[GHS02 GHS07]) + end + + it 'sets H statement to safety phrases' do + described_class.set_safety_phrases(chemical, 'h_statements', 'H225-H302') + expect(chemical['chemical_data'][0]['safetyPhrases']['h_statements']).to eq( + { + 'H225' => ' Highly flammable liquid and vapour', + 'H302' => ' Harmful if swallowed', + }, + ) + end + + it 'sets P statement to safety phrases' do + described_class.set_safety_phrases(chemical, 'p_statements', 'P101,P102,P103') + expect(chemical['chemical_data'][0]['safetyPhrases']['p_statements']).to eq( + { + 'P101' => ' If medical advice is needed, have product container or label at hand.', + 'P102' => ' Keep out of reach of children.', + 'P103' => ' Read label before use.', + }, + ) + end + end + + describe '.sets amount of chemical' do + let(:chemical) { { 'chemical_data' => [{}] } } + + it 'add amount value and unit' do + described_class.set_amount(chemical, '10mg') + expect(chemical['chemical_data'][0]['amount']['value']).to eq(10.0) + expect(chemical['chemical_data'][0]['amount']['unit']).to eq('mg') + end + end + + describe '.to_snake_case' do + it 'converts a string with spaces to snake case' do + expect(described_class.to_snake_case('H statements')).to eq('h_statements') + end + end + + describe '.should_process_key' do + it 'returns true for keys that should be processed' do + expect(described_class.should_process_key('cas')).to be(false) + end + end + + describe '.process_column' do + let(:chemical) { Chemical.new } + let(:column_header) { 'Amount' } + let(:value) { '5g' } + + it 'sets the amount hash when amount is present' do + chemical['chemical_data'] = [{}] + described_class.process_column(chemical, column_header, value) + expect(chemical['chemical_data'][0]['amount']).to eq( + { + 'value' => 5, + 'unit' => 'g', + }, + ) + end + + it 'sets the safety sheet info when a safety sheet link is present' do + chemical['chemical_data'] = [{}] + safety_sheet_value = 'http://www.sigmaaldrich.com/MSDS/MSDS/DisplayMSDSPage.do?country=DE&language=DE&productNumber=131377&brand=ALDRICH' + product_link = 'https://www.sigmaaldrich.com/US/en/product/aldrich/131377' + content_type = 'application/pdf' + response_body = 'sample response body' + allow(HTTParty).to receive(:get).with(safety_sheet_value || product_link, anything).and_return( + instance_double(HTTParty::Response, headers: { 'Content-Type' => content_type }, body: response_body), + ) + described_class.process_column(chemical, 'Safety Sheet Link Merck', safety_sheet_value) + expect(chemical['chemical_data'][0]['merckProductInfo']).to be_present + described_class.process_column(chemical, 'product link', product_link) + expect(chemical['chemical_data'][0]['merckProductInfo']).to be_present + end + + it 'sets the safety phrases when safety phrases are present' do + chemical['chemical_data'] = [{}] + described_class.process_column(chemical, 'H Statements', 'H350-H351') + expect(chemical['chemical_data'][0]['safetyPhrases']['h_statements']).to eq( + { + 'H350' => ' May cause cancer', + 'H351' => ' Suspected of causing cancer', + }, + ) + end + end + + describe 'build_chemical' do + let(:row) { { 'cas' => '123-45-6', 'price' => '50 EUR' } } + let(:header) { %w[cas price] } + let(:chemical) { create(:chemical) } + + it 'creates a chemical with valid data' do + allow(PubChem).to receive(:get_cid_from_inchikey).and_return('12345') + expect(described_class.build_chemical(row, header)).not_to be_nil + end + end +end diff --git a/spec/lib/import/import_samples_spec.rb b/spec/lib/import/import_samples_spec.rb index b87534077e..3a6ee264fa 100644 --- a/spec/lib/import/import_samples_spec.rb +++ b/spec/lib/import/import_samples_spec.rb @@ -7,7 +7,7 @@ let(:collection_id) { create(:collection).id } let(:file_path) { 'spec/fixtures/import/sample_import_template.xlsx' } let(:file_name) { File.basename(file_path) } - let(:importer) { Import::ImportSamples.new(user_id, collection_id, file_path, file_name) } + let(:importer) { Import::ImportSamples.new(user_id, collection_id, file_path, file_name, 'sample') } describe '.format_to_interval_syntax' do let(:processed_row) { importer.send(:format_to_interval_syntax, unprocessed_row) } @@ -16,7 +16,6 @@ let(:unprocessed_row) { '1' } it 'returns single number' do - # expect(formated_value).to eq ['13.0'] expect(processed_row).to eq '[1.0, Infinity]' end end @@ -25,7 +24,6 @@ let(:unprocessed_row) { '1.234' } it 'returns single number' do - # expect(formated_value).to eq ['13.0'] expect(processed_row).to eq '[1.234, Infinity]' end end @@ -34,7 +32,6 @@ let(:unprocessed_row) { '1.234-2.345' } it 'returns interval' do - # expect(formated_value).to eq ['13.0'] expect(processed_row).to eq '[1.234, 2.345]' end end @@ -43,7 +40,6 @@ let(:unprocessed_row) { '1.234-1' } it 'returns interval' do - # expect(formated_value).to eq ['13.0'] expect(processed_row).to eq '[1.234, 1.0]' end end @@ -52,7 +48,6 @@ let(:unprocessed_row) { '1.23.4-1' } it 'returns infinity interval' do - # expect(formated_value).to eq ['13.0'] expect(processed_row).to eq '[-Infinity, Infinity]' end end @@ -61,7 +56,6 @@ let(:unprocessed_row) { '1.234--1' } it 'returns interval' do - # expect(formated_value).to eq ['13.0'] expect(processed_row).to eq '[1.234, -1.0]' end end