+
{dropdownTemplateSelector}
-
+
>
);
}
diff --git a/app/javascript/src/fetchers/AttachmentFetcher.js b/app/javascript/src/fetchers/AttachmentFetcher.js
index 93abd108d6..a077f10ec3 100644
--- a/app/javascript/src/fetchers/AttachmentFetcher.js
+++ b/app/javascript/src/fetchers/AttachmentFetcher.js
@@ -487,6 +487,45 @@ export default class AttachmentFetcher {
});
}
+ static downloadZipByDeviceDescription(deviceDescriptionId) {
+ let fileName = 'dataset.zip';
+ return fetch(`/api/v1/attachments/device_description_analyses/${deviceDescriptionId}`, {
+ credentials: 'same-origin',
+ method: 'GET',
+ })
+ .then((response) => {
+ const disposition = response.headers.get('Content-Disposition');
+ if (disposition != null) {
+ if (disposition && disposition.indexOf('attachment') !== -1) {
+ const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+ const matches = filenameRegex.exec(disposition);
+ if (matches != null && matches[1]) {
+ fileName = matches[1].replace(/['"]/g, '');
+ }
+ }
+
+ return response.blob();
+ }
+ NotificationActions.notifyExImportStatus('Analysis download', 204);
+ return null;
+ })
+ .then((blob) => {
+ if (blob && blob.type != null) {
+ const a = document.createElement('a');
+ a.style = 'display: none';
+ document.body.appendChild(a);
+ const url = window.URL.createObjectURL(blob);
+ a.href = url;
+ a.download = fileName;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ }
+ })
+ .catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ }
+
static saveSpectrum(
attId,
peaksStr,
@@ -660,10 +699,10 @@ export default class AttachmentFetcher {
credentials: 'same-origin',
method: 'POST',
headers:
- {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- },
+ {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
body: JSON.stringify({
spectra_ids: jcampIds,
front_spectra_idx: curveIdx,
diff --git a/app/javascript/src/fetchers/DeviceDescriptionFetcher.js b/app/javascript/src/fetchers/DeviceDescriptionFetcher.js
new file mode 100644
index 0000000000..e14b11d498
--- /dev/null
+++ b/app/javascript/src/fetchers/DeviceDescriptionFetcher.js
@@ -0,0 +1,150 @@
+import 'whatwg-fetch';
+import BaseFetcher from 'src/fetchers/BaseFetcher';
+import DeviceDescription from 'src/models/DeviceDescription';
+import AttachmentFetcher from 'src/fetchers/AttachmentFetcher';
+
+export default class DeviceDescriptionFetcher {
+ static fetchByCollectionId(id, queryParams = {}, isSync = false) {
+ return BaseFetcher.fetchByCollectionId(id, queryParams, isSync, 'device_descriptions', DeviceDescription);
+ }
+
+ static fetchDeviceDescriptionsByUIStateAndLimit(params) {
+ const limit = params.limit ? limit : null;
+
+ return fetch('/api/v1/device_descriptions/ui_state/',
+ {
+ ...this._httpOptions('POST'),
+ body: JSON.stringify(params)
+ }
+ ).then(response => response.json())
+ .then((json) => {
+ return json.device_descriptions.map((d) => new DeviceDescription(d))
+ })
+ .catch(errorMessage => console.log(errorMessage));
+ }
+
+ static splitAsSubDeviceDescription(params) {
+ return fetch('/api/v1/device_descriptions/sub_device_descriptions/',
+ {
+ ...this._httpOptions('POST'),
+ body: JSON.stringify(params)
+ }
+ ).then(response => response.json())
+ .then((json) => json)
+ .catch(errorMessage => console.log(errorMessage));
+ }
+
+ static fetchById(deviceDescriptionId) {
+ return fetch(
+ `/api/v1/device_descriptions/${deviceDescriptionId}`,
+ { ...this._httpOptions() }
+ ).then(response => response.json())
+ .then((json) => {
+ if (json.error) {
+ return new DeviceDescription({ id: `${id}:error:DeviceDescription ${id} is not accessible!`, is_new: true });
+ } else {
+ const deviceDescription = new DeviceDescription(json.device_description);
+ deviceDescription._checksum = deviceDescription.checksum();
+ return deviceDescription;
+ }
+ })
+ .catch(errorMessage => console.log(errorMessage));
+ }
+
+ static fetchSegmentKlassIdsByNewOntology(deviceDescriptionId, params) {
+ return fetch(
+ `/api/v1/device_descriptions/byontology/${deviceDescriptionId}`,
+ {
+ ...this._httpOptions('PUT'),
+ body: JSON.stringify(params)
+ }
+ ).then(response => response.json())
+ .then((json) => {
+ if (json.error) {
+ return [];
+ } else {
+ return json;
+ }
+ })
+ .catch(errorMessage => console.log(errorMessage));
+ }
+
+ static createDeviceDescription(deviceDescription) {
+ const containerFiles = AttachmentFetcher.getFileListfrom(deviceDescription.container);
+ const newFiles = (deviceDescription.attachments || []).filter((a) => a.is_new && !a.is_deleted);
+
+ const promise = () => fetch(
+ `/api/v1/device_descriptions`,
+ {
+ ...this._httpOptions('POST'),
+ body: JSON.stringify(deviceDescription)
+ }
+ ).then(response => response.json())
+ .then((json) => {
+ if (newFiles.length <= 0) {
+ return new DeviceDescription(json.device_description);
+ }
+ return AttachmentFetcher.updateAttachables(newFiles, 'DeviceDescription', json.device_description.id, [])()
+ .then(() => new DeviceDescription(json.device_description));
+ })
+ .catch(errorMessage => console.log(errorMessage));
+
+ if (containerFiles.length > 0) {
+ const tasks = [];
+ containerFiles.forEach((file) => tasks.push(AttachmentFetcher.uploadFile(file).then()));
+ return Promise.all(tasks).then(() => promise());
+ }
+ return promise();
+ }
+
+ static updateDeviceDescription(deviceDescription) {
+ const containerFiles = AttachmentFetcher.getFileListfrom(deviceDescription.container);
+ const newFiles = (deviceDescription.attachments || []).filter((a) => a.is_new && !a.is_deleted);
+ const delFiles = (deviceDescription.attachments || []).filter((a) => !a.is_new && a.is_deleted);
+
+ const promise = () => fetch(
+ `/api/v1/device_descriptions/${deviceDescription.id}`,
+ {
+ ...this._httpOptions('PUT'),
+ body: JSON.stringify(deviceDescription)
+ }
+ ).then((response) => response.json())
+ .then((json) => {
+ const updatedDeviceDescription = new DeviceDescription(json.device_description);
+ updatedDeviceDescription.updated = true;
+ updatedDeviceDescription.updateChecksum();
+ return updatedDeviceDescription;
+ })
+ .catch(errorMessage => console.log(errorMessage));
+
+ const tasks = [];
+ if (containerFiles.length > 0) {
+ containerFiles.forEach((file) => tasks.push(AttachmentFetcher.uploadFile(file).then()));
+ }
+ if (newFiles.length > 0 || delFiles.length > 0) {
+ tasks.push(AttachmentFetcher.updateAttachables(newFiles, 'DeviceDescription', deviceDescription.id, delFiles)());
+ }
+ return Promise.all(tasks)
+ .then(() => BaseFetcher.updateAnnotations(deviceDescription))
+ .then(() => promise());
+ }
+
+ static deleteDeviceDescription(deviceDescriptionId) {
+ return fetch(
+ `/api/v1/device_descriptions/${deviceDescriptionId}`,
+ { ...this._httpOptions('DELETE') }
+ ).then(response => response.json())
+ .catch(errorMessage => console.log(errorMessage));
+ }
+
+ static _httpOptions(method = 'GET') {
+ return {
+ credentials: 'same-origin',
+ method: method,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ };
+ }
+}
diff --git a/app/javascript/src/models/DeviceDescription.js b/app/javascript/src/models/DeviceDescription.js
new file mode 100644
index 0000000000..3cabbe916e
--- /dev/null
+++ b/app/javascript/src/models/DeviceDescription.js
@@ -0,0 +1,105 @@
+import Element from 'src/models/Element';
+import Container from 'src/models/Container';
+import UserStore from 'src/stores/alt/stores/UserStore';
+
+export default class DeviceDescription extends Element {
+ static buildEmpty(collectionID) {
+ return new DeviceDescription({
+ collection_id: collectionID,
+ type: 'device_description',
+ name: 'New device description',
+ short_label: '',
+ device_type: '',
+ device_type_detail: '',
+ operation_mode: '',
+ vendor_device_name: '',
+ vendor_device_id: '',
+ serial_number: '',
+ vendor_company_name: '',
+ vendor_id: '',
+ ontologies: [],
+ description: '',
+ general_tags: '',
+ tags: '',
+ version_number: '',
+ version_installation_start_date: '',
+ version_installation_end_date: '',
+ version_identifier_type: '',
+ version_doi: '',
+ version_doi_url: '',
+ version_characterization: '',
+ operators: [],
+ university_campus: '',
+ institute: '',
+ building: '',
+ room: '',
+ infrastructure_assignment: '',
+ access_options: '',
+ access_comments: '',
+ size: '',
+ weight: '',
+ application_name: '',
+ application_version: '',
+ vendor_url: '',
+ helpers_uploaded: false,
+ policies_and_user_information: '',
+ description_for_methods_part: '',
+ setup_descriptions: {},
+ maintenance_contract_available: '',
+ maintenance_scheduling: '',
+ contact_for_maintenance: [],
+ planned_maintenance: [],
+ consumables_needed_for_maintenance: [],
+ unexpected_maintenance: [],
+ measures_after_full_shut_down: '',
+ measures_after_short_shut_down: '',
+ measures_to_plan_offline_period: '',
+ restart_after_planned_offline_period: '',
+ isNew: true,
+ changed: false,
+ updated: false,
+ can_copy: false,
+ container: Container.init(),
+ attachments: [],
+ segments: [],
+ });
+ }
+
+ static buildNewShortLabel() {
+ const { currentUser } = UserStore.getState();
+ if (!currentUser) { return 'NEW DEVICE DESCRIPTION'; }
+ return `${currentUser.initials}-Dev${currentUser.device_descriptions_count + 1}`;
+ }
+
+ static copyFromDeviceDescriptionAndCollectionId(device_description, collection_id) {
+ const newDeviceDescription = device_description.buildCopy();
+ newDeviceDescription.collection_id = collection_id;
+ if (device_description.name) { newDeviceDescription.name = device_description.name; }
+
+ return newDeviceDescription;
+ }
+
+ title() {
+ const short_label = this.short_label ? this.short_label : '';
+ return this.name ? `${short_label} ${this.name}` : short_label;
+ }
+
+ get attachmentCount() {
+ if (this.attachments) { return this.attachments.length; }
+ return this.attachment_count;
+ }
+
+ getAttachmentByIdentifier(identifier) {
+ return this.attachments
+ .filter((attachment) => attachment.identifier === identifier)[0];
+ }
+
+ buildCopy() {
+ const device_description = super.buildCopy();
+ device_description.short_label = DeviceDescription.buildNewShortLabel();
+ device_description.container = Container.init();
+ device_description.can_copy = false;
+ device_description.attachments = []
+ return device_description;
+ }
+}
diff --git a/app/javascript/src/stores/alt/actions/ClipboardActions.js b/app/javascript/src/stores/alt/actions/ClipboardActions.js
index 47df998503..f3452b658e 100644
--- a/app/javascript/src/stores/alt/actions/ClipboardActions.js
+++ b/app/javascript/src/stores/alt/actions/ClipboardActions.js
@@ -1,6 +1,8 @@
import alt from 'src/stores/alt/alt';
import SamplesFetcher from 'src/fetchers/SamplesFetcher';
import WellplatesFetcher from 'src/fetchers/WellplatesFetcher';
+import DeviceDescriptionFetcher from 'src/fetchers/DeviceDescriptionFetcher';
+import DeviceDescription from 'src/models/DeviceDescription';
class ClipboardActions {
@@ -26,11 +28,30 @@ class ClipboardActions {
};
}
+ fetchDeviceDescriptionsByUIState(params, action) {
+ return (dispatch) => {
+ DeviceDescriptionFetcher.fetchDeviceDescriptionsByUIStateAndLimit(params)
+ .then((result) => {
+ dispatch({ device_descriptions: result, collection_id: params.ui_state.collection_id, action: action });
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
fetchElementAndBuildCopy(sample, collection_id, action) {
sample.collection_id = collection_id;
return (
{ samples: [sample], collection_id: collection_id, action: action }
)
}
+
+ fetchDeviceDescriptionAndBuildCopy(device_description, collection_id, action) {
+ const newDeviceDescription = new DeviceDescription(device_description);
+ newDeviceDescription.collection_id = collection_id;
+ return (
+ { device_descriptions: [newDeviceDescription], collection_id: collection_id, action: action }
+ )
+ }
}
export default alt.createActions(ClipboardActions);
diff --git a/app/javascript/src/stores/alt/actions/ElementActions.js b/app/javascript/src/stores/alt/actions/ElementActions.js
index ebf2d47713..8b84a0f6ec 100644
--- a/app/javascript/src/stores/alt/actions/ElementActions.js
+++ b/app/javascript/src/stores/alt/actions/ElementActions.js
@@ -20,6 +20,7 @@ import ContainerFetcher from 'src/fetchers/ContainerFetcher';
import GenericElsFetcher from 'src/fetchers/GenericElsFetcher';
import PrivateNoteFetcher from 'src/fetchers/PrivateNoteFetcher'
import MetadataFetcher from 'src/fetchers/MetadataFetcher';
+import DeviceDescriptionFetcher from 'src/fetchers/DeviceDescriptionFetcher';
import GenericEl from 'src/models/GenericEl';
import Sample from 'src/models/Sample';
@@ -28,6 +29,7 @@ import Wellplate from 'src/models/Wellplate';
import CellLine from 'src/models/cellLine/CellLine';
import Screen from 'src/models/Screen';
import ResearchPlan from 'src/models/ResearchPlan';
+import DeviceDescription from 'src/models/DeviceDescription';
import Report from 'src/models/Report';
import Format from 'src/models/Format';
import Graph from 'src/models/Graph';
@@ -327,7 +329,6 @@ class ElementActions {
};
}
-
fetchResearchPlansByCollectionId(id, queryParams = {}, collectionIsSync = false) {
return (dispatch) => {
ResearchPlansFetcher.fetchByCollectionId(id, queryParams, collectionIsSync)
@@ -349,6 +350,17 @@ class ElementActions {
};
}
+ fetchDeviceDescriptionsByCollectionId(id, queryParams = {}, collectionIsSync = false) {
+ return (dispatch) => {
+ DeviceDescriptionFetcher.fetchByCollectionId(id, queryParams, collectionIsSync)
+ .then((result) => {
+ dispatch(result);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
// -- Samples --
fetchSampleById(id) {
@@ -924,6 +936,60 @@ class ElementActions {
};
}
+ // -- DeviceDescriptions --
+
+ fetchDeviceDescriptionById(id) {
+ return (dispatch) => {
+ DeviceDescriptionFetcher.fetchById(id)
+ .then((result) => {
+ dispatch(result);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
+ updateDeviceDescription(params) {
+ return (dispatch) => {
+ DeviceDescriptionFetcher.updateDeviceDescription(params)
+ .then((result) => {
+ dispatch(result);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
+ generateEmptyDeviceDescription(collection_id) {
+ return DeviceDescription.buildEmpty(collection_id);
+ }
+
+ createDeviceDescription(params) {
+ return (dispatch) => {
+ DeviceDescriptionFetcher.createDeviceDescription(params)
+ .then((result) => {
+ dispatch(result);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
+ copyDeviceDescriptionFromClipboard(collection_id) {
+ return collection_id;
+ }
+
+ splitAsSubDeviceDescription(ui_state) {
+ return (dispatch) => {
+ DeviceDescriptionFetcher.splitAsSubDeviceDescription(ui_state)
+ .then((result) => {
+ dispatch(ui_state.ui_state);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ };
+ }
+
// -- DataCite/Radar metadata --
fetchMetadata(collection_id) {
diff --git a/app/javascript/src/stores/alt/stores/ClipboardStore.js b/app/javascript/src/stores/alt/stores/ClipboardStore.js
index b318357234..879c5add56 100644
--- a/app/javascript/src/stores/alt/stores/ClipboardStore.js
+++ b/app/javascript/src/stores/alt/stores/ClipboardStore.js
@@ -6,19 +6,25 @@ class ClipboardStore {
constructor() {
this.state = {
samples: [],
- wellplates: []
+ wellplates: [],
+ device_descriptions: [],
};
this.bindListeners({
- handleFetchSamplesByUIStateAndLimit: [ClipboardActions.fetchSamplesByUIStateAndLimit, ClipboardActions.fetchElementAndBuildCopy],
- handleFetchWellplatesByUIState: ClipboardActions.fetchWellplatesByUIState
+ handleFetchSamplesByUIStateAndLimit: [
+ ClipboardActions.fetchSamplesByUIStateAndLimit, ClipboardActions.fetchElementAndBuildCopy
+ ],
+ handleFetchWellplatesByUIState: ClipboardActions.fetchWellplatesByUIState,
+ handleFetchDeviceDescriptionAndBuildCopy: [
+ ClipboardActions.fetchDeviceDescriptionAndBuildCopy, ClipboardActions.fetchDeviceDescriptionsByUIState
+ ],
})
}
handleFetchSamplesByUIStateAndLimit(result) {
this.state.samples = result.samples;
- switch(result.action) {
+ switch (result.action) {
case 'template_wellplate':
Aviator.navigate(result.isSync
? `/scollection/${result.collection_id}/wellplate/template`
@@ -33,13 +39,20 @@ class ClipboardStore {
handleFetchWellplatesByUIState(result) {
this.state.wellplates = result.wellplates;
- switch(result.action) {
+ switch (result.action) {
case 'template_screen':
Aviator.navigate(result.isSync
? `/scollection/${result.collection_id}/screen/template`
: `/collection/${result.collection_id}/screen/template`);
}
}
+
+ handleFetchDeviceDescriptionAndBuildCopy(result) {
+ this.state.device_descriptions = result.device_descriptions;
+ Aviator.navigate(result.isSync
+ ? `/scollection/${result.collection_id}/device_description/copy`
+ : `/collection/${result.collection_id}/device_description/copy`);
+ }
}
export default alt.createStore(ClipboardStore, 'ClipboardStore');
diff --git a/app/javascript/src/stores/alt/stores/ElementStore.js b/app/javascript/src/stores/alt/stores/ElementStore.js
index 454a7fd7e4..a5a6ff7e0d 100644
--- a/app/javascript/src/stores/alt/stores/ElementStore.js
+++ b/app/javascript/src/stores/alt/stores/ElementStore.js
@@ -18,6 +18,7 @@ import Reaction from 'src/models/Reaction';
import ResearchPlan from 'src/models/ResearchPlan';
import Wellplate from 'src/models/Wellplate';
import Screen from 'src/models/Screen';
+import DeviceDescription from 'src/models/DeviceDescription';
import Device from 'src/models/Device';
import Container from 'src/models/Container';
@@ -103,6 +104,13 @@ class ElementStore {
pages: null,
perPage: null
},
+ device_descriptions: {
+ elements: [],
+ totalElements: 0,
+ page: null,
+ pages: null,
+ perPage: null
+ },
};
this.state = {
@@ -157,6 +165,7 @@ class ElementStore {
handleFetchScreensByCollectionId: ElementActions.fetchScreensByCollectionId,
handlefetchResearchPlansByCollectionId: ElementActions.fetchResearchPlansByCollectionId,
handlefetchCellLinesByCollectionId: ElementActions.fetchCellLinesByCollectionId,
+ handlefetchDeviceDescriptionsByCollectionId: ElementActions.fetchDeviceDescriptionsByCollectionId,
handleFetchSampleById: ElementActions.fetchSampleById,
handleCreateSample: ElementActions.createSample,
@@ -211,6 +220,10 @@ class ElementStore {
handleImportWellplateIntoResearchPlan: ElementActions.importWellplateIntoResearchPlan,
handleImportTableFromSpreadsheet: ElementActions.importTableFromSpreadsheet,
+ handlefetchDeviceDescriptionById: ElementActions.fetchDeviceDescriptionById,
+ handleCreateDeviceDescription: ElementActions.createDeviceDescription,
+ handleCopyDeviceDescriptionFromClipboard: ElementActions.copyDeviceDescriptionFromClipboard,
+
handleCreatePrivateNote: ElementActions.createPrivateNote,
handleUpdatePrivateNote: ElementActions.updatePrivateNote,
@@ -228,6 +241,7 @@ class ElementStore {
ElementActions.generateEmptySample,
ElementActions.generateEmptyReaction,
ElementActions.generateEmptyCellLine,
+ ElementActions.generateEmptyDeviceDescription,
ElementActions.showReportContainer,
ElementActions.showFormatContainer,
ElementActions.showComputedPropsGraph,
@@ -246,6 +260,7 @@ class ElementStore {
handleSplitElements: ElementActions.splitElements,
handleSplitAsSubwellplates: ElementActions.splitAsSubwellplates,
handleSplitAsSubCellLines: ElementActions.splitAsSubCellLines,
+ handleSplitAsSubDeviceDescription: ElementActions.splitAsSubDeviceDescription,
// formerly from DetailStore
handleSelect: DetailActions.select,
handleClose: DetailActions.close,
@@ -266,6 +281,7 @@ class ElementStore {
ElementActions.updateScreen,
ElementActions.updateResearchPlan,
ElementActions.updateCellLine,
+ ElementActions.updateDeviceDescription,
ElementActions.updateGenericEl,
],
handleUpdateEmbeddedResearchPlan: ElementActions.updateEmbeddedResearchPlan,
@@ -537,7 +553,7 @@ class ElementStore {
handleDeleteElements(options) {
this.waitFor(UIStore.dispatchToken);
const ui_state = UIStore.getState();
- const { sample, reaction, wellplate, screen, research_plan, currentCollection, cell_line } = ui_state;
+ const { sample, reaction, wellplate, screen, research_plan, currentCollection, cell_line, device_description } = ui_state;
const selecteds = this.state.selecteds.map(s => ({ id: s.id, type: s.type }));
const params = {
options,
@@ -548,7 +564,8 @@ class ElementStore {
research_plan,
currentCollection,
selecteds,
- cell_line
+ cell_line,
+ device_description
};
const currentUser = (UserStore.getState() && UserStore.getState().currentUser) || {};
@@ -601,6 +618,7 @@ class ElementStore {
if (layout.wellplate && layout.wellplate > 0) { this.handleRefreshElements('wellplate'); }
if (layout.screen && layout.screen > 0) { this.handleRefreshElements('screen'); }
if (layout.cell_line && layout.cell_line > 0) { this.handleRefreshElements('cell_line'); }
+ if (layout.device_description && layout.device_description > 0) { this.handleRefreshElements('device_description'); }
if (!isSync && layout.research_plan && layout.research_plan > 0) { this.handleRefreshElements('research_plan'); }
@@ -659,6 +677,10 @@ class ElementStore {
this.state.elements.cell_lines = result;
}
+ handlefetchDeviceDescriptionsByCollectionId(result) {
+ this.state.elements.device_descriptions = result;
+ }
+
// -- Samples --
handleFetchSampleById(result) {
@@ -943,6 +965,30 @@ class ElementStore {
this.setState({ selecteds: newSelecteds });
}
+ // -- DeviceDescriptions --
+
+ handlefetchDeviceDescriptionById(result) {
+ this.changeCurrentElement(result);
+ }
+
+ handleCreateDeviceDescription(device_description) {
+ this.handleRefreshElements('device_description');
+ this.navigateToNewElement(device_description);
+ }
+
+ handleCopyDeviceDescriptionFromClipboard(collection_id) {
+ const clipboardDeviceDescriptions = ClipboardStore.getState().device_descriptions;
+ if (clipboardDeviceDescriptions && clipboardDeviceDescriptions.length > 0) {
+ this.changeCurrentElement(DeviceDescription.copyFromDeviceDescriptionAndCollectionId(clipboardDeviceDescriptions[0], collection_id));
+ }
+ }
+
+ handleSplitAsSubDeviceDescription(ui_state) {
+ ElementActions.fetchDeviceDescriptionsByCollectionId(
+ ui_state.currentCollectionId, {}, ui_state.isSync
+ );
+ }
+
// -- Reactions --
handleFetchReactionById(result) {
@@ -1013,7 +1059,7 @@ class ElementStore {
Aviator.navigate(`/collection/${result.colId}/${result.element.type}/copy`);
}
- handleCopyCellLine(result){
+ handleCopyCellLine(result) {
UserActions.fetchCurrentUser(); //Needed to update the cell line counter in frontend
Aviator.navigate(`/collection/${result.collectionId}/cell_line/${result.id}`);
}
@@ -1115,7 +1161,8 @@ class ElementStore {
'fetchWellplatesByCollectionId',
'fetchScreensByCollectionId',
'fetchResearchPlansByCollectionId',
- 'fetchCellLinesByCollectionId'
+ 'fetchCellLinesByCollectionId',
+ 'fetchDeviceDescriptionsByCollectionId'
];
if (allowedActions.includes(fn)) {
ElementActions[fn](uiState.currentCollection.id, params, uiState.isSync, moleculeSort);
@@ -1364,6 +1411,10 @@ class ElementStore {
this.handleUpdateWellplateAttaches(updatedElement);
this.handleRefreshElements('sample');
break;
+ case 'device_description':
+ this.changeCurrentElement(updatedElement);
+ this.handleRefreshElements('device_description');
+ break;
case 'genericEl':
this.handleRefreshElements('genericEl');
break;
diff --git a/app/javascript/src/stores/alt/stores/LoadingStore.js b/app/javascript/src/stores/alt/stores/LoadingStore.js
index c3daf20e36..80c3d0ae8e 100644
--- a/app/javascript/src/stores/alt/stores/LoadingStore.js
+++ b/app/javascript/src/stores/alt/stores/LoadingStore.js
@@ -35,6 +35,8 @@ class LoadingStore {
ElementActions.createWellplate,
ElementActions.updateWellplate,
ElementActions.importWellplateSpreadsheet,
+ ElementActions.createDeviceDescription,
+ ElementActions.updateDeviceDescription,
ElementActions.storeMetadata,
InboxActions.fetchInbox,
InboxActions.fetchInboxContainer,
diff --git a/app/javascript/src/stores/alt/stores/TextTemplateStore.js b/app/javascript/src/stores/alt/stores/TextTemplateStore.js
index 4cd8603648..8022e547e0 100644
--- a/app/javascript/src/stores/alt/stores/TextTemplateStore.js
+++ b/app/javascript/src/stores/alt/stores/TextTemplateStore.js
@@ -12,6 +12,7 @@ class TextTemplateStore {
screen: Map(),
wellplate: Map(),
researchPlan: Map(),
+ deviceDescription: Map(),
reactionDescription: Map(),
predefinedTemplateNames: OrderedSet(),
fetchedPredefinedTemplates: Map()
diff --git a/app/javascript/src/stores/alt/stores/UIStore.js b/app/javascript/src/stores/alt/stores/UIStore.js
index ca73545e2b..1cae2e7379 100644
--- a/app/javascript/src/stores/alt/stores/UIStore.js
+++ b/app/javascript/src/stores/alt/stores/UIStore.js
@@ -59,6 +59,15 @@ class UIStore {
currentId: null,
page: 1,
},
+ device_description: {
+ checkedAll: false,
+ checkedIds: List(),
+ uncheckedIds: List(),
+ currentId: null,
+ page: 1,
+ activeTab: 0,
+ activeAnalysis: 0,
+ },
showPreviews: true,
showAdvancedSearch: false,
filterCreatedAt: true,
@@ -252,6 +261,7 @@ class UIStore {
this.handleUncheckAllElements({ type: 'wellplate', range: 'all' });
this.handleUncheckAllElements({ type: 'research_plan', range: 'all' });
this.handleUncheckAllElements({ type: 'cell_line', range: 'all' });
+ this.handleUncheckAllElements({ type: 'device_description', range: 'all' });
this.state.klasses?.forEach((klass) => { this.handleUncheckAllElements({ type: klass, range: 'all' }); });
}
@@ -293,6 +303,7 @@ class UIStore {
this.state.reaction.currentId = null;
this.state.wellplate.currentId = null;
this.state.research_plan.currentId = null;
+ this.state.device_description.currentId = null;
}
handleSelectElement(element) {
@@ -367,18 +378,26 @@ class UIStore {
Object.assign(params, { page: state.cell_line.page }),
);
}
+ if (!isSync && layout.device_description && layout.device_description > 0) {
+ ElementActions.fetchDeviceDescriptionsByCollectionId(
+ collection.id,
+ Object.assign(params, { page: state.device_description.page }),
+ );
+ }
- Object.keys(layout).filter(l => !['sample', 'reaction', 'screen', 'wellplate', 'research_plan', 'cell_line'].includes(l)).forEach((key) => {
- if (typeof layout[key] !== 'undefined' && layout[key] > 0) {
- const page = state[key] ? state[key].page : 1;
- ElementActions.fetchGenericElsByCollectionId(
- collection.id,
- Object.assign(params, { page, name: key }),
- isSync,
- key
- );
- }
- });
+ Object.keys(layout)
+ .filter(l => !['sample', 'reaction', 'screen', 'wellplate', 'research_plan', 'cell_line', 'device_description'].includes(l))
+ .forEach((key) => {
+ if (typeof layout[key] !== 'undefined' && layout[key] > 0) {
+ const page = state[key] ? state[key].page : 1;
+ ElementActions.fetchGenericElsByCollectionId(
+ collection.id,
+ Object.assign(params, { page, name: key }),
+ isSync,
+ key
+ );
+ }
+ });
}
}
}
diff --git a/app/javascript/src/stores/mobx/CalendarStore.jsx b/app/javascript/src/stores/mobx/CalendarStore.jsx
index 72864c97f6..7747096c67 100644
--- a/app/javascript/src/stores/mobx/CalendarStore.jsx
+++ b/app/javascript/src/stores/mobx/CalendarStore.jsx
@@ -12,6 +12,7 @@ const CalendarTypes = {
Sample: ['handover', 'reminder', 'report'],
ResearchPlan: ['handover', 'reminder', 'report'],
Screen: ['reminder', 'report'],
+ DeviceDescription: ['reminder', 'report'],
// Element all types like default
};
diff --git a/app/javascript/src/stores/mobx/DeviceDescriptionsStore.jsx b/app/javascript/src/stores/mobx/DeviceDescriptionsStore.jsx
new file mode 100644
index 0000000000..313faa4b42
--- /dev/null
+++ b/app/javascript/src/stores/mobx/DeviceDescriptionsStore.jsx
@@ -0,0 +1,285 @@
+import { keys, values } from 'mobx';
+import { flow, types } from 'mobx-state-tree';
+
+import DeviceDescriptionFetcher from 'src/fetchers/DeviceDescriptionFetcher';
+import AttachmentFetcher from 'src/fetchers/AttachmentFetcher';
+import DeviceDescription from 'src/models/DeviceDescription';
+import Container from 'src/models/Container';
+
+const toggableContents = {
+ 'general': true,
+ 'version_specific': true,
+ 'operators_and_locations': true,
+ 'physical_data': true,
+ 'software_interfaces': true,
+ 'manuals': true,
+ 'publications': true,
+ 'setup': true,
+ 'ontology': true,
+ 'ontology_segments': true,
+ 'general_aspects': true,
+ 'planned_maintenance': true,
+ 'unexpected_maintenance': true,
+ 'unexpected_power_shutdown': true,
+ 'planned_offline_period': true,
+};
+
+const multiRowFields = [
+ 'operators', 'contact_for_maintenance', 'planned_maintenance',
+ 'consumables_needed_for_maintenance', 'unexpected_maintenance',
+];
+
+export const DeviceDescriptionsStore = types
+ .model({
+ device_description: types.optional(types.frozen({}), {}),
+ device_description_checksum: types.optional(types.string, ''),
+ devices_descriptions: types.optional(types.optional(types.array(types.frozen({})), [])),
+ active_tab_key: types.optional(types.number, 1),
+ key_prefix: types.optional(types.string, ''),
+ toggable_contents: types.optional(types.frozen({}), toggableContents),
+ toggable_segments: types.optional(types.array(types.string), []),
+ analysis_mode: types.optional(types.string, 'edit'),
+ analysis_open_panel: types.optional(types.union(types.string, types.number), 'none'),
+ analysis_comment_box: types.optional(types.boolean, false),
+ analysis_start_export: types.optional(types.boolean, false),
+ attachment_editor: types.optional(types.boolean, false),
+ attachment_extension: types.optional(types.frozen({}), {}),
+ attachment_image_edit_modal_shown: types.optional(types.boolean, false),
+ attachment_selected: types.optional(types.frozen({}), {}),
+ attachment_show_import_confirm: types.optional(types.array(types.frozen({})), []),
+ attachment_filter_text: types.optional(types.string, ''),
+ attachment_sort_by: types.optional(types.string, 'name'),
+ attachment_sort_direction: types.optional(types.string, 'asc'),
+ filtered_attachments: types.optional(types.array(types.frozen({})), []),
+ show_ontology_modal: types.optional(types.boolean, false),
+ ontology_mode: types.optional(types.string, 'edit'),
+ selected_segment_id: types.optional(types.number, 0),
+ list_grouped_by: types.optional(types.string, 'serial_number'),
+ show_all_groups: types.optional(types.boolean, true),
+ all_groups: types.optional(types.array(types.string), []),
+ shown_groups: types.optional(types.array(types.string), []),
+ select_is_open: types.optional(types.array(types.frozen({})), []),
+ multi_row_fields: types.optional(types.array(types.string), multiRowFields),
+ })
+ .actions(self => ({
+ setDeviceDescription(device_description, initial = false) {
+ if (initial) {
+ self.device_description_checksum = device_description._checksum;
+ }
+ device_description.changed = false;
+ const deviceDescription = new DeviceDescription(device_description);
+
+ if (deviceDescription.checksum() != self.device_description_checksum || deviceDescription.isNew) {
+ deviceDescription.changed = true;
+ }
+ self.device_description = deviceDescription;
+ },
+ setDeviceDescriptions(devices_descriptions) {
+ self.devices_descriptions = devices_descriptions;
+ },
+ clearDeviceDescription() {
+ self.device_description = {};
+ },
+ changeDeviceDescription(field, value, type) {
+ let device_description = { ...self.device_description };
+ const fieldElements = field.split('-');
+
+ if (values(self.multi_row_fields).includes(fieldElements[0]) && fieldElements.length > 1) {
+ let element = [...self.device_description[fieldElements[0]]];
+ element[fieldElements[2]][fieldElements[1]] = value;
+ device_description[fieldElements[0]] = element;
+ } else if (field.includes('setup_descriptions')) {
+ device_description = self.changeSetupDescriptions(field, value, type, device_description);
+ } else {
+ device_description[field] = value;
+ }
+
+ device_description.updated = false;
+ self.setDeviceDescription(device_description);
+ },
+ changeSetupDescriptions(field, value, type, device_description) {
+ const fieldElements = field.split('-');
+ const elementField = fieldElements.length > 1 ? fieldElements[0] : field;
+ const elementType = type !== undefined ? type : fieldElements[1];
+ let device_description_field = { ...device_description[elementField] };
+
+ if (device_description_field === null) {
+ device_description_field = { [elementType]: value };
+ } else if (fieldElements.length > 1) {
+ device_description_field[elementType][fieldElements[3]][fieldElements[2]] = value;
+ } else {
+ device_description_field[elementType] = value;
+ }
+ device_description[elementField] = device_description_field;
+ return device_description;
+ },
+ setActiveTabKey(key) {
+ self.active_tab_key = key;
+ },
+ setKeyPrefix(prefix) {
+ self.key_prefix = `${prefix}-${self.device_description.collection_id}`;
+ },
+ toggleContent(content) {
+ let contents = { ...self.toggable_contents };
+ contents[content] = !contents[content];
+ self.toggable_contents = contents;
+ },
+ toggleSegment(segment) {
+ let segments = [...self.toggable_segments];
+ if (segments.includes(segment)) {
+ segments = segments.filter((s) => { return s != segment });
+ } else {
+ segments.push(segment);
+ }
+ self.toggable_segments = segments;
+ },
+ changeAnalysisMode(mode) {
+ self.analysis_mode = mode;
+ },
+ changeAnalysisOpenPanel(panel) {
+ self.analysis_open_panel = panel;
+ },
+ addEmptyAnalysisContainer() {
+ const container = Container.buildEmpty();
+ container.container_type = "analysis"
+ let device_description = { ...self.device_description };
+ device_description.container.children[0].children.push(container);
+ self.setDeviceDescription(device_description);
+ },
+ changeAnalysisContainerContent(container) {
+ let device_description = { ...self.device_description };
+ const index = device_description.container.children[0].children.findIndex((c) => c.id === container.id);
+ device_description.container.children[0].children[index] = container;
+ self.setDeviceDescription(device_description);
+ },
+ changeAnalysisContainer(children) {
+ let device_description = { ...self.device_description };
+ device_description.container.children[0].children = children;
+ self.setDeviceDescription(device_description);
+ },
+ toggleAnalysisCommentBox() {
+ self.analysis_comment_box = !self.analysis_comment_box;
+ },
+ changeAnalysisComment(comment) {
+ let device_description = { ...self.device_description };
+ let container = { ...self.device_description.container }
+ container.description = comment;
+ device_description.container = container;
+ self.setDeviceDescription(device_description);
+ },
+ toggleAnalysisStartExport() {
+ self.analysis_start_export = !self.analysis_start_export;
+ },
+ setAttachmentEditor(value) {
+ self.attachment_editor = value;
+ },
+ setAttachmentExtension(value) {
+ self.attachment_extension = value;
+ },
+ setFilteredAttachments(attachments) {
+ self.filtered_attachments = attachments;
+ },
+ setShowImportConfirm(value) {
+ self.attachment_show_import_confirm = value;
+ },
+ toogleAttachmentModal() {
+ self.attachment_image_edit_modal_shown = !self.attachment_image_edit_modal_shown;
+ },
+ setAttachmentSelected(attachment) {
+ self.attachment_selected = attachment;
+ },
+ setAttachmentFilterText(value) {
+ self.attachment_filter_text = value;
+ },
+ setAttachmentSortBy(value) {
+ self.attachment_sort_by = value;
+ },
+ setAttachmentSortDirectory(value) {
+ self.attachment_sort_direction = value;
+ },
+ changeAttachment(index, key, value, initial = false) {
+ let device_description = { ...self.device_description };
+ let attachment = { ...device_description.attachments[index] };
+ attachment[key] = value;
+ device_description.attachments[index] = attachment;
+ self.setFilteredAttachments(device_description.attachments);
+ self.setDeviceDescription(device_description, initial);
+ },
+ loadPreviewImagesOfAttachments(device_description) {
+ if (device_description.attachments.length === 0) { return device_description }
+ let deviceDescription = { ...device_description }
+
+ deviceDescription.attachments.map((attachment, index) => {
+ let attachment_object = { ...device_description.attachments[index] };
+ if (attachment.thumb) {
+ AttachmentFetcher.fetchThumbnail({ id: attachment.id })
+ .then((result) => {
+ let preview = result != null ? `data:image/png;base64,${result}` : '/images/wild_card/not_available.svg';
+ attachment_object.preview = preview;
+ deviceDescription.attachments[index] = attachment_object;
+ self.setFilteredAttachments(deviceDescription.attachments);
+ });
+ }
+ });
+ },
+ toggleOntologyModal() {
+ self.show_ontology_modal = !self.show_ontology_modal;
+ },
+ changeOntologyMode(mode) {
+ self.ontology_mode = mode;
+ },
+ setSelectedSegmentId(segment_id) {
+ self.selected_segment_id = segment_id;
+ },
+ setListGroupedBy(value) {
+ self.list_grouped_by = value;
+ },
+ toggleAllGroups() {
+ self.show_all_groups = !self.show_all_groups;
+
+ if (self.show_all_groups) {
+ self.removeAllGroupsFromShownGroups();
+ } else {
+ self.addAllGroupsToShownGroups();
+ }
+ },
+ addGroupToAllGroups(group_key) {
+ const index = self.all_groups.findIndex((g) => { return g == group_key });
+ if (index === -1) {
+ self.all_groups.push(group_key);
+ }
+ },
+ addAllGroupsToShownGroups() {
+ self.all_groups.map((group_key) => {
+ self.addGroupToShownGroups(group_key);
+ });
+ },
+ addGroupToShownGroups(group_key) {
+ self.shown_groups.push(group_key);
+ },
+ removeGroupFromShownGroups(group_key) {
+ const shownGroups = self.shown_groups.filter((g) => { return g !== group_key });
+ self.shown_groups = shownGroups;
+ },
+ removeAllGroupsFromShownGroups() {
+ self.shown_groups = [];
+ },
+ setSelectIsOpen(field, value) {
+ const index = self.select_is_open.findIndex((x) => { return x[field] !== undefined });
+ const newValue = { [field]: value }
+ if (index >= 0) {
+ let fieldObject = { ...self.select_is_open[index] };
+ fieldObject = newValue;
+ self.select_is_open[index] = fieldObject;
+ } else {
+ self.select_is_open.push(newValue);
+ }
+ }
+ }))
+ .views(self => ({
+ get deviceDescriptionsValues() { return values(self.devices_descriptions) },
+ get filteredAttachments() { return values(self.filtered_attachments) },
+ get shownGroups() { return values(self.shown_groups) },
+ get selectIsOpen() { return values(self.select_is_open) },
+ get multiRowFields() { return values(self.multi_row_fields) },
+ }));
diff --git a/app/javascript/src/stores/mobx/RootStore.jsx b/app/javascript/src/stores/mobx/RootStore.jsx
index daf7904b28..8b34a5215f 100644
--- a/app/javascript/src/stores/mobx/RootStore.jsx
+++ b/app/javascript/src/stores/mobx/RootStore.jsx
@@ -8,6 +8,7 @@ import { DevicesStore } from 'src/stores/mobx/DevicesStore';
import { DeviceMetadataStore } from 'src/stores/mobx/DeviceMetadataStore';
import { AttachmentNotificationStore } from 'src/stores/mobx/AttachmentNotificationStore';
import { CalendarStore } from 'src/stores/mobx/CalendarStore';
+import { DeviceDescriptionsStore } from 'src/stores/mobx/DeviceDescriptionsStore';
export const RootStore = types
.model({
@@ -19,6 +20,7 @@ export const RootStore = types
deviceMetadataStore: types.optional(DeviceMetadataStore, {}),
attachmentNotificationStore: types.optional(AttachmentNotificationStore, {}),
calendarStore: types.optional(CalendarStore, {}),
+ deviceDescriptionsStore: types.optional(DeviceDescriptionsStore, {}),
})
.views(self => ({
get measurements() { return self.measurementsStore },
@@ -29,5 +31,6 @@ export const RootStore = types
get deviceMetadata() { return self.deviceMetadataStore },
get attachmentNotifications() { return self.attachmentNotificationStore },
get calendar() { return self.calendarStore },
+ get deviceDescriptions() { return self.deviceDescriptionsStore },
}));
export const StoreContext = React.createContext(RootStore.create({}));
diff --git a/app/javascript/src/utilities/DndConst.js b/app/javascript/src/utilities/DndConst.js
index ada7107fd9..7054aeb4d6 100644
--- a/app/javascript/src/utilities/DndConst.js
+++ b/app/javascript/src/utilities/DndConst.js
@@ -19,7 +19,8 @@ const DragDropItemTypes = {
GENERALPROCEDURE: 'generalProcedure',
ELEMENT_FIELD: 'element_field',
GENERIC_GRID: 'generic_grid',
- CELL_LINE: 'cell_line'
+ CELL_LINE: 'cell_line',
+ DEVICE_DESCRIPTION: 'device_description'
};
const dropTargetTypes = [
@@ -37,8 +38,8 @@ const collectTarget = (connect, monitor) => ({
const canDrop = (_props, monitor) => {
const itemType = monitor.getItemType();
return itemType === DragDropItemTypes.DATA
- || itemType === DragDropItemTypes.UNLINKED_DATA
- || itemType === DragDropItemTypes.DATASET;
+ || itemType === DragDropItemTypes.UNLINKED_DATA
+ || itemType === DragDropItemTypes.DATASET;
};
// define dataTarget, collectTarget, and dropTargetTypes ()
diff --git a/app/javascript/src/utilities/routesUtils.js b/app/javascript/src/utilities/routesUtils.js
index 6824f2f0a7..532687d19c 100644
--- a/app/javascript/src/utilities/routesUtils.js
+++ b/app/javascript/src/utilities/routesUtils.js
@@ -34,7 +34,8 @@ const collectionShow = (e) => {
ElementActions.fetchBasedOnSearchSelectionAndCollection({
selection: currentSearchSelection,
collectionId: collection.id,
- isSync: !!collection.is_sync_to_me });
+ isSync: !!collection.is_sync_to_me
+ });
} else {
if (currentSearchByID) {
UIActions.clearSearchById();
@@ -43,11 +44,12 @@ const collectionShow = (e) => {
}
// if (!e.params['sampleID'] && !e.params['reactionID'] &&
- // !e.params['wellplateID'] && !e.params['screenID']) {
+ // !e.params['wellplateID'] && !e.params['screenID']) {
UIActions.uncheckAllElements({ type: 'sample', range: 'all' });
UIActions.uncheckAllElements({ type: 'reaction', range: 'all' });
UIActions.uncheckAllElements({ type: 'wellplate', range: 'all' });
UIActions.uncheckAllElements({ type: 'screen', range: 'all' });
+ UIActions.uncheckAllElements({ type: 'device_description', range: 'all' });
elementNames(false).then((klassArray) => {
klassArray.forEach((klass) => {
UIActions.uncheckAllElements({ type: klass, range: 'all' });
@@ -83,7 +85,8 @@ const scollectionShow = (e) => {
ElementActions.fetchBasedOnSearchSelectionAndCollection({
selection: currentSearchSelection,
collectionId: collection.id,
- isSync: !!collection.is_sync_to_me });
+ isSync: !!collection.is_sync_to_me
+ });
} else {
UIActions.selectCollection(collection);
if (currentSearchByID) {
@@ -96,6 +99,7 @@ const scollectionShow = (e) => {
UIActions.uncheckAllElements({ type: 'reaction', range: 'all' });
UIActions.uncheckAllElements({ type: 'wellplate', range: 'all' });
UIActions.uncheckAllElements({ type: 'screen', range: 'all' });
+ UIActions.uncheckAllElements({ type: 'device_description', range: 'all' });
elementNames(false).then((klassArray) => {
klassArray.forEach((klass) => {
UIActions.uncheckAllElements({ type: klass, range: 'all' });
@@ -132,13 +136,13 @@ const sampleShowOrNew = (e) => {
};
const cellLineShowOrNew = (e) => {
- if(e.params.new_cellLine||(e.params.new_cellLine===undefined&&e.params.cell_lineID==="new")){
- ElementActions.generateEmptyCellLine(e.params.collectionID,e.params.cell_line_template);
- }else{
- if(e.params.cellLineID){
- e.params.cellLineId=e.params.cellLineID
+ if (e.params.new_cellLine || (e.params.new_cellLine === undefined && e.params.cell_lineID === "new")) {
+ ElementActions.generateEmptyCellLine(e.params.collectionID, e.params.cell_line_template);
+ } else {
+ if (e.params.cellLineID) {
+ e.params.cellLineId = e.params.cellLineID
}
- ElementActions.tryFetchCellLineElById.defer(e.params.cellLineId);
+ ElementActions.tryFetchCellLineElById.defer(e.params.cellLineId);
}
}
@@ -250,12 +254,28 @@ const metadataShowOrNew = (e) => {
}
};
+const deviceDescriptionShowOrNew = (e) => {
+ const { device_descriptionID, collectionID } = e.params;
+ const { selecteds, activeKey } = ElementStore.getState();
+ const index = selecteds.findIndex(el => el.type === 'device_description' && el.id === device_descriptionID);
+
+ if (device_descriptionID === 'new' || device_descriptionID === undefined) {
+ ElementActions.generateEmptyDeviceDescription(collectionID);
+ } else if (device_descriptionID === 'copy') {
+ ElementActions.copyDeviceDescriptionFromClipboard.defer(collectionID);
+ } else if (index < 0) {
+ ElementActions.fetchDeviceDescriptionById(device_descriptionID);
+ } else if (index !== activeKey) {
+ DetailActions.select(index);
+ }
+}
+
const genericElShowOrNew = (e, type) => {
const { collectionID } = e.params;
let itype = '';
if (typeof type === 'undefined' || typeof type === 'object' || type == null || type == '') {
const keystr = e.params && Object.keys(e.params).filter(k => k != 'collectionID' && k.includes('ID'));
- itype = keystr && keystr[0] && keystr[0].slice(0,-2);
+ itype = keystr && keystr[0] && keystr[0].slice(0, -2);
} else {
itype = type;
}
@@ -273,7 +293,7 @@ const genericElShowOrNew = (e, type) => {
const elementShowOrNew = (e) => {
const type = e.type;
- switch(type) {
+ switch (type) {
case 'sample':
sampleShowOrNew(e);
break;
@@ -295,6 +315,9 @@ const elementShowOrNew = (e) => {
case 'cell_line':
cellLineShowOrNew(e);
break;
+ case 'device_description':
+ deviceDescriptionShowOrNew(e);
+ break;
default:
if (e && e.klassType == 'GenericEl') {
genericElShowOrNew(e, type);
@@ -322,6 +345,7 @@ export {
deviceShowDeviceManagement,
researchPlanShowOrNew,
metadataShowOrNew,
+ deviceDescriptionShowOrNew,
elementShowOrNew,
predictionShowFwdRxn,
genericElShowOrNew,
diff --git a/app/jobs/download_analyses_job.rb b/app/jobs/download_analyses_job.rb
index 78a25e2f3d..c4c01de13d 100644
--- a/app/jobs/download_analyses_job.rb
+++ b/app/jobs/download_analyses_job.rb
@@ -1,49 +1,56 @@
+# frozen_string_literal: true
+
class DownloadAnalysesJob < ApplicationJob
include ActiveJob::Status
queue_as :download_analyses
after_perform do |job|
- if @rt == false
+ if @rt == false && @success
begin
CleanExportFilesJob.set(queue: "remove_files_#{job.job_id}", wait: 24.hours)
- .perform_later(job.job_id, 'zip')
+ .perform_later(job.job_id, 'zip')
# Notify ELNer
Message.create_msg_notification(
channel_subject: Channel::DOWNLOAD_ANALYSES_ZIP,
- data_args: { expires_at: @expires_at, sample_name: @sample.short_label },
+ data_args: { expires_at: @expires_at, element_name: @element.short_label },
message_from: @user_id,
- url: @link
+ url: @link,
)
AnalysesMailer.mail_export_completed(
@user_id,
- @sample.short_label,
+ @element.short_label,
@link,
- @expires_at
+ @expires_at,
).deliver_now
rescue StandardError => e
Delayed::Worker.logger.error e
- end if @success
+ end
end
end
- def perform(sid, user_id, rt=true)
- @sample = Sample.find(sid)
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/MethodLength
+ # rubocop:disable Metrics/PerceivedComplexity
+ # rubocop:disable Style/OptionalBooleanParameter
+
+ def perform(id, user_id, ret = true, element = 'sample')
+ @element = element.camelize.constantize.find(id)
@filename = "#{job_id}.zip"
- @rt = rt
+ @rt = ret
@success = true
@user_id = user_id
@file_path = Rails.public_path.join('zip', @filename)
begin
@link = "#{Rails.application.config.root_url}/zip/#{@filename}"
- @expires_at = Time.now + 24.hours
+ @expires_at = 24.hours.from_now
- zip = Zip::OutputStream.write_buffer do |zip|
-
- @sample.analyses.each do |analysis|
+ zip_file = Zip::OutputStream.write_buffer do |zip|
+ @element.analyses.each do |analysis|
analysis.children.each do |dataset|
dataset.attachments.each do |att|
zip.put_next_entry att.filename
@@ -52,16 +59,16 @@ def perform(sid, user_id, rt=true)
end
end
- zip.put_next_entry "sample_#{@sample.short_label} analytical_files.txt"
+ zip.put_next_entry "#{element}_#{@element.short_label} analytical_files.txt"
zip.write <<~DESC
- sample short label: #{@sample.short_label}
- sample id: #{@sample.id}
- analyses count: #{@sample.analyses&.length || 0}
+ #{element} short label: #{@element.short_label}
+ #{element} id: #{@element.id}
+ analyses count: #{@element.analyses&.length || 0}
- Files:
+ Files:
DESC
- @sample.analyses&.each do |analysis|
+ @element.analyses&.each do |analysis|
zip.write "analysis name: #{analysis.name}\n"
zip.write "analysis type: #{analysis.extended_metadata.fetch('kind', nil)}\n\n"
analysis.children.each do |dataset|
@@ -76,12 +83,17 @@ def perform(sid, user_id, rt=true)
end
if rt == false
- zip.rewind
- File.write(@file_path, zip.read)
+ zip_file.rewind
+ File.write(@file_path, zip_file.read)
end
rescue StandardError => e
-
+ Delayed::Worker.logger.error e
end
- zip
+ zip_file
end
+ # rubocop:enable Metrics/AbcSize
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/MethodLength
+ # rubocop:enable Metrics/PerceivedComplexity
+ # rubocop:enable Style/OptionalBooleanParameter
end
diff --git a/app/models/collection.rb b/app/models/collection.rb
index 7d36dfa4a6..23b1d76788 100644
--- a/app/models/collection.rb
+++ b/app/models/collection.rb
@@ -52,6 +52,7 @@ class Collection < ApplicationRecord
has_many :collections_wellplates, dependent: :destroy
has_many :collections_screens, dependent: :destroy
has_many :collections_research_plans, dependent: :destroy
+ has_many :collections_device_descriptions, dependent: :destroy
has_many :collections_elements, dependent: :destroy, class_name: 'Labimotion::CollectionsElement'
has_many :collections_vessels, dependent: :destroy
has_many :collections_celllines, dependent: :destroy
@@ -61,6 +62,7 @@ class Collection < ApplicationRecord
has_many :screens, through: :collections_screens
has_many :research_plans, through: :collections_research_plans
has_many :vessels, through: :collections_vessels
+ has_many :device_descriptions, through: :collections_device_descriptions
has_many :elements, through: :collections_elements
has_many :cellline_samples, through: :collections_celllines
diff --git a/app/models/collections_device_description.rb b/app/models/collections_device_description.rb
new file mode 100644
index 0000000000..e0c1c220e8
--- /dev/null
+++ b/app/models/collections_device_description.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: collections_device_descriptions
+#
+# id :bigint not null, primary key
+# collection_id :integer
+# device_description_id :integer
+# deleted_at :datetime
+#
+# Indexes
+#
+# index_collections_device_descriptions_on_collection_id (collection_id)
+# index_collections_device_descriptions_on_deleted_at (deleted_at)
+# index_on_device_description_and_collection (device_description_id,collection_id) UNIQUE
+#
+class CollectionsDeviceDescription < ApplicationRecord
+ acts_as_paranoid
+ belongs_to :collection
+ belongs_to :device_description
+
+ include Tagging
+ include Collecting
+
+ def self.remove_in_collection(element_ids, collection_ids)
+ # Remove from collections
+ delete_in_collection(element_ids, collection_ids)
+ # update element tag with collection info
+ update_tag_by_element_ids(element_ids)
+ end
+
+ def self.move_to_collection(element_ids, from_col_ids, to_col_ids)
+ # Delete in collection
+ delete_in_collection(element_ids, from_col_ids)
+ # Upsert in target collection
+ insert_in_collection(element_ids, to_col_ids)
+ # Update element tag with collection info
+ update_tag_by_element_ids(element_ids)
+ end
+
+ def self.create_in_collection(element_ids, collection_ids)
+ # upsert in target collection
+ # update element tag with collection info
+ static_create_in_collection(element_ids, collection_ids)
+ end
+end
diff --git a/app/models/comment.rb b/app/models/comment.rb
index df7b34ad56..ecaac68bf4 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -24,7 +24,7 @@
#
class Comment < ApplicationRecord
- COMMENTABLE_TYPE = %w[Sample Reaction Screen Wellplate ResearchPlan].freeze
+ COMMENTABLE_TYPE = %w[Sample Reaction Screen Wellplate ResearchPlan DeviceDescription].freeze
enum sample_section: {
properties: 'sample_properties',
@@ -63,12 +63,21 @@ class Comment < ApplicationRecord
metadata: 'research_plan_metadata',
}, _prefix: true
+ enum device_description_section: {
+ properties: 'device_description_properties',
+ detail: 'device_description_detail',
+ analyses: 'device_description_analyses',
+ attachments: 'device_description_attachments',
+ maintenance: 'device_description_maintenance',
+ }, _prefix: true
+
enum header_section: {
sample: 'sample_header',
reaction: 'reaction_header',
wellplate: 'wellplate_header',
screen: 'screen_header',
research_plan: 'research_plan_header',
+ device_description: 'device_description_header',
}, _prefix: true
belongs_to :commentable, polymorphic: true
diff --git a/app/models/concerns/tagging.rb b/app/models/concerns/tagging.rb
index 7ff7274d3d..159f5285f5 100644
--- a/app/models/concerns/tagging.rb
+++ b/app/models/concerns/tagging.rb
@@ -33,7 +33,7 @@ def update_tag
end
element = 'sample'
when 'CollectionsReaction', 'CollectionsWellplate', 'CollectionsSample', 'Labimotion::CollectionsElement',
- 'CollectionsScreen', 'CollectionsResearchPlan'
+ 'CollectionsScreen', 'CollectionsResearchPlan', 'CollectionsDeviceDescription'
args = { collection_tag: true }
element = Labimotion::Utils.elname_by_collection(klass)
when 'CollectionsCellline'
diff --git a/app/models/device.rb b/app/models/device.rb
index 0b9948f756..7b722d22c5 100644
--- a/app/models/device.rb
+++ b/app/models/device.rb
@@ -57,6 +57,7 @@ class Device < ApplicationRecord
has_many :groups, through: :users_devices, source: :user, class_name: 'Group'
has_one :device_metadata, dependent: :destroy
+ has_many :device_descriptions, dependent: :nullify
validates :name, presence: true
validate :unique_name_abbreviation
diff --git a/app/models/device_description.rb b/app/models/device_description.rb
new file mode 100644
index 0000000000..ea7e522c8d
--- /dev/null
+++ b/app/models/device_description.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: device_descriptions
+#
+# id :bigint not null, primary key
+# device_id :integer
+# name :string
+# short_label :string
+# vendor_id :string
+# vendor_url :string
+# serial_number :string
+# version_doi :string
+# version_doi_url :string
+# device_type :string
+# device_type_detail :string
+# operation_mode :string
+# version_installation_start_date :datetime
+# version_installation_end_date :datetime
+# description :text
+# operators :jsonb
+# university_campus :string
+# institute :string
+# building :string
+# room :string
+# infrastructure_assignment :string
+# access_options :string
+# comments :string
+# size :string
+# weight :string
+# application_name :string
+# application_version :string
+# description_for_methods_part :text
+# created_at :datetime not null
+# updated_at :datetime not null
+# vendor_device_name :string
+# vendor_device_id :string
+# vendor_company_name :string
+# tags :string
+# policies_and_user_information :text
+# version_number :string
+# version_characterization :text
+# deleted_at :datetime
+# created_by :integer
+# ontologies :jsonb
+# ancestry :string
+#
+# Indexes
+#
+# index_device_descriptions_on_device_id (device_id)
+#
+class DeviceDescription < ApplicationRecord
+ attr_accessor :collection_id, :is_split
+
+ include ElementUIStateScopes
+ # include PgSearch::Model
+ include Collectable
+ include ElementCodes
+ include AnalysisCodes
+ include Taggable
+ include Labimotion::Segmentable
+
+ acts_as_paranoid
+
+ belongs_to :device, optional: true
+ has_many :collections_device_descriptions, inverse_of: :device_description, dependent: :destroy
+ has_many :collections, through: :collections_device_descriptions
+
+ belongs_to :creator, foreign_key: :created_by, class_name: 'User', inverse_of: :device_descriptions
+
+ has_many :attachments, as: :attachable, inverse_of: :attachable, dependent: :nullify
+ has_many :sync_collections_users, through: :collections
+
+ has_many :comments, as: :commentable, inverse_of: :commentable, dependent: :destroy
+ has_one :container, as: :containable, inverse_of: :containable, dependent: :nullify
+ has_ancestry orphan_strategy: :adopt
+
+ accepts_nested_attributes_for :collections_device_descriptions
+
+ scope :includes_for_list_display, -> { includes(:tag) }
+
+ after_create :set_short_label
+
+ def analyses
+ container ? container.analyses : []
+ end
+
+ def set_short_label
+ return if is_split == true
+
+ prefix = 'Dev'
+ counter = creator.increment_counter 'device_descriptions' # rubocop:disable Rails/SkipsModelValidations
+ user_label = creator.name_abbreviation
+
+ update(short_label: "#{user_label}-#{prefix}#{counter}")
+ end
+
+ def counter_for_split_short_label
+ element_children = children.with_deleted.order('created_at')
+ last_child_label = element_children.where('short_label LIKE ?', "#{short_label}-%").last&.short_label
+ last_child_counter = (last_child_label&.match(/^#{short_label}-(\d+)/) && ::Regexp.last_match(1).to_i) || 0
+
+ [last_child_counter, element_children.count].max
+ end
+
+ def all_collections(user, collection_ids)
+ Collection.where(id: collection_ids) | Collection.where(user_id: user, label: 'All', is_locked: true)
+ end
+
+ def create_sub_device_description(user, collection_ids)
+ device_description = dup
+ segments = device_description.segments
+
+ device_description.is_split = true
+ device_description.short_label = "#{short_label}-#{counter_for_split_short_label + 1}"
+ device_description.parent = self
+ device_description.created_by = user.id
+ device_description.container = Container.create_root_container
+ device_description.attachments = []
+ device_description.segments = []
+ device_description.collections << all_collections(user, collection_ids)
+ device_description.save!
+
+ device_description.save_segments(segments: segments, current_user_id: user.id) if segments
+
+ device_description.reload
+ end
+end
diff --git a/app/models/device_description_text_template.rb b/app/models/device_description_text_template.rb
new file mode 100644
index 0000000000..a1b43d7ef6
--- /dev/null
+++ b/app/models/device_description_text_template.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: text_templates
+#
+# id :integer not null, primary key
+# type :string
+# user_id :integer not null
+# data :jsonb
+# deleted_at :datetime
+# created_at :datetime not null
+# updated_at :datetime not null
+# name :string
+#
+# Indexes
+#
+# index_predefined_template (name) UNIQUE WHERE ((type)::text = 'PredefinedTextTemplate'::text)
+# index_text_templates_on_deleted_at (deleted_at)
+# index_text_templates_on_user_id (user_id)
+#
+
+class DeviceDescriptionTextTemplate < TextTemplate
+ # STI: this file is only here because of rails model autoloading.
+ # place all code in app/models/text_template.rb.
+end
diff --git a/app/models/profile.rb b/app/models/profile.rb
index 95997c6ce0..e075c548ae 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -88,6 +88,7 @@ def data_default_layout
'screen' => 4,
'research_plan' => 5,
'cell_line' => -1000,
+ 'device_description' => -1100,
})
end
end
diff --git a/app/models/sync_collections_user.rb b/app/models/sync_collections_user.rb
index 8fc597af75..710465b0a3 100644
--- a/app/models/sync_collections_user.rb
+++ b/app/models/sync_collections_user.rb
@@ -18,6 +18,7 @@
# updated_at :datetime
# element_detail_level :integer default(10)
# celllinesample_detail_level :integer default(10)
+# devicedescription_detail_level :integer default(10)
#
# Indexes
#
@@ -37,6 +38,7 @@ class SyncCollectionsUser < ApplicationRecord
has_many :screens, through: :collection
has_many :research_plans, through: :collection
has_many :cellline_samples, through: :collection
+ has_many :device_descriptions, through: :collection
before_create :auto_set_synchronized_flag
after_destroy :check_collection_if_synced
diff --git a/app/models/text_template.rb b/app/models/text_template.rb
index ea24b0ff35..5a900643b2 100644
--- a/app/models/text_template.rb
+++ b/app/models/text_template.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
#
# Table name: text_templates
@@ -22,16 +24,18 @@ class TextTemplate < ApplicationRecord
belongs_to :user
TYPES = %w[SampleTextTemplate ReactionTextTemplate WellplateTextTemplate ScreenTextTemplate
- ResearchPlanTextTemplate ReactionDescriptionTextTemplate ElementTextTemplate].freeze
+ ResearchPlanTextTemplate ReactionDescriptionTextTemplate DeviceDescriptionTextTemplate
+ ElementTextTemplate].freeze
DEFAULT_TEMPLATES = {
- 'MS': %w[ei fab esi apci asap maldi m+ hr hr-ei hr-fab aa bb],
- '_toolbar': %w[ndash h-nmr c-nmr ir uv ea]
+ MS: %w[ei fab esi apci asap maldi m+ hr hr-ei hr-fab aa bb],
+ _toolbar: %w[ndash h-nmr c-nmr ir uv ea],
}.freeze
def self.default_templates
def_names = {}
- name.to_s.constantize::DEFAULT_TEMPLATES.each { |k, v| def_names[k] = PredefinedTextTemplate.where(name: v).pluck(:name) }
+ name.to_s.constantize::DEFAULT_TEMPLATES
+ .each { |k, v| def_names[k] = PredefinedTextTemplate.where(name: v).pluck(:name) }
def_names
end
@@ -65,9 +69,13 @@ class ResearchPlanTextTemplate < TextTemplate
class ElementTextTemplate < TextTemplate
end
+class DeviceDescriptionTextTemplate < TextTemplate
+end
+
class PredefinedTextTemplate < TextTemplate
def self.init_seeds
- predefined_template_seeds_path = File.join(Rails.root, 'db', 'seeds', 'json', 'text_template_seeds.json')
+ filepath = Rails.root.join('db/seeds/json/text_template_seeds.json')
+ predefined_template_seeds_path = File.join(filepath)
predefined_templates = JSON.parse(File.read(predefined_template_seeds_path))
predefined_templates.each do |template|
@@ -78,7 +86,7 @@ def self.init_seeds
type: 'PredefinedTextTemplate',
name: template_name,
data: template,
- user_id: Admin.first&.id
+ user_id: Admin.first&.id,
)
end
end
@@ -86,10 +94,10 @@ def self.init_seeds
class ReactionDescriptionTextTemplate < TextTemplate
DEFAULT_TEMPLATES = {
- '_toolbar': %w[
+ _toolbar: %w[
ndash water-free resin-solvent resin-solvent-reagent hand-stop
reaction-procedure gpx-a gpx-b washed-nahco3 acidified-hcl tlc-control
dried isolated residue-purified residue-adsorbed residue-dissolved
- ]
+ ],
}.freeze
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4989587e63..68cafd4ed6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -77,6 +77,7 @@ class User < ApplicationRecord
# created vessels will be kept when the creator goes (dependent: nil).
has_many :created_vessels, class_name: 'Vessel', inverse_of: :creator, dependent: nil
has_many :cellline_samples, through: :collections
+ has_many :device_descriptions, through: :collections
has_many :samples_created, foreign_key: :created_by, class_name: 'Sample'
@@ -103,6 +104,7 @@ class User < ApplicationRecord
has_one :screen_text_template, dependent: :destroy
has_one :wellplate_text_template, dependent: :destroy
has_one :research_plan_text_template, dependent: :destroy
+ has_one :device_description_text_template, dependent: :destroy
has_many :element_text_templates, dependent: :destroy
has_many :calendar_entries, foreign_key: :created_by, inverse_of: :creator, dependent: :destroy
has_many :comments, foreign_key: :created_by, inverse_of: :creator, dependent: :destroy
diff --git a/app/proxies/element_permission_proxy.rb b/app/proxies/element_permission_proxy.rb
index 5ce84b7a09..88dcb20a4c 100644
--- a/app/proxies/element_permission_proxy.rb
+++ b/app/proxies/element_permission_proxy.rb
@@ -91,6 +91,8 @@ def max_detail_level_by_element_class
10
when CelllineSample
10
+ when DeviceDescription
+ 10
end
end
diff --git a/app/services/element_detail_level_calculator.rb b/app/services/element_detail_level_calculator.rb
index 304f005f34..cfde295798 100644
--- a/app/services/element_detail_level_calculator.rb
+++ b/app/services/element_detail_level_calculator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop: disable Metrics/CyclomaticComplexity
+# rubocop:disable Metrics/CyclomaticComplexity
class ElementDetailLevelCalculator
attr_reader :user, :element, :detail_levels
@@ -12,6 +12,7 @@ class ElementDetailLevelCalculator
wellplate_detail_level
screen_detail_level
celllinesample_detail_level
+ devicedescription_detail_level
].freeze
def initialize(user:, element:)
@@ -22,7 +23,7 @@ def initialize(user:, element:)
private
- def calculate_detail_levels # rubocop:disable Metrics/AbcSize
+ def calculate_detail_levels # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
detail_levels = Hash.new(0)
all_collections_detail_levels = user_collection_detail_levels + sync_collection_detail_levels
@@ -33,6 +34,7 @@ def calculate_detail_levels # rubocop:disable Metrics/AbcSize
detail_levels[Screen] = all_collections_detail_levels.pluck(:screen_detail_level).max || 0
detail_levels[Wellplate] = all_collections_detail_levels.pluck(:wellplate_detail_level).max || 0
detail_levels[CelllineSample] = all_collections_detail_levels.pluck(:celllinesample_detail_level).max || 0
+ detail_levels[DeviceDescription] = all_collections_detail_levels.pluck(:devicedescription_detail_level).max || 0
detail_levels[Well] = detail_levels[Wellplate]
detail_levels
@@ -73,4 +75,4 @@ def sync_collection_detail_levels
.map { |values| Hash[DETAIL_LEVEL_FIELDS.zip(values)] }
end
end
-# rubocop: enable Metrics/CyclomaticComplexity
\ No newline at end of file
+# rubocop:enable Metrics/CyclomaticComplexity Metrics/PerceivedComplexity
diff --git a/app/usecases/device_descriptions/create.rb b/app/usecases/device_descriptions/create.rb
new file mode 100644
index 0000000000..3372c402a1
--- /dev/null
+++ b/app/usecases/device_descriptions/create.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Usecases
+ module DeviceDescriptions
+ class Create
+ attr_reader :params, :current_user
+
+ def initialize(params, current_user)
+ @params = params
+ @current_user = current_user
+ @segments = params[:segments]
+ end
+
+ def execute
+ ActiveRecord::Base.transaction do
+ device_description = DeviceDescription.create!(params.except(:segments))
+ save_segments(device_description)
+ device_description.reload
+
+ is_shared_collection = false
+ if user_collection
+ CollectionsDeviceDescription.create(device_description: device_description, collection: user_collection)
+ elsif sync_collection_user
+ is_shared_collection = true
+ CollectionsDeviceDescription.create(device_description: device_description,
+ collection: sync_collection_user.collection)
+
+ CollectionsDeviceDescription.create(device_description: device_description,
+ collection: all_collection_of_sharer)
+ end
+
+ unless is_shared_collection
+ CollectionsDeviceDescription.create(
+ device_description: device_description,
+ collection: all_collection_of_current_user,
+ )
+ end
+
+ device_description
+ end
+ end
+
+ private
+
+ def user_collection
+ @user_collection ||= @current_user.collections.find_by(id: @params[:collection_id])
+ end
+
+ def sync_collection_user
+ @sync_collection_user ||= @current_user.all_sync_in_collections_users.find_by(id: @params[:collection_id])
+ end
+
+ def all_collection_of_sharer
+ Collection.get_all_collection_for_user(sync_collection_user.shared_by_id)
+ end
+
+ def all_collection_of_current_user
+ Collection.get_all_collection_for_user(@current_user.id)
+ end
+
+ def save_segments(device_description)
+ return if @segments.blank?
+
+ device_description.save_segments(segments: @segments, current_user_id: current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/usecases/device_descriptions/update.rb b/app/usecases/device_descriptions/update.rb
new file mode 100644
index 0000000000..7ecdc1c20b
--- /dev/null
+++ b/app/usecases/device_descriptions/update.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+module Usecases
+ module DeviceDescriptions
+ class Update
+ attr_reader :params, :device_description, :current_user
+
+ def initialize(params, device_description, current_user)
+ @params = params
+ @device_description = device_description
+ @segments = params[:segments]
+ @current_user = current_user
+ end
+
+ def execute
+ ActiveRecord::Base.transaction do
+ attributes = remove_ontologies_marked_as_deleted
+ attributes = add_matching_segment_klasses(attributes)
+
+ save_segments
+ device_description.reload
+ device_description.update!(attributes.except(:segments))
+
+ device_description
+ end
+ end
+
+ def segment_klass_ids_by_new_ontology
+ segment_klasses = find_segment_klasses_by_ontology(params[:ontology], params[:id])
+ return [] if segment_klasses.blank?
+
+ segment_ids = []
+ segment_klasses.map do |segment_klass|
+ segment_ids << { segment_klass_id: segment_klass.segment_klass_id }
+ end
+ segment_ids
+ end
+
+ private
+
+ def remove_ontologies_marked_as_deleted
+ return params if params[:ontologies].blank?
+
+ ontology_params = []
+ params[:ontologies].each do |ontology|
+ if ontology[:data][:is_deleted]
+ remove_segments_of_deleted_ontology(ontology)
+ else
+ ontology_params << ontology
+ end
+ end
+ params[:ontologies] = ontology_params
+ params
+ end
+
+ def remove_segments_of_deleted_ontology(ontology)
+ return if ontology[:segments].blank?
+
+ ontology[:segments].each do |segment|
+ labimotion_segments = Labimotion::Segment.where(segment_klass_id: segment[:segment_klass_id])
+ labimotion_segments.delete_all if labimotion_segments.present?
+ idx = @segments.index { |s| s[:segment_klass_id] == segment[:segment_klass_id] }
+ @segments.delete_at(idx) if idx.present?
+ end
+ end
+
+ def add_matching_segment_klasses(attributes)
+ return attributes if attributes[:ontologies].blank?
+ return attributes if attributes[:ontologies] == device_description[:ontologies]
+
+ attributes[:ontologies].each_with_index do |ontology, i|
+ segment_klasses = find_segment_klasses_by_ontology(ontology, attributes[:id])
+ next if segment_klasses.blank?
+
+ segment_ids = []
+ segment_klasses.map do |segment_klass|
+ segment_ids << { segment_klass_id: segment_klass.segment_klass_id }
+ end
+ attributes[:ontologies][i][:segments] = segment_ids
+ end
+
+ attributes
+ end
+
+ def find_segment_klasses_by_ontology(ontology, object_id)
+ Labimotion::SegmentKlass
+ .select(
+ 'segment_klasses.id AS segment_klass_id,
+ CASE
+ WHEN segments.properties IS NOT NULL THEN segments.properties
+ ELSE segment_klasses.properties_template
+ END AS properties',
+ )
+ .where(segment_klasses: { is_active: true })
+ .where(ontology_query_for_segment_klasses(ontology))
+ .joins("
+ LEFT OUTER JOIN segments ON segments.segment_klass_id = segment_klasses.id
+ AND segments.element_id = '#{object_id}'
+ AND segments.element_type = 'DeviceDescription'
+ ")
+ end
+
+ def ontology_query_for_segment_klasses(ontology)
+ query = "jsonb_path_query_array(
+ segment_klasses.properties_template -> 'layers', '$.*.fields[*].ontology.short_form'
+ )::TEXT ILIKE '%#{ontology[:data][:short_form]}%'"
+ ontology[:paths].each do |path|
+ query += "OR
+ jsonb_path_query_array(
+ segment_klasses.properties_template -> 'layers', '$.*.fields[*].ontology.short_form'
+ )::TEXT ILIKE '%#{path[:short_form]}%'"
+ end
+ query
+ end
+
+ def extract_segments(ontology, segment_klass, segments)
+ idx =
+ if ontology[:segments].present?
+ ontology[:segments].index { |s| s[:segment_klass_id] == segment_klass.segment_klass_id }
+ end
+
+ segments << ontology[:segments][idx][:segment] if idx.present? && ontology[:segments][idx][:segment].present?
+ segments
+ end
+
+ def segments_to_save
+ segments = []
+ @segments.each_with_index do |segment, i|
+ dd_index = device_description.segments.index { |s| s[:id] == segment[:id] }
+ dd_segment = device_description.segments[dd_index ||= i]
+ layer = dd_segment.present? ? dd_segment.properties['layers'] : []
+ next if dd_index.present? && layer == segment['properties']['layers']
+
+ segments << segment
+ end
+ segments
+ end
+
+ def save_segments
+ return if @segments.blank?
+
+ device_description.save_segments(segments: segments_to_save, current_user_id: current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/usecases/sharing/share_with_users.rb b/app/usecases/sharing/share_with_users.rb
index b27e302e23..f1d699ebf4 100644
--- a/app/usecases/sharing/share_with_users.rb
+++ b/app/usecases/sharing/share_with_users.rb
@@ -20,6 +20,7 @@ def execute!
screen_ids: @params.fetch(:screen_ids, []),
research_plan_ids: @params.fetch(:research_plan_ids, []),
cell_line_ids: @params.fetch(:cell_line_ids, []),
+ device_description_ids: @params.fetch(:device_description_ids, []),
element_ids: @params.fetch(:element_ids, [])
).execute!
end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index e142283197..9e4b47c99d 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -22,7 +22,7 @@
host = url.host
port = url.port
- src = [:self, 'http://localhost:3035', 'http://webpacker:3035', 'ws://localhost:3035']
+ src = [:self, 'http://localhost:3035', 'http://webpacker:3035', 'ws://localhost:3035', 'https://service.tib.eu']
src += ["ws://#{host}:3035", "#{scheme}://#{host}:3035"] if host.present?
src += ["#{scheme}://#{host}:#{url.port}"] if host.present? && port.present?
puts "connect_src: #{src}"
diff --git a/config/profile_default.yml.example b/config/profile_default.yml.example
index 0a533148ee..b14e3144db 100644
--- a/config/profile_default.yml.example
+++ b/config/profile_default.yml.example
@@ -7,6 +7,7 @@ development:
:screen: 4
:research_plan: 5
:cell_line: -1000
+ :device_description: -1100
:layout_detail_research_plan:
:research_plan:
1
@@ -54,6 +55,17 @@ development:
1
:analyses:
2
+ :layout_detail_device_description:
+ :properties:
+ 1
+ :detail:
+ 2
+ :analyses:
+ 3
+ :attachments:
+ 4
+ :maintainance:
+ 5
production:
:layout:
@@ -64,6 +76,7 @@ production:
:screen: 4
:research_plan: 5
:cell_line: -1000
+ :device_description: -1100
:layout_detail_research_plan:
:research_plan:
1
@@ -111,6 +124,17 @@ production:
1
:analyses:
2
+ :layout_detail_device_description:
+ :properties:
+ 1
+ :detail:
+ 2
+ :analyses:
+ 3
+ :attachments:
+ 4
+ :maintainance:
+ 5
test:
:layout:
@@ -121,6 +145,7 @@ test:
:screen: 4
:research_plan: 5
:cell_line: -1000
+ :device_description: -1100
:layout_detail_research_plan:
:research_plan:
1
@@ -168,3 +193,14 @@ test:
1
:analyses:
2
+ :layout_detail_device_description:
+ :properties:
+ 1
+ :detail:
+ 2
+ :analyses:
+ 3
+ :attachments:
+ 4
+ :maintainance:
+ 5
diff --git a/db/migrate/20240206164554_create_device_descriptions.rb b/db/migrate/20240206164554_create_device_descriptions.rb
new file mode 100644
index 0000000000..e61104e88a
--- /dev/null
+++ b/db/migrate/20240206164554_create_device_descriptions.rb
@@ -0,0 +1,38 @@
+class CreateDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def change
+ create_table :device_descriptions do |t|
+ t.integer :device_id
+ t.string :name
+ t.string :short_label
+ t.string :vendor_name
+ t.string :vendor_id
+ t.string :vendor_url
+ t.string :serial_number
+ t.string :doi
+ t.string :doi_url
+ t.string :device_type
+ t.string :device_type_detail
+ t.string :operation_mode
+ t.datetime :installation_start_date
+ t.datetime :installation_end_date
+ t.text :description_and_comments
+ t.jsonb :technical_operator
+ t.jsonb :administrative_operator
+ t.string :university_campus
+ t.string :institute
+ t.string :building
+ t.string :room
+ t.string :infrastructure_assignment
+ t.string :access_options
+ t.string :comments
+ t.string :size
+ t.string :weight
+ t.string :application_name
+ t.string :application_version
+ t.text :description_for_methods_part
+
+ t.timestamps
+ end
+ add_index :device_descriptions, :device_id
+ end
+end
diff --git a/db/migrate/20240206171038_create_collections_device_descriptions.rb b/db/migrate/20240206171038_create_collections_device_descriptions.rb
new file mode 100644
index 0000000000..1548dcc44c
--- /dev/null
+++ b/db/migrate/20240206171038_create_collections_device_descriptions.rb
@@ -0,0 +1,12 @@
+class CreateCollectionsDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def change
+ create_table :collections_device_descriptions do |t|
+ t.integer :collection_id
+ t.integer :device_description_id
+ t.datetime :deleted_at
+ end
+ add_index :collections_device_descriptions, [:device_description_id, :collection_id], unique: true, name: :index_on_device_description_and_collection
+ add_index :collections_device_descriptions, :collection_id
+ add_index :collections_device_descriptions, :deleted_at
+ end
+end
diff --git a/db/migrate/20240207121720_init_device_description_to_element_klasses.rb b/db/migrate/20240207121720_init_device_description_to_element_klasses.rb
new file mode 100644
index 0000000000..4560051ab2
--- /dev/null
+++ b/db/migrate/20240207121720_init_device_description_to_element_klasses.rb
@@ -0,0 +1,49 @@
+class InitDeviceDescriptionToElementKlasses < ActiveRecord::Migration[6.1]
+ def up
+ klass = Labimotion::ElementKlass.where(name: 'device_description').first
+ if klass.nil?
+ klass = Labimotion::ElementKlass.create(name: 'device_description')
+ uuid = SecureRandom.uuid
+ properties = klass.properties_template || { uuid: uuid, layers: {}, select_options: {} }
+ properties['uuid'] = uuid
+ properties['eln'] = Chemotion::Application.config.version
+ properties['klass'] = 'ElementKlass'
+ select_options = properties['select_options']
+ select_options&.map { |k, v| select_options[k] = { desc: k, options: v } }
+ properties['select_options'] = select_options || {}
+
+ attributes = {
+ name: 'device_description',
+ label: 'Device Description',
+ desc: 'ELN Device Description',
+ icon_name: 'icon-device_description',
+ is_active: true,
+ klass_prefix: '',
+ is_generic: false,
+ place: 5,
+ uuid: uuid,
+ released_at: DateTime.now,
+ properties_template: properties,
+ properties_release: properties
+ }
+ klass.update(attributes)
+ klass.reload
+
+ revision_attributes = {
+ element_klass_id: klass.id,
+ uuid: klass.uuid,
+ deleted_at: klass.deleted_at,
+ properties_release: klass.properties_template,
+ released_at: klass.released_at
+ }
+ Labimotion::ElementKlassesRevision.create(revision_attributes)
+ end
+ end
+
+ def down
+ klass = Labimotion::ElementKlass.where(name: 'device_description').first
+ if klass.present?
+ klass.destroy!
+ end
+ end
+end
diff --git a/db/migrate/20240207123444_add_device_description_detail_level_to_collections.rb b/db/migrate/20240207123444_add_device_description_detail_level_to_collections.rb
new file mode 100644
index 0000000000..5bc2dc3718
--- /dev/null
+++ b/db/migrate/20240207123444_add_device_description_detail_level_to_collections.rb
@@ -0,0 +1,5 @@
+class AddDeviceDescriptionDetailLevelToCollections < ActiveRecord::Migration[6.1]
+ def change
+ add_column :collections, :device_description_detail_level, :integer, default: 10
+ end
+end
diff --git a/db/migrate/20240209120817_add_and_change_some_fields_at_device_descriptions.rb b/db/migrate/20240209120817_add_and_change_some_fields_at_device_descriptions.rb
new file mode 100644
index 0000000000..cff8c2614d
--- /dev/null
+++ b/db/migrate/20240209120817_add_and_change_some_fields_at_device_descriptions.rb
@@ -0,0 +1,71 @@
+class AddAndChangeSomeFieldsAtDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def up
+ add_column :device_descriptions, :vendor_device_name, :string
+ add_column :device_descriptions, :vendor_device_id, :string
+ add_column :device_descriptions, :vendor_company_name, :string
+ add_column :device_descriptions, :tags, :string
+ add_column :device_descriptions, :policies_and_user_information, :text
+ add_column :device_descriptions, :version_number, :string
+ add_column :device_descriptions, :version_characterization, :text
+ add_column :device_descriptions, :deleted_at, :datetime
+ add_column :device_descriptions, :created_by, :integer
+
+ rename_column :device_descriptions, :description_and_comments, :description
+ rename_column :device_descriptions, :installation_start_date, :version_installation_start_date
+ rename_column :device_descriptions, :installation_end_date, :version_installation_end_date
+ rename_column :device_descriptions, :doi, :version_doi
+ rename_column :device_descriptions, :doi_url, :version_doi_url
+ rename_column :device_descriptions, :technical_operator, :operators
+
+ remove_column :device_descriptions, :vendor_name
+ remove_column :device_descriptions, :administrative_operator
+
+ User.all.each do |user|
+ user.counters['device_descriptions']="0"
+ user.update_column(:counters, user.counters)
+ end
+
+ Profile.all.each do |profile|
+ next unless profile.data['layout']
+ next if profile.data['layout']['device_description']
+
+ profile.data['layout']['device_description']=-1100
+ profile.save
+ end
+ end
+
+ def down
+ remove_column :device_descriptions, :vendor_device_name
+ remove_column :device_descriptions, :vendor_device_id
+ remove_column :device_descriptions, :vendor_company_name
+ remove_column :device_descriptions, :tags
+ remove_column :device_descriptions, :policies_and_user_information
+ remove_column :device_descriptions, :version_number
+ remove_column :device_descriptions, :version_characterization
+ remove_column :device_descriptions, :deleted_at
+ remove_column :device_descriptions, :created_by
+
+ rename_column :device_descriptions, :description, :description_and_comments
+ rename_column :device_descriptions, :version_installation_start_date, :installation_start_date
+ rename_column :device_descriptions, :version_installation_end_date, :installation_end_date
+ rename_column :device_descriptions, :version_doi, :doi
+ rename_column :device_descriptions, :version_doi_url, :doi_url
+ rename_column :device_descriptions, :operators, :technical_operator
+
+ add_column :device_descriptions, :vendor_name, :string
+ add_column :device_descriptions, :administrative_operator, :jsonb
+
+ User.all.each do |user|
+ user.counters.delete('device_descriptions')
+ user.update_column(:counters, user.counters)
+ end
+
+ Profile.all.each do |profile|
+ next unless profile.data['layout']
+ next unless profile.data['layout']['device_description']
+
+ profile.data['layout'].delete('device_description')
+ profile.save
+ end
+ end
+end
diff --git a/db/migrate/20240229180204_change_detail_level_field_for_device_description.rb b/db/migrate/20240229180204_change_detail_level_field_for_device_description.rb
new file mode 100644
index 0000000000..351204ec00
--- /dev/null
+++ b/db/migrate/20240229180204_change_detail_level_field_for_device_description.rb
@@ -0,0 +1,6 @@
+class ChangeDetailLevelFieldForDeviceDescription < ActiveRecord::Migration[6.1]
+ def change
+ rename_column :collections, :device_description_detail_level, :devicedescription_detail_level
+ add_column :sync_collections_users, :devicedescription_detail_level, :integer, default: 10
+ end
+end
diff --git a/db/migrate/20240312151708_add_ontologies_to_device_descriptions.rb b/db/migrate/20240312151708_add_ontologies_to_device_descriptions.rb
new file mode 100644
index 0000000000..3fded0286d
--- /dev/null
+++ b/db/migrate/20240312151708_add_ontologies_to_device_descriptions.rb
@@ -0,0 +1,5 @@
+class AddOntologiesToDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :device_descriptions, :ontologies, :jsonb
+ end
+end
diff --git a/db/migrate/20240417092033_rename_fields_at_device_descriptions.rb b/db/migrate/20240417092033_rename_fields_at_device_descriptions.rb
new file mode 100644
index 0000000000..f5ceebca52
--- /dev/null
+++ b/db/migrate/20240417092033_rename_fields_at_device_descriptions.rb
@@ -0,0 +1,6 @@
+class RenameFieldsAtDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def change
+ rename_column :device_descriptions, :comments, :access_comments
+ rename_column :device_descriptions, :tags, :general_tags
+ end
+end
diff --git a/db/migrate/20240424181556_add_ancestry_to_device_descriptions.rb b/db/migrate/20240424181556_add_ancestry_to_device_descriptions.rb
new file mode 100644
index 0000000000..428c2065de
--- /dev/null
+++ b/db/migrate/20240424181556_add_ancestry_to_device_descriptions.rb
@@ -0,0 +1,6 @@
+class AddAncestryToDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :device_descriptions, :ancestry, :string
+ add_index :device_descriptions, :ancestry
+ end
+end
diff --git a/db/migrate/20240515090140_add_more_fields_to_device_descriptions.rb b/db/migrate/20240515090140_add_more_fields_to_device_descriptions.rb
new file mode 100644
index 0000000000..081e6ebcee
--- /dev/null
+++ b/db/migrate/20240515090140_add_more_fields_to_device_descriptions.rb
@@ -0,0 +1,15 @@
+class AddMoreFieldsToDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def up
+ change_column :device_descriptions, :general_tags, :string, array: true, default: [], using: "(string_to_array(general_tags, ','))"
+ add_column :device_descriptions, :version_identifier_type, :string
+ add_column :device_descriptions, :helpers_uploaded, :boolean, default: false
+ add_column :device_descriptions, :setup_descriptions, :jsonb
+ end
+
+ def down
+ change_column :device_descriptions, :general_tags, :string, array: false, default: nil, using: "(array_to_string(general_tags, ','))"
+ remove_column :device_descriptions, :version_identifier_type
+ remove_column :device_descriptions, :helpers_uploaded
+ remove_column :device_descriptions, :setup_descriptions
+ end
+end
diff --git a/db/migrate/20240531122129_add_maintenance_fields_to_device_descriptions.rb b/db/migrate/20240531122129_add_maintenance_fields_to_device_descriptions.rb
new file mode 100644
index 0000000000..739e2d2ffd
--- /dev/null
+++ b/db/migrate/20240531122129_add_maintenance_fields_to_device_descriptions.rb
@@ -0,0 +1,14 @@
+class AddMaintenanceFieldsToDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :device_descriptions, :maintenance_contract_available, :string
+ add_column :device_descriptions, :maintenance_scheduling, :string
+ add_column :device_descriptions, :contact_for_maintenance, :jsonb
+ add_column :device_descriptions, :planned_maintenance, :jsonb
+ add_column :device_descriptions, :consumables_needed_for_maintenance, :jsonb
+ add_column :device_descriptions, :unexpected_maintenance, :jsonb
+ add_column :device_descriptions, :measures_after_full_shut_down, :text
+ add_column :device_descriptions, :measures_after_short_shut_down, :text
+ add_column :device_descriptions, :measures_to_plan_offline_period, :text
+ add_column :device_descriptions, :restart_after_planned_offline_period, :text
+ end
+end
diff --git a/db/migrate/20250120162008_add_weight_unit_to_device_descriptions.rb b/db/migrate/20250120162008_add_weight_unit_to_device_descriptions.rb
new file mode 100644
index 0000000000..fdb15799ee
--- /dev/null
+++ b/db/migrate/20250120162008_add_weight_unit_to_device_descriptions.rb
@@ -0,0 +1,5 @@
+class AddWeightUnitToDeviceDescriptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :device_descriptions, :weight_unit, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a5a6b7d34e..5a3fc3175d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2024_09_17_085816) do
+ActiveRecord::Schema.define(version: 2025_01_20_162008) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
@@ -192,6 +192,7 @@
t.integer "researchplan_detail_level", default: 10
t.integer "element_detail_level", default: 10
t.jsonb "tabs_segment", default: {}
+ t.integer "devicedescription_detail_level", default: 10
t.integer "celllinesample_detail_level", default: 10
t.bigint "inventory_id"
t.index ["ancestry"], name: "index_collections_on_ancestry"
@@ -209,6 +210,15 @@
t.index ["deleted_at"], name: "index_collections_celllines_on_deleted_at"
end
+ create_table "collections_device_descriptions", force: :cascade do |t|
+ t.integer "collection_id"
+ t.integer "device_description_id"
+ t.datetime "deleted_at"
+ t.index ["collection_id"], name: "index_collections_device_descriptions_on_collection_id"
+ t.index ["deleted_at"], name: "index_collections_device_descriptions_on_deleted_at"
+ t.index ["device_description_id", "collection_id"], name: "index_on_device_description_and_collection", unique: true
+ end
+
create_table "collections_elements", id: :serial, force: :cascade do |t|
t.integer "collection_id"
t.integer "element_id"
@@ -424,6 +434,65 @@
t.index ["priority", "run_at"], name: "delayed_jobs_priority"
end
+ create_table "device_descriptions", force: :cascade do |t|
+ t.integer "device_id"
+ t.string "name"
+ t.string "short_label"
+ t.string "vendor_id"
+ t.string "vendor_url"
+ t.string "serial_number"
+ t.string "version_doi"
+ t.string "version_doi_url"
+ t.string "device_type"
+ t.string "device_type_detail"
+ t.string "operation_mode"
+ t.datetime "version_installation_start_date"
+ t.datetime "version_installation_end_date"
+ t.text "description"
+ t.jsonb "operators"
+ t.string "university_campus"
+ t.string "institute"
+ t.string "building"
+ t.string "room"
+ t.string "infrastructure_assignment"
+ t.string "access_options"
+ t.string "access_comments"
+ t.string "size"
+ t.string "weight"
+ t.string "application_name"
+ t.string "application_version"
+ t.text "description_for_methods_part"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.string "vendor_device_name"
+ t.string "vendor_device_id"
+ t.string "vendor_company_name"
+ t.string "general_tags", default: [], array: true
+ t.text "policies_and_user_information"
+ t.string "version_number"
+ t.text "version_characterization"
+ t.datetime "deleted_at"
+ t.integer "created_by"
+ t.jsonb "ontologies"
+ t.string "ancestry"
+ t.string "version_identifier_type"
+ t.boolean "helpers_uploaded", default: false
+ t.jsonb "setup_descriptions"
+ t.string "maintenance_contract_available"
+ t.string "maintenance_scheduling"
+ t.jsonb "contact_for_maintenance"
+ t.jsonb "planned_maintenance"
+ t.jsonb "consumables_needed_for_maintenance"
+ t.jsonb "unexpected_maintenance"
+ t.text "measures_after_full_shut_down"
+ t.text "measures_after_short_shut_down"
+ t.text "measures_to_plan_offline_period"
+ t.text "restart_after_planned_offline_period"
+ t.string "weight_unit"
+ t.index ["ancestry"], name: "index_device_descriptions_on_ancestry"
+ t.index ["device_id"], name: "index_device_descriptions_on_device_id"
+ end
+
create_table "device_metadata", id: :serial, force: :cascade do |t|
t.integer "device_id"
t.string "doi"
@@ -1345,6 +1414,7 @@
t.datetime "updated_at"
t.integer "element_detail_level", default: 10
t.integer "celllinesample_detail_level", default: 10
+ t.integer "devicedescription_detail_level", default: 10
t.index ["collection_id"], name: "index_sync_collections_users_on_collection_id"
t.index ["shared_by_id", "user_id", "fake_ancestry"], name: "index_sync_collections_users_on_shared_by_id"
t.index ["user_id", "fake_ancestry"], name: "index_sync_collections_users_on_user_id_and_fake_ancestry"
diff --git a/spec/api/chemotion/attachment_api_spec.rb b/spec/api/chemotion/attachment_api_spec.rb
index 55d6a85627..4aed3957d6 100644
--- a/spec/api/chemotion/attachment_api_spec.rb
+++ b/spec/api/chemotion/attachment_api_spec.rb
@@ -23,8 +23,8 @@
'identifier' => attachment.identifier,
'thumb' => attachment.thumb,
# 'thumbnail' => attachment.thumb ? Base64.encode64(attachment.read_thumbnail) : nil,
- 'created_at' => kind_of(String),
- 'updated_at' => kind_of(String),
+ # 'created_at' => kind_of(String),
+ # 'updated_at' => kind_of(String),
},
}
end
@@ -70,7 +70,8 @@
end
it 'returns the deleted attachment' do
- expect(parsed_json_response).to include(expected_response)
+ response = parsed_json_response['attachment'].except('created_at', 'updated_at')
+ expect(response).to include(expected_response['attachment'])
end
it 'deletes the attachment on database', :enable_usecases_attachments_delete do
@@ -119,7 +120,8 @@
end
it 'returns the deleted attachment' do
- expect(parsed_json_response).to include(expected_response)
+ response = parsed_json_response['attachment'].except('created_at', 'updated_at')
+ expect(response).to include(expected_response['attachment'])
end
it 'unlinks the attachment from container', :enable_usecases_attachments_unlink do
diff --git a/spec/api/chemotion/device_description_api_spec.rb b/spec/api/chemotion/device_description_api_spec.rb
new file mode 100644
index 0000000000..242ed2420b
--- /dev/null
+++ b/spec/api/chemotion/device_description_api_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+describe Chemotion::DeviceDescriptionAPI do
+ include_context 'api request authorization context'
+
+ let(:user) { create(:user) }
+ let(:collection) { create(:collection, user_id: user.id, devicedescription_detail_level: 10) }
+ let(:device_description) do
+ create(:device_description, :with_ontologies, collection_id: collection.id, created_by: collection.user_id)
+ end
+ let(:device_description2) do
+ create(:device_description, :with_ontologies, collection_id: collection.id, created_by: collection.user_id)
+ end
+ let(:device_description_collection) do
+ create(:collections_device_description, device_description: device_description, collection: collection)
+ end
+ let(:segment_klass) { create(:segment_klass, :with_ontology_properties_template) }
+
+ describe 'GET /api/v1/device_descriptions/' do
+ before do
+ CollectionsDeviceDescription.create!(device_description: device_description, collection: collection)
+ end
+
+ let(:params) do
+ { collection_id: collection.id }
+ end
+
+ it 'fetches device descriptions by collection id' do
+ get '/api/v1/device_descriptions/', params: params
+
+ expect(parsed_json_response['device_descriptions'].size).to be(1)
+ end
+ end
+
+ describe 'POST /api/v1/device_descriptions' do
+ let(:device_description_params) { attributes_for(:device_description, collection_id: collection.id) }
+
+ context 'when creating a device description' do
+ it 'returns a device description' do
+ post '/api/v1/device_descriptions', params: device_description_params
+
+ expect(parsed_json_response['device_description']['short_label']).to include('Dev')
+ end
+
+ it 'has taggable_data' do
+ post '/api/v1/device_descriptions', params: device_description_params
+
+ expect(parsed_json_response['device_description']['tag']['taggable_data'].size).to be(1)
+ end
+ end
+ end
+
+ describe 'GET /api/v1/device_descriptions/:id' do
+ before do
+ device_description
+ end
+
+ it 'fetches an device description by id' do
+ get "/api/v1/device_descriptions/#{device_description.id}"
+
+ expect(parsed_json_response['device_description']['name']).to eql(device_description.name)
+ end
+ end
+
+ describe 'PUT /api/v1/device_descriptions/byontology/:id' do
+ before do
+ segment_klass
+ end
+
+ context 'when selecting an ontology' do
+ let(:params) do
+ {
+ id: device_description.id,
+ ontology: device_description.ontologies.first,
+ }
+ end
+
+ let(:params2) do
+ {
+ id: device_description.id,
+ ontology: device_description.ontologies.last,
+ }
+ end
+
+ it 'returns segment klass id' do
+ put "/api/v1/device_descriptions/byontology/#{device_description.id}", params: params
+
+ expect(parsed_json_response).to eql([{ 'segment_klass_id' => segment_klass.id }])
+ end
+
+ it 'returns empty array' do
+ put "/api/v1/device_descriptions/byontology/#{device_description.id}", params: params2
+
+ expect(parsed_json_response).to eql([])
+ end
+ end
+ end
+
+ describe 'POST /api/v1/device_descriptions/ui_state/' do
+ before do
+ device_description_collection
+ end
+
+ let(:params) do
+ {
+ ui_state: {
+ all: false,
+ included_ids: [device_description.id, device_description2.id],
+ excluded_ids: [],
+ collection_id: collection.id,
+ },
+ limit: 1,
+ }
+ end
+
+ it 'fetches only one device description' do
+ post '/api/v1/device_descriptions/ui_state/', params: params, as: :json
+
+ expect(parsed_json_response['device_descriptions'].size).to be(1)
+ end
+ end
+
+ describe 'POST /api/v1/device_descriptions/sub_device_descriptions/' do
+ before do
+ device_description_collection
+ end
+
+ let(:params) do
+ {
+ ui_state: {
+ currentCollectionId: collection.id,
+ device_description: {
+ all: false,
+ included_ids: [device_description.id],
+ excluded_ids: [],
+ },
+ isSync: false,
+ },
+ }
+ end
+
+ it 'creates a split of selected device description' do
+ post '/api/v1/device_descriptions/sub_device_descriptions/', params: params, as: :json
+
+ expect(device_description.reload.children.size).to be(1)
+ end
+ end
+
+ describe 'PUT /api/v1/device_descriptions/:id' do
+ context 'when updating an device description' do
+ let(:params) do
+ {
+ name: 'new name',
+ short_label: 'CU1-DD1-2',
+ }
+ end
+
+ it 'returns the updated device description' do
+ put "/api/v1/device_descriptions/#{device_description.id}", params: device_description.attributes.merge(params)
+
+ expect(parsed_json_response['device_description']).to include(params.stringify_keys)
+ end
+ end
+ end
+
+ describe 'DELETE /api/v1/device_descriptions/:id' do
+ it 'deletes a device description' do
+ delete "/api/v1/device_descriptions/#{device_description.id}"
+
+ expect(parsed_json_response).to include('deleted' => device_description.id)
+ end
+ end
+end
diff --git a/spec/factories/collections_device_descriptions.rb b/spec/factories/collections_device_descriptions.rb
new file mode 100644
index 0000000000..bd6547860a
--- /dev/null
+++ b/spec/factories/collections_device_descriptions.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :collections_device_description do
+ collection_id { 1 }
+ device_description_id { 1 }
+ end
+end
diff --git a/spec/factories/device_descriptions.rb b/spec/factories/device_descriptions.rb
new file mode 100644
index 0000000000..9611ec1efd
--- /dev/null
+++ b/spec/factories/device_descriptions.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :device_description do
+ sequence(:name) { |i| "Device description #{i}" }
+ sequence(:short_label) { |i| "CU1-Dev#{i}" }
+ serial_number { '123abc456def' }
+ collection_id { 1 }
+ created_by { 1 }
+
+ trait :with_ontologies do
+ ontologies do
+ [
+ {
+ 'data' => {
+ 'id' => 'chmo:class:http://purl.obolibrary.org/obo/CHMO_0002231',
+ 'iri' => 'http://purl.obolibrary.org/obo/CHMO_0002231',
+ 'type' => 'class',
+ 'label' => 'purification',
+ 'obo_id' => 'CHMO:0002231',
+ 'short_form' => 'CHMO_0002231',
+ 'description' => [
+ 'Any technique used to physically separate an analyte from byproducts,
+ reagents or contaminating substances.',
+ ],
+ 'ontology_name' => 'chmo',
+ 'ontology_prefix' => 'CHMO',
+ },
+ 'paths' => [
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/OBI_0000094',
+ 'label' => 'material processing',
+ 'short_form' => 'OBI_0000094',
+ },
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/CHMO_0000999',
+ 'label' => 'separation method',
+ 'short_form' => 'CHMO_0000999',
+ },
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/CHMO_0002231',
+ 'label' => 'purification',
+ 'short_form' => 'CHMO_0002231',
+ },
+ ],
+ },
+ {
+ 'data' => {
+ 'id' => 'chmo:class:http://purl.obolibrary.org/obo/CHMO_0002413',
+ 'iri' => 'http://purl.obolibrary.org/obo/CHMO_0002413',
+ 'type' => 'class',
+ 'label' => 'cryogenic electron microscopy',
+ 'obo_id' => 'CHMO:0002413',
+ 'short_form' => 'CHMO_0002413',
+ 'description' => [
+ 'Microscopy where the specimen, which is cooled in liquid ethane to 180 °C',
+ ],
+ 'ontology_name' => 'chmo',
+ 'ontology_prefix' => 'CHMO',
+ },
+ 'paths' => [
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/OBI_0000070',
+ 'label' => 'assay',
+ 'short_form' => 'OBI_0000070',
+ },
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/OBI_0000185',
+ 'label' => 'imaging assay',
+ 'short_form' => 'OBI_0000185',
+ },
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/CHMO_0000067',
+ 'label' => 'microscopy',
+ 'short_form' => 'CHMO_0000067',
+ },
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/CHMO_0000068',
+ 'label' => 'electron microscopy',
+ 'short_form' => 'CHMO_0000068',
+ },
+ {
+ 'iri' => 'http://purl.obolibrary.org/obo/CHMO_0002413',
+ 'label' => 'cryogenic electron microscopy',
+ 'short_form' => 'CHMO_0002413',
+ },
+ ],
+ },
+ ]
+ end
+ end
+
+ callback(:before_create) do |device_description|
+ device_description.created_by = FactoryBot.build(:user) unless device_description.created_by
+ device_description.collections << FactoryBot.build(:collection, user_id: device_description.created_by)
+ device_description.container = FactoryBot.create(:container, :with_analysis) unless device_description.container
+ end
+ end
+end
diff --git a/spec/factories/setment_klasses.rb b/spec/factories/setment_klasses.rb
index caa9b53d64..eac2d34e5b 100644
--- a/spec/factories/setment_klasses.rb
+++ b/spec/factories/setment_klasses.rb
@@ -4,5 +4,69 @@
factory :segment_klass, class: 'Labimotion::SegmentKlass' do
label { 'segment' }
element_klass { FactoryBot.build(:element_klass) }
+
+ trait :with_ontology_properties_template do
+ properties_template do
+ {
+ 'pkg' => {
+ 'eln' => { 'version' => '1.9.0', 'base_revision' => 'a714b63f6', 'current_revision' => 0 },
+ 'name' => 'chem-generic-ui',
+ 'version' => '1.1.1',
+ 'labimotion' => '1.1.4',
+ },
+ 'uuid' => '1e3aa4fa-bbb1-468a-aab3-505c14bdca12',
+ 'klass' => 'SegmentKlass',
+ 'layers' => {
+ 'fields' => {
+ 'wf' => false,
+ 'key' => 'fields',
+ 'cols' => 2,
+ 'color' => 'info',
+ 'label' => 'Fields',
+ 'style' => 'panel_generic_heading',
+ 'fields' => [
+ {
+ 'type' => 'text',
+ 'field' => 'material',
+ 'label' => 'material',
+ 'default' => '',
+ 'ontology' => {
+ 'id' => 'obi:class:http://purl.obolibrary.org/obo/OBI_0000094',
+ 'iri' => 'http://purl.obolibrary.org/obo/OBI_0000094',
+ 'type' => 'class',
+ 'label' => 'material processing',
+ 'obo_id' => 'OBI:0000094',
+ 'short_form' => 'OBI_0000094',
+ 'description' => [
+ 'A planned process which results in physical changes in a specified input material',
+ ],
+ 'ontology_name' => 'obi',
+ 'ontology_prefix' => 'OBI',
+ },
+ 'position' => 1,
+ 'sub_fields' => [],
+ 'text_sub_fields' => [],
+ },
+ {
+ 'type' => 'text',
+ 'field' => 'weight',
+ 'label' => 'weight',
+ 'default' => '',
+ 'position' => 2,
+ 'sub_fields' => [],
+ 'text_sub_fields' => [],
+ },
+ ],
+ 'position' => 10,
+ 'timeRecord' => '',
+ 'wf_position' => 0,
+ },
+ },
+ 'version' => '2.0',
+ 'identifier' => '',
+ 'select_options' => {},
+ }
+ end
+ end
end
end
diff --git a/spec/factories/text_template.rb b/spec/factories/text_template.rb
index 649b89e149..1075174770 100644
--- a/spec/factories/text_template.rb
+++ b/spec/factories/text_template.rb
@@ -15,13 +15,14 @@
end
with_options(parent: :text_template) do
- factory :element_text_template, class: ElementTextTemplate
- factory :predefined_text_template, class: PredefinedTextTemplate
- factory :reaction_description_text_template, class: ReactionDescriptionTextTemplate
- factory :reaction_text_template, class: ReactionTextTemplate
- factory :research_plan_text_template, class: ResearchPlanTextTemplate
- factory :sample_text_template, class: SampleTextTemplate
- factory :screen_text_template, class: ScreenTextTemplate
- factory :wellplate_text_template, class: WellplateTextTemplate
+ factory :element_text_template, class: 'ElementTextTemplate'
+ factory :predefined_text_template, class: 'PredefinedTextTemplate'
+ factory :reaction_description_text_template, class: 'ReactionDescriptionTextTemplate'
+ factory :reaction_text_template, class: 'ReactionTextTemplate'
+ factory :research_plan_text_template, class: 'ResearchPlanTextTemplate'
+ factory :sample_text_template, class: 'SampleTextTemplate'
+ factory :screen_text_template, class: 'ScreenTextTemplate'
+ factory :wellplate_text_template, class: 'WellplateTextTemplate'
+ factory :device_description_text_template, class: 'DeviceDescriptionTextTemplate'
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 948e673667..4a01f53536 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -21,6 +21,7 @@
reactions: 0,
wellplates: 0,
celllines: 0,
+ device_descriptions: 0,
}
end
@@ -35,6 +36,7 @@
'screen' => 4,
'research_plan' => 5,
'cell_line' => -1000,
+ 'device_descriptions' => -1100,
}
profile.update_columns(data: data) # rubocop:disable Rails/SkipsModelValidations
end
diff --git a/spec/models/collections_device_description_spec.rb b/spec/models/collections_device_description_spec.rb
new file mode 100644
index 0000000000..65cdb68bc9
--- /dev/null
+++ b/spec/models/collections_device_description_spec.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+# require 'rails_helper'
+
+# RSpec.describe CollectionsDeviceDescription, type: :model do
+# end
diff --git a/spec/models/device_description_spec.rb b/spec/models/device_description_spec.rb
new file mode 100644
index 0000000000..dd900a30c7
--- /dev/null
+++ b/spec/models/device_description_spec.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+# RSpec.describe DeviceDescription, type: :model do
+# end
diff --git a/spec/services/element_detail_level_calculator_spec.rb b/spec/services/element_detail_level_calculator_spec.rb
index 516870f108..bc662ea14a 100644
--- a/spec/services/element_detail_level_calculator_spec.rb
+++ b/spec/services/element_detail_level_calculator_spec.rb
@@ -43,6 +43,7 @@
Well => 0,
Wellplate => 0,
CelllineSample => 0,
+ DeviceDescription => 0,
})
end