Skip to content

Commit

Permalink
feat: Copy research plan (#1667)
Browse files Browse the repository at this point in the history
* add copy research plan feature

* create new derivatives for copy research plan

* update copied annonated file

* update identifier in copied research plan body

* update copy attachment method


---------

Co-authored-by: Mehreen <mehreen.mansur>
  • Loading branch information
mehreenmansur authored Mar 26, 2024
1 parent 2e63c06 commit 7584fef
Show file tree
Hide file tree
Showing 20 changed files with 229 additions and 32 deletions.
11 changes: 8 additions & 3 deletions app/api/chemotion/research_plan_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,22 @@ class ResearchPlanAPI < Grape::API
optional :collection_id, type: Integer, desc: 'Collection ID'
requires :container, type: Hash, desc: 'Containers'
optional :segments, type: Array, desc: 'Segments'
optional :attachments, type: Array, desc: 'Attachments'
end
post do
attributes = {
name: params[:name],
body: params[:body]
}

attributes.delete(:can_copy)
research_plan = ResearchPlan.new attributes
research_plan.creator = current_user
research_plan.container = update_datamodel(params[:container])
research_plan.save!
research_plan.save_segments(segments: params[:segments], current_user_id: current_user.id)

clone_attachs = params[:attachments]&.reject { |a| a[:is_new] }
Usecases::Attachments::Copy.execute!(clone_attachs, research_plan, current_user.id) if clone_attachs

if params[:collection_id]
collection = current_user.collections.where(id: params[:collection_id]).take
Expand Down Expand Up @@ -157,7 +160,8 @@ class ResearchPlanAPI < Grape::API
end
route_param :id do
before do
error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, ResearchPlan.find(params[:id])).read?
@element_policy = ElementPolicy.new(current_user, ResearchPlan.find(params[:id]))
error!('401 Unauthorized', 401) unless @element_policy.read?
end
get do
research_plan = ResearchPlan.find(params[:id])
Expand All @@ -170,7 +174,8 @@ class ResearchPlanAPI < Grape::API
{
research_plan: Entities::ResearchPlanEntity.represent(
research_plan,
detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: research_plan).detail_levels
detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: research_plan).detail_levels,
policy: @element_policy,
),
attachments: Entities::AttachmentEntity.represent(research_plan.attachments),
}
Expand Down
4 changes: 4 additions & 0 deletions app/api/entities/application_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def detail_levels
minimal_default_levels.merge(options[:detail_levels])
end

def can_copy
options[:policy].try(:copy?) || false
end

class MissingCurrentUserError < StandardError
end
end
Expand Down
4 changes: 0 additions & 4 deletions app/api/entities/reaction_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ def can_update
options[:policy].try(:update?) || false
end

def can_copy
options[:policy].try(:copy?) || false
end

def code_log
displayed_in_list? ? nil : object.code_log
end
Expand Down
1 change: 1 addition & 0 deletions app/api/entities/research_plan_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Entities
class ResearchPlanEntity < ApplicationEntity
# rubocop:disable Layout/ExtraSpacing
with_options(anonymize_below: 0) do
expose! :can_copy, unless: :displayed_in_list
expose! :body
expose! :container, using: 'Entities::ContainerEntity'
expose! :id
Expand Down
4 changes: 0 additions & 4 deletions app/api/entities/sample_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,6 @@ def can_update
options[:policy].try(:update?) || false
end

def can_copy
options[:policy].try(:copy?) || false
end

def can_publish
options[:policy].try(:destroy?) || false
end
Expand Down
8 changes: 0 additions & 8 deletions app/models/attachment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ class Attachment < ApplicationRecord
where(attachable_type: 'Template')
}

def copy(**args)
d = dup
d.identifier = nil
d.duplicated = true
d.update(args)
d
end

def extname
File.extname(filename.to_s)
end
Expand Down
17 changes: 14 additions & 3 deletions app/models/research_plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class ResearchPlan < ApplicationRecord
before_destroy :delete_attachment
accepts_nested_attributes_for :collections_research_plans

attr_accessor :can_copy

unless Dir.exists?(path = Rails.root.to_s + '/public/images/research_plans')
Dir.mkdir path
Expand Down Expand Up @@ -98,15 +99,25 @@ def svg_files
svg_files
end

def update_body_attachments(original_identifier, copy_identifier)
attach = body&.detect { |x| x['value']['public_name'] == original_identifier }
if attach.present?
attach['id'] = SecureRandom.uuid
attach['value']['public_name'] = copy_identifier
end

save!
end

private

def delete_attachment
if Rails.env.production?
attachments.each { |attachment|
attachments.each do |attachment|
attachment.delay(run_at: 96.hours.from_now, queue: 'attachment_deletion').destroy!
}
end
else
attachments.each(&:destroy!)
end
end

end
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { unionBy, findIndex } from 'lodash';
import Immutable from 'immutable';
import ElementCollectionLabels from 'src/apps/mydb/elements/labels/ElementCollectionLabels';
import UIStore from 'src/stores/alt/stores/UIStore';
import UIActions from 'src/stores/alt/actions/UIActions';
import ElementActions from 'src/stores/alt/actions/ElementActions';
import DetailActions from 'src/stores/alt/actions/DetailActions';
Expand Down Expand Up @@ -36,6 +37,7 @@ import HeaderCommentSection from 'src/components/comments/HeaderCommentSection';
import CommentSection from 'src/components/comments/CommentSection';
import CommentActions from 'src/stores/alt/actions/CommentActions';
import CommentModal from 'src/components/common/CommentModal';
import CopyElementModal from 'src/components/common/CopyElementModal';
import { formatTimeStampsOfElement } from 'src/utilities/timezoneHelper';
import UserStore from 'src/stores/alt/stores/UserStore';
import MatrixCheck from 'src/components/common/MatrixCheck';
Expand Down Expand Up @@ -487,7 +489,16 @@ export default class ResearchPlanDetails extends Component {
} /* eslint-enable */

renderPanelHeading(researchPlan) {
const { currentCollection } = UIStore.getState();
const rootCol = currentCollection && currentCollection.is_shared === false &&
currentCollection.is_locked === false && currentCollection.label !== 'All' ? currentCollection.id : null;
const titleTooltip = formatTimeStampsOfElement(researchPlan || {});
const copyBtn = (
<CopyElementModal
element={researchPlan}
defCol={rootCol}
/>
);

return (
<Panel.Heading>
Expand All @@ -513,14 +524,15 @@ export default class ResearchPlanDetails extends Component {
<i className="fa fa-floppy-o" aria-hidden="true" />
</Button>
</OverlayTrigger>
<OverlayTrigger placement="bottom" overlay={<Tooltip id="fullSample">Fullresearch_plan</Tooltip>}>
<OverlayTrigger placement="bottom" overlay={<Tooltip id="fullSample">Full Research Plan</Tooltip>}>
<Button bsStyle="info" bsSize="xsmall" className="button-right" onClick={this.toggleFullScreen}>
<i className="fa fa-expand" aria-hidden="true" />
</Button>
</OverlayTrigger>
{researchPlan.isNew
? null
: <OpenCalendarButton isPanelHeader eventableId={researchPlan.id} eventableType="ResearchPlan" />}
{copyBtn}
</Panel.Heading>
);
}
Expand Down Expand Up @@ -623,7 +635,7 @@ export default class ResearchPlanDetails extends Component {
<ButtonToolbar>
<Button bsStyle="primary" onClick={() => DetailActions.close(researchPlan)}>Close</Button>
{
researchPlan.changed ? (
(researchPlan.changed || researchPlan.is_copy) ? (
<Button bsStyle="warning" onClick={() => this.handleSubmit()}>
{researchPlan.isNew ? 'Create' : 'Save'}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default class ResearchPlanDetailsName extends Component {
value={value || ''}
onChange={(event) => onChange(event.target.value)}
disabled={disabled}
name="research_plan_name"
/>
</FormGroup>
</Col>
Expand Down
2 changes: 2 additions & 0 deletions app/packs/src/components/common/CopyElementModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export default class CopyElementModal extends React.Component {
ClipboardActions.fetchElementAndBuildCopy(element, selectedCol, 'copy_sample');
} else if (element.type === 'reaction') {
ElementActions.copyReaction(element, selectedCol);
} else if (element.type === 'research_plan') {
ElementActions.copyResearchPlan(element, selectedCol);
} else {
ElementActions.copyElement(element, selectedCol);
}
Expand Down
6 changes: 6 additions & 0 deletions app/packs/src/models/Attachment.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-underscore-dangle */
import Element from 'src/models/Element';
import { cloneDeep } from 'lodash';

export default class Attachment extends Element {
static NO_PREVIEW_AVAILABLE_PATH = '/images/wild_card/not_available.svg';
Expand Down Expand Up @@ -36,6 +37,11 @@ export default class Attachment extends Element {
this._preview = preview;
}

static buildCopy(_attachments) {
const newAttachments = cloneDeep(_attachments);
return newAttachments;
}

serialize() {
return super.serialize({
filename: this.filename,
Expand Down
34 changes: 34 additions & 0 deletions app/packs/src/models/ResearchPlan.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Element from 'src/models/Element';
import Container from 'src/models/Container';
import Segment from 'src/models/Segment';
import Wellplate from 'src/models/Wellplate';
import Attachment from './Attachment';

const uuidv4 = require('uuid/v4');

Expand Down Expand Up @@ -267,4 +268,37 @@ export default class ResearchPlan extends Element {
return this.attachments
.filter((attachment) => attachment.is_deleted === true && !attachment.is_new);
}

buildCopy(params = {}) {
const copy = super.buildCopy();
Object.assign(copy, params);
copy.attachments = this.attachments;
copy.container = Container.init();
copy.is_new = true;
copy.is_copy = true;
copy.can_update = true;
copy.can_copy = true;

return copy;
}

static copyFromResearchPlanAndCollectionId(research_plan, collection_id) {
const attachments = research_plan.attachments.map(
attach => Attachment.buildCopy(attach)
);
const params = {
collection_id,
name: research_plan.name,
body: research_plan.body,
}
const copy = research_plan.buildCopy(params);
copy.can_copy = false;
copy.changed = true;
copy.collection_id = collection_id;
copy.mode = 'edit';
copy.attachments = attachments;
copy.origin = { id: research_plan.id };

return copy;
}
}
11 changes: 11 additions & 0 deletions app/packs/src/stores/alt/actions/ElementActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,17 @@ class ElementActions {
};
}

copyResearchPlan(research_plan, colId) {
return (dispatch) => {
ResearchPlansFetcher.fetchById(research_plan.id)
.then((result) => {
dispatch({ research_plan: result, colId: colId });
}).catch((errorMessage) => {
console.log(errorMessage);
});
};
}

copyElement(element, colId) {
return (
{ element: element, colId: colId }
Expand Down
7 changes: 7 additions & 0 deletions app/packs/src/stores/alt/stores/ElementStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import UIStore from 'src/stores/alt/stores/UIStore';
import ClipboardStore from 'src/stores/alt/stores/ClipboardStore';
import Sample from 'src/models/Sample';
import Reaction from 'src/models/Reaction';
import ResearchPlan from 'src/models/ResearchPlan';
import Wellplate from 'src/models/Wellplate';
import Screen from 'src/models/Screen';

Expand Down Expand Up @@ -186,6 +187,7 @@ class ElementStore {
handleCreateReaction: ElementActions.createReaction,
handleCopyReactionFromId: ElementActions.copyReactionFromId,
handleCopyReaction: ElementActions.copyReaction,
handleCopyResearchPlan: ElementActions.copyResearchPlan,
handleCopyElement: ElementActions.copyElement,
handleOpenReactionDetails: ElementActions.openReactionDetails,

Expand Down Expand Up @@ -986,6 +988,11 @@ class ElementStore {
Aviator.navigate(`/collection/${result.colId}/reaction/copy`);
}

handleCopyResearchPlan(result) {
this.changeCurrentElement(ResearchPlan.copyFromResearchPlanAndCollectionId(result.research_plan, result.colId));
Aviator.navigate(`/collection/${result.colId}/research_plan/copy`);
}

handleCopyElement(result) {
this.changeCurrentElement(GenericEl.copyFromCollectionId(result.element, result.colId));
Aviator.navigate(`/collection/${result.colId}/${result.element.type}/copy`);
Expand Down
2 changes: 2 additions & 0 deletions app/packs/src/utilities/routesUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ const researchPlanShowOrNew = (e) => {

if (research_planID === 'new') {
ElementActions.generateEmptyResearchPlan(collectionID);
} else if (research_planID === 'copy') {
//
} else if (index < 0) {
ElementActions.fetchResearchPlanById(research_planID);
} else if (index !== activeKey) {
Expand Down
2 changes: 1 addition & 1 deletion app/usecases/attachments/annotation/annotation_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def get_annotation_of_attachment(attachment_id)
annotation = File.open(location_of_annotation, 'rb') if File.exist?(location_of_annotation)
raise 'could not find annotation of attachment (file not found)' unless annotation

annotation.read
annotation.read.force_encoding('UTF-8')
end

def annotation_json_present(data)
Expand Down
8 changes: 8 additions & 0 deletions app/usecases/attachments/annotation/annotation_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ def replace_link_with_base64(location_of_file, svg_string, mime_type)
xml
end

def updated_annotated_string(annotation_data, attachment_id)
annotation_data = annotation_data.gsub(
%r{/api/v1/attachments/image/([0-9])*},
"/api/v1/attachments/image/#{attachment_id}",
)
update_annotation(annotation_data, attachment_id)
end

class ThumbnailerWrapper
def create_thumbnail(tmp_path)
Thumbnailer.create(tmp_path)
Expand Down
41 changes: 41 additions & 0 deletions app/usecases/attachments/copy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Usecases
module Attachments
class Copy
def self.execute!(attachments, element, current_user_id)
attachments.each do |attach|
original_attach = Attachment.find attach[:id]
copy_attach = Attachment.new(
attachable_id: element.id,
attachable_type: element.class.name,
created_by: current_user_id,
created_for: current_user_id,
filename: original_attach.filename,
)
copy_attach.save

copy_io = original_attach.attachment_attacher.get.to_io
attacher = copy_attach.attachment_attacher
attacher.attach copy_io
copy_attach.file_path = copy_io.path
copy_attach.save

update_annotation(original_attach.id, copy_attach.id)

if element.instance_of?(::ResearchPlan)
element.update_body_attachments(original_attach.identifier, copy_attach.identifier)
end
end
end

def self.update_annotation(original_attach_id, copy_attach_id)
loader = Usecases::Attachments::Annotation::AnnotationLoader.new
svg = loader.get_annotation_of_attachment(original_attach_id)

updater = Usecases::Attachments::Annotation::AnnotationUpdater.new
updater.updated_annotated_string(svg, copy_attach_id)
end
end
end
end
Loading

0 comments on commit 7584fef

Please sign in to comment.