diff --git a/app/api/chemotion/cell_line_api.rb b/app/api/chemotion/cell_line_api.rb
index 47953f297d..6ec3ba8c07 100644
--- a/app/api/chemotion/cell_line_api.rb
+++ b/app/api/chemotion/cell_line_api.rb
@@ -5,6 +5,7 @@ class CellLineAPI < Grape::API
include Grape::Kaminari
helpers ParamsHelpers
helpers ContainerHelpers
+ helpers CellLineApiParamsHelpers
rescue_from ActiveRecord::RecordNotFound do
error!('Ressource not found', 401)
@@ -12,16 +13,13 @@ class CellLineAPI < Grape::API
resource :cell_lines do
desc 'return cell lines of a collection'
params do
- optional :collection_id, type: Integer, desc: 'Collection id'
- optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id'
- optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at'
- optional :from_date, type: Integer, desc: 'created_date from in ms'
- optional :to_date, type: Integer, desc: 'created_date to in ms'
+ use :cell_line_get_params
end
paginate per_page: 5, offset: 0
before do
params[:per_page].to_i > 50 && (params[:per_page] = 50)
end
+
get do
scope = if params[:collection_id]
begin
@@ -82,29 +80,7 @@ class CellLineAPI < Grape::API
desc 'Create a new Cell line sample'
params do
- optional :organism, type: String, desc: 'name of the donor organism of the cell'
- optional :tissue, type: String, desc: 'tissue from which the cell originates'
- requires :amount, type: Integer, desc: 'amount of cells'
- requires :unit, type: String, desc: 'unit of cell amount'
- requires :passage, type: Integer, desc: 'passage of cells'
- optional :disease, type: String, desc: 'deasease of cells'
- requires :material_names, type: String, desc: 'names of cell line e.g. name1;name2'
- requires :collection_id, type: Integer, desc: 'Collection of the cell line sample'
- optional :cell_type, type: String, desc: 'type of cells'
- optional :biosafety_level, type: String, desc: 'biosafety_level of cells'
- optional :growth_medium, type: String, desc: 'growth medium of cells'
- optional :variant, type: String, desc: 'variant of cells'
- optional :optimal_growth_temp, type: Float, desc: 'optimal_growth_temp of cells'
- optional :cryo_pres_medium, type: String, desc: 'cryo preservation medium of cells'
- optional :gender, type: String, desc: 'gender of donor organism'
- optional :material_description, type: String, desc: 'description of cell line concept'
- optional :contamination, type: String, desc: 'contamination of a cell line sample'
- requires :source, type: String, desc: 'source of a cell line sample'
- optional :name, type: String, desc: 'name of a cell line sample'
- optional :mutation, type: String, desc: 'mutation of a cell line'
- optional :description, type: String, desc: 'description of a cell line sample'
- optional :short_label, type: String, desc: 'short label of a cell line sample'
- requires :container, type: Hash, desc: 'root Container of element'
+ use :cell_line_creation_params
end
post do
error!('401 Unauthorized', 401) unless current_user.collections.find(params[:collection_id])
@@ -116,28 +92,7 @@ class CellLineAPI < Grape::API
end
desc 'Update a Cell line sample'
params do
- requires :cell_line_sample_id, type: String, desc: 'id of the cell line to update'
- optional :organism, type: String, desc: 'name of the donor organism of the cell'
- optional :mutation, type: String, desc: 'mutation of a cell line'
- optional :tissue, type: String, desc: 'tissue from which the cell originates'
- requires :amount, type: Integer, desc: 'amount of cells'
- requires :unit, type: String, desc: 'unit of amount of cells'
- optional :passage, type: Integer, desc: 'passage of cells'
- optional :disease, type: String, desc: 'deasease of cells'
- optional :material_names, type: String, desc: 'names of cell line e.g. name1;name2'
- optional :collection_id, type: Integer, desc: 'Collection of the cell line sample'
- optional :cell_type, type: String, desc: 'type of cells'
- optional :biosafety_level, type: String, desc: 'biosafety_level of cells'
- optional :variant, type: String, desc: 'variant of cells'
- optional :optimal_growth_temp, type: Float, desc: 'optimal_growth_temp of cells'
- optional :cryo_pres_medium, type: String, desc: 'cryo preservation medium of cells'
- optional :gender, type: String, desc: 'gender of donor organism'
- optional :material_description, type: String, desc: 'description of cell line concept'
- optional :contamination, type: String, desc: 'contamination of a cell line sample'
- optional :source, type: String, desc: 'source of a cell line sample'
- optional :name, type: String, desc: 'name of a cell line sample'
- optional :description, type: String, desc: 'description of a cell line sample'
- requires :container, type: Hash, desc: 'root Container of element'
+ use :cell_line_update_params
end
put do
use_case = Usecases::CellLines::Update.new(params, current_user)
@@ -146,12 +101,59 @@ class CellLineAPI < Grape::API
return present cell_line_sample, with: Entities::CellLineSampleEntity
end
+ desc 'Copy a cell line'
+ params do
+ requires :id, type: Integer, desc: 'id of cell line sample to copy'
+ requires :collection_id, type: Integer, desc: 'id of collection of copied cell line sample'
+ requires :container, type: Hash, desc: 'root container of element'
+ end
+ namespace :copy do
+ post do
+ cell_line_to_copy = @current_user.cellline_samples.where(id: [params[:id]]).reorder('id')
+
+ error!('401 Unauthorized', 401) unless ElementsPolicy.new(@current_user, cell_line_to_copy).update?
+
+ begin
+ use_case = Usecases::CellLines::Copy.new(cell_line_to_copy.first, @current_user, params[:collection_id])
+ copied_cell_line_sample = use_case.execute!
+ copied_cell_line_sample.container = update_datamodel(params[:container])
+ rescue StandardError => e
+ error!(e, 400)
+ end
+ return present copied_cell_line_sample, with: Entities::CellLineSampleEntity
+ end
+ end
+
+ desc 'Splits a cell line'
+ params do
+ requires :id, type: Integer, desc: 'id of cell line sample to copy'
+ requires :collection_id, type: Integer, desc: 'id of collection of copied cell line sample'
+ optional :container, type: Hash, desc: 'root container of element'
+ end
+ namespace :split do
+ post do
+ cell_line_to_copy = @current_user.cellline_samples.where(id: [params[:id]]).reorder('id')
+
+ error!('401 Unauthorized', 401) unless ElementsPolicy.new(@current_user, cell_line_to_copy).update?
+
+ begin
+ use_case = Usecases::CellLines::Split.new(cell_line_to_copy.first, @current_user, params[:collection_id])
+ splitted_cell_line_sample = use_case.execute!
+ splitted_cell_line_sample.container = update_datamodel(params[:container]) if @params.key?('container')
+ rescue StandardError => e
+ error!(e, 400)
+ end
+ return present splitted_cell_line_sample, with: Entities::CellLineSampleEntity
+ end
+ end
+
resource :names do
desc 'Returns all accessable cell line material names and their id'
get 'all' do
return present CelllineMaterial.all, with: Entities::CellLineMaterialNameEntity
end
end
+
resource :material do
params do
requires :id, type: Integer, desc: 'id of cell line material to load'
diff --git a/app/api/helpers/cell_line_api_params_helpers.rb b/app/api/helpers/cell_line_api_params_helpers.rb
new file mode 100644
index 0000000000..662c834c30
--- /dev/null
+++ b/app/api/helpers/cell_line_api_params_helpers.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module CellLineApiParamsHelpers
+ extend Grape::API::Helpers
+
+ params :cell_line_get_params do
+ optional :collection_id, type: Integer, desc: 'Collection id'
+ optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id'
+ optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at'
+ optional :from_date, type: Integer, desc: 'created_date from in ms'
+ optional :to_date, type: Integer, desc: 'created_date to in ms'
+ end
+
+ params :cell_line_creation_params do
+ optional :organism, type: String, desc: 'name of the donor organism of the cell'
+ optional :tissue, type: String, desc: 'tissue from which the cell originates'
+ requires :amount, type: Integer, desc: 'amount of cells'
+ requires :unit, type: String, desc: 'unit of cell amount'
+ requires :passage, type: Integer, desc: 'passage of cells'
+ optional :disease, type: String, desc: 'deasease of cells'
+ requires :material_names, type: String, desc: 'names of cell line e.g. name1;name2'
+ requires :collection_id, type: Integer, desc: 'Collection of the cell line sample'
+ optional :cell_type, type: String, desc: 'type of cells'
+ optional :biosafety_level, type: String, desc: 'biosafety_level of cells'
+ optional :growth_medium, type: String, desc: 'growth medium of cells'
+ optional :variant, type: String, desc: 'variant of cells'
+ optional :optimal_growth_temp, type: Float, desc: 'optimal_growth_temp of cells'
+ optional :cryo_pres_medium, type: String, desc: 'cryo preservation medium of cells'
+ optional :gender, type: String, desc: 'gender of donor organism'
+ optional :material_description, type: String, desc: 'description of cell line concept'
+ optional :contamination, type: String, desc: 'contamination of a cell line sample'
+ requires :source, type: String, desc: 'source of a cell line sample'
+ optional :name, type: String, desc: 'name of a cell line sample'
+ optional :mutation, type: String, desc: 'mutation of a cell line'
+ optional :description, type: String, desc: 'description of a cell line sample'
+ optional :short_label, type: String, desc: 'short label of a cell line sample'
+ requires :container, type: Hash, desc: 'root Container of element'
+ end
+
+ params :cell_line_update_params do
+ requires :cell_line_sample_id, type: String, desc: 'id of the cell line to update'
+ optional :organism, type: String, desc: 'name of the donor organism of the cell'
+ optional :mutation, type: String, desc: 'mutation of a cell line'
+ optional :tissue, type: String, desc: 'tissue from which the cell originates'
+ requires :amount, type: Integer, desc: 'amount of cells'
+ requires :unit, type: String, desc: 'unit of amount of cells'
+ optional :passage, type: Integer, desc: 'passage of cells'
+ optional :disease, type: String, desc: 'deasease of cells'
+ optional :material_names, type: String, desc: 'names of cell line e.g. name1;name2'
+ optional :collection_id, type: Integer, desc: 'Collection of the cell line sample'
+ optional :cell_type, type: String, desc: 'type of cells'
+ optional :biosafety_level, type: String, desc: 'biosafety_level of cells'
+ optional :variant, type: String, desc: 'variant of cells'
+ optional :optimal_growth_temp, type: Float, desc: 'optimal_growth_temp of cells'
+ optional :cryo_pres_medium, type: String, desc: 'cryo preservation medium of cells'
+ optional :gender, type: String, desc: 'gender of donor organism'
+ optional :material_description, type: String, desc: 'description of cell line concept'
+ optional :contamination, type: String, desc: 'contamination of a cell line sample'
+ optional :source, type: String, desc: 'source of a cell line sample'
+ optional :name, type: String, desc: 'name of a cell line sample'
+ optional :description, type: String, desc: 'description of a cell line sample'
+ requires :container, type: Hash, desc: 'root Container of element'
+ end
+end
diff --git a/app/javascript/src/components/contextActions/CreateButton.js b/app/javascript/src/components/contextActions/CreateButton.js
index 99e5f1ab3a..8785c1b577 100644
--- a/app/javascript/src/components/contextActions/CreateButton.js
+++ b/app/javascript/src/components/contextActions/CreateButton.js
@@ -75,6 +75,11 @@ export default class CreateButton extends React.Component {
return uiState.reaction.checkedIds.first();
}
+ getCellLineId(){
+ let uiState = UIStore.getState();
+ return uiState.cell_line.checkedIds.first();
+ }
+
isCopySampleDisabled() {
let sampleFilter = this.getSampleFilter();
return !sampleFilter.all && sampleFilter.included_ids.size == 0;
@@ -102,6 +107,17 @@ export default class CreateButton extends React.Component {
ElementActions.copyReactionFromId(reactionId);
}
+ isCopyCellLineDisabled() {
+ let cellLineId = this.getCellLineId();
+ return !cellLineId;
+ }
+
+ copyCellLine() {
+ let uiState = UIStore.getState();
+ let cellLineId = this.getCellLineId();
+ ElementActions.copyCellLineFromId(parseInt(cellLineId),uiState.currentCollection.id);
+ }
+
createWellplateFromSamples() {
let uiState = UIStore.getState();
let sampleFilter = this.filterParamsFromUIStateByElementType(uiState, "sample");
@@ -290,6 +306,9 @@ export default class CreateButton extends React.Component {
this.copyReaction()} disabled={this.isCopyReactionDisabled()}>
Copy Reaction
+ this.copyCellLine()} disabled={this.isCopyCellLineDisabled()}>
+ Copy Cell line
+
);
}
diff --git a/app/javascript/src/components/contextActions/SplitElementButton.js b/app/javascript/src/components/contextActions/SplitElementButton.js
index 93a22f855c..d872ee5587 100644
--- a/app/javascript/src/components/contextActions/SplitElementButton.js
+++ b/app/javascript/src/components/contextActions/SplitElementButton.js
@@ -91,6 +91,12 @@ export default class SplitElementButton extends React.Component {
>
Split Wellplate
+ ElementActions.splitAsSubCellLines(UIStore.getState())}
+ disabled={this.noSelected('cell_line') || this.isAllCollection()}
+ >
+ Split Cell line
+
{sortedGenericEls.map((el) => (
CellLine.createFromRestResponse(params.collection_id, json))
.then((cellLineItem) => {
NotificationActions.add(successfullyCreatedParameter);
- user.cell_lines_count = user.cell_lines_count +1;
+ user.cell_lines_count += 1;
return cellLineItem;
})
.catch((errorMessage) => {
@@ -125,6 +102,29 @@ export default class CellLinesFetcher {
}).then((response) => response.json());
}
+ static copyCellLine(cellLineId, collectionId) {
+ const params = {
+ id: cellLineId,
+ collection_id: collectionId,
+ container: Container.init()
+ };
+
+ return fetch('/api/v1/cell_lines/copy/', {
+ credentials: 'same-origin',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST',
+ body: JSON.stringify(params)
+ }).then((response) => response.json())
+
+ .then((json) => {
+ NotificationActions.add(successfullyCopiedParameter);
+ return CellLine.createFromRestResponse(collectionId, json);
+ });
+ }
+
static update(cellLineItem) {
const params = extractApiParameter(cellLineItem);
const promise = CellLinesFetcher.uploadAttachments(cellLineItem)
@@ -138,8 +138,8 @@ export default class CellLinesFetcher {
body: JSON.stringify(params)
}))
.then((response) => response.json())
- .then(() => {BaseFetcher.updateAnnotationsInContainer(cellLineItem)})
- .then(()=> CellLinesFetcher.fetchById(cellLineItem.id))
+ .then(() => { BaseFetcher.updateAnnotationsInContainer(cellLineItem); })
+ .then(() => CellLinesFetcher.fetchById(cellLineItem.id))
.then((loadedCellLineSample) => {
NotificationActions.add(successfullyUpdatedParameter);
return loadedCellLineSample;
@@ -151,4 +151,25 @@ export default class CellLinesFetcher {
});
return promise;
}
+
+ // Here better as parameter list of ids
+ static splitAsSubCellLines(ids, collectionId) {
+ const promises = [];
+
+ ids.forEach((id) => {
+ const params = { id, collection_id: collectionId };
+ promises.push(fetch('/api/v1/cell_lines/split', {
+ credentials: 'same-origin',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST',
+ body: JSON.stringify(params)
+ }));
+ });
+
+ return Promise.all(promises)
+ .then(() => { NotificationActions.add(successfullySplittedParameter); });
+ }
}
diff --git a/app/javascript/src/stores/alt/actions/ElementActions.js b/app/javascript/src/stores/alt/actions/ElementActions.js
index 181839607e..ebf2d47713 100644
--- a/app/javascript/src/stores/alt/actions/ElementActions.js
+++ b/app/javascript/src/stores/alt/actions/ElementActions.js
@@ -480,6 +480,17 @@ class ElementActions {
return cellLineSample;
}
+ copyCellLineFromId(id,collectionId ) {
+ return (dispatch) => {
+ CellLinesFetcher.copyCellLine(id,collectionId)
+ .then((result) => {
+ dispatch(result);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
splitAsSubsamples(ui_state) {
return (dispatch) => {
SamplesFetcher.splitAsSubsamples(ui_state)
@@ -676,6 +687,20 @@ class ElementActions {
};
}
+ splitAsSubCellLines(ui_state) {
+ return (dispatch) => {
+ const ids = ui_state["cell_line"].checkedIds.toArray();
+ const collection_id = ui_state.currentCollection.id;
+
+ CellLinesFetcher.splitAsSubCellLines(ids,collection_id)
+ .then((result) => {
+ dispatch(ui_state);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
bulkCreateWellplatesFromSamples(params) {
let { collection_id, samples, wellplateCount } = params;
diff --git a/app/javascript/src/stores/alt/stores/ElementStore.js b/app/javascript/src/stores/alt/stores/ElementStore.js
index 1c53562f62..454a7fd7e4 100644
--- a/app/javascript/src/stores/alt/stores/ElementStore.js
+++ b/app/javascript/src/stores/alt/stores/ElementStore.js
@@ -189,6 +189,7 @@ class ElementStore {
handleCopyReaction: ElementActions.copyReaction,
handleCopyResearchPlan: ElementActions.copyResearchPlan,
handleCopyElement: ElementActions.copyElement,
+ handleCopyCellLine: ElementActions.copyCellLineFromId,
handleOpenReactionDetails: ElementActions.openReactionDetails,
handleBulkCreateWellplatesFromSamples:
@@ -244,6 +245,7 @@ class ElementStore {
handleSplitAsSubsamples: ElementActions.splitAsSubsamples,
handleSplitElements: ElementActions.splitElements,
handleSplitAsSubwellplates: ElementActions.splitAsSubwellplates,
+ handleSplitAsSubCellLines: ElementActions.splitAsSubCellLines,
// formerly from DetailStore
handleSelect: DetailActions.select,
handleClose: DetailActions.close,
@@ -750,6 +752,10 @@ class ElementStore {
);
}
+ handleSplitAsSubCellLines(ui_state) {
+ ElementActions.fetchCellLinesByCollectionId(ui_state.currentCollection.id);
+ }
+
// Molecules
handleFetchMoleculeByMolfile(result) {
// Attention: This is intended to update SampleDetails
@@ -1007,6 +1013,11 @@ class ElementStore {
Aviator.navigate(`/collection/${result.colId}/${result.element.type}/copy`);
}
+ handleCopyCellLine(result){
+ UserActions.fetchCurrentUser(); //Needed to update the cell line counter in frontend
+ Aviator.navigate(`/collection/${result.collectionId}/cell_line/${result.id}`);
+ }
+
handleOpenReactionDetails(reaction) {
this.changeCurrentElement(reaction);
this.handleRefreshElements('sample')
diff --git a/app/javascript/src/utilities/CellLineUtils.js b/app/javascript/src/utilities/CellLineUtils.js
index a546fc538a..f2eaf434fc 100644
--- a/app/javascript/src/utilities/CellLineUtils.js
+++ b/app/javascript/src/utilities/CellLineUtils.js
@@ -1,32 +1,79 @@
-const extractApiParameter = (cellLine) => {
- return {
- "cell_line_sample_id":cellLine.id,
- "organism":cellLine.organism,
- "tissue":cellLine.tissue,
- "amount":cellLine.amount,
- "passage":cellLine.passage,
- "disease":cellLine.disease,
- "growth_medium":cellLine.growthMedium,
- "material_names":cellLine.cellLineName,
- "collection_id":cellLine.collectionId,
- "cell_type":cellLine.cellType,
- "mutation":cellLine.mutation,
- "unit":cellLine.unit,
- "biosafety_level":cellLine.bioSafetyLevel,
- "variant":cellLine.variant,
- "optimal_growth_temp":cellLine.optimal_growth_temp,
- "cryo_pres_medium":cellLine.cryopreservationMedium,
- "gender":cellLine.gender,
- "material_description":cellLine.materialDescription,
- "contamination":cellLine.contamination,
- "source":cellLine.source,
- "name":cellLine.itemName,
- "description":cellLine.itemDescription,
- "short_label":cellLine.short_label,
- "container":cellLine.container
+const extractApiParameter = (cellLine) => ({
+ cell_line_sample_id: cellLine.id,
+ organism: cellLine.organism,
+ tissue: cellLine.tissue,
+ amount: cellLine.amount,
+ passage: cellLine.passage,
+ disease: cellLine.disease,
+ growth_medium: cellLine.growthMedium,
+ material_names: cellLine.cellLineName,
+ collection_id: cellLine.collectionId,
+ cell_type: cellLine.cellType,
+ mutation: cellLine.mutation,
+ unit: cellLine.unit,
+ biosafety_level: cellLine.bioSafetyLevel,
+ variant: cellLine.variant,
+ optimal_growth_temp: cellLine.optimal_growth_temp,
+ cryo_pres_medium: cellLine.cryopreservationMedium,
+ gender: cellLine.gender,
+ material_description: cellLine.materialDescription,
+ contamination: cellLine.contamination,
+ source: cellLine.source,
+ name: cellLine.itemName,
+ description: cellLine.itemDescription,
+ short_label: cellLine.short_label,
+ container: cellLine.container
+});
+
+const successfullyCreatedParameter = {
+ title: 'Element created',
+ message: 'Cell line sample successfully added',
+ level: 'info',
+ dismissible: 'button',
+ autoDismiss: 10,
+ position: 'tr'
+};
+
+const successfullyCopiedParameter = {
+ title: 'Element copied',
+ message: 'Cell line sample successfully copied',
+ level: 'info',
+ dismissible: 'button',
+ autoDismiss: 10,
+ position: 'tr'
+};
+
+const successfullyUpdatedParameter = {
+ title: 'Element updated',
+ message: 'Cell line sample successfully updated',
+ level: 'info',
+ dismissible: 'button',
+ autoDismiss: 10,
+ position: 'tr'
+};
+const successfullySplittedParameter = {
+ title: 'Element splitted',
+ message: 'Cell line sample successfully splitted',
+ level: 'info',
+ dismissible: 'button',
+ autoDismiss: 10,
+ position: 'tr'
+};
+
+const errorMessageParameter = {
+ title: 'Error',
+ message: 'Unfortunately, the last action failed. Please try again or contact your admin.',
+ level: 'error',
+ dismissible: 'button',
+ autoDismiss: 30,
+ position: 'tr'
};
-}
export {
- extractApiParameter
-};
\ No newline at end of file
+ extractApiParameter,
+ successfullyCreatedParameter,
+ successfullyCopiedParameter,
+ successfullyUpdatedParameter,
+ errorMessageParameter,
+ successfullySplittedParameter
+};
diff --git a/app/models/cellline_sample.rb b/app/models/cellline_sample.rb
index 30a98daf92..ce5e2f2417 100644
--- a/app/models/cellline_sample.rb
+++ b/app/models/cellline_sample.rb
@@ -22,6 +22,7 @@
# rubocop:disable Rails/InverseOf, Rails/HasManyOrHasOneDependent
class CelllineSample < ApplicationRecord
acts_as_paranoid
+ has_ancestry
include ElementUIStateScopes
include Taggable
@@ -35,6 +36,10 @@ class CelllineSample < ApplicationRecord
belongs_to :cellline_material
belongs_to :creator, class_name: 'User', foreign_key: 'user_id'
+ has_many :sync_collections_users, through: :collections
+
+ after_create :create_root_container
+
scope :by_sample_name, lambda { |query, collection_id|
joins(:collections).where(collections: { id: collection_id })
.where('name ILIKE ?', "%#{sanitize_sql_like(query)}%")
@@ -46,5 +51,9 @@ class CelllineSample < ApplicationRecord
.where('collections.id=?', collection_id)
.where('cellline_materials.name ILIKE ?', "%#{sanitize_sql_like(query)}%")
}
+
+ def create_root_container
+ self.container = Container.create_root_container if container.nil?
+ end
end
# rubocop:enable Rails/InverseOf, Rails/HasManyOrHasOneDependent
diff --git a/app/usecases/cell_lines/copy.rb b/app/usecases/cell_lines/copy.rb
new file mode 100644
index 0000000000..049036d6b3
--- /dev/null
+++ b/app/usecases/cell_lines/copy.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Usecases
+ module CellLines
+ class Copy
+ def initialize(cell_line_sample_to_copy, current_user, collection_id)
+ @current_user = current_user
+ @cell_line_sample_to_copy = cell_line_sample_to_copy
+ @collection_id = collection_id
+ end
+
+ def execute!
+ copied_cell_line_sample = copy_cellline_sample
+ create_collection_links(copied_cell_line_sample.id)
+ @current_user.increment_counter('celllines') # rubocop: disable Rails/SkipsModelValidations
+ copied_cell_line_sample
+ end
+
+ private
+
+ def copy_cellline_sample
+ CelllineSample.create(
+ cellline_material: @cell_line_sample_to_copy.cellline_material,
+ creator: @current_user,
+ amount: @cell_line_sample_to_copy[:amount],
+ unit: @cell_line_sample_to_copy[:unit],
+ passage: @cell_line_sample_to_copy[:passage],
+ contamination: @cell_line_sample_to_copy[:contamination],
+ name: @cell_line_sample_to_copy[:name],
+ description: @cell_line_sample_to_copy[:description],
+ short_label: create_short_label,
+ )
+ end
+
+ def create_short_label
+ "#{@current_user.name_abbreviation}-C#{@current_user.counters['celllines']}"
+ end
+
+ def create_collection_links(id)
+ CollectionsCellline.create(
+ collection: Collection.find(@collection_id),
+ cellline_sample_id: id,
+ )
+ CollectionsCellline.create(
+ collection: all_collection_of_current_user,
+ cellline_sample_id: id,
+ )
+ end
+
+ def all_collection_of_current_user
+ Collection.get_all_collection_for_user(@current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/usecases/cell_lines/split.rb b/app/usecases/cell_lines/split.rb
new file mode 100644
index 0000000000..b2c9f1b4a9
--- /dev/null
+++ b/app/usecases/cell_lines/split.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Usecases
+ module CellLines
+ class Split
+ def initialize(cell_line_sample_to_copy, current_user, collection_id)
+ @current_user = current_user
+ @cell_line_sample_to_copy = cell_line_sample_to_copy
+ @collection_id = collection_id
+ end
+
+ def execute!
+ @copied_cell_line_sample = Usecases::CellLines::Copy.new(@cell_line_sample_to_copy, @current_user,
+ @collection_id).execute!
+ decrease_cell_line_counter
+
+ create_ancestry_relationship
+
+ create_short_label
+
+ @copied_cell_line_sample
+ end
+
+ private
+
+ def create_ancestry_relationship
+ @copied_cell_line_sample.parent = @cell_line_sample_to_copy
+ @copied_cell_line_sample.save
+ end
+
+ def create_short_label
+ next_child_index = @cell_line_sample_to_copy.children.count.to_s
+ @copied_cell_line_sample.short_label = "#{@cell_line_sample_to_copy.short_label}-#{next_child_index}"
+ @copied_cell_line_sample.save
+ end
+
+ def decrease_cell_line_counter
+ @current_user.counters['celllines'] = (@current_user.counters['celllines'].to_i - 1).to_s
+ end
+ end
+ end
+end
diff --git a/db/migrate/20240531112321_add_ancestry_to_cellline_sample.rb b/db/migrate/20240531112321_add_ancestry_to_cellline_sample.rb
new file mode 100644
index 0000000000..784d39046d
--- /dev/null
+++ b/db/migrate/20240531112321_add_ancestry_to_cellline_sample.rb
@@ -0,0 +1,6 @@
+class AddAncestryToCelllineSample < ActiveRecord::Migration[6.1]
+ def change
+ add_column :cellline_samples, :ancestry, :string
+ add_index :cellline_samples, :ancestry
+ end
+end
diff --git a/spec/api/chemotion/cell_line_api_spec.rb b/spec/api/chemotion/cell_line_api_spec.rb
index f46b2246c9..c4d77c66b0 100644
--- a/spec/api/chemotion/cell_line_api_spec.rb
+++ b/spec/api/chemotion/cell_line_api_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable RSpec/LetSetup, RSpec/NestedGroups
+# rubocop:disable RSpec/LetSetup, RSpec/NestedGroups, RSpec/AnyInstance
require 'rails_helper'
@@ -249,5 +249,122 @@
end
end
end
+
+ describe 'POST /api/v1/cell_lines/copy' do
+ let(:collection) { create(:collection, label: 'other collection') }
+ let!(:user) { create(:user, collections: [collection]) }
+ let!(:cell_line) { create(:cellline_sample, collections: [collection]) }
+ let(:allow_creation) { true }
+ let(:container_param) do
+ { 'name' => 'new',
+ 'children' =>
+[{ 'name' => 'new',
+ 'children' => [],
+ 'attachments' => [],
+ 'is_deleted' => false,
+ 'description' => '',
+ 'extended_metadata' => { 'report' => true },
+ 'container_type' => 'analyses',
+ 'id' => '656936a0-0627-11ef-b812-d3c35856aafa',
+ 'is_new' => true,
+ '_checksum' => '6901ba2b29f8464ede2dce839d1dfba710dbbfa6d2c4ad80f6bd3a933e792028' }],
+ 'attachments' => [],
+ 'is_deleted' => false,
+ 'description' => '',
+ 'extended_metadata' => { 'report' => true },
+ 'container_type' => 'root',
+ 'id' => '65690f90-0627-11ef-b812-d3c35856aafa',
+ 'is_new' => true,
+ '_checksum' => '7a0f02ddb8c73d674640466b84ce50e53465a7d91265aa0e8ece271f099d04f5' }
+ end
+ let(:params) do
+ {
+ id: cell_line.id,
+ collection_id: collection.id,
+ container: container_param,
+ }
+ end
+
+ before do
+ allow_any_instance_of(ElementsPolicy).to receive(:update?).and_return(allow_creation)
+ post '/api/v1/cell_lines/copy', params: params
+ end
+
+ context 'when cell line not accessable' do
+ let(:params) { { id: '-1', collection_id: collection.id, container: container_param } }
+
+ it 'returns correct response code 400' do
+ expect(response).to have_http_status :bad_request
+ end
+ end
+
+ context 'when user only has read access' do
+ let(:allow_creation) { false }
+
+ before do
+ allow_any_instance_of(ElementPolicy).to receive(:update?).and_return(false)
+ post '/api/v1/cell_lines/copy', params: params
+ end
+
+ it 'returns correct response code 401' do
+ expect(response).to have_http_status :unauthorized
+ end
+ end
+
+ context 'when user has write access' do
+ it 'returns correct response code' do
+ expect(response).to have_http_status :created
+ end
+
+ it 'copied cell line sample was created' do
+ loaded_cell_line_sample = CelllineSample.find(parsed_json_response['id'])
+ expect(loaded_cell_line_sample).not_to be_nil
+ end
+
+ it 'copied cell line added to all and original collection' do
+ loaded_cell_line_sample = CelllineSample.find(parsed_json_response['id'])
+ collection_labels = loaded_cell_line_sample.collections.order(:label).pluck(:label)
+ expect(collection_labels).to eq ['All', 'other collection']
+ end
+ end
+ end
+
+ describe 'POST /api/v1/cell_lines/split' do
+ let(:collection) { create(:collection, label: 'other collection') }
+ let!(:user) { create(:user, collections: [collection]) }
+ let!(:cell_line) { create(:cellline_sample, collections: [collection]) }
+ let(:allow_creation) { true }
+ let(:params) do
+ {
+ id: cell_line.id,
+ collection_id: collection.id,
+ }
+ end
+
+ context 'when user has write access' do
+ before do
+ allow_any_instance_of(ElementsPolicy).to receive(:update?).and_return(allow_creation)
+ post '/api/v1/cell_lines/split', params: params
+ end
+
+ it 'returns correct response code' do
+ expect(response).to have_http_status :created
+ end
+
+ it 'splitted cell_line_sample was created' do
+ expect(parsed_json_response['id']).not_to be cell_line.id
+ end
+
+ it 'splitted cell_line short label is correct' do
+ expect(parsed_json_response['short_label']).to eq "#{cell_line.short_label}-1"
+ end
+
+ it 'check if ancestry relationship is correct' do
+ splitted_cellline = CelllineSample.find(parsed_json_response['id'])
+ expect(splitted_cellline.parent.id).to be cell_line.id
+ expect(cell_line.reload.children.first.id).to be splitted_cellline.id
+ end
+ end
+ end
end
-# rubocop:enable RSpec/LetSetup, RSpec/NestedGroups
+# rubocop:enable RSpec/LetSetup, RSpec/NestedGroups, RSpec/AnyInstance
diff --git a/spec/factories/cell_line_sample.rb b/spec/factories/cell_line_sample.rb
index fef6633012..33f7e8d2ea 100644
--- a/spec/factories/cell_line_sample.rb
+++ b/spec/factories/cell_line_sample.rb
@@ -9,6 +9,10 @@
amount { 999 }
passage { 10 }
unit { 'g' }
+ description { 'description' }
+ contamination { 'contamination' }
+ name { 'name' }
+ sequence(:short_label) { |i| "C#{i}" }
container { FactoryBot.create(:container, :with_analysis) }
end
diff --git a/spec/usecases/cell_lines/copy_spec.rb b/spec/usecases/cell_lines/copy_spec.rb
new file mode 100644
index 0000000000..deaba168ff
--- /dev/null
+++ b/spec/usecases/cell_lines/copy_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+# rubocop:disable RSpec/MultipleExpectations
+
+require 'spec_helper'
+
+RSpec.describe Usecases::CellLines::Copy do
+ let(:user) { create(:user) }
+ let(:collection) { create(:collection) }
+ let(:cell_line_sample_to_copy) { create(:cellline_sample) }
+ let(:use_case) { described_class.new(cell_line_sample_to_copy, user, collection.id) }
+
+ describe 'execute!' do
+ let(:cell_line_sample_copied) { use_case.execute! }
+ let(:loaded_cell_line_sample_copied) { CelllineSample.find(cell_line_sample_copied.id) }
+
+ context 'when cell line is copyable' do
+ it 'cell line sample was copied' do
+ expect(loaded_cell_line_sample_copied).not_to be_nil
+ end
+
+ it 'cell line sample match' do # rubocop:disable RSpec/MultipleExpectations
+ expect(loaded_cell_line_sample_copied.amount).to be cell_line_sample_to_copy.amount
+ expect(loaded_cell_line_sample_copied.passage).to be cell_line_sample_to_copy.passage
+ expect(loaded_cell_line_sample_copied.contamination).to eq(cell_line_sample_to_copy.contamination)
+ expect(loaded_cell_line_sample_copied.name).to eq(cell_line_sample_to_copy.name)
+ expect(loaded_cell_line_sample_copied.description).to eq(cell_line_sample_to_copy.description)
+
+ expect(loaded_cell_line_sample_copied.cellline_material.id).to be cell_line_sample_to_copy.cellline_material.id
+ end
+
+ it 'attributes are deep copied' do
+ expected_amount = cell_line_sample_to_copy.amount
+ expected_name = cell_line_sample_to_copy.name
+ expect(loaded_cell_line_sample_copied.amount).to be expected_amount
+ expect(loaded_cell_line_sample_copied.name).to eq(expected_name)
+ cell_line_sample_to_copy.amount = 100
+ cell_line_sample_to_copy.name = 'new'
+ expect(loaded_cell_line_sample_copied.amount).to be expected_amount
+ expect(loaded_cell_line_sample_copied.name).to eq(expected_name)
+ end
+
+ it 'cell line sample label was created' do
+ expect(loaded_cell_line_sample_copied.short_label).not_to be cell_line_sample_to_copy.short_label
+ end
+
+ it 'user cell line amount was changed' do
+ loaded_cell_line_sample_copied
+ old_value = user.counters['celllines']
+ expect(user.reload.counters['celllines']).not_to be old_value
+ end
+ end
+ end
+end
+# rubocop:enable RSpec/MultipleExpectations