Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show research plan links in reaction #1575

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/api/chemotion/reaction_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,15 @@ class ReactionAPI < Grape::API

get do
reaction = Reaction.find(params[:id])
class_name = reaction&.class&.name

{
reaction: Entities::ReactionEntity.represent(
reaction,
policy: @element_policy,
detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: reaction).detail_levels,
),
literatures: Entities::LiteratureEntity.represent(citation_for_elements(params[:id], 'Reaction')),
literatures: Entities::LiteratureEntity.represent(citation_for_elements(params[:id], class_name)),
}
end
end
Expand Down
17 changes: 15 additions & 2 deletions app/api/chemotion/research_plan_api.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Chemotion
# rubocop: disable Metrics/ClassLength

Expand All @@ -20,8 +22,8 @@ class ResearchPlanAPI < Grape::API
get do
scope = if params[:collection_id]
begin
Collection.belongs_to_or_shared_by(current_user.id,current_user.group_ids).
find(params[:collection_id]).research_plans
Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout/IndentationWidth: Use 2 (not -6) spaces for indentation.

.find(params[:collection_id]).research_plans
rescue ActiveRecord::RecordNotFound
ResearchPlan.none
end
Expand Down Expand Up @@ -138,6 +140,17 @@ class ResearchPlanAPI < Grape::API
end
end

desc 'Return element linked to research plan'
params do
requires :id, type: Integer, desc: 'Research plan id'
requires :element, type: String, desc: 'Sample or Reaction'
end

get 'linked' do
type = "#{params[:element]}_id"
ResearchPlan.where('body @> ?', [{ value: { type => params[:id] } }].to_json).select(:id, :name)
end

desc 'Return serialized research plan by id'
params do
requires :id, type: Integer, desc: 'Research plan id'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import SvgFileZoomPan from 'react-svg-file-zoom-pan-latest';
import { findIndex } from 'lodash';
import ElementCollectionLabels from 'src/apps/mydb/elements/labels/ElementCollectionLabels';
import ElementResearchPlanLabels from 'src/apps/mydb/elements/labels/ElementResearchPlanLabels';
import ElementAnalysesLabels from 'src/apps/mydb/elements/labels/ElementAnalysesLabels';
import ElementActions from 'src/stores/alt/actions/ElementActions';
import DetailActions from 'src/stores/alt/actions/DetailActions';
Expand Down Expand Up @@ -279,6 +280,10 @@ export default class ReactionDetails extends Component {
<ElementCollectionLabels element={reaction} key={reaction.id} placement="right" />
);

const rsPlanLabel = (reaction.isNew || _.isEmpty(reaction.research_plans)) ? null : (
<ElementResearchPlanLabels plans={reaction.research_plans} key={reaction.id} placement="right" />
);

return (
<div>
<OverlayTrigger placement="bottom" overlay={<Tooltip id="sampleDates">{titleTooltip}</Tooltip>}>
Expand Down Expand Up @@ -344,6 +349,7 @@ export default class ReactionDetails extends Component {
</OverlayTrigger>
<div style={{ display: "inline-block", marginLeft: "10px" }}>
{colLabel}
{rsPlanLabel}
<ElementAnalysesLabels element={reaction} key={reaction.id + "_analyses"} />
<HeaderCommentSection element={reaction} />
</div>
Expand Down Expand Up @@ -474,7 +480,6 @@ export default class ReactionDetails extends Component {
green_chemistry: 'Green Chemistry'
}


addSegmentTabs(reaction, this.handleSegmentsChange, tabContentsMap);

const tabContents = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import ResearchPlanDetailsFieldImage from 'src/apps/mydb/elements/details/resear
import ResearchPlanDetailsFieldTable from 'src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsFieldTable';
import ResearchPlanDetailsFieldSample from 'src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsFieldSample';
import ResearchPlanDetailsFieldReaction from 'src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsFieldReaction';
import AttachmentFetcher from 'src/fetchers/AttachmentFetcher';

export default class ResearchPlanDetailsField extends Component {
render() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import { Label, Button, OverlayTrigger, Popover } from 'react-bootstrap';

export default class ElementResearchPlanLabels extends React.Component {
constructor(props) {

super(props);
this.state = {
research_plans: props.plans
};

this.handleOnClick = this.handleOnClick.bind(this);
this.preventOnClick = this.preventOnClick.bind(this);
}

handleOnClick(label, e) {
e.stopPropagation();

let url = "/research_plan/" + label.id;
Aviator.navigate(url);
}

preventOnClick(e) {
e.stopPropagation();
}

formatLabels(labels) {
return labels.map((label, index) => {
return (
<span className="collection-label" key={index}>
<Button bsStyle="default" bsSize="xs" onClick={e => this.handleOnClick(label, e)}>
{label.name}
</Button>
&nbsp;
</span>
);
});
}

renderCollectionsLabels(research_plans) {
if (research_plans == undefined) return <span />;

return (
<div>
<h3 className="popover-title">Research Plans</h3>
<div className="popover-content">
{this.formatLabels(research_plans)}
</div>
</div>
);
}

render() {
const { research_plans } = this.state;

let placement = 'right';
let researchPlanOverlay = (
<Popover className="collection-overlay" id="element-collections">
{this.renderCollectionsLabels(research_plans)}
</Popover>
);

return (
<div style={{display: "inline-block"}} onClick={this.preventOnClick}>
<OverlayTrigger
trigger="click"
rootClose
placement={placement}
overlay={researchPlanOverlay}
>
<span className="collection-label" >
<Label>
<i className="fa fa-file-text-o" />
{" " + research_plans.length}
</Label>
</span>
</OverlayTrigger>
</div>
);
}
}
63 changes: 37 additions & 26 deletions app/packs/src/fetchers/ReactionsFetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,43 @@ import Reaction from 'src/models/Reaction';
import AttachmentFetcher from 'src/fetchers/AttachmentFetcher';
import Literature from 'src/models/Literature';
import GenericElsFetcher from 'src/fetchers/GenericElsFetcher';
import ResearchPlansFetcher from './ResearchPlansFetcher';

// TODO: Extract common base functionality into BaseFetcher
export default class ReactionsFetcher {
static fetchById(id) {
return fetch(`/api/v1/reactions/${id}.json`, {
credentials: 'same-origin'
}).then(response => response.json())
.then((json) => {
if (json.hasOwnProperty("reaction")) {
const reaction = new Reaction(json.reaction);
if (json.literatures && json.literatures.length > 0) {
const tliteratures = json.literatures.map(literature => new Literature(literature));
const lits = tliteratures.reduce((acc, l) => acc.set(l.literal_id, l), new Immutable.Map());
reaction.literatures = lits;
static async fetchById(id) {
try {
const response = await fetch(`/api/v1/reactions/${id}.json`, {
credentials: 'same-origin'
}).then(response => response.json())
.then((json) => {
if (json.hasOwnProperty("reaction")) {
const reaction = new Reaction(json.reaction);
if (json.literatures && json.literatures.length > 0) {
const tliteratures = json.literatures.map(literature => new Literature(literature));
const lits = tliteratures.reduce((acc, l) => acc.set(l.literal_id, l), new Immutable.Map());
reaction.literatures = lits;
}
if (json.research_plans && json.research_plans.length > 0) {
reaction.research_plans = json.research_plans;
}
reaction.updateMaxAmountOfProducts();
return reaction;
}
reaction.updateMaxAmountOfProducts();
return reaction;
}
const rReaction = new Reaction(json.reaction);
if (json.error) {
rReaction.id = `${id}:error:Reaction ${id} is not accessible!`;
}
return rReaction;
}).catch((errorMessage) => {
console.log(errorMessage);
});
const rReaction = new Reaction(json.reaction);
if (json.error) {
rReaction.id = `${id}:error:Reaction ${id} is not accessible!`;
}
return rReaction;
}).catch((errorMessage) => {
console.log(errorMessage);
});
const researchPlans = await ResearchPlansFetcher.fetchResearchPlansForElements(id, response.type);
response['research_plans'] = researchPlans;
return response;
} catch (error) {
console.error(error);
}
}

static fetchByCollectionId(id, queryParams = {}, isSync = false) {
Expand Down Expand Up @@ -78,18 +89,18 @@ export default class ReactionsFetcher {
return Promise.all(tasks).then(() => {
return promise();
});
}
}

return promise();
}
}

static updateAnnotationsInReaction(reaction){
const tasks=[];
reaction.products.forEach( e => tasks.push(BaseFetcher.updateAnnotationsInContainer(e)));
return Promise.all(tasks);
}

static update(reaction) {
static update(reaction) {
return ReactionsFetcher.create(reaction, 'put');
}
}
13 changes: 13 additions & 0 deletions app/packs/src/fetchers/ResearchPlansFetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,17 @@ export default class ResearchPlansFetcher {

return Promise.all(updateTasks);
}

static fetchResearchPlansForElements(id, element) {
return fetch(`/api/v1/research_plans/linked?id=${id}&element=${element}`, {
credentials: 'same-origin',
method: 'get',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
}).then((response) => response.json())
.then((json) => json)
.catch((errorMessage) => { console.log(errorMessage); });
}
}
10 changes: 10 additions & 0 deletions app/packs/src/models/Reaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export default class Reaction extends Element {
duration: '',
durationDisplay: DurationDefault,
literatures: {},
research_plans: {},
name: '',
observation: Reaction.quillDefault(),
products: [],
Expand Down Expand Up @@ -175,6 +176,7 @@ export default class Reaction extends Element {
durationCalc: this.durationCalc(),
id: this.id,
literatures: this.literatures,
research_plans: this.research_plans,
materials: {
starting_materials: this.starting_materials.map(s => s.serializeMaterial()),
reactants: this.reactants.map(s => s.serializeMaterial()),
Expand Down Expand Up @@ -864,10 +866,18 @@ export default class Reaction extends Element {
return this._literatures || {};
}

get research_plans() {
return this._research_plans || {};
}

set literatures(literatures) {
this._literatures = literatures;
}

set research_plans(research_plans) {
this._research_plans = research_plans;
}

get totalVolume() {
let totalVolume = 0.0;
const materials = [...this.starting_materials,
Expand Down
43 changes: 18 additions & 25 deletions spec/api/research_plan_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,6 @@
end
end

describe 'GET /api/v1/research_plans' do
let!(:c) { create(:collection, label: 'C1', user: user, is_shared: false) }
let(:rp) { create(:research_plan) }
let!(:research_plan_metadata) { create(:research_plan_metadata) }

before do
rp.research_plan_metadata = research_plan_metadata
CollectionsResearchPlan.create!(research_plan: rp, collection: c)
end

it 'returns serialized research_plans of logged in user' do
get '/api/v1/research_plans'
first_rp = JSON.parse(response.body)['research_plans'].first
expect(response.status).to eq 200
expect(first_rp).to include(
'type' => 'research_plan',
'name' => rp.name
)
expect(first_rp['research_plan_metadata']).to include(
'id' => research_plan_metadata.id,
'doi' => research_plan_metadata.doi
)
end
end

describe 'POST /api/v1/research_plans' do
context 'with valid parameters' do
let(:params) do
Expand Down Expand Up @@ -135,5 +110,23 @@
expect(first_row['readout_1_unit']).to eq first_readout['unit']
end
end

describe 'GET /api/v1/research_plans/linked' do
let!(:c) { create(:collection, label: 'C1', user: user) }
mehreenmansur marked this conversation as resolved.
Show resolved Hide resolved
let!(:research_plan) { create(:research_plan, :with_linked) }

before do
get '/api/v1/research_plans/linked', params: { id: 100, element: 'reaction' }
end

it 'returns 200 status code' do
expect(response.status).to eq 200
mehreenmansur marked this conversation as resolved.
Show resolved Hide resolved
end

it 'returns research_plans linked to an element' do
response_body = JSON.parse(response.body)
expect(response_body[0]['name']).to eq research_plan.name
end
end
end
end
Loading