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 ? (
<>
-
{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