Commit e69ed5e5 authored by pierre.tremouilhac's avatar pierre.tremouilhac
Browse files

Merge branch 'resin-cs' into 'development'

Set alias as resin/polymer + use DelayedJob for importing

See merge request ComPlat/chemotion_ELN!823
parents ab123e33 92cb5df2
......@@ -214,6 +214,7 @@ module Chemotion
current_user.id,
params[:collection_id]
)
true
end
end
......
......@@ -152,10 +152,18 @@ module ChemScannerHelpers
rgroup = reaction.send(group)
rgroup.each_with_index do |mol, idx|
mlabel = group.chomp('s') + " #{idx + 1}"
alias_info = (mol.atom_map || {}).each_with_object([]) do |(_, atom), arr|
next unless atom.is_alias || !atom.alias_text.empty?
arr.push(id: atom.idx, text: atom.alias_text)
end
desc[mlabel] = {
text: mol.text.strip,
label: mol.label.strip,
mdl: mol.mdl
mdl: mol.mdl,
alias: alias_info
}
rdetails[mlabel] = mol.details.to_h
......@@ -200,11 +208,18 @@ module ChemScannerHelpers
def mol_info(mol)
return if mol.nil?
alias_info = mol.atom_map.each_with_object([]) do |(key, atom), arr|
next unless atom.is_alias || atom.alias_text.empty?
arr.push(id: key, text: atom.alias_text)
end
res = {
description: mol.text,
details: mol.details.to_h,
smi: mol.cano_smiles,
mdl: mol.mdl
mdl: mol.mdl,
alias: alias_info
}
res
......
......@@ -101,7 +101,7 @@ class ChemScanner extends React.Component {
const {
files, selected, getMol, abbManagement, addFile, removeFile,
changeType, changeAbbManagement, selectSmi, removeSmi, editSmiles,
editComment, exportSmi, modal
editComment, exportSmi, modal, setResin
} = this.props;
let listItems = <span />;
......@@ -126,6 +126,7 @@ class ChemScanner extends React.Component {
removeSmi={removeSmi}
editComment={editComment}
selected={selected}
setResin={setResin}
/>
);
......@@ -242,6 +243,7 @@ ChemScanner.propTypes = {
editSmiles: PropTypes.func.isRequired,
exportSmi: PropTypes.func.isRequired,
setCdd: PropTypes.func.isRequired,
setResin: PropTypes.func.isRequired,
getMol: PropTypes.bool.isRequired,
abbManagement: PropTypes.bool.isRequired,
modal: PropTypes.string
......@@ -251,4 +253,4 @@ ChemScanner.defaultProps = {
modal: ''
};
export default scriptLoader('https://chemdrawdirect.perkinelmer.cloud/js/chemdrawweb/chemdrawweb.js')(ChemScanner);
export default scriptLoader('cdjs/chemdrawweb/chemdrawweb.js')(ChemScanner);
......@@ -33,6 +33,8 @@ export default class ChemScannerContainer extends React.Component {
this.changeType = this.changeType.bind(this);
this.changeAbbManagement = this.changeAbbManagement.bind(this);
this.setResin = this.setResin.bind(this);
this.setCdd = this.setCdd.bind(this);
this.getSvg = this.getSvg.bind(this);
}
......@@ -102,9 +104,9 @@ export default class ChemScannerContainer extends React.Component {
selectSmi(uid, cdIdx, smiIdx) {
const { selected } = this.state;
const newSelected = _.cloneDeep(selected);
const s = newSelected.findIndex(
x => x.uid === uid && x.cdIdx === cdIdx && x.smiIdx === smiIdx
);
const s = newSelected.findIndex(x => (
x.uid === uid && x.cdIdx === cdIdx && x.smiIdx === smiIdx
));
if (s >= 0) {
newSelected.splice(s, 1);
......@@ -359,6 +361,22 @@ export default class ChemScannerContainer extends React.Component {
this.setState({ files: newFiles });
}
setResin(uid, cdIdx, idx, descLabel, atomIdx) {
const { files } = this.state;
const newFiles = _.cloneDeep(files);
const file = newFiles.filter(x => x.uid === uid);
const info = file[0].cds[cdIdx].info[idx];
const desc = info.description[descLabel];
if (!desc) return;
const aliasIdx = desc.alias.findIndex(x => x.id === atomIdx);
if (aliasIdx < 0) return;
const { isResin } = desc.alias[aliasIdx];
desc.alias[aliasIdx].isResin = !(isResin || false);
this.setState({ files: newFiles });
}
render() {
const {
files, selected, getMol, abbManagement
......@@ -382,6 +400,7 @@ export default class ChemScannerContainer extends React.Component {
editComment={this.editComment}
changeType={this.changeType}
changeAbbManagement={this.changeAbbManagement}
setResin={this.setResin}
setCdd={this.setCdd}
/>
</div>
......
......@@ -10,7 +10,7 @@ import ScannedItem from './ScannedItem';
import PreviewFileZoomPan from './PreviewFileZoomPan';
function ChemScannerCds({
cds, uid, selectSmi, removeSmi, editComment, selected, modal
cds, uid, selectSmi, removeSmi, editComment, selected, modal, setResin
}) {
if (!cds || cds.length === 0) return <span />;
......@@ -36,6 +36,7 @@ function ChemScannerCds({
selectSmi={selectSmi}
removeSmi={removeSmi}
editComment={editComment}
setResin={setResin}
selected={selected}
content={i}
/>
......@@ -55,6 +56,7 @@ ChemScannerCds.propTypes = {
selectSmi: PropTypes.func.isRequired,
removeSmi: PropTypes.func.isRequired,
editComment: PropTypes.func.isRequired,
setResin: PropTypes.func.isRequired,
modal: PropTypes.string
};
......@@ -63,7 +65,7 @@ ChemScannerCds.defaultProps = {
};
function ChemScannerExtraction({
files, selectSmi, removeSmi, editComment, selected, modal
files, selectSmi, removeSmi, editComment, selected, modal, setResin
}) {
if (files.length === 0) return <span />;
......@@ -93,6 +95,7 @@ function ChemScannerExtraction({
selectSmi={selectSmi}
removeSmi={removeSmi}
editComment={editComment}
setResin={setResin}
selected={selected}
/>
</Panel.Body>
......@@ -109,6 +112,7 @@ ChemScannerExtraction.propTypes = {
selectSmi: PropTypes.func.isRequired,
removeSmi: PropTypes.func.isRequired,
editComment: PropTypes.func.isRequired,
setResin: PropTypes.func.isRequired,
modal: PropTypes.string
};
......
import React from 'react';
import PropTypes from 'prop-types';
import { Label } from 'react-bootstrap';
import { Label, Button } from 'react-bootstrap';
function renderReactionStatus(status) {
if (status === 'Failed') {
......@@ -13,7 +13,57 @@ function renderReactionStatus(status) {
return (<Label bsStyle="success">Success</Label>);
}
function ListProps({ label, listProps, style }) {
class ResinLabel extends React.Component {
constructor() {
super();
this.onClickLabel = this.onClickLabel.bind(this);
}
onClickLabel() {
const {
onClick, uid, idx, cdIdx, atomId, descLabel
} = this.props;
onClick(uid, idx, cdIdx, descLabel, atomId);
}
render() {
const { label, isResin } = this.props;
if (!label) return <span />;
return (
<Button
bsSize="xsmall"
onClick={this.onClickLabel}
bsStyle={isResin ? 'info' : 'default'}
>
{label}
</Button>
);
}
}
ResinLabel.propTypes = {
descLabel: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
uid: PropTypes.string,
idx: PropTypes.number,
cdIdx: PropTypes.number,
atomId: PropTypes.number,
isResin: PropTypes.bool
};
ResinLabel.defaultProps = {
uid: '',
idx: 0,
cdIdx: 0,
isResin: false
};
function ListProps({
label, listProps, style, setResin, uid, idx, cdIdx
}) {
if (!listProps) return <span />;
if (listProps.constructor === String) {
return (
......@@ -30,7 +80,8 @@ function ListProps({ label, listProps, style }) {
x !== 'ID' && x !== 'parentID' && x !== 'mdl'
&& listProps[x] && typeof listProps[x] !== 'object'
));
if (list.length === 0) return <span />;
const { alias } = listProps;
if (list.length === 0 && alias.length === 0) return <span />;
const propsList = list.map((k) => {
const bold = `${k}: `;
......@@ -46,11 +97,36 @@ function ListProps({ label, listProps, style }) {
);
});
let aliasList = <span />;
if (alias && alias.length > 0) {
const aliases = alias.map(obj => (
<ResinLabel
key={obj.id}
descLabel={label}
label={obj.text}
onClick={setResin}
atomId={obj.id}
uid={uid}
idx={idx}
cdIdx={cdIdx}
isResin={obj.isResin}
/>
));
aliasList = (
<li key="alias">
<b>Alias: </b>
{aliases}
</li>
);
}
return (
<div style={style}>
<Label>{label}</Label>
<ul>
{propsList}
{aliasList}
</ul>
</div>
);
......@@ -59,12 +135,20 @@ function ListProps({ label, listProps, style }) {
ListProps.propTypes = {
label: PropTypes.string.isRequired,
// listProps: PropTypes.object.isRequired,
setResin: PropTypes.func,
uid: PropTypes.string,
idx: PropTypes.number,
cdIdx: PropTypes.number,
// eslint-disable-next-line react/forbid-prop-types
style: PropTypes.object
};
ListProps.defaultProps = {
style: {}
style: {},
uid: '',
idx: 0,
cdIdx: 0,
setResin: () => null
};
export default ListProps;
......@@ -13,7 +13,8 @@ import PerkinElnDetails from './PerkinElnDetails';
import { renderSvg } from './ChemScannerObjectHelper';
function ScannedItem({
uid, cdIdx, idx, removeSmi, editComment, selectSmi, selected, content, modal
uid, cdIdx, idx, removeSmi, editComment, selectSmi,
selected, content, modal, setResin
}) {
const {
svg, smi, description, details, comment,
......@@ -39,37 +40,45 @@ function ScannedItem({
};
if (description.reaction) {
descList.push(
<ListProps
key="reaction"
label="reaction"
listProps={description.reaction}
style={{ marginRight: '10px', display: 'flow-root' }}
/>
);
descList.push(<ListProps
key="reaction"
label="reaction"
listProps={description.reaction}
style={{ marginRight: '10px', display: 'flow-root' }}
/>);
Object.keys(description).forEach((key) => {
if (key === 'reaction') return;
const obj = description[key];
descList.push(
const listProps = (
<ListProps
key={key}
label={key}
listProps={obj}
uid={uid}
idx={idx}
cdIdx={cdIdx}
setResin={setResin}
style={descStyle}
/>
);
descList.push(listProps);
});
} else {
descList.push(
const listProps = (
<ListProps
key="sample"
label="Description"
listProps={description}
uid={uid}
idx={idx}
cdIdx={cdIdx}
setResin={setResin}
style={descStyle}
/>
);
descList.push(listProps);
}
const container = document.getElementById(modal);
......@@ -117,6 +126,7 @@ ScannedItem.propTypes = {
selectSmi: PropTypes.func.isRequired,
removeSmi: PropTypes.func.isRequired,
editComment: PropTypes.func.isRequired,
setResin: PropTypes.func.isRequired,
uid: PropTypes.string.isRequired,
idx: PropTypes.number.isRequired,
cdIdx: PropTypes.number.isRequired,
......
import React from 'react';
import PropTypes from 'prop-types';
import ScannedItem from './ScannedItem';
import { renderSvg } from './ChemScannerObjectHelper';
export default class ScannedItemContainer extends React.Component {
constructor() {
super();
this.selectSmi = this.selectSmi.bind(this);
}
selectSmi() {
const { uid, idx, selectSmi } = this.props;
selectSmi(uid, idx);
}
render() {
const {
uid, idx, selected, content, removeSmi, editComment
} = this.props;
const {
svg, smi, editedSmi, description, details, comment
} = content;
const isSelected = selected.filter(x => (
x.uid === uid && x.smiIdx === idx
)).length > 0;
let displayedSmi = smi;
if (editedSmi && editedSmi !== '') {
const smiArr = smi.split('>');
const allSolvents = smiArr[1].split('.').concat(editedSmi.split(','));
smiArr[1] = allSolvents.filter(x => x).join('.');
displayedSmi = smiArr.join('>');
}
return (
<ScannedItem
details={details}
description={description}
comment={comment}
idx={idx}
removeSmi={removeSmi}
selectSmi={this.selectSmi}
editComment={editComment}
svg={renderSvg(svg)}
smi={displayedSmi}
selected={isSelected}
uid={uid}
/>
);
}
}
ScannedItemContainer.propTypes = {
selectSmi: PropTypes.func.isRequired,
removeSmi: PropTypes.func.isRequired,
editComment: PropTypes.func.isRequired,
uid: PropTypes.string.isRequired,
content: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
idx: PropTypes.number.isRequired,
selected: PropTypes.arrayOf(PropTypes.shape).isRequired
};
......@@ -11,4 +11,5 @@ class Channel < ActiveRecord::Base
INBOX_ARRIVALS_TO_ME = 'Inbox Arrivals To Me'
REPORT_GENERATOR_NOTIFICATION = 'Report Generator Notification'
SEND_INDIVIDUAL_USERS = 'Send Individual Users'
SEND_IMPORT_NOTIFICATION = 'Import Notification'
end
class AddImportNotification < ActiveRecord::Migration
def change
channel = Channel.find_by(subject: Channel::SEND_IMPORT_NOTIFICATION)
return unless channel.nil?
data = 'Imported %<success>s reactions successfully, %<failed>s failed to import!'
msg_template = '{"data": "' + data + '"}'
attributes = {
subject: Channel::SEND_IMPORT_NOTIFICATION,
channel_type: 8,
msg_template: msg_template
}
Channel.create(attributes)
end
end
# frozen_string_literal: true
require 'roo'
# Import reactions
module Import
# Import reactions from ChemScanner UI
......@@ -30,13 +28,38 @@ module Import
)
return if valid_check
success = 0
failed = 0
valid_list = list.reject { |r| r.reactants.empty? || r.products.empty? }
valid_list.each do |reaction|
import = new(creator_id, collection_id)
import.process_reaction(reaction)
import.reaction_save
begin
import = new(creator_id, collection_id)
import.process_reaction(reaction)
check = import.reaction_save
if check
success += 1
else
failed += 1
end
rescue
failed += 1
end
end
channel = Channel.find_by(subject: Channel::SEND_IMPORT_NOTIFICATION)
return if channel.nil?
content = channel.msg_template
return if content.nil?
data = { success: success, failed: failed }
content['data'] = format(content['data'], data)
Message.create_msg_notification(
channel.id, content, creator_id, [creator_id]
)
end
handle_asynchronously :from_list
end
def process_reaction(reaction)
......@@ -125,6 +148,8 @@ module Import
desc = {} if desc.nil?
mdl = desc.dig(:mdl)
resin_info = (desc.dig(:alias) || []).select { |x| x[:isResin] }
mdl = convert_to_polymer(mdl, resin_info)
molecule = if mdl.nil?
Molecule.find_or_create_by_cano_smiles(smiles)
......@@ -143,6 +168,20 @@ module Import
assign_sample_attributes(sample, desc)
resin_info.each do |resin|
residue = Residue.new
residue.residue_type = 'polymer'
residue.custom_info = {
'formula': resin[:text],
'loading': nil,
'loading_type': 'external',
'polymer_type': 'polystyrene',
'external_loading': 0
}
sample.residues << residue
end
sample
end
......@@ -191,15 +230,16 @@ module Import
end
def extract_sample_info(desc, details = nil)
if desc.nil?
info = {}
else
info = {
mdl: desc[:mdl],
label: desc[:label],
text: desc[:text]
}
end
info = if desc.nil?
{}
else
{
mdl: desc[:mdl],
label: desc[:label],
text: desc[:text],
alias: desc[:alias]
}
end
return info if details.nil?
......@@ -325,7 +365,7 @@ module Import
end
def reaction_save
return if @starting_materials.count.zero? || @products.count.zero?
return false if @starting_materials.count.zero? || @products.count.zero?
CollectionsReaction.create(reaction: @reaction, collection: @collection)
CollectionsReaction.create(
......@@ -373,5 +413,49 @@ module Import
container.children.create(container_type: 'analyses')
container
end