diff --git a/app/api/chemotion/research_plan_api.rb b/app/api/chemotion/research_plan_api.rb
index 740482bb3f..e04cdaa512 100644
--- a/app/api/chemotion/research_plan_api.rb
+++ b/app/api/chemotion/research_plan_api.rb
@@ -68,6 +68,7 @@ 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 = {
@@ -75,12 +76,14 @@ class ResearchPlanAPI < Grape::API
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
@@ -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])
@@ -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),
}
diff --git a/app/api/entities/application_entity.rb b/app/api/entities/application_entity.rb
index 05e788ee0c..e8519a0692 100644
--- a/app/api/entities/application_entity.rb
+++ b/app/api/entities/application_entity.rb
@@ -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
diff --git a/app/api/entities/reaction_entity.rb b/app/api/entities/reaction_entity.rb
index 6fe11e51fb..cbcbb1036c 100644
--- a/app/api/entities/reaction_entity.rb
+++ b/app/api/entities/reaction_entity.rb
@@ -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
diff --git a/app/api/entities/research_plan_entity.rb b/app/api/entities/research_plan_entity.rb
index a5694a5762..6934c90788 100644
--- a/app/api/entities/research_plan_entity.rb
+++ b/app/api/entities/research_plan_entity.rb
@@ -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
diff --git a/app/api/entities/sample_entity.rb b/app/api/entities/sample_entity.rb
index ac01d08f3a..1e07bb0b5d 100644
--- a/app/api/entities/sample_entity.rb
+++ b/app/api/entities/sample_entity.rb
@@ -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
diff --git a/app/models/attachment.rb b/app/models/attachment.rb
index 81d695fc84..c81b6f5660 100644
--- a/app/models/attachment.rb
+++ b/app/models/attachment.rb
@@ -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
diff --git a/app/models/research_plan.rb b/app/models/research_plan.rb
index 19abd4839e..27c5b47a04 100644
--- a/app/models/research_plan.rb
+++ b/app/models/research_plan.rb
@@ -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
@@ -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
diff --git a/app/packs/src/apps/mydb/elements/details/researchPlans/ResearchPlanDetails.js b/app/packs/src/apps/mydb/elements/details/researchPlans/ResearchPlanDetails.js
index f49cdb0ff3..81fc28481c 100644
--- a/app/packs/src/apps/mydb/elements/details/researchPlans/ResearchPlanDetails.js
+++ b/app/packs/src/apps/mydb/elements/details/researchPlans/ResearchPlanDetails.js
@@ -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';
@@ -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';
@@ -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 = (
+
+ );
return (
@@ -513,7 +524,7 @@ export default class ResearchPlanDetails extends Component {
- Fullresearch_plan}>
+ Full Research Plan}>
@@ -521,6 +532,7 @@ export default class ResearchPlanDetails extends Component {
{researchPlan.isNew
? null
: }
+ {copyBtn}
);
}
@@ -623,7 +635,7 @@ export default class ResearchPlanDetails extends Component {
{
- researchPlan.changed ? (
+ (researchPlan.changed || researchPlan.is_copy) ? (
diff --git a/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsName.js b/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsName.js
index b4bfcf8ffb..c0eb1c3695 100644
--- a/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsName.js
+++ b/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsName.js
@@ -53,6 +53,7 @@ export default class ResearchPlanDetailsName extends Component {
value={value || ''}
onChange={(event) => onChange(event.target.value)}
disabled={disabled}
+ name="research_plan_name"
/>
diff --git a/app/packs/src/components/common/CopyElementModal.js b/app/packs/src/components/common/CopyElementModal.js
index 271a2c4f0a..3e7b5b2ef2 100644
--- a/app/packs/src/components/common/CopyElementModal.js
+++ b/app/packs/src/components/common/CopyElementModal.js
@@ -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);
}
diff --git a/app/packs/src/models/Attachment.js b/app/packs/src/models/Attachment.js
index 08c7e4d839..9be9a7a528 100644
--- a/app/packs/src/models/Attachment.js
+++ b/app/packs/src/models/Attachment.js
@@ -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';
@@ -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,
diff --git a/app/packs/src/models/ResearchPlan.js b/app/packs/src/models/ResearchPlan.js
index 1830cf43a9..2873847f4c 100644
--- a/app/packs/src/models/ResearchPlan.js
+++ b/app/packs/src/models/ResearchPlan.js
@@ -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');
@@ -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;
+ }
}
diff --git a/app/packs/src/stores/alt/actions/ElementActions.js b/app/packs/src/stores/alt/actions/ElementActions.js
index 33dd8ef1a9..5f42be62b2 100644
--- a/app/packs/src/stores/alt/actions/ElementActions.js
+++ b/app/packs/src/stores/alt/actions/ElementActions.js
@@ -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 }
diff --git a/app/packs/src/stores/alt/stores/ElementStore.js b/app/packs/src/stores/alt/stores/ElementStore.js
index 2dfd3f13b0..5050ed88b9 100644
--- a/app/packs/src/stores/alt/stores/ElementStore.js
+++ b/app/packs/src/stores/alt/stores/ElementStore.js
@@ -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';
@@ -186,6 +187,7 @@ class ElementStore {
handleCreateReaction: ElementActions.createReaction,
handleCopyReactionFromId: ElementActions.copyReactionFromId,
handleCopyReaction: ElementActions.copyReaction,
+ handleCopyResearchPlan: ElementActions.copyResearchPlan,
handleCopyElement: ElementActions.copyElement,
handleOpenReactionDetails: ElementActions.openReactionDetails,
@@ -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`);
diff --git a/app/packs/src/utilities/routesUtils.js b/app/packs/src/utilities/routesUtils.js
index 11e0573cb5..695ce7280d 100644
--- a/app/packs/src/utilities/routesUtils.js
+++ b/app/packs/src/utilities/routesUtils.js
@@ -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) {
diff --git a/app/usecases/attachments/annotation/annotation_loader.rb b/app/usecases/attachments/annotation/annotation_loader.rb
index 45323f60e6..2d977bc898 100644
--- a/app/usecases/attachments/annotation/annotation_loader.rb
+++ b/app/usecases/attachments/annotation/annotation_loader.rb
@@ -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)
diff --git a/app/usecases/attachments/annotation/annotation_updater.rb b/app/usecases/attachments/annotation/annotation_updater.rb
index 9ae52e2510..d2df60f4ba 100644
--- a/app/usecases/attachments/annotation/annotation_updater.rb
+++ b/app/usecases/attachments/annotation/annotation_updater.rb
@@ -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)
diff --git a/app/usecases/attachments/copy.rb b/app/usecases/attachments/copy.rb
new file mode 100644
index 0000000000..e77bdb84c3
--- /dev/null
+++ b/app/usecases/attachments/copy.rb
@@ -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
diff --git a/lib/import/import_collections.rb b/lib/import/import_collections.rb
index 92694fc1b1..a374b69b73 100644
--- a/lib/import/import_collections.rb
+++ b/lib/import/import_collections.rb
@@ -135,13 +135,7 @@ def import_annotation(zip_file, entry, attachment)
annotation_data = annotation_entry.get_input_stream.read.force_encoding('UTF-8')
updater = Usecases::Attachments::Annotation::AnnotationUpdater.new
-
- annotation_data = annotation_data.gsub(
- %r{/api/v1/attachments/image/([0-9])*},
- "/api/v1/attachments/image/#{attachment.id}",
- )
-
- updater.update_annotation(annotation_data, attachment.id)
+ updater.updated_annotated_string(annotation_data, attachment.id)
end
def import_collections
diff --git a/spec/features/copy_research_plan_spec.rb b/spec/features/copy_research_plan_spec.rb
new file mode 100644
index 0000000000..544febc4ea
--- /dev/null
+++ b/spec/features/copy_research_plan_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Copy research plans' do
+ let!(:user_one) { create(:user, first_name: 'Hello', last_name: 'Complat', account_active: true) }
+ let!(:user_two) { create(:user, first_name: 'User2', last_name: 'Complat', account_active: true) }
+ let(:rp_one) { create(:research_plan, creator: user_one, name: 'RP 1', body: []) }
+ let!(:col_one) { create(:collection, user_id: user_one.id, label: 'Col1') }
+
+ let(:rp_two) { create(:research_plan, creator: user_one, name: 'RP 2', body: []) }
+ let!(:root_share) { create(:collection, user: user_one, shared_by_id: user_two.id, is_shared: true, is_locked: true) }
+ let!(:col_share) do
+ create(:collection, user: user_one, label: 'share-col', permission_level: 10, shared_by_id: user_two.id,
+ is_shared: true, ancestry: root_share.id.to_s)
+ end
+
+ before do
+ sign_in(user_one)
+ fp = Rails.public_path.join('images', 'molecules', 'molecule.svg')
+ svg_path = Rails.root.join('spec/fixtures/images/molecule.svg')
+ `ln -s #{svg_path} #{fp} ` unless File.exist?(fp)
+ CollectionsResearchPlan.find_or_create_by!(research_plan: rp_one, collection: col_one)
+ CollectionsResearchPlan.find_or_create_by!(research_plan: rp_two, collection: col_share)
+ end
+
+ it 'new research plan', :js do
+ find_by_id('tree-id-Col1').click
+ first('i.icon-research_plan').click
+ expect(page).to have_no_button('copy-element-btn', wait: 5)
+ end
+
+ it 'to same collection', :js do
+ find_by_id('tree-id-Col1').click
+ first('i.icon-research_plan').click
+ find_by_id('tree-id-Col1').click
+ expect(page).to have_content('RP 1')
+ find('div.preview-table').click
+ first('i.fa-clone').click
+ find_by_id('submit-copy-element-btn').click
+ fill_in('research_plan_name', with: 'RP copy')
+ find_field('research_plan_name').set('RP copy').send_keys(:enter)
+ expect(page).to have_content('RP copy')
+ end
+
+ it 'to diff collection', :js do
+ find_by_id('tree-id-Col1').click
+ first('i.icon-research_plan').click
+ find_by_id('tree-id-Col1').click
+ expect(page).to have_content('RP 1')
+ find('div.preview-table').click
+ first('i.fa-clone').click
+ # to diff col:
+ find_field('modal-collection-id-select').set('Col2').send_keys(:enter)
+ find_by_id('submit-copy-element-btn').click
+ find_field('research_plan_name').set('RP copy').send_keys(:enter)
+ expect(page).to have_content('RP copy', wait: 5)
+ end
+
+ it 'to shared collection with permission', :js do
+ find_by_id('shared-home-link').click
+ find_all('span.glyphicon-plus')[0].click
+ find_by_id('tree-id-share-col').click
+ first('i.icon-research_plan').click
+ expect(page).to have_content('RP 2', wait: 5)
+ find('div.preview-table').click
+ first('i.fa-clone').click
+ find_field('modal-collection-id-select').set('Col1').send_keys(:enter)
+ find_by_id('submit-copy-element-btn').click
+ find('.tree-view', text: 'Col1').click
+ first('i.icon-research_plan').click
+ expect(page).to have_content('RP 2', wait: 5)
+ end
+end