diff --git a/.dockerignore b/.dockerignore index 9eef03d767..d00dc62e82 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ backup uploads public +!/public/ontologies_default/ .gitignore node_modules tmp diff --git a/.eslintrc b/.eslintrc index 576e1e3874..ae66d4ec9a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -20,6 +20,13 @@ "no-relative-import-paths/no-relative-import-paths": [ "error", { "allowSameFolder": false, "rootDir": "app/packs" } + ], + "max-len": [ + "error", + { + "code": 120, + "ignoreComments": true + } ] }, "settings": { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 426801e88c..c2bad8a7ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: ruby/setup-ruby@v1 with: - ruby-version: '2.7.7' + ruby-version: '3.3.0' - name: install linting dependencies # IMPORTANT: install rubocop first for pinning to take effect (`gem` doesn't resolve dependencies but simply installs gems in order) @@ -49,7 +49,7 @@ jobs: run: shell: bash - container: complat/chemotion_eln_runner:v1.6.0-1 + container: complat/chemotion_eln_runner:main services: postgres: diff --git a/.gitignore b/.gitignore index 116a5efd61..748c3cc2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ /config/scifinder_n.yml /config/radar.yml +/doc/* + /node_modules /public/images/molecules/* @@ -93,6 +95,8 @@ !/public/ontologies/rxno.default.json !/public/ontologies/rxno.default.edited.json +/public/data_type.json + /uploads/* !/public/attachments/.keep @@ -145,6 +149,7 @@ aliasifyConfig.js /app/assets/javascripts/components/extra/* /app/packs/src/components/extra/* +/app/packs/klasses.json # Backups folder backup/deploy_backup @@ -189,3 +194,8 @@ public/sprite.png #svgeditor /public/svgedit + +!spec/fixtures/import/sample_import_template.xlsx + +# generic elements +data/klasses.json diff --git a/.nvmrc b/.nvmrc index 4a9c19cb52..bb52a169c1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v14.21.3 +v18.18.2 diff --git a/.service-dependencies b/.service-dependencies index ae41e87279..2b46202f24 100644 --- a/.service-dependencies +++ b/.service-dependencies @@ -1,4 +1,5 @@ #syntax=v1 -CONVERTER=ComPlat/chemotion-converter-app@v0.9.0 +CONVERTER=ComPlat/chemotion-converter-app@v1.2.0 KETCHER=ptrxyz/chemotion-ketchersvc@main -SPECTRA=ComPlat/chem-spectra-app@0.12.0 +SPECTRA=ComPlat/chem-spectra-app@1.1.0 +NMRIUMWRAPPER=NFDI4Chem/nmrium-react-wrapper@v0.4.0 diff --git a/.tool-versions b/.tool-versions index c320be7867..46b0dfb13c 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -nodejs 14.21.3 +nodejs 18.18.2 ruby 2.7.7 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..4fb564aed6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "sqltools.connections": [ + { + "previewLimit": 50, + "server": "postgres", + "port": 5432, + "driver": "PostgreSQL", + "name": "development", + "database": "chemotion_dev", + "username": "postgres", + "password": "" + }, + { + "previewLimit": 50, + "server": "postgres", + "port": 5432, + "driver": "PostgreSQL", + "name": "test", + "database": "chemotion_test", + "username": "postgres", + "password": "" + } + ], +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0000872bf4..b68ae9c1b8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -14,6 +14,11 @@ "type": "shell", "command": "RAILS_ENV=test $(which bundle) exec rails server -b 0.0.0.0 -p 3000" }, + { + "label": "Rails - delayed job worker", + "type": "shell", + "command": "RAILS_ENV=development $(which bundle) exec rake jobs:work" + }, { "label": "RSpec - all", "type": "shell", @@ -40,4 +45,4 @@ "command": "$(which bundle) exec rubocop -c .rubocop.yml -a ${file}", }, ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 32248abc64..24e5873ffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,244 @@ - # Chemotion_ELN Changelog +## Latest +* Bug fixes + + +## [v1.8.2] +> (2024-01-18) + +* Features and enhancements + * feat: converter metadata added to dataset download ([#1688](https://github.com/ComPlat/chemotion_ELN/pull/1688)) + + +## [v1.8.1] +> (2023-12-21) + +* Features and enhancements + * converter trigger on inbox items ([#1583](https://github.com/ComPlat/chemotion_ELN/pull/1583)) + * add the option to sort reaction list by updated time ([#1461](https://github.com/ComPlat/chemotion_ELN/pull/1461)) + * sample list for decoupled ([#1612](https://github.com/ComPlat/chemotion_ELN/pull/1612)) + * report peaks from XRD ([#1614](https://github.com/ComPlat/chemotion_ELN/pull/1614)) + * display mail collector address ([#1529](https://github.com/ComPlat/chemotion_ELN/pull/1529)) + * drag samples and elements to segment ([#1623](https://github.com/ComPlat/chemotion_ELN/pull/1623)) + * export/import collection with chemicals ([#1604](https://github.com/ComPlat/chemotion_ELN/pull/1604)) + * relax Mail collector rules ([#1566](https://github.com/ComPlat/chemotion_ELN/pull/1566)) + * add volume field in inventory tab ([#1613](https://github.com/ComPlat/chemotion_ELN/pull/1613)) + * show research plan links in reaction ([#1575](https://github.com/ComPlat/chemotion_ELN/pull/1575)) + * sorting option for datasets and attachments in the inbox by creation-time or name ([#1446](https://github.com/ComPlat/chemotion_ELN/pull/1446)) + * add the option to change the inbox sizing ([#1645](https://github.com/ComPlat/chemotion_ELN/pull/1645)) + * add chemspectra with ref peaks ([#1596](https://github.com/ComPlat/chemotion_ELN/pull/1596)) + + UX/UI + * remove the inbox section from the side panel ([#1593](https://github.com/ComPlat/chemotion_ELN/pull/1593)) + * file size is listed in the analyses tab ([#1601](https://github.com/ComPlat/chemotion_ELN/pull/1601)) + +* Bug fixes + * bead not visible in preview and reaction details ([#1607](https://github.com/ComPlat/chemotion_ELN/pull/1607)) + * the attachment does not get deleted from the inbox when it is assigned to sample ([#1631](https://github.com/ComPlat/chemotion_ELN/pull/1631)) + * remove blank line when saving peak ([#1629](https://github.com/ComPlat/chemotion_ELN/pull/1629)) + * allow import of molecule_name on sample import for xslx format ([#1598](https://github.com/ComPlat/chemotion_ELN/pull/1598)) + * collection management right click on the add button to not drag things around ([#1639](https://github.com/ComPlat/chemotion_ELN/pull/1639)) + * reaction sort column value not being persistent for updated_at column ([#1643](https://github.com/ComPlat/chemotion_ELN/pull/1643)) + * si-spectra report generation to work even without preview ([#1654](https://github.com/ComPlat/chemotion_ELN/pull/1654)) + * camelcasing attributes for proper display of SVGs ([#1670](https://github.com/ComPlat/chemotion_ELN/pull/1670)) + * attached research_plans in screens not being imported from collection ([#1671](https://github.com/ComPlat/chemotion_ELN/pull/1671)) + + ChemSpectra and NMRIUM + * correctly trigger action spinner when saving peaks to avoid race condition ([#1651](https://github.com/ComPlat/chemotion_ELN/pull/1651)) + * order of J value ([#1649](https://github.com/ComPlat/chemotion_ELN/pull/1649)) + * react-spectra-editor upd to correct molecule display with svg zoom pan ([#1656](https://github.com/ComPlat/chemotion_ELN/pull/1656)) + * prevent crash on CV layout ([#1637](https://github.com/ComPlat/chemotion_ELN/pull/1637)) + * update nmrglue in spectra-app to read some bruker file issue ([#1603](https://github.com/ComPlat/chemotion_ELN/pull/1603)) + * update react-spectra-editor version to fix `Add/remove multiplicity peak` buttons ([#1630](https://github.com/ComPlat/chemotion_ELN/pull/1630)) + * remove original data in nmrium file ([#1661](https://github.com/ComPlat/chemotion_ELN/pull/1661)) + + UX/UI + * molecule title layout and element table header responsiveness ([#1650](https://github.com/ComPlat/chemotion_ELN/pull/1650)) + + +* Chores + * upgrade-converter-to-v1.1.1 ([#1634](https://github.com/ComPlat/chemotion_ELN/pull/1634)) + * Bump rmagick from 5.0.0 to 5.3.0 ([#1609](https://github.com/ComPlat/chemotion_ELN/pull/1609)) + * upd node engine for dev container ([#1635](https://github.com/ComPlat/chemotion_ELN/pull/1635)) + +* CI + * improve Dev Setup by autorecognizing the installed tool versions ([#1665](https://github.com/ComPlat/chemotion_ELN/pull/1665)) + + +## [v1.8.0] +> (2023-10-24) + +* Features and enhancements + * Reaction Variations ([#1409](https://github.com/ComPlat/chemotion_ELN/pull/1409), [#1561](https://github.com/ComPlat/chemotion_ELN/pull/1561), [#1567](https://github.com/ComPlat/chemotion_ELN/pull/1567)) [Docs](https://chemotion.net/docs/eln/ui/details_modal?_highlight=variation#variations-tab) + * LabiIMotion Integration ([#1504](https://github.com/ComPlat/chemotion_ELN/pull/1504)) [Docs](https://chemotion.net/docs/eln/admin/generic_config) + * Enhance import samples for sdf ([#1364](https://github.com/ComPlat/chemotion_ELN/pull/1364)) + * Import export sample as chemical ([#1524](https://github.com/ComPlat/chemotion_ELN/pull/1524)) + * Dry-solvent properties in the solvents section in the reactions table ([#1432](https://github.com/ComPlat/chemotion_ELN/pull/1432)) + * Expand calendar function to generic element ([#1585](https://github.com/ComPlat/chemotion_ELN/pull/1585)) + + UX/UI + * Move sample task inbox to header bar ([#1517](https://github.com/ComPlat/chemotion_ELN/pull/1517)) + * Admin: Filter options for user list management ([#1510](https://github.com/ComPlat/chemotion_ELN/pull/1510)) + + ChemSpectra and NMRIUM + * Display label in CV layout ([#1546](https://github.com/ComPlat/chemotion_ELN/pull/1546)) + * Nmrium button for reaction and researchplan ([#1471](https://github.com/ComPlat/chemotion_ELN/pull/1471)) + +* Bug fixes + * assets precompilation css issue ([#1538](https://github.com/ComPlat/chemotion_ELN/pull/1538)) + * comment fetch issue on new entities with code refactoring ([#1547](https://github.com/ComPlat/chemotion_ELN/pull/1547)) + * show example label for reaction ([#1556](https://github.com/ComPlat/chemotion_ELN/pull/1556)) + * current_user.matrix getting null value ([#1554](https://github.com/ComPlat/chemotion_ELN/pull/1554)) + * load cas for molecules ([#1555](https://github.com/ComPlat/chemotion_ELN/pull/1555)) + * no attachments after research plan save ([#1564](https://github.com/ComPlat/chemotion_ELN/pull/1564)) + * sample properties tab ([#1503](https://github.com/ComPlat/chemotion_ELN/pull/1503)) + * Admin seed: ensure exisiting Admins have a profile ([#1572](https://github.com/ComPlat/chemotion_ELN/pull/1572)) + * assign only boolean values for decoupled column in import samples ([#1571](https://github.com/ComPlat/chemotion_ELN/pull/1571)) + * disable spectra button when just uploading an image ([#1568](https://github.com/ComPlat/chemotion_ELN/pull/1568)) + * atttachment converter trigger ([#1578](https://github.com/ComPlat/chemotion_ELN/pull/1578)) + * reaction calculation when no reference material present ([#1589](https://github.com/ComPlat/chemotion_ELN/pull/1589)) + * reaction list display break when reaction status not standard ([#1592](https://github.com/ComPlat/chemotion_ELN/pull/1592)) + + ChemSpectra and NMRIUM + * UI with cv layout ([#1526](https://github.com/ComPlat/chemotion_ELN/pull/1526)) + * nmrium button does not display when selecting some chemical ontology ([#1563](https://github.com/ComPlat/chemotion_ELN/pull/1563)) + * change value of reference solvent for NMR layout ([#1557](https://github.com/ComPlat/chemotion_ELN/pull/1557)) + +* Code refactoring - Test - CI - Chores + * "yarn test" errors & warnings ([#1523](https://github.com/ComPlat/chemotion_ELN/pull/1523)) + * update runner image ([#1576](https://github.com/ComPlat/chemotion_ELN/pull/1576)) + * minor dep updates ([#1569](https://github.com/ComPlat/chemotion_ELN/pull/1569)) + * Bump @adobe/css-tools from 4.2.0 to 4.3.1 ([#1511](https://github.com/ComPlat/chemotion_ELN/pull/1511)) + * Bump @babel/traverse from 7.16.10 to 7.23.2 ([#1580](https://github.com/ComPlat/chemotion_ELN/pull/1580)) + + +## [v1.8.0-rc4] +> (2023-10-16) + + +## [v1.8.0-rc3] +> (2023-10-09) + + +## [v1.8.0-rc2] +> (2023-10-09) + + +## [v1.8.0-rc1] +> (2023-09-20) + +* Features and Improvements: + * Enhance import samples for sdf [#1364](https://github.com/ComPlat/chemotion_ELN/pull/1364) + * Move sample task inbox to header bar [#1517](https://github.com/ComPlat/chemotion_ELN/pull/1517) + * filter options for admin user management [#1510](https://github.com/ComPlat/chemotion_ELN/pull/1510) + * Reaction Variations [#1409](https://github.com/ComPlat/chemotion_ELN/pull/1409) + * LabiIMotion Integration [#1504](https://github.com/ComPlat/chemotion_ELN/pull/1504) + +## [v1.7.3] +> (2023-09-20) + +* Features and Improvements: + * Always sort new sample tasks on top of list [#1456](https://github.com/ComPlat/chemotion_ELN/pull/1456) + * add fluorescence (emission), DLS ACF, DLS Intensity layouts [#1374](https://github.com/ComPlat/chemotion_ELN/pull/1374) + * Update CDCl3 solvent value on chemspectra and fix typo [#1480](https://github.com/ComPlat/chemotion_ELN/pull/1480) + * Reaction table dropdown value updates [#1433](https://github.com/ComPlat/chemotion_ELN/pull/1433) + * select all option for device inbox folder by [#1437](https://github.com/ComPlat/chemotion_ELN/pull/1437) + * Show sample name in SampleTask Api [#1518](https://github.com/ComPlat/chemotion_ELN/pull/1518) + * update ext links in the Navbar menu dropdown [#1534](https://github.com/ComPlat/chemotion_ELN/pull/1534) + * Allow deletion of SampleTasks and fix SampleTask Inbox scroll issues [#1444](https://github.com/ComPlat/chemotion_ELN/pull/1444) + * add-analysis button always visible [#1465](https://github.com/ComPlat/chemotion_ELN/pull/1465) + +* chore: + * upgrade converter 1.0.0 [#1450](https://github.com/ComPlat/chemotion_ELN/pull/1450) + * update information of chem-spectra-app [#1484](https://github.com/ComPlat/chemotion_ELN/pull/1484) + * Add Cypress dependencies to Dockerfiles [#1491](https://github.com/ComPlat/chemotion_ELN/pull/1491) + * upg nodejs LTS to 18 [#1489](https://github.com/ComPlat/chemotion_ELN/pull/1489) + * puma from 5.6.5 to 5.6.7 [#1488](https://github.com/ComPlat/chemotion_ELN/pull/1488) + * update README - acknowledge NFDI [#1472](https://github.com/ComPlat/chemotion_ELN/pull/1472) + +* Fixes: + * disable_chemrepoidjob [#1451](https://github.com/ComPlat/chemotion_ELN/pull/1451) + * quill_to_html when type HashWithIndifferentAccess [#1458](https://github.com/ComPlat/chemotion_ELN/pull/1458) + * display the not-accessible panel for 401 status on sample fetched by id [#1469](https://github.com/ComPlat/chemotion_ELN/pull/1469) + * image annotation tool image preview does not work as expected [#1467](https://github.com/ComPlat/chemotion_ELN/pull/1467) + * White screen research plan [#1452](https://github.com/ComPlat/chemotion_ELN/pull/1452) + * wellplates multiple readouts design tab [#1474](https://github.com/ComPlat/chemotion_ELN/pull/1474) + * Cypress Tests [#1481](https://github.com/ComPlat/chemotion_ELN/pull/1481) + * the issue with NMRium wrapper version 0.4.0 [#1436](https://github.com/ComPlat/chemotion_ELN/pull/1436) + * nmrium button [#1460](https://github.com/ComPlat/chemotion_ELN/pull/1460) + * replace toSorted with manual sorting in SampleTaskInbox [#1485](https://github.com/ComPlat/chemotion_ELN/pull/1485) + * duplicate jdx files by [#1479](https://github.com/ComPlat/chemotion_ELN/pull/1479) + * sorting multiplicity values [#1478](https://github.com/ComPlat/chemotion_ELN/pull/1478) + * inbox UnsortedBox issues [#1447](https://github.com/ComPlat/chemotion_ELN/pull/1447) + * deletion of literature [#1502](https://github.com/ComPlat/chemotion_ELN/pull/1502) + * doi not accepted [#1486](https://github.com/ComPlat/chemotion_ELN/pull/1486) + * fixed wrong literatures mapping [#1506](https://github.com/ComPlat/chemotion_ELN/pull/1506) + * ignore predictions when it is null [#1507](https://github.com/ComPlat/chemotion_ELN/pull/1507) + * crash when selecting multiplicity checkbox on chemspectra [#1509](https://github.com/ComPlat/chemotion_ELN/pull/1509) + * sync chemspectra nmrium eln v173 [#1513](https://github.com/ComPlat/chemotion_ELN/pull/1513) + * Update Chemspectra to handle 'FL Spectrum' datatype and fix cannot read processed Bruker data NMR [#1528](https://github.com/ComPlat/chemotion_ELN/pull/1528) + * yield percentage error for reactions with decoupled products and … [#1531](https://github.com/ComPlat/chemotion_ELN/pull/1531) + * reaction sort column default to created_at [#1533](https://github.com/ComPlat/chemotion_ELN/pull/1533) + + +## [v1.7.2] +> (2023-08-01) + +* Fixes: + * Comment functionality, closes #1435 + * Sort reactions by creation time 4922fc3, closes #1439 + * display wrong shifted peaks after zoom, closes #1443 + +## [v1.7.1] +> 2023-07-27 + +* Fixes: + * Report creation for shared reaction [#1412](https://github.com/ComPlat/chemotion_ELN/pull/1412) + * Collection tab profile [#1411](https://github.com/ComPlat/chemotion_ELN/pull/1411) [1427](https://github.com/ComPlat/chemotion_ELN/pull/1427) + * opening a dataset without making changes [#1410](https://github.com/ComPlat/chemotion_ELN/pull/1410) + * inbox (de)select boxes [#1416](https://github.com/ComPlat/chemotion_ELN/pull/1416) + * sort reaction list by creation date [#1429](https://github.com/ComPlat/chemotion_ELN/pull/1429) + * change ref area and display shift ref [#1431]( https://github.com/ComPlat/chemotion_ELN/pull/1431) + * total element count in list tabs [#1426]( https://github.com/ComPlat/chemotion_ELN/pull/1426) + + +## [v1.7.0] +> 2023-07-11 + +* Features and Improvements: + * Inventory Feature [#1262](https://github.com/ComPlat/chemotion_ELN/pull/1262) - [see docs](https://chemotion.net/docs/eln/ui/inventory#creating-chemical-entry) + * Comment functionality on shared and synchronized collections [#1237](https://github.com/ComPlat/chemotion_ELN/pull/1237) - [see docs](https://chemotion.net/docs/eln/ui/comments?_highlight=comment) + * calendar [#1189](https://github.com/ComPlat/chemotion_ELN/pull/1189) + * collection profile for element tab layout [#681](https://github.com/ComPlat/chemotion_ELN/pull/681) - [see docs]() + * cas as option in import samples to collection function [#1306](https://github.com/ComPlat/chemotion_ELN/pull/1306) + * chemspectra with aif layout [#1335](https://github.com/ComPlat/chemotion_ELN/pull/1335) + * Feature/elements grouping [#1188](https://github.com/ComPlat/chemotion_ELN/pull/1188) + * enhance import sample feature [#1347](https://github.com/ComPlat/chemotion_ELN/pull/1341) + * Groups ui revamp and making group admins set/unset admins [#1396](https://github.com/ComPlat/chemotion_ELN/pull/1396) + * Add inbox pagination [#1108](https://github.com/ComPlat/chemotion_ELN/pull/1108) + * login-and-signup-configurable [#1377](https://github.com/ComPlat/chemotion_ELN/pull/1377) + +* Fixes + * port fixes from v1.6.1 v1.6.2 + * Datacollector api fx [#1344](https://github.com/ComPlat/chemotion_ELN/pull/1344) + * notification timestamps and formatting notification button [#1362](https://github.com/ComPlat/chemotion_ELN/pull/1362) + * saving data from NMRium [#1348](https://github.com/ComPlat/chemotion_ELN/pull/1348) + * Structure editor with decoupled sample [#1393](https://github.com/ComPlat/chemotion_ELN/pull/1393) + * User select in UI feature [#1385](https://github.com/ComPlat/chemotion_ELN/pull/1385) + * Unsaved sample changes retained when reselected from list [#1397](https://github.com/ComPlat/chemotion_ELN/pull/1397) + + + +## [v1.6.2] +> 2023-07-10 +* Fixes + * Expose target amount in sample task api (#1373) + * User select in UI feature (#1385) + * refactoring transfer (#1320) + * login-and-signup-configurable (#1377) + * structure editor with decoupled sample (#1393) + * Text editor in researchPlan is now getting removed properly (#1363) ## [v1.6.1] > 2023-06-19 @@ -13,7 +251,7 @@ * nmrium: display preview image after saving [#1356](https://github.com/ComPlat/chemotion_ELN/pull/1356) -## [v1.6.0] +## [v1.6.0] > 2023-05-09 * Features and Improvements: @@ -22,7 +260,7 @@ * Fixes * port fixes from v1.5.3 v1.5.4 - + ## [v1.5.4] > 2023-05-09 * Admin: it is possible to reprocess SVGs images of molecules and samples ([see docs](https://chemotion.net/docs/eln/troubleshooting#fixing-sample-or-molecule-svg-images)) @@ -35,7 +273,7 @@ * Migration fixes [#1307](https://github.com/ComPlat/chemotion_ELN/pull/1307) * reactants from reagent list do not appear above reaction arrow [#1308](https://github.com/ComPlat/chemotion_ELN/pull/1308) * Search api: fix ActiveRecord::UnknownAttributeReference on sum-formulae [#1310](https://github.com/ComPlat/chemotion_ELN/pull/1310) - + ## [v1.5.3] > 2023-04-21 @@ -53,12 +291,12 @@ * show results from calculations [#1291](https://github.com/ComPlat/chemotion_ELN/pull/1291) * report issue fix [#1296](https://github.com/ComPlat/chemotion_ELN/pull/1296) -## [v1.6.0-rc1] +## [v1.6.0-rc1] > 2023-04-11 * Fixes - * port fixes from v1.5.2 - + * port fixes from v1.5.2 + ## [v1.5.2] > 2023-04-11 @@ -115,9 +353,9 @@ * NMRium: NMR data can be processed in [NMRium](https://www.nmrium.org/) ([docs](https://chemotion.net/docs/chemspectra/nmr?_highlight=nmrium#analysis-using-nmrium)) * Integration of Chem-converter v0.9.0 ([docs]( https://chemotion.net/docs/chemconverter/) * Wellplate/Screen/ResearchPlan Workflow - * Chemspectra: better UI for Cyclic Volt. - * add cas to sample export - * + * Chemspectra: better UI for Cyclic Volt. + * add cas to sample export + * * Fixes * Chemspectra: The issue of multiplicities on chemspectra frontend are not removed when changing between layouts with the old JCAMP design is fixed @@ -125,7 +363,7 @@ * Affiliation autocomplete (sign up page) * CAS not searchable (index search might need to be rebuilt) * molecule image cropped in chemspectra - * [others](https://github.com/ComPlat/chemotion_ELN/issues?q=is%3Aissue+is%3Aclosed+closed%3A2022-11-10..2023-02-28+label%3Abug+) + * [others](https://github.com/ComPlat/chemotion_ELN/issues?q=is%3Aissue+is%3Aclosed+closed%3A2022-11-10..2023-02-28+label%3Abug+) diff --git a/Dockerfile.chemotion-dev b/Dockerfile.chemotion-dev index bfda962404..9b7b3108ce 100644 --- a/Dockerfile.chemotion-dev +++ b/Dockerfile.chemotion-dev @@ -6,15 +6,11 @@ FROM --platform=linux/amd64 ubuntu:jammy ARG DEBIAN_FRONTEND=noninteractive -ARG VRUBY=2.7.7 -ARG VNODE=14.21.3 -ARG VNODENEXT=16.16.0 -ARG ASDF_BRANCH=v0.11.3 RUN set -xe && apt-get update -yqqq --fix-missing && apt-get upgrade -y RUN apt update && apt-get install -yqq --fix-missing bash ca-certificates wget apt-transport-https git gpg\ imagemagick libmagic-dev libmagickcore-dev libmagickwand-dev curl gnupg2 \ - build-essential nodejs sudo postgresql-client libappindicator1 swig \ + build-essential sudo postgresql-client libappindicator1 swig \ gconf-service libasound2 libgconf-2-4 cmake \ libnspr4 libnss3 libpango1.0-0 libxss1 xdg-utils tzdata libpq-dev \ gtk2-engines-pixbuf \ @@ -25,7 +21,8 @@ RUN apt update && apt-get install -yqq --fix-missing bash ca-certificates wget a fonts-crosextra-caladea fonts-crosextra-carlito \ fonts-dejavu fonts-dejavu-core fonts-dejavu-extra fonts-liberation2 fonts-liberation \ fonts-linuxlibertine fonts-noto-core fonts-noto-extra fonts-noto-ui-core \ - fonts-opensymbol fonts-sil-gentium fonts-sil-gentium-basic inkscape + fonts-opensymbol fonts-sil-gentium fonts-sil-gentium-basic inkscape \ + libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libxtst6 xauth xvfb RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list \ && apt-get update -yqqq && apt-get -y install google-chrome-stable \ @@ -49,20 +46,10 @@ RUN mkdir /home/chemotion-dev/node_modules SHELL ["/bin/bash", "-c"] -RUN git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch $ASDF_BRANCH - +# Even if asdf and the related tools are only installed by running run-ruby-dev.sh, we set the PATH variables here, so when we enter the container via docker exec, we have the path set correctly ENV ASDF_DIR=/home/chemotion-dev/.asdf ENV PATH=/home/chemotion-dev/.asdf/shims:/home/chemotion-dev/.asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -RUN asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git -RUN asdf install nodejs $VNODE -RUN asdf install nodejs $VNODENEXT -RUN asdf global nodejs $VNODE - -RUN asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git -RUN asdf install ruby $VRUBY -RUN asdf global ruby $VRUBY - RUN echo 'network-timeout 600000' > /home/chemotion-dev/.yarnrc # use node modules from outside the application directory RUN echo '--modules-folder /home/chemotion-dev/node_modules/' >> /home/chemotion-dev/.yarnrc diff --git a/Dockerfile.github-ci b/Dockerfile.github-ci index 599a7f844d..614e7edaf1 100644 --- a/Dockerfile.github-ci +++ b/Dockerfile.github-ci @@ -1,8 +1,9 @@ -FROM ptrxyz/chemotion:eln-1.5.0 +FROM ptrxyz/internal:eln-1.8.0-rc4 ARG BRANCH=main RUN apt-get install -y --no-install-recommends --autoremove --fix-missing \ build-essential\ - openssh-server + openssh-server\ + libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb WORKDIR "/chemotion/app" diff --git a/Gemfile b/Gemfile index 33872413df..1c2f598e2b 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ gem 'dotenv-rails', require: 'dotenv/rails-now' gem 'ed25519' +gem 'faker', require: false gem 'faraday' gem 'faraday-follow_redirects' gem 'faraday-multipart' @@ -60,7 +61,10 @@ gem 'kaminari' gem 'kaminari-grape' gem 'ketcherails', git: 'https://github.com/complat/ketcher-rails.git', branch: 'upgrade-to-rails-6' +gem 'labimotion', '1.1.3' + gem 'mimemagic', '0.3.10' +gem 'mime-types' # locked to enforce latest version of net-scp. without lock net-ssh would be updated first which locks # out newer net-scp versions @@ -104,6 +108,8 @@ gem 'sassc-rails' gem 'scenic' gem 'schmooze' gem 'semacode', git: 'https://github.com/toretore/semacode.git', branch: 'master' # required for Barby but not listed... + +gem 'sentry-delayed_job' gem 'sentry-rails' gem 'sentry-ruby' gem 'shrine', '~> 3.0' @@ -186,12 +192,13 @@ group :test do gem 'database_cleaner-active_record' gem 'factory_bot_rails' - gem 'faker' gem 'launchy' gem 'rspec-repeat' + gem 'shoulda-matchers' + gem 'simplecov', require: false gem 'simplecov-lcov', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d8f2e63b9e..8e43b27038 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -117,68 +117,68 @@ GEM specs: aasm (5.4.0) concurrent-ruby (~> 1.0) - actioncable (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + actioncable (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailbox (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) mail (>= 2.7.1) - actionmailer (6.1.7.3) - actionpack (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailer (6.1.7.6) + actionpack (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activesupport (= 6.1.7.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.3) - actionview (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionpack (6.1.7.6) + actionview (= 6.1.7.6) + activesupport (= 6.1.7.6) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.3) - actionpack (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actiontext (6.1.7.6) + actionpack (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) nokogiri (>= 1.8.5) - actionview (6.1.7.3) - activesupport (= 6.1.7.3) + actionview (6.1.7.6) + activesupport (= 6.1.7.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_model_serializers (0.9.8) + active_model_serializers (0.9.9) activemodel (>= 3.2) concurrent-ruby (~> 1.0) - activejob (6.1.7.3) - activesupport (= 6.1.7.3) + activejob (6.1.7.6) + activesupport (= 6.1.7.6) globalid (>= 0.3.6) activejob-status (0.2.2) activejob (>= 4.2) activesupport (>= 4.2) - activemodel (6.1.7.3) - activesupport (= 6.1.7.3) - activerecord (6.1.7.3) - activemodel (= 6.1.7.3) - activesupport (= 6.1.7.3) + activemodel (6.1.7.6) + activesupport (= 6.1.7.6) + activerecord (6.1.7.6) + activemodel (= 6.1.7.6) + activesupport (= 6.1.7.6) activerecord-nulldb-adapter (0.9.0) activerecord (>= 5.2.0, < 7.1) - activestorage (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activesupport (= 6.1.7.3) + activestorage (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activesupport (= 6.1.7.6) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.3) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -197,7 +197,7 @@ GEM api-pagination (5.0.0) ast (2.4.2) attr_required (1.0.1) - autoprefixer-rails (10.4.7.0) + autoprefixer-rails (10.4.16.0) execjs (~> 2) awesome_print (1.9.2) backport (1.2.0) @@ -205,6 +205,7 @@ GEM open4 (~> 1.3.0) thor (>= 0.15.4, < 2) barby (0.6.8) + base64 (0.2.0) bcrypt (3.1.18) bcrypt_pbkdf (1.1.0) benchmark (0.2.0) @@ -295,7 +296,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.3) + date (3.3.4) debase (0.2.4.1) debase-ruby_core_source (>= 0.10.2) debase-ruby_core_source (0.10.17) @@ -304,7 +305,7 @@ GEM fugit (>= 1.5) delayed_job (4.1.11) activesupport (>= 3.0, < 8.0) - delayed_job_active_record (4.1.7) + delayed_job_active_record (4.1.8) activerecord (>= 3.0, < 8.0) delayed_job (>= 3.0, < 5) devise (4.8.1) @@ -321,29 +322,26 @@ GEM railties (>= 3.2) down (5.3.1) addressable (~> 2.8) - dry-container (0.11.0) - concurrent-ruby (~> 1.0) - dry-core (0.9.1) + dry-core (1.0.0) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) - dry-inflector (0.3.0) - dry-logic (1.3.0) + dry-inflector (1.0.0) + dry-logic (1.5.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.9, >= 0.9) + dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-types (1.6.1) + dry-types (1.7.1) concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.9, >= 0.9) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.3, >= 1.3) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) zeitwerk (~> 2.6) e2mmap (0.1.0) ed25519 (1.3.0) erubi (1.12.0) et-orbi (1.2.7) tzinfo - execjs (2.8.1) + execjs (2.9.1) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) @@ -351,16 +349,17 @@ GEM railties (>= 5.0.0) faker (2.23.0) i18n (>= 1.8.11, < 2) - faraday (2.6.0) + faraday (2.8.1) + base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (3.0.1) + faraday-net_http (3.0.2) fast_stack (0.2.0) - ffi (1.15.5) + ffi (1.16.3) flamegraph (0.9.5) font-awesome-rails (4.7.0.8) railties (>= 3.2, < 8.0) @@ -370,10 +369,10 @@ GEM fx (0.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - globalid (1.1.0) - activesupport (>= 5.0) - grape (1.6.2) - activesupport + globalid (1.2.1) + activesupport (>= 6.1) + grape (1.8.0) + activesupport (>= 5) builder dry-types (>= 1.1) mustermann-grape (~> 1.0.0) @@ -396,7 +395,7 @@ GEM grape-swagger-rails (0.3.1) railties (>= 3.2.12) graphql (2.0.15) - haml (6.0.7) + haml (6.2.3) temple (>= 0.8.2) thor tilt @@ -414,7 +413,7 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) icalendar (2.8.0) ice_cube (~> 0.16) @@ -423,7 +422,7 @@ GEM mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) jaro_winkler (1.5.4) - jquery-rails (4.5.0) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -456,15 +455,17 @@ GEM rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) + labimotion (1.1.3) + rails (~> 6.1.7) latex-decode (0.4.0) launchy (2.5.0) addressable (~> 2.7) listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.19.1) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -478,16 +479,15 @@ GEM rack-contrib (>= 1.1, < 3) railties (>= 3.0.0, < 7) method_source (1.0.0) - mime-types (3.4.1) + mime-types (3.5.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.1205) mimemagic (0.3.10) nokogiri (~> 1) rake mini_magick (4.11.0) - mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.18.0) + mini_mime (1.1.5) + minitest (5.20.0) msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) @@ -496,23 +496,22 @@ GEM ruby2_keywords (~> 0.0.1) mustermann-grape (1.0.2) mustermann (>= 1.0.0) - net-imap (0.3.4) + net-imap (0.4.7) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-sftp (3.0.0) net-ssh (>= 5.0.0, < 7.0.0) - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol net-ssh (6.1.0) - nio4r (2.5.8) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + nio4r (2.7.0) + nokogiri (1.15.5-x86_64-linux) racc (~> 1.4) numerizer (0.1.1) oauth2 (2.0.9) @@ -567,6 +566,7 @@ GEM pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) + pkg-config (1.5.5) prawn (2.4.0) pdf-core (~> 0.9.0) ttfunk (~> 1.7) @@ -583,13 +583,13 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (5.0.0) - puma (5.6.5) + puma (5.6.8) nio4r (~> 2.0) pundit (2.2.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.2) - rack (2.2.6.4) + racc (1.7.3) + rack (2.2.8) rack-accept (0.4.5) rack (>= 0.4) rack-contrib (2.3.0) @@ -606,34 +606,36 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.3) - actioncable (= 6.1.7.3) - actionmailbox (= 6.1.7.3) - actionmailer (= 6.1.7.3) - actionpack (= 6.1.7.3) - actiontext (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activemodel (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + rails (6.1.7.6) + actioncable (= 6.1.7.6) + actionmailbox (= 6.1.7.6) + actionmailer (= 6.1.7.6) + actionpack (= 6.1.7.6) + actiontext (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activemodel (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) bundler (>= 1.15.0) - railties (= 6.1.7.3) + railties (= 6.1.7.6) sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) - railties (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) method_source rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -645,7 +647,8 @@ GEM reverse_markdown (2.1.1) nokogiri rexml (3.2.5) - rmagick (5.0.0) + rmagick (5.3.0) + pkg-config (~> 1.4) roo (2.9.0) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) @@ -733,11 +736,16 @@ GEM rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.0.0) - sentry-rails (5.5.0) + sentry-delayed_job (5.11.0) + delayed_job (>= 4.0) + sentry-ruby (~> 5.11.0) + sentry-rails (5.11.0) railties (>= 5.0) - sentry-ruby (~> 5.5.0) - sentry-ruby (5.5.0) + sentry-ruby (~> 5.11.0) + sentry-ruby (5.11.0) concurrent-ruby (~> 1.0, >= 1.0.2) + shoulda-matchers (5.3.0) + activesupport (>= 5.2.0) shrine (3.4.0) content_disposition (~> 1.0) down (~> 5.1) @@ -786,10 +794,10 @@ GEM httpclient (>= 2.4) sys-filesystem (1.4.3) ffi (~> 1.1) - temple (0.9.1) - thor (1.2.1) - tilt (2.0.11) - timeout (0.3.2) + temple (0.10.3) + thor (1.3.0) + tilt (2.3.0) + timeout (0.4.1) ttfunk (1.7.0) tty-screen (0.8.1) turbo-sprockets-rails4 (1.2.5) @@ -829,7 +837,7 @@ GEM hashdiff (>= 0.4.0, < 2.0.0) webrick (1.7.0) websocket (1.2.9) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) whenever (1.0.0) @@ -843,10 +851,10 @@ GEM rake (>= 0.8.7) yard (0.9.28) webrick (~> 1.7.0) - zeitwerk (2.6.7) + zeitwerk (2.6.12) PLATFORMS - ruby + x86_64-linux DEPENDENCIES aasm @@ -917,10 +925,12 @@ DEPENDENCIES kaminari kaminari-grape ketcherails! + labimotion (= 1.1.3) launchy listen memory_profiler meta_request + mime-types mimemagic (= 0.3.10) net-scp (= 3.0.0) net-sftp @@ -969,8 +979,10 @@ DEPENDENCIES scenic schmooze semacode! + sentry-delayed_job sentry-rails sentry-ruby + shoulda-matchers shrine (~> 3.0) simplecov simplecov-lcov @@ -990,4 +1002,4 @@ DEPENDENCIES yaml_db BUNDLED WITH - 2.1.4 + 2.4.19 diff --git a/README-DEV.md b/README-DEV.md new file mode 100644 index 0000000000..2e79f6fd29 --- /dev/null +++ b/README-DEV.md @@ -0,0 +1,22 @@ +## Prepare installation + +**in config/ create** +- datacollectors.yml +- database.yml +- storage.yml + +**write temporary in run-ruby-dev.sh** +- rake db:create +- rake db:schema:load + +## Installation + +docker-compose -f docker-compose.dev.yml up + +OR + +docker-compose -f docker-compose.dev.yml up postgres app webpacker + +## Working inside app container + +docker exec -it chemotion_eln-app-1 /bin/bash diff --git a/README.md b/README.md index 769d33676d..1aa1126d08 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,21 @@ An **Electronic Lab Notebook** for chemists! --- -## Sponsors +## Acknowledgments This project has been funded by the **[DFG]**. [![DFG Logo]][DFG] + +Funded by the [Deutsche Forschungsgemeinschaft (DFG, German Research Foundation)](https://www.dfg.de/) under the [National Research Data Infrastructure – NFDI4Chem](https://nfdi4chem.de/) – Projektnummer **441958208** since 2020. + + --- ## License -**Copyright © `2015` - `2022` [Nicole Jung]** 
+**Copyright © `2015` - `2023` [Nicole Jung]** 
of the **[Karlsruhe Institute of Technology]**. > This program is free software: @@ -49,12 +53,12 @@ of the **[Karlsruhe Institute of Technology]**. -[Installation]: https://www.chemotion.net/docs/eln/install_configure/ -[Documentation]: https://www.chemotion.net/docs/eln/ +[Installation]: https://www.chemotion.net/docs/eln/install_configure +[Documentation]: https://www.chemotion.net/docs/eln [Changelog]: CHANGELOG.md [DFG]: https://www.dfg.de/en/ -[DFG Logo]: https://www.dfg.de/zentralablage/bilder/service/logos_corporate_design/logo_negativ_267.png +[DFG Logo]: https://chemotion.net/img/logos/DFG_logo.png [Nicole Jung]: mailto:nicole.jung@kit.edu [Karlsruhe Institute of Technology]: https://www.kit.edu/english/ diff --git a/VERSION b/VERSION index 3b41efd3d3..c9083f866d 100644 --- a/VERSION +++ b/VERSION @@ -1,3 +1,3 @@ -version: 1.6.0 -base_revision: 23809bf7e +version: 1.8.0 +base_revision: 7bd6a5d81 current_revision: 0 diff --git a/app/api/api.rb b/app/api/api.rb index 94333fdba0..b6d48a1f38 100644 --- a/app/api/api.rb +++ b/app/api/api.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength + # module API require 'grape-entity' require 'grape-swagger' @@ -9,13 +11,13 @@ class API < Grape::API prefix :api version 'v1' - # TODO needs to be tested, + # TODO: needs to be tested, # source: http://funonrails.com/2014/03/api-authentication-using-devise-token/ helpers do def present(*args) options = args.count > 1 ? args.extract_options! : {} - options.merge!(current_user: current_user) + options[:current_user] = current_user super(*args, options) end @@ -36,7 +38,7 @@ def detect_current_user_from_jwt decoded_token = JsonWebToken.decode(current_token) user_id = decoded_token[:user_id] - User.find_by!(id: user_id) + User.find(user_id) rescue StandardError nil end @@ -65,25 +67,23 @@ def is_public_request? '/api/v1/chemspectra/', '/api/v1/ketcher/layout', '/api/v1/gate/receiving', - '/api/v1/gate/ping' + '/api/v1/gate/ping', ) end - def cache_key search_method, arg, molfile, collection_id, molecule_sort, opt - molecule_sort = molecule_sort == 1 ? true : false + def cache_key(search_method, arg, molfile, collection_id, molecule_sort, opt) # rubocop:disable Metrics/ParameterLists + molecule_sort = molecule_sort == 1 inchikey = Chemotion::OpenBabelService.inchikey_from_molfile molfile - cache_key = [ + [ latest_updated, search_method, arg, inchikey, collection_id, molecule_sort, - opt + opt, ] - - return cache_key end def to_snake_case_key(k) @@ -95,7 +95,7 @@ def to_rails_snake_case(val) when Array val.map { |v| to_rails_snake_case(v) } when Hash - Hash[val.map { |k, v| [to_snake_case_key(k), to_rails_snake_case(v)] }] + Hash[val.map { |k, v| [to_snake_case_key(k), to_rails_snake_case(v)] }] # rubocop:disable Style/HashConversion else val end @@ -110,7 +110,7 @@ def to_json_camel_case(val) when Array val.map { |v| to_json_camel_case(v) } when Hash - Hash[val.map { |k, v| [to_camelcase_key(k), to_json_camel_case(v)] }] + Hash[val.map { |k, v| [to_camelcase_key(k), to_json_camel_case(v)] }] # rubocop:disable Style/HashConversion else val end @@ -123,13 +123,29 @@ def to_json_camel_case(val) # desc: whitelisted tables and columns for advanced_search WL_TABLES = { - 'samples' => %w(name short_label external_label xref) - } + 'samples' => %w[ + name short_label external_label xref content is_top_secret decoupled + stereo boiling_point melting_point density molarity_value target_amount_value + description location purity solvent inventory_sample sum_formula molecular_mass + dry_solvent + ], + 'reactions' => %w[ + name short_label status conditions rxno content temperature duration + role purification tlc_solvents tlc_description rf_value dangerous_products + plain_text_description plain_text_observation + ], + 'wellplates' => %w[name short_label readout_titles content plain_text_description], + 'screens' => %w[name collaborator requirements conditions result content plain_text_description], + 'research_plans' => %w[name body content], + 'elements' => %w[name short_label], + }.freeze + TARGET = Rails.env.production? ? 'https://www.chemotion-repository.net/' : 'http://localhost:3000/' - ELEMENTS = %w[research_plan screen wellplate reaction sample] + ELEMENTS = %w[research_plan screen wellplate reaction sample cell_line].freeze - TEXT_TEMPLATE = %w[SampleTextTemplate ReactionTextTemplate WellplateTextTemplate ScreenTextTemplate ResearchPlanTextTemplate ReactionDescriptionTextTemplate ElementTextTemplate ] + TEXT_TEMPLATE = %w[SampleTextTemplate ReactionTextTemplate WellplateTextTemplate ScreenTextTemplate + ResearchPlanTextTemplate ReactionDescriptionTextTemplate ElementTextTemplate].freeze mount Chemotion::LiteratureAPI mount Chemotion::ContainerAPI @@ -164,28 +180,34 @@ def to_json_camel_case(val) mount Chemotion::MessageAPI mount Chemotion::AdminAPI mount Chemotion::AdminUserAPI - mount Chemotion::AdminGenericAPI mount Chemotion::EditorAPI mount Chemotion::UiAPI mount Chemotion::OlsTermsAPI mount Chemotion::PredictionAPI mount Chemotion::ComputeTaskAPI mount Chemotion::TextTemplateAPI - mount Chemotion::GenericElementAPI - mount Chemotion::SegmentAPI - mount Chemotion::GenericDatasetAPI mount Chemotion::ReportTemplateAPI mount Chemotion::PrivateNoteAPI mount Chemotion::NmrdbAPI mount Chemotion::MeasurementsAPI - mount Chemotion::ConverterAPI mount Chemotion::AttachableAPI mount Chemotion::SampleTaskAPI mount Chemotion::ThirdPartyAppAPI mount Chemotion::CalendarEntryAPI - - add_swagger_documentation(info: { - "title": "Chemotion ELN", - "version": "1.0" - }) if Rails.env.development? + mount Chemotion::CommentAPI + mount Chemotion::CellLineAPI + mount Labimotion::ConverterAPI + mount Labimotion::GenericElementAPI + mount Labimotion::GenericDatasetAPI + mount Labimotion::SegmentAPI + mount Labimotion::LabimotionHubAPI + mount Chemotion::InventoryAPI + + if Rails.env.development? + add_swagger_documentation(info: { + title: 'Chemotion ELN', + version: '1.0', + }) + end end +# rubocop:enable Metrics/ClassLength diff --git a/app/api/chemotion/admin_api.rb b/app/api/chemotion/admin_api.rb index 65b6862ea9..867013646f 100644 --- a/app/api/chemotion/admin_api.rb +++ b/app/api/chemotion/admin_api.rb @@ -58,7 +58,7 @@ class AdminAPI < Grape::API end end - desc 'Sychronize chemotion deviceMetadata to DataCite' + desc 'Synchronize chemotion deviceMetadata to DataCite' params do requires :device_id, type: Integer, desc: 'device id' end @@ -234,22 +234,6 @@ class AdminAPI < Grape::API end end - namespace :name do - desc 'Find top 4 matched user names by type' - params do - requires :type, type: String, values: %w[Group Device User Person Admin] - requires :name, type: String, desc: 'user name' - end - get do - return { users: [] } if params[:name].blank? - - users = User.where(type: params[:type]) - .by_name(params[:name]) - .limit(4) - present users, with: Entities::UserSimpleEntity, root: 'users' - end - end - namespace :create do desc 'create a group of persons' params do @@ -410,6 +394,19 @@ class AdminAPI < Grape::API end end + namespace :data_types do + desc 'Update data types' + put do + file_path = Rails.configuration.path_spectra_data_type + new_data_types = JSON.parse(request.body.read) + begin + File.write(file_path, JSON.pretty_generate(new_data_types)) + rescue Errno::EACCES + error!('Save files error!', 500) + end + end + end + resource :jobs do desc 'list queued delayed jobs' get do diff --git a/app/api/chemotion/admin_generic_api.rb b/app/api/chemotion/admin_generic_api.rb deleted file mode 100644 index 9b7a6199e6..0000000000 --- a/app/api/chemotion/admin_generic_api.rb +++ /dev/null @@ -1,368 +0,0 @@ -# frozen_string_literal: true - -module Chemotion - class AdminGenericAPI < Grape::API # rubocop:disable Metrics/ClassLength - resource :admin_generic do # rubocop:disable Metrics/BlockLength - namespace :update_element_template do - desc 'update Generic Element Properties Template' - params do - requires :id, type: Integer, desc: 'Element Klass ID' - optional :label, type: String, desc: 'Element Klass Label' - requires :properties_template, type: Hash - optional :is_release, type: Boolean, default: false - end - post do - klass = ElementKlass.find(params[:id]) - uuid = SecureRandom.uuid - properties = params[:properties_template] - properties['uuid'] = uuid - properties['eln'] = Chemotion::Application.config.version - properties['klass'] = 'ElementKlass' - klass.properties_template = properties - klass.save! - klass.reload - klass.create_klasses_revision(current_user.id) if params[:is_release] == true - - present klass, with: Entities::ElementKlassEntity - end - end - - namespace :create_element_klass do - desc 'create Generic Element Properties Template' - params do - requires :name, type: String, desc: 'Element Klass Name' - requires :label, type: String, desc: 'Element Klass Label' - requires :klass_prefix, type: String, desc: 'Element Klass Short Label Prefix' - optional :icon_name, type: String, desc: 'Element Klass Icon Name' - optional :desc, type: String, desc: 'Element Klass Desc' - optional :properties_template, type: Hash, desc: 'Element Klass properties template' - end - post do - uuid = SecureRandom.uuid - template = { uuid: uuid, layers: {}, select_options: {} } - attributes = declared(params, include_missing: false) - attributes[:properties_template]['uuid'] = uuid if attributes[:properties_template].present? - attributes[:properties_template] = template unless attributes[:properties_template].present? - attributes[:properties_template]['eln'] = Chemotion::Application.config.version if attributes[:properties_template].present? - attributes[:properties_template]['klass'] = 'ElementKlass' if attributes[:properties_template].present? - attributes[:is_active] = false - attributes[:uuid] = uuid - attributes[:released_at] = DateTime.now - attributes[:properties_release] = attributes[:properties_template] - attributes[:created_by] = current_user.id - - new_klass = ElementKlass.create!(attributes) - new_klass.reload - new_klass.create_klasses_revision(current_user.id) - klass_names_file = Rails.root.join('config/klasses.json') - klasses = ElementKlass.where(is_active: true)&.pluck(:name) || [] - bytes_written = File.write(klass_names_file, klasses) - if bytes_written == klasses.length - puts "File successfully written" - else - puts "Error writing file" - end - status 201 - rescue ActiveRecord::RecordInvalid => e - { error: e.message } - end - end - - namespace :update_element_klass do - desc 'update Generic Element Klass' - params do - requires :id, type: Integer, desc: 'Element Klass ID' - optional :label, type: String, desc: 'Element Klass Label' - optional :klass_prefix, type: String, desc: 'Element Klass Short Label Prefix' - optional :icon_name, type: String, desc: 'Element Klass Icon Name' - optional :desc, type: String, desc: 'Element Klass Desc' - optional :place, type: String, desc: 'Element Klass Place' - end - post do - place = params[:place] - begin - place = place.to_i if place.present? && place.to_i == place.to_f - rescue StandardError - place = 100 - end - klass = ElementKlass.find(params[:id]) - klass.label = params[:label] if params[:label].present? - klass.klass_prefix = params[:klass_prefix] if params[:klass_prefix].present? - klass.icon_name = params[:icon_name] if params[:icon_name].present? - klass.desc = params[:desc] if params[:desc].present? - klass.place = place - klass.save! - - present klass, with: Entities::ElementKlassEntity - end - end - - namespace :de_active_element_klass do - desc 'activate or inactive Generic Element Klass' - params do - requires :klass_id, type: Integer, desc: 'Element Klass ID' - requires :is_active, type: Boolean, desc: 'Active or Inactive Klass' - end - post do - klass = ElementKlass.find(params[:klass_id]) - klass&.update!(is_active: params[:is_active]) - klass_dir = File.join(Rails.root, 'config') - !File.directory?(klass_dir) && FileUtils.mkdir_p(klass_dir) - klass_names_file = File.join(klass_dir, 'klasses.json') - klasses = ElementKlass.where(is_active: true)&.pluck(:name) || [] - File.write(klass_names_file, klasses) - load Rails.root.join('config/klasses.json') - - - present klass, with: Entities::ElementKlassEntity - end - end - - namespace :delete_element_klass do - desc 'delete Generic Element Klass' - params do - requires :klass_id, type: Integer, desc: 'Element Klass ID' - end - post do - klass = ElementKlass.find(params[:klass_id]) - klass&.destroy! - - klass_dir = File.join(Rails.root, 'config') - !File.directory?(klass_dir) && FileUtils.mkdir_p(klass_dir) - klass_names_file = File.join(klass_dir, 'klasses.json') - klasses = ElementKlass.where(is_active: true)&.pluck(:name) || [] - File.write(klass_names_file, klasses) - - status 201 - end - end - - - namespace :create_segment_klass do - desc 'create Generic Segment Klass' - params do - requires :label, type: String, desc: 'Segment Klass Label' - requires :element_klass, type: Integer, desc: 'Element Klass Id' - optional :desc, type: String, desc: 'Segment Klass Desc' - optional :place, type: String, desc: 'Segment Klass Place', default: '100' - optional :properties_template, type: Hash, desc: 'Element Klass properties template' - end - after_validation do - @klass = ElementKlass.find(params[:element_klass]) - error!('Klass is invalid. Please re-select.', 500) if @klass.nil? - end - post do - place = params[:place] - begin - place = place.to_i if place.present? && place.to_i == place.to_f - rescue StandardError - place = 100 - end - - uuid = SecureRandom.uuid - template = { uuid: uuid, layers: {}, select_options: {} } - attributes = declared(params, include_missing: false) - attributes[:properties_template]['uuid'] = uuid if attributes[:properties_template].present? - template = attributes[:properties_template].present? ? attributes[:properties_template] : template - template['eln'] = Chemotion::Application.config.version - template['klass'] = 'SegmentKlass' - attributes.merge!(properties_template: template, element_klass: @klass, created_by: current_user.id, place: place) - attributes[:uuid] = uuid - attributes[:released_at] = DateTime.now - attributes[:properties_release] = attributes[:properties_template] - klass = SegmentKlass.create!(attributes) - klass.reload - klass.create_klasses_revision(current_user.id) - - {} # FE does not use the result - rescue ActiveRecord::RecordInvalid => e - { error: e.message } - end - end - - namespace :update_segment_klass do - desc 'update Generic Segment Klass' - params do - requires :id, type: Integer, desc: 'Segment Klass ID' - optional :label, type: String, desc: 'Segment Klass Label' - optional :desc, type: String, desc: 'Segment Klass Desc' - optional :place, type: String, desc: 'Segment Klass Place', default: '100' - end - after_validation do - @segment = SegmentKlass.find(params[:id]) - error!('Segment is invalid. Please re-select.', 500) if @segment.nil? - end - post do - place = params[:place] - begin - place = place.to_i if place.present? && place.to_i == place.to_f - rescue StandardError - place = 100 - end - attributes = declared(params, include_missing: false) - attributes.delete(:id) - attributes[:place] = place - @segment&.update!(attributes) - - {} # FE does not use the result - end - end - - namespace :de_active_segment_klass do - desc 'activate or inactive Generic Segment Klass' - params do - requires :id, type: Integer, desc: 'Segment Klass ID' - requires :is_active, type: Boolean, desc: 'Active or Inactive Segment' - end - after_validation do - @segment = SegmentKlass.find(params[:id]) - error!('Segment is invalid. Please re-select.', 500) if @segment.nil? - end - post do - present @segment&.update!(is_active: params[:is_active]), with: Entities::SegmentKlassEntity - end - end - - namespace :klass_revisions do - desc 'list Generic Element Revisions' - params do - requires :id, type: Integer, desc: 'Generic Element Klass Id' - requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass] - end - get do - klass = params[:klass].constantize.find_by(id: params[:id]) - list = klass.send("#{params[:klass].underscore}es_revisions") unless klass.nil? - - present list.sort_by(&:released_at).reverse, with: Entities::KlassRevisionEntity, root: 'revisions' - end - end - - namespace :list_segment_klass do - desc 'list Generic Segment Klass' - params do - optional :is_active, type: Boolean, desc: 'Active or Inactive Segment' - end - get do - list = params[:is_active].present? ? SegmentKlass.where(is_active: params[:is_active]) : SegmentKlass.all - list.order(place: :asc) - - present list, with: Entities::SegmentKlassEntity, root: 'klass' - end - end - - namespace :update_segment_template do - desc 'update Generic Segment Properties Template' - params do - requires :id, type: Integer, desc: 'Segment Klass ID' - requires :properties_template, type: Hash - optional :is_release, type: Boolean, default: false - end - after_validation do - @segment = SegmentKlass.find(params[:id]) - error!('Segment is invalid. Please re-select.', 500) if @segment.nil? - end - post do - uuid = SecureRandom.uuid - properties = params[:properties_template] - properties['uuid'] = uuid - properties['eln'] = Chemotion::Application.config.version - properties['klass'] = @segment.class.name - - @segment.properties_template = properties - @segment.save! - @segment.reload - @segment.create_klasses_revision(current_user.id) if params[:is_release] == true - - present @segment, with: Entities::SegmentKlassEntity - end - end - - # TODO: Endpoint is currently unused - namespace :delete_segment_klass do - desc 'delete Generic Segment Klass' - route_param :id do - before do - @segment = SegmentKlass.find(params[:id]) - end - delete do - @segment&.destroy! - end - end - end - - namespace :delete_klass_revision do - desc 'delete Generic Element Klass' - params do - requires :id, type: Integer, desc: 'Revision ID' - requires :klass_id, type: Integer, desc: 'Klass ID' - requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass] - end - post do - revision = "#{params[:klass]}esRevision".constantize.find(params[:id]) - klass = params[:klass].constantize.find_by(id: params[:klass_id]) unless revision.nil? - error!('Revision is invalid.', 404) if revision.nil? - error!('Can not delete the active revision.', 405) if revision.uuid == klass.uuid - revision&.destroy! - - status 201 - end - end - - namespace :list_dataset_klass do - desc 'list Generic Dataset Klass' - params do - optional :is_active, type: Boolean, desc: 'Active or Inactive Dataset' - end - get do - list = params[:is_active].present? ? DatasetKlass.where(is_active: params[:is_active]) : DatasetKlass.all - list.order(place: :asc) - - present list, with: Entities::DatasetKlassEntity, root: 'klass' - end - end - - namespace :de_active_dataset_klass do - desc 'activate or inactive Generic Dataset Klass' - params do - requires :id, type: Integer, desc: 'Dataset Klass ID' - requires :is_active, type: Boolean, desc: 'Active or Inactive Dataset' - end - after_validation do - @dataset = DatasetKlass.find(params[:id]) - error!('Dataset is invalid. Please re-select.', 500) if @dataset.nil? - end - post do - @dataset&.update!(is_active: params[:is_active]) - - {} # result is not used by FE - end - end - - namespace :update_dataset_template do - desc 'update Generic Dataset Properties Template' - params do - requires :id, type: Integer, desc: 'Dataset Klass ID' - requires :properties_template, type: Hash - optional :is_release, type: Boolean, default: false - end - after_validation do - @klass = DatasetKlass.find(params[:id]) - error!('Dataset is invalid. Please re-select.', 500) if @klass.nil? - end - post do - uuid = SecureRandom.uuid - properties = params[:properties_template] - properties['uuid'] = uuid - properties['eln'] = Chemotion::Application.config.version - properties['klass'] = @klass.class.name - @klass.properties_template = properties - @klass.save! - @klass.reload - @klass.create_klasses_revision(current_user.id) if params[:is_release] == true - - present @klass, with: Entities::DatasetKlassEntity - end - end - end - end -end diff --git a/app/api/chemotion/admin_user_api.rb b/app/api/chemotion/admin_user_api.rb index 0ae6302382..29d7d34487 100644 --- a/app/api/chemotion/admin_user_api.rb +++ b/app/api/chemotion/admin_user_api.rb @@ -3,12 +3,28 @@ module Chemotion # Publish-Subscription MessageAPI class AdminUserAPI < Grape::API - resource :admin_user do # rubocop:disable Metrics/BlockLength + resource :admin_user do namespace :listUsers do desc 'Find all users' get 'all' do present User.all.order('type desc, id'), with: Entities::UserEntity, root: 'users' end + + desc 'Find top (5) matched user by name and by type' + params do + requires :name, type: String, desc: 'user name' + optional :type, type: [String], desc: 'user types', + coerce_with: ->(val) { val.split(/[\s|,]+/) }, values: %w[Group Device Person Admin] + optional :limit, type: Integer, default: 5 + end + get 'byname' do + return { users: [] } if params[:name].blank? + + users = params[:type] ? User.where(type: params[:type]) : User + users = users.by_name(params[:name]) + .limit(params[:limit]) + present users, with: Entities::UserSimpleEntity, root: 'users' + end end namespace :resetPassword do @@ -74,7 +90,7 @@ class AdminUserAPI < Grape::API end end - namespace :updateAccount do # rubocop:disable Metrics/BlockLength + namespace :updateAccount do desc 'update account' params do requires :user_id, type: Integer, desc: 'user id' @@ -85,6 +101,11 @@ class AdminUserAPI < Grape::API optional :molecule_editor, type: Boolean, desc: 'enable or disable molecule moderation' optional :converter_admin, type: Boolean, desc: 'converter profile' optional :account_active, type: Boolean, desc: 'active or inactive this user' + optional :auth_generic_admin, type: Hash do + optional :elements, type: Boolean, desc: 'un-authorize the user as generic elements admin' + optional :segments, type: Boolean, desc: 'un-authorize the user as generic segments admin' + optional :datasets, type: Boolean, desc: 'un-authorize the user as generic datasets admin' + end end post do @@ -98,8 +119,9 @@ class AdminUserAPI < Grape::API end end - if params[:reconfirm_user].present? - user.update_columns(email: user.unconfirmed_email, unconfirmed_email: nil) if params[:reconfirm_user] == true + if params[:reconfirm_user].present? && (params[:reconfirm_user] == true) + user.update_columns(email: user.unconfirmed_email, + unconfirmed_email: nil) end unless params[:confirm_user].nil? @@ -142,32 +164,19 @@ class AdminUserAPI < Grape::API end user.update!(account_active: params[:account_active]) unless params[:account_active].nil? + if params[:auth_generic_admin].present? + profile = user.profile + pdata = profile.data || {} + data = pdata.deep_merge('generic_admin' => params[:auth_generic_admin]) + profile.update!(data: data) + end present user, with: Entities::UserEntity end end end - resource :matrix do # rubocop:disable Metrics/BlockLength - namespace :find_user do - desc 'Find top 5 matched user/group names by type' - params do - requires :name, type: String - end - get do - if params[:name].present? - users = User.where(type: %w[Person Group]) - .by_name(params[:name]) - .limit(5) - .select('first_name', 'last_name', 'name', 'id', 'name_abbreviation', 'name_abbreviation as abb', 'type as user_type') - .map(&:attributes) - { users: users } - else - { users: [] } - end - end - end - + resource :matrix do namespace :list do desc 'Find all matrices' get do diff --git a/app/api/chemotion/attachable_api.rb b/app/api/chemotion/attachable_api.rb index 0fe29704c3..facfeb907c 100644 --- a/app/api/chemotion/attachable_api.rb +++ b/app/api/chemotion/attachable_api.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop:disable Rails/SkipsModelValidations + module Chemotion class AttachableAPI < Grape::API resource :attachable do @@ -7,12 +9,16 @@ class AttachableAPI < Grape::API optional :files, type: Array[File], desc: 'files', default: [] optional :attachable_type, type: String, desc: 'attachable_type' optional :attachable_id, type: Integer, desc: 'attachable id' + optional :attfilesIdentifier, type: Array[String], desc: 'file identifier' optional :del_files, type: Array[Integer], desc: 'del file id', default: [] end after_validation do case params[:attachable_type] when 'ResearchPlan' - error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, ResearchPlan.find_by(id: params[:attachable_id])).update? + error!('401 Unauthorized', 401) unless ElementPolicy.new( + current_user, + ResearchPlan.find_by(id: params[:attachable_id]), + ).update? end end @@ -20,13 +26,15 @@ class AttachableAPI < Grape::API post 'update_attachments_attachable' do attachable_type = params[:attachable_type] attachable_id = params[:attachable_id] + if params.fetch(:files, []).any? attach_ary = [] rp_attach_ary = [] - params[:files].each do |file| + params[:files].each_with_index do |file, index| next unless (tempfile = file[:tempfile]) a = Attachment.new( + identifier: params[:attfilesIdentifier][index], bucket: file[:container_id], filename: file[:filename], file_path: file[:tempfile], @@ -34,23 +42,26 @@ class AttachableAPI < Grape::API created_for: current_user.id, content_type: file[:type], attachable_type: attachable_type, - attachable_id: attachable_id + attachable_id: attachable_id, ) + begin a.save! attach_ary.push(a.id) - rp_attach_ary.push(a.id) if a.attachable_type.in?(%w[ResearchPlan Wellplate Element]) + rp_attach_ary.push(a.id) if a.attachable_type.in?(%w[ResearchPlan Wellplate Labimotion::Element]) ensure tempfile.close tempfile.unlink end end - - TransferThumbnailToPublicJob.set(queue: "transfer_thumbnail_to_public_#{current_user.id}").perform_later(rp_attach_ary) if rp_attach_ary.any? end - Attachment.where('id IN (?) AND attachable_type = (?)', params[:del_files].map!(&:to_i), attachable_type).update_all(attachable_id: nil) if params[:del_files].any? + if params[:del_files].any? + Attachment.where('id IN (?) AND attachable_type = (?)', params[:del_files].map!(&:to_i), + attachable_type).update_all(attachable_id: nil) + end true end end end end +# rubocop:enable Rails/SkipsModelValidations diff --git a/app/api/chemotion/attachment_api.rb b/app/api/chemotion/attachment_api.rb index 3e461d58cf..92bff04b90 100644 --- a/app/api/chemotion/attachment_api.rb +++ b/app/api/chemotion/attachment_api.rb @@ -37,6 +37,13 @@ def writable?(attachment) def upload_chunk_error_message { ok: false, statusText: 'File key is not valid' } end + + def remove_duplicated(att) + old_att = Attachment.find_by(filename: att.filename, attachable_id: att.attachable_id) + return unless old_att.id != att.id + + old_att&.destroy + end end rescue_from ActiveRecord::RecordNotFound do |_error| @@ -44,6 +51,30 @@ def upload_chunk_error_message error!(message, 404) end + + resource :export_ds do + before do + @container = Container.find_by(id: params[:container_id]) + element = @container.root.containable + can_read = ElementPolicy.new(current_user, element).read? + can_dwnld = can_read && + ElementPermissionProxy.new(current_user, element, user_ids).read_dataset? + error!('401 Unauthorized', 401) unless can_dwnld + end + desc "Download the dataset attachment file" + get 'dataset/:container_id' do + env['api.format'] = :binary + export = Labimotion::ExportDataset.new + export.export(params[:container_id]) + export.spectra(params[:container_id]) + content_type('application/vnd.ms-excel') + ds_filename = export.res_name(params[:container_id]) + filename = URI.escape(ds_filename) + header('Content-Disposition', "attachment; filename=\"#{filename}\"") + export.read + end + end + resource :attachments do before do @attachment = Attachment.find_by(id: params[:attachment_id]) @@ -264,6 +295,14 @@ def upload_chunk_error_message end end + if Labimotion::Dataset.find_by(element_id: params[:container_id], element_type: 'Container').present? + export = Labimotion::ExportDataset.new + export.export(params[:container_id]) + export.spectra(params[:container_id]) + zip.put_next_entry export.res_name(params[:container_id]) + zip.write export.read + end + hyperlinks_text = '' JSON.parse(@container.extended_metadata.fetch('hyperlinks', '[]')).each do |link| hyperlinks_text += "#{link} \n" @@ -377,6 +416,8 @@ def upload_chunk_error_message Attachment.where(id: pm[:original]).each do |att| next unless writable?(att) + remove_duplicated(att) + att.set_regenerating att.save end @@ -400,6 +441,8 @@ def upload_chunk_error_message Attachment.where(id: pm[:edited]).each do |att| next unless writable?(att) + remove_duplicated(att) + # TODO: do not use abs_path result = Chemotion::Jcamp::RegenerateJcamp.spectrum( att.abs_path, t_molfile.path @@ -432,6 +475,8 @@ def upload_chunk_error_message optional :cyclicvolta, type: String optional :curveIdx, type: Integer optional :simulatenmr, type: Boolean + optional :axesUnits, type: String + optional :detector, type: String end post 'save_spectrum' do jcamp_att = @attachment.generate_spectrum( diff --git a/app/api/chemotion/cell_line_api.rb b/app/api/chemotion/cell_line_api.rb new file mode 100644 index 0000000000..47953f297d --- /dev/null +++ b/app/api/chemotion/cell_line_api.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +module Chemotion + class CellLineAPI < Grape::API + include Grape::Kaminari + helpers ParamsHelpers + helpers ContainerHelpers + + rescue_from ActiveRecord::RecordNotFound do + error!('Ressource not found', 401) + end + resource :cell_lines do + desc 'return cell lines of a collection' + params do + optional :collection_id, type: Integer, desc: 'Collection id' + optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id' + optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at' + optional :from_date, type: Integer, desc: 'created_date from in ms' + optional :to_date, type: Integer, desc: 'created_date to in ms' + end + paginate per_page: 5, offset: 0 + before do + params[:per_page].to_i > 50 && (params[:per_page] = 50) + end + get do + scope = if params[:collection_id] + begin + Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids) + .find(params[:collection_id]).cellline_samples + rescue ActiveRecord::RecordNotFound + CelllineSample.none + end + elsif params[:sync_collection_id] + begin + current_user.all_sync_in_collections_users + .find(params[:sync_collection_id]) + .collection + .cellline_samples + rescue ActiveRecord::RecordNotFound + CelllineSample.none + end + else + # All collection of current_user + CelllineSample.none.joins(:collections).where(collections: { user_id: current_user.id }).distinct + end.order('created_at DESC') + + from = params[:from_date] + to = params[:to_date] + by_created_at = params[:filter_created_at] || false + + scope = scope.created_time_from(Time.zone.at(from)) if from && by_created_at + scope = scope.created_time_to(Time.zone.at(to) + 1.day) if to && by_created_at + scope = scope.updated_time_from(Time.zone.at(from)) if from && !by_created_at + scope = scope.updated_time_to(Time.zone.at(to) + 1.day) if to && !by_created_at + + reset_pagination_page(scope) + + cell_line_samples = paginate(scope).map do |cell_line_sample| + Entities::CellLineSampleEntity.represent( + cell_line_sample, + displayed_in_list: true, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, + element: cell_line_sample).detail_levels, + ) + end + { cell_lines: cell_line_samples } + end + + desc 'Get a cell line by id' + params do + requires :id, type: Integer, desc: 'id of cell line sample to load' + end + get ':id' do + use_case = Usecases::CellLines::Load.new(params[:id], current_user) + begin + cell_line_sample = use_case.execute! + rescue StandardError => e + error!(e, 400) + end + return present cell_line_sample, with: Entities::CellLineSampleEntity + end + + desc 'Create a new Cell line sample' + params do + optional :organism, type: String, desc: 'name of the donor organism of the cell' + optional :tissue, type: String, desc: 'tissue from which the cell originates' + requires :amount, type: Integer, desc: 'amount of cells' + requires :unit, type: String, desc: 'unit of cell amount' + requires :passage, type: Integer, desc: 'passage of cells' + optional :disease, type: String, desc: 'deasease of cells' + requires :material_names, type: String, desc: 'names of cell line e.g. name1;name2' + requires :collection_id, type: Integer, desc: 'Collection of the cell line sample' + optional :cell_type, type: String, desc: 'type of cells' + optional :biosafety_level, type: String, desc: 'biosafety_level of cells' + optional :growth_medium, type: String, desc: 'growth medium of cells' + optional :variant, type: String, desc: 'variant of cells' + optional :optimal_growth_temp, type: Float, desc: 'optimal_growth_temp of cells' + optional :cryo_pres_medium, type: String, desc: 'cryo preservation medium of cells' + optional :gender, type: String, desc: 'gender of donor organism' + optional :material_description, type: String, desc: 'description of cell line concept' + optional :contamination, type: String, desc: 'contamination of a cell line sample' + requires :source, type: String, desc: 'source of a cell line sample' + optional :name, type: String, desc: 'name of a cell line sample' + optional :mutation, type: String, desc: 'mutation of a cell line' + optional :description, type: String, desc: 'description of a cell line sample' + optional :short_label, type: String, desc: 'short label of a cell line sample' + requires :container, type: Hash, desc: 'root Container of element' + end + post do + error!('401 Unauthorized', 401) unless current_user.collections.find(params[:collection_id]) + use_case = Usecases::CellLines::Create.new(params, current_user) + cell_line_sample = use_case.execute! + cell_line_sample.container = update_datamodel(params[:container]) + + return present cell_line_sample, with: Entities::CellLineSampleEntity + end + desc 'Update a Cell line sample' + params do + requires :cell_line_sample_id, type: String, desc: 'id of the cell line to update' + optional :organism, type: String, desc: 'name of the donor organism of the cell' + optional :mutation, type: String, desc: 'mutation of a cell line' + optional :tissue, type: String, desc: 'tissue from which the cell originates' + requires :amount, type: Integer, desc: 'amount of cells' + requires :unit, type: String, desc: 'unit of amount of cells' + optional :passage, type: Integer, desc: 'passage of cells' + optional :disease, type: String, desc: 'deasease of cells' + optional :material_names, type: String, desc: 'names of cell line e.g. name1;name2' + optional :collection_id, type: Integer, desc: 'Collection of the cell line sample' + optional :cell_type, type: String, desc: 'type of cells' + optional :biosafety_level, type: String, desc: 'biosafety_level of cells' + optional :variant, type: String, desc: 'variant of cells' + optional :optimal_growth_temp, type: Float, desc: 'optimal_growth_temp of cells' + optional :cryo_pres_medium, type: String, desc: 'cryo preservation medium of cells' + optional :gender, type: String, desc: 'gender of donor organism' + optional :material_description, type: String, desc: 'description of cell line concept' + optional :contamination, type: String, desc: 'contamination of a cell line sample' + optional :source, type: String, desc: 'source of a cell line sample' + optional :name, type: String, desc: 'name of a cell line sample' + optional :description, type: String, desc: 'description of a cell line sample' + requires :container, type: Hash, desc: 'root Container of element' + end + put do + use_case = Usecases::CellLines::Update.new(params, current_user) + cell_line_sample = use_case.execute! + cell_line_sample.container = update_datamodel(params[:container]) + return present cell_line_sample, with: Entities::CellLineSampleEntity + end + + resource :names do + desc 'Returns all accessable cell line material names and their id' + get 'all' do + return present CelllineMaterial.all, with: Entities::CellLineMaterialNameEntity + end + end + resource :material do + params do + requires :id, type: Integer, desc: 'id of cell line material to load' + end + get ':id' do + return CelllineMaterial.find(params[:id]) + end + end + end + end +end diff --git a/app/api/chemotion/chem_spectra_api.rb b/app/api/chemotion/chem_spectra_api.rb index ac1d5437a1..1ac77436ae 100644 --- a/app/api/chemotion/chem_spectra_api.rb +++ b/app/api/chemotion/chem_spectra_api.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength + # Belong to Chemotion module module Chemotion # API for ChemSpectra manipulation @@ -80,6 +82,41 @@ def raw_file(att) nil end end + + def compare_data_type_mapping(response) # rubocop:disable Metrics/AbcSize + default_data_types = JSON.parse(response.body) + file_path = Rails.configuration.path_spectra_data_type + + current_data_types = {} + current_data_types = JSON.parse(File.read(file_path)) if File.exist?(file_path) + + keys_to_check = default_data_types['datatypes'].keys + result = { + 'default_data_types' => {}, + 'current_data_types' => {}, + } + keys_to_check.each do |key| + current_values = current_data_types['datatypes'][key] || [] if current_data_types != {} + default_values = default_data_types['datatypes'][key] || [] + + if current_values.nil? + current_data_types = default_data_types + result['current_data_types'][key] = default_values + else + merged_values = (current_values | default_values).uniq + current_data_types['datatypes'][key] = merged_values + result['current_data_types'][key] = merged_values + end + + result['default_data_types'][key] = default_values + end + save_data_types(file_path, current_data_types) + result + end + + def save_data_types(file_path, current_data_types) + File.write(file_path, JSON.pretty_generate(current_data_types)) + end end resource :chemspectra do # rubocop:disable BlockLength @@ -145,6 +182,56 @@ def raw_file(att) post 'refresh' do convert_for_refresh(params) end + + desc 'Combine spectra' + params do + requires :spectra_ids, type: [Integer] + requires :front_spectra_idx, type: Integer # index of front spectra + end + post 'combine_spectra' do + pm = to_rails_snake_case(params) + + list_file = [] + list_file_names = [] + container_id = -1 + combined_image_filename = '' + Attachment.where(id: pm[:spectra_ids]).each do |att| + container = att.container + combined_image_filename = "#{container.name}.new_combined.png" + container_id = att.attachable_id + list_file_names.push(att.filename) + list_file.push(att.abs_path) + end + + _, image = Chemotion::Jcamp::CombineImg.combine( + list_file, pm[:front_spectra_idx], list_file_names + ) + + content_type('application/json') + unless image.nil? + att = Attachment.find_by(filename: combined_image_filename, attachable_id: container_id) + if att.nil? + att = Attachment.new( + bucket: container_id, + filename: combined_image_filename, + created_by: current_user.id, + created_for: current_user.id, + file_path: image.path, + attachable_type: 'Container', + attachable_id: container_id, + ) + att.save! + else + att.update!( + file_path: image.path, + attachable_type: 'Container', + attachable_id: container_id, + ) + end + end + + { status: true } + end end resource :predict do @@ -196,6 +283,25 @@ def raw_file(att) end end + resource :spectra_layouts do + desc 'Get all spectra layouts and data types' + get do + url = Rails.configuration.spectra.chemspectra.url + if url + api_endpoint = "#{url}/api/v1/chemspectra/spectra_layouts" + response = HTTParty.get(api_endpoint) + case response.code + when 404 + error_message = 'API endpoint not found' + error!(error_message, 404) + when 200 + data_types = compare_data_type_mapping(response) + data_types + end + end + end + end + resource :nmrium_wrapper do desc 'Return url of nmrium wrapper' route_param :host_name do @@ -208,3 +314,5 @@ def raw_file(att) end end end + +# rubocop:enable Metrics/ClassLength diff --git a/app/api/chemotion/chemical_api.rb b/app/api/chemotion/chemical_api.rb index c26cc9d9ef..7aa80525a4 100644 --- a/app/api/chemotion/chemical_api.rb +++ b/app/api/chemotion/chemical_api.rb @@ -58,7 +58,7 @@ class ChemicalAPI < Grape::API get do Chemotion::ChemicalsService.handle_exceptions do data = params[:data] - molecule = Molecule.find(params[:id]) + molecule = Molecule.find(params[:id]) if params[:id] != 'null' vendor = data[:vendor] language = data[:language] case data[:option] diff --git a/app/api/chemotion/collection_api.rb b/app/api/chemotion/collection_api.rb index 0cdd9e9113..acc172d396 100644 --- a/app/api/chemotion/collection_api.rb +++ b/app/api/chemotion/collection_api.rb @@ -1,4 +1,6 @@ module Chemotion + # rubocop: disable Metrics/ClassLength, Style/MultilineIfModifier, Layout/MultilineMethodCallBraceLayout + class CollectionAPI < Grape::API helpers CollectionHelpers helpers ParamsHelpers @@ -101,7 +103,7 @@ class CollectionAPI < Grape::API collects = Collection.where(user_id: current_user.id).unlocked.unshared.order('id') .select( <<~SQL - id, label, ancestry, is_synchronized, permission_level, position, collection_shared_names(user_id, id) as shared_names, + id, label, ancestry, is_synchronized, permission_level, tabs_segment, position, collection_shared_names(user_id, id) as shared_names, reaction_detail_level, sample_detail_level, screen_detail_level, wellplate_detail_level, element_detail_level, is_locked,is_shared, case when (ancestry is null) then cast(id as text) else concat(ancestry, chr(47), id) end as ancestry_root SQL @@ -116,7 +118,7 @@ class CollectionAPI < Grape::API .select( <<~SQL id, user_id, label,ancestry, permission_level, user_as_json(collections.user_id) as shared_to, - is_shared, is_locked, is_synchronized, false as is_remoted, + is_shared, is_locked, is_synchronized, false as is_remoted, tabs_segment, reaction_detail_level, sample_detail_level, screen_detail_level, wellplate_detail_level, element_detail_level, case when (ancestry is null) then cast(id as text) else concat(ancestry, chr(47), id) end as ancestry_root SQL @@ -130,7 +132,7 @@ class CollectionAPI < Grape::API collects = Collection.remote(current_user.id).where([" user_id in (select user_ids(?))",current_user.id]).order("id") .select( <<~SQL - id, user_id, label, ancestry, permission_level, user_as_json(collections.shared_by_id) as shared_by, + id, user_id, label, ancestry, permission_level, user_as_json(collections.shared_by_id) as shared_by, tabs_segment, case when (ancestry is null) then cast(id as text) else concat(ancestry, chr(47), id) end as ancestry_root, reaction_detail_level, sample_detail_level, screen_detail_level, wellplate_detail_level, is_locked, is_shared, shared_user_as_json(collections.user_id, #{current_user.id}) as shared_to,position @@ -190,6 +192,9 @@ class CollectionAPI < Grape::API optional :research_plan, type: Hash do use :ui_state_params end + optional :cell_line, type: Hash do + use :ui_state_params + end end requires :collection_attributes, type: Hash do requires :permission_level, type: Integer @@ -215,30 +220,38 @@ class CollectionAPI < Grape::API wellplates = Wellplate.by_collection_id(@cid).by_ui_state(params[:elements_filter][:wellplate]).for_user_n_groups(user_ids) screens = Screen.by_collection_id(@cid).by_ui_state(params[:elements_filter][:screen]).for_user_n_groups(user_ids) research_plans = ResearchPlan.by_collection_id(@cid).by_ui_state(params[:elements_filter][:research_plan]).for_user_n_groups(user_ids) + cell_lines = CelllineSample.by_collection_id(@cid) + .by_ui_state(params[:elements_filter][:cell_line]) + .for_user_n_groups(user_ids) elements = {} - ElementKlass.find_each { |klass| - elements[klass.name] = Element.by_collection_id(@cid).by_ui_state(params[:elements_filter][klass.name]).for_user_n_groups(user_ids) - } + Labimotion::ElementKlass.find_each do |klass| + elements[klass.name] = Labimotion::Element.by_collection_id(@cid).by_ui_state(params[:elements_filter][klass.name]).for_user_n_groups(user_ids) + end top_secret_sample = samples.pluck(:is_top_secret).any? top_secret_reaction = reactions.flat_map(&:samples).map(&:is_top_secret).any? top_secret_wellplate = wellplates.flat_map(&:samples).map(&:is_top_secret).any? top_secret_screen = screens.flat_map(&:wellplates).flat_map(&:samples).map(&:is_top_secret).any? is_top_secret = top_secret_sample || top_secret_wellplate || top_secret_reaction || top_secret_screen - share_samples = ElementsPolicy.new(current_user, samples).share? share_reactions = ElementsPolicy.new(current_user, reactions).share? share_wellplates = ElementsPolicy.new(current_user, wellplates).share? share_screens = ElementsPolicy.new(current_user, screens).share? share_research_plans = ElementsPolicy.new(current_user, research_plans).share? + share_cell_lines = ElementsPolicy.new(current_user, cell_lines).share? share_elements = !(elements&.length > 0) elements.each do |k, v| share_elements = ElementsPolicy.new(current_user, v).share? break unless share_elements end - sharing_allowed = share_samples && share_reactions && - share_wellplates && share_screens && share_research_plans && share_elements + sharing_allowed = share_samples && + share_reactions && + share_wellplates && + share_screens && + share_research_plans && + share_cell_lines && + share_elements error!('401 Unauthorized', 401) if (!sharing_allowed || is_top_secret) @sample_ids = samples.pluck(:id) @@ -246,6 +259,7 @@ class CollectionAPI < Grape::API @wellplate_ids = wellplates.pluck(:id) @screen_ids = screens.pluck(:id) @research_plan_ids = research_plans.pluck(:id) + @cell_line_ids = cell_lines.pluck(:id) @element_ids = elements&.transform_values { |v| v && v.pluck(:id) } end @@ -259,6 +273,7 @@ class CollectionAPI < Grape::API User.where(email: val).pluck :id end end.flatten.compact.uniq + Usecases::Sharing::ShareWithUsers.new( user_ids: uids, sample_ids: @sample_ids, @@ -266,6 +281,7 @@ class CollectionAPI < Grape::API wellplate_ids: @wellplate_ids, screen_ids: @screen_ids, research_plan_ids: @research_plan_ids, + cell_line_ids: @cell_line_ids, element_ids: @element_ids, collection_attributes: params[:collection_attributes].merge(shared_by_id: current_user.id) ).execute! @@ -302,28 +318,33 @@ class CollectionAPI < Grape::API API::ELEMENTS.each do |element| ui_state = params[:ui_state][element] next unless ui_state + ui_state[:checkedAll] = ui_state[:checkedAll] || ui_state[:all] ui_state[:checkedIds] = ui_state[:checkedIds].presence || ui_state[:included_ids] ui_state[:uncheckedIds] = ui_state[:uncheckedIds].presence || ui_state[:excluded_ids] + next unless ui_state[:checkedAll] || ui_state[:checkedIds].present? - collections_element_klass = ('collections_' + element).classify.constantize - element_klass = element.classify.constantize - ids = element_klass.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) - collections_element_klass.move_to_collection(ids, from_collection.id, to_collection_id) - collections_element_klass.remove_in_collection(ids, Collection.get_all_collection_for_user(current_user.id)[:id]) if params[:is_sync_to_me] + + classes = create_classes_of_element(element) + ids = classes[0].by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) + classes[1].move_to_collection(ids, from_collection.id, to_collection_id) + classes[1].remove_in_collection( + ids, + Collection.get_all_collection_for_user(current_user.id)[:id]) if params[:is_sync_to_me] end - klasses = ElementKlass.find_each do |klass| + klasses = Labimotion::ElementKlass.find_each do |klass| ui_state = params[:ui_state][klass.name] next unless ui_state + ui_state[:checkedAll] = ui_state[:checkedAll] || ui_state[:all] ui_state[:checkedIds] = ui_state[:checkedIds].presence || ui_state[:included_ids] ui_state[:uncheckedIds] = ui_state[:uncheckedIds].presence || ui_state[:excluded_ids] next unless ui_state[:checkedAll] || ui_state[:checkedIds].present? - ids = Element.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) - CollectionsElement.move_to_collection(ids, from_collection.id, to_collection_id, klass.name) - CollectionsElement.remove_in_collection(ids, Collection.get_all_collection_for_user(current_user.id)[:id]) if params[:is_sync_to_me] + ids = Labimotion::Element.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) + Labimotion::CollectionsElement.move_to_collection(ids, from_collection.id, to_collection_id, klass.name) + Labimotion::CollectionsElement.remove_in_collection(ids, Collection.get_all_collection_for_user(current_user.id)[:id]) if params[:is_sync_to_me] end status 204 @@ -349,26 +370,28 @@ class CollectionAPI < Grape::API API::ELEMENTS.each do |element| ui_state = params[:ui_state][element] next unless ui_state + ui_state[:checkedAll] = ui_state[:checkedAll] || ui_state[:all] ui_state[:checkedIds] = ui_state[:checkedIds].presence || ui_state[:included_ids] ui_state[:uncheckedIds] = ui_state[:uncheckedIds].presence || ui_state[:excluded_ids] next unless ui_state[:checkedAll] || ui_state[:checkedIds].present? - collections_element_klass = ('collections_' + element).classify.constantize - element_klass = element.classify.constantize - ids = element_klass.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) - collections_element_klass.create_in_collection(ids, to_collection_id) + classes = create_classes_of_element(element) + ids = classes[0].by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) + classes[1].create_in_collection(ids, to_collection_id) end - klasses = ElementKlass.find_each do |klass| + klasses = Labimotion::ElementKlass.find_each do |klass| ui_state = params[:ui_state][klass.name] next unless ui_state + ui_state[:checkedAll] = ui_state[:checkedAll] || ui_state[:all] ui_state[:checkedIds] = ui_state[:checkedIds].presence || ui_state[:included_ids] ui_state[:uncheckedIds] = ui_state[:uncheckedIds].presence || ui_state[:excluded_ids] next unless ui_state[:checkedAll] || ui_state[:checkedIds].present? - ids = Element.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) - CollectionsElement.create_in_collection(ids, to_collection_id, klass.name) + + ids = Labimotion::Element.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) + Labimotion::CollectionsElement.create_in_collection(ids, to_collection_id, klass.name) end status 204 @@ -392,29 +415,30 @@ class CollectionAPI < Grape::API API::ELEMENTS.each do |element| ui_state = params[:ui_state][element] next unless ui_state + ui_state[:checkedAll] = ui_state[:checkedAll] || ui_state[:all] ui_state[:checkedIds] = ui_state[:checkedIds].presence || ui_state[:included_ids] ui_state[:uncheckedIds] = ui_state[:uncheckedIds].presence || ui_state[:excluded_ids] ui_state[:collection_ids] = from_collection.id next unless ui_state[:checkedAll] || ui_state[:checkedIds].present? - collections_element_klass = ('collections_' + element).classify.constantize - element_klass = element.classify.constantize - ids = element_klass.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) - collections_element_klass.remove_in_collection(ids, from_collection.id) - end + classes = create_classes_of_element(element) + ids = classes[0].by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) + classes[1].remove_in_collection(ids, from_collection.id) + end - klasses = ElementKlass.find_each do |klass| + klasses = Labimotion::ElementKlass.find_each do |klass| ui_state = params[:ui_state][klass.name] next unless ui_state + ui_state[:checkedAll] = ui_state[:checkedAll] || ui_state[:all] ui_state[:checkedIds] = ui_state[:checkedIds].presence || ui_state[:included_ids] ui_state[:uncheckedIds] = ui_state[:uncheckedIds].presence || ui_state[:excluded_ids] ui_state[:collection_ids] = from_collection.id next unless ui_state[:checkedAll] || ui_state[:checkedIds].present? - ids = Element.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) - CollectionsElement.remove_in_collection(ids, from_collection.id) + ids = Labimotion::Element.by_collection_id(from_collection.id).by_ui_state(ui_state).pluck(:id) + Labimotion::CollectionsElement.remove_in_collection(ids, from_collection.id) end status 204 @@ -437,7 +461,7 @@ class CollectionAPI < Grape::API desc "Create export job" params do requires :collections, type: Array[Integer] - requires :format, type: Symbol, values: [:json, :zip, :udm] + requires :format, type: Symbol, values: %i[json zip udm] requires :nested, type: Boolean end @@ -459,8 +483,7 @@ class CollectionAPI < Grape::API error!('401 Unauthorized', 401) unless collection end end - - ExportCollectionsJob.perform_later(collection_ids, params[:format].to_s, nested, current_user.id) + ExportCollectionsJob.perform_now(collection_ids, params[:format].to_s, nested, current_user.id) status 204 end end @@ -489,12 +512,41 @@ class CollectionAPI < Grape::API tempfile.unlink end # run the asyncronous import job and return its id to the client - ImportCollectionsJob.perform_later(att, current_user.id) + ImportCollectionsJob.perform_now(att, current_user.id) status 204 end end end + namespace :tabs do + after_validation do + @collection = Collection.find(params[:id]) + error!('404 Collection with given id not found', 404) if @collection.nil? + error!('401 Unauthorized', 401) unless @collection.user_id == current_user.id + end + desc 'insert tab segments' + params do + requires :id, type: Integer, desc: 'collection id' + requires :segments, type: Hash, desc: 'orientation of the tabs' + end + post do + collection = Collection.find(params[:id]) + collection.update(tabs_segment: params[:segments]) + collection + end + + desc 'Update tab segment' + params do + requires :id, type: Integer, desc: 'Collection id' + optional :segment, type: Hash, desc: 'Tab segment type' + end + + patch do + @collection.update(tabs_segment: params[:segment]) + status 204 + end + end end end end +# rubocop: enable Metrics/ClassLength, Style/MultilineIfModifier, Layout/MultilineMethodCallBraceLayout diff --git a/app/api/chemotion/comment_api.rb b/app/api/chemotion/comment_api.rb new file mode 100644 index 0000000000..85660467b9 --- /dev/null +++ b/app/api/chemotion/comment_api.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +module Chemotion + class CommentAPI < Grape::API + helpers CommentHelpers + + rescue_from ActiveRecord::RecordNotFound do + error!('Comment not found', 400) + end + + helpers do + def authorize_commentable_access(commentable) + collections = Collection.where(id: commentable.collections.ids) + allowed_user_ids = authorized_users(collections) + + error!('401 Unauthorized', 401) unless allowed_user_ids.include?(current_user.id) + end + + def authorize_update_access(comment) + error!('401 Unauthorized', 401) unless comment.created_by == current_user.id || params[:status].eql?('Resolved') + end + + def authorize_delete_access(comment) + error!('401 Unauthorized', 401) unless comment.created_by == current_user.id + end + + def validate_comment_status(comment) + error!('422 Unprocessable Entity', 422) if comment.resolved? + end + + def find_commentable(commentable_type, commentable_id) + commentable_type.classify.constantize.find(commentable_id) + end + end + + resource :comments do + route_param :id do + desc 'Get a comment' + params do + requires :id, type: Integer, desc: 'Comment ID' + end + + get do + comment = Comment.find(params[:id]) + authorize_commentable_access(comment.commentable) + + present comment, with: Entities::CommentEntity, root: 'comment' + end + + desc 'Update a comment' + params do + requires :id, type: Integer, desc: 'Comment id' + requires :content, type: String + optional :status, type: String, values: %w[Pending Resolved] + optional :commentable_id, type: Integer + optional :commentable_type, type: String, values: Comment::COMMENTABLE_TYPE + end + + put do + comment = Comment.find(params[:id]) + + authorize_update_access(comment) + validate_comment_status(comment) + + attributes = declared(params, include_missing: false) + if params[:status].eql?('Resolved') + attributes[:resolver_name] = "#{current_user.first_name} #{current_user.last_name}" + end + comment.update!(attributes) + + if comment.saved_change_to_status? && comment.created_by != current_user.id + notify_comment_resolved(comment, current_user) + end + + present comment, with: Entities::CommentEntity, root: 'comment' + end + + desc 'Delete a comment' + params do + requires :id, type: Integer, desc: 'Comment id' + end + + delete do + comment = Comment.find(params[:id]) + authorize_delete_access(comment) + + comment.destroy + end + end + + desc 'Create a comment' + params do + requires :content, type: String + requires :commentable_id, type: Integer + requires :commentable_type, type: String, values: Comment::COMMENTABLE_TYPE + requires :section, + type: String, + values: Comment.sample_sections.values + + Comment.reaction_sections.values + + Comment.wellplate_sections.values + + Comment.screen_sections.values + + Comment.research_plan_sections.values + + Comment.header_sections.values + end + + post do + commentable = find_commentable(params[:commentable_type], params[:commentable_id]) + collections = Collection.where(id: commentable.collections.ids) + + allowed_user_ids = authorized_users(collections) + + error!('401 Unauthorized', 401) unless allowed_user_ids.include? current_user.id + + attributes = { + content: params[:content], + commentable_id: params[:commentable_id], + commentable_type: params[:commentable_type], + section: params[:section], + created_by: current_user.id, + submitter: "#{current_user.first_name} #{current_user.last_name}", + } + comment = Comment.new(attributes) + comment.save! + + create_message_notification(collections, current_user, commentable) + + present comment, with: Entities::CommentEntity, root: 'comment' + end + + desc 'Return comment by commentable_id and commentable_type' + params do + requires :commentable_id, type: Integer, desc: 'Commentable id' + requires :commentable_type, type: String, values: Comment::COMMENTABLE_TYPE + end + + get do + commentable = find_commentable(params[:commentable_type], params[:commentable_id]) + + authorize_commentable_access(commentable) + + comments = Comment.where( + commentable_id: params[:commentable_id], + commentable_type: params[:commentable_type], + ).order(:status, :section, created_at: :desc) + + present comments, with: Entities::CommentEntity, root: 'comments' + end + end + end +end diff --git a/app/api/chemotion/editor_api.rb b/app/api/chemotion/editor_api.rb index f94983874f..87e23f36ee 100644 --- a/app/api/chemotion/editor_api.rb +++ b/app/api/chemotion/editor_api.rb @@ -21,7 +21,9 @@ class EditorAPI < Grape::API end before do @attachment = Attachment.find_by(id: params[:attachment_id]) - unless ElementPolicy.new(current_user, ResearchPlan.find_by(id: @attachment[:attachable_id])).update? + if %w[ResearchPlan Labimotion::Element].include?(@attachment.attachable_type) + error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @attachment.attachable).update? + else error!('401 Unauthorized', 401) end # error!('401 Unauthorized', 401) if @attachment.oo_editing? diff --git a/app/api/chemotion/element_api.rb b/app/api/chemotion/element_api.rb index b173c8d39e..a5fa586371 100644 --- a/app/api/chemotion/element_api.rb +++ b/app/api/chemotion/element_api.rb @@ -8,6 +8,7 @@ class ElementAPI < Grape::API helpers ParamsHelpers helpers CollectionHelpers helpers LiteratureHelpers + helpers ReflectionHelpers namespace :ui_state do desc 'Delete elements by UI state' @@ -34,6 +35,9 @@ class ElementAPI < Grape::API optional :research_plan, type: Hash do use :ui_state_params end + optional :cell_line, type: Hash do + use :ui_state_params + end optional :selecteds, desc: 'Elements currently opened in detail tabs', type: Array do optional :type, type: String optional :id, type: Integer @@ -72,20 +76,23 @@ class ElementAPI < Grape::API desc "delete element from ui state selection." delete do - deleted = { 'sample' => [] } - %w[sample reaction wellplate screen research_plan].each do |element| + %w[sample reaction wellplate screen research_plan cell_line].each do |element| + next unless params[element] next unless params[element][:checkedAll] || params[element][:checkedIds].present? - deleted[element] = @collection.send(element + 's').by_ui_state(params[element]).destroy_all.map(&:id) + + assoziation_name = get_assoziation_name_in_collections(element) + deleted[element] = @collection.send(assoziation_name).by_ui_state(params[element]).destroy_all.map(&:id) end # explicit inner join on reactions_samples to get soft deleted reactions_samples entries + sql_join = "inner join reactions_samples on reactions_samples.sample_id = samples.id" sql_join += " and reactions_samples.type in ('ReactionsSolventSample','ReactionsReactantSample')" unless params[:options][:deleteSubsamples] deleted['sample'] += Sample.joins(sql_join).joins(:collections) .where(collections: { id: @collection.id }, reactions_samples: { reaction_id: deleted['reaction'] }) .destroy_all.map(&:id) - klasses = ElementKlass.find_each do |klass| + klasses = Labimotion::ElementKlass.find_each do |klass| next unless params[klass.name].present? && (params[klass.name][:checkedAll] || params[klass.name][:checkedIds].present?) deleted[klass.name] = @collection.send('elements').by_ui_state(params[klass.name]).destroy_all.map(&:id) end diff --git a/app/api/chemotion/generic_dataset_api.rb b/app/api/chemotion/generic_dataset_api.rb deleted file mode 100644 index 71cd641cad..0000000000 --- a/app/api/chemotion/generic_dataset_api.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Chemotion - class GenericDatasetAPI < Grape::API - include Grape::Kaminari - - resource :generic_dataset do - namespace :klasses do - desc "get dataset klasses" - get do - list = DatasetKlass.where(is_active: true) - present list.sort_by(&:place), with: Entities::DatasetKlassEntity, root: 'klass' - end - end - - namespace :list_dataset_klass do - desc 'list Generic Dataset Klass' - params do - optional :is_active, type: Boolean, desc: 'Active or Inactive Dataset' - end - get do - list = DatasetKlass.where(is_active: params[:is_active]) if params[:is_active].present? - list = DatasetKlass.all if params[:is_active].blank? - present list.sort_by(&:place), with: Entities::DatasetKlassEntity, root: 'klass' - end - end - end - end -end diff --git a/app/api/chemotion/generic_element_api.rb b/app/api/chemotion/generic_element_api.rb deleted file mode 100644 index 3b84f3e778..0000000000 --- a/app/api/chemotion/generic_element_api.rb +++ /dev/null @@ -1,329 +0,0 @@ -# frozen_string_literal: true - -module Chemotion - # Generic Element API - - # rubocop:disable Metrics/ClassLength - # rubocop:disable Style/MultilineIfModifier - # rubocop:disable Metrics/BlockLength - # rubocop:disable Style/MultilineIfThen - - class GenericElementAPI < Grape::API - include Grape::Kaminari - helpers ContainerHelpers - helpers ParamsHelpers - helpers CollectionHelpers - helpers SampleAssociationHelpers - helpers GenericHelpers - - resource :generic_elements do - namespace :klass do - desc 'get klass info' - params do - requires :name, type: String, desc: 'element klass name' - end - get do - ek = ElementKlass.find_by(name: params[:name]) - present ek, with: Entities::ElementKlassEntity, root: 'klass' - end - end - - namespace :klasses do - desc 'get klasses' - params do - optional :generic_only, type: Boolean, desc: 'list generic element only' - end - get do - list = ElementKlass - .where(is_active: true, is_generic: true) - .order('place') if params[:generic_only].present? && params[:generic_only] == true - list = ElementKlass - .where(is_active: true) - .order('place') unless params[:generic_only].present? && params[:generic_only] == true - - present list, with: Entities::ElementKlassEntity, root: 'klass' - end - end - - namespace :element_revisions do - desc 'list Generic Element Revisions' - params do - requires :id, type: Integer, desc: 'Generic Element Id' - end - get do - klass = Element.find(params[:id]) - list = klass.elements_revisions unless klass.nil? - present list&.sort_by(&:created_at).reverse, with: Entities::ElementRevisionEntity, root: 'revisions' - end - end - - namespace :delete_revision do - desc 'list Generic Element Revisions' - params do - requires :id, type: Integer, desc: 'Revision Id' - requires :element_id, type: Integer, desc: 'Element ID' - requires :klass, type: String, desc: 'Klass', values: %w[Element Segment Dataset] - end - post do - revision = "#{params[:klass]}sRevision".constantize.find(params[:id]) - element = params[:klass].constantize.find_by(id: params[:element_id]) unless revision.nil? - error!('Revision is invalid.', 404) if revision.nil? - error!('Can not delete the active revision.', 405) if revision.uuid == element.uuid - revision&.destroy! - status 201 - end - end - - namespace :segment_revisions do - desc 'list Generic Element Revisions' - params do - optional :id, type: Integer, desc: 'Generic Element Id' - end - get do - klass = Segment.find(params[:id]) - list = klass.segments_revisions unless klass.nil? - present list&.sort_by(&:created_at).reverse, with: Entities::SegmentRevisionEntity, root: 'revisions' - end - end - - namespace :upload_generics_files do - desc 'upload generic files' - params do - requires :att_id, type: Integer, desc: 'Element Id' - requires :att_type, type: String, desc: 'Element Type' - end - - after_validation do - el = params[:att_type].constantize.find_by(id: params[:att_id]) - error!('401 Unauthorized', 401) if el.nil? - - policy_updatable = ElementPolicy.new(current_user, el).update? - error!('401 Unauthorized', 401) unless policy_updatable - end - post do - attach_ary = [] - att_ary = create_uploads( - 'Element', - params[:att_id], - params[:elfiles], - params[:elInfo], - current_user.id, - ) if params[:elfiles].present? && params[:elInfo].present? - - (attach_ary << att_ary).flatten! unless att_ary&.empty? - - att_ary = create_uploads( - 'Segment', - params[:att_id], - params[:sefiles], - params[:seInfo], - current_user.id, - ) if params[:sefiles].present? && params[:seInfo].present? - - (attach_ary << att_ary).flatten! unless att_ary&.empty? - - if params[:attfiles].present? || params[:delfiles].present? then - att_ary = create_attachments( - params[:attfiles], - params[:delfiles], - params[:att_type], - params[:att_id], - params[:attfilesIdentifier], - current_user.id, - ) - end - (attach_ary << att_ary).flatten! unless att_ary&.empty? - true - end - end - - namespace :klasses_all do - desc 'get all klasses for admin function' - get do - list = ElementKlass.all.sort_by { |e| e.place } - present list, with: Entities::ElementKlassEntity, root: 'klass' - end - end - - desc 'Return serialized elements of current user' - params do - optional :collection_id, type: Integer, desc: 'Collection id' - optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id' - optional :el_type, type: String, desc: 'element klass name' - optional :from_date, type: Integer, desc: 'created_date from in ms' - optional :to_date, type: Integer, desc: 'created_date to in ms' - optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at' - end - paginate per_page: 7, offset: 0, max_per_page: 100 - get do - collection_id = - if params[:collection_id] - Collection - .belongs_to_or_shared_by(current_user.id, current_user.group_ids) - .find_by(id: params[:collection_id])&.id - elsif params[:sync_collection_id] - current_user - .all_sync_in_collections_users - .find_by(id: params[:sync_collection_id])&.collection&.id - end - - scope = - if collection_id - Element - .joins(:element_klass, :collections_elements) - .where( - element_klasses: { name: params[:el_type] }, - collections_elements: { collection_id: collection_id }, - ) - else - Element.none - end - - from = params[:from_date] - to = params[:to_date] - by_created_at = params[:filter_created_at] || false - - scope = scope.order('created_at DESC') - scope = scope.elements_created_time_from(Time.at(from)) if from && by_created_at - scope = scope.elements_created_time_to(Time.at(to) + 1.day) if to && by_created_at - scope = scope.elements_updated_time_from(Time.at(from)) if from && !by_created_at - scope = scope.elements_updated_time_to(Time.at(to) + 1.day) if to && !by_created_at - - reset_pagination_page(scope) - - generic_elements = paginate(scope).map do |element| - Entities::ElementEntity.represent( - element, - displayed_in_list: true, - detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, - ) - end - { generic_elements: generic_elements } - end - - desc 'Return serialized element by id' - params do - requires :id, type: Integer, desc: 'Element id' - end - route_param :id do - before do - error!('401 Unauthorized', 401) unless current_user.matrix_check_by_name('genericElement') && - ElementPolicy.new(current_user, Element.find(params[:id])).read? - end - - get do - element = Element.find(params[:id]) - { - element: Entities::ElementEntity.represent( - element, - detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, - ), - attachments: Entities::AttachmentEntity.represent(element.attachments), - } - end - end - - desc 'Create a element' - params do - requires :element_klass, type: Hash - requires :name, type: String - optional :properties, type: Hash - optional :collection_id, type: Integer - requires :container, type: Hash - optional :segments, type: Array, desc: 'Segments' - end - post do - klass = params[:element_klass] || {} - uuid = SecureRandom.uuid - params[:properties]['uuid'] = uuid - params[:properties]['klass_uuid'] = klass[:uuid] - params[:properties]['eln'] = Chemotion::Application.config.version - params[:properties]['klass'] = 'Element' - attributes = { - name: params[:name], - element_klass_id: klass[:id], - uuid: uuid, - klass_uuid: klass[:uuid], - properties: params[:properties], - created_by: current_user.id, - } - element = Element.new(attributes) - - if params[:collection_id] - collection = current_user.collections.find(params[:collection_id]) - element.collections << collection - end - - all_coll = Collection.get_all_collection_for_user(current_user.id) - element.collections << all_coll - element.save! - - element.properties = update_sample_association(element, params[:properties], current_user) - element.container = update_datamodel(params[:container]) - element.save! - element.save_segments(segments: params[:segments], current_user_id: current_user.id) - - present( - element, - with: Entities::ElementEntity, - root: :element, - detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, - ) - end - - desc 'Update element by id' - params do - requires :id, type: Integer, desc: 'element id' - optional :name, type: String - optional :properties, type: Hash - requires :container, type: Hash - optional :segments, type: Array, desc: 'Segments' - end - route_param :id do - before do - error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, Element.find(params[:id])).update? - end - - put do - element = Element.find(params[:id]) - - update_datamodel(params[:container]) - properties = update_sample_association(element, params[:properties], current_user) - params.delete(:container) - params.delete(:properties) - - attributes = declared(params.except(:segments), include_missing: false) - properties['eln'] = Chemotion::Application.config.version if properties['eln'] != - Chemotion::Application.config.version - if element.klass_uuid != - properties['klass_uuid'] || - element.properties != properties || - element.name != params[:name] - properties['klass'] = 'Element' - uuid = SecureRandom.uuid - properties['uuid'] = uuid - attributes['properties'] = properties - attributes['properties']['uuid'] = uuid - attributes['uuid'] = uuid - attributes['klass_uuid'] = properties['klass_uuid'] - - element.update(attributes) - end - element.save_segments(segments: params[:segments], current_user_id: current_user.id) - - { - element: Entities::ElementEntity.represent( - element, - detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, - ), - attachments: Entities::AttachmentEntity.represent(element.attachments), - } - end - end - end - end -end -# rubocop:enable Metrics/ClassLength -# rubocop:enable Style/MultilineIfModifier -# rubocop:enable Metrics/BlockLength -# rubocop:enable Style/MultilineIfThen diff --git a/app/api/chemotion/inbox_api.rb b/app/api/chemotion/inbox_api.rb index 39fa29fdd5..508b01d8af 100644 --- a/app/api/chemotion/inbox_api.rb +++ b/app/api/chemotion/inbox_api.rb @@ -4,9 +4,20 @@ module Chemotion class InboxAPI < Grape::API helpers ParamsHelpers + helpers do + def build_sort_params(params_sort_column) + sort_column = params_sort_column.eql?('created_at') ? params_sort_column : 'filename' + sort_direction = sort_column.eql?('created_at') ? 'DESC' : 'ASC' + { sort_column: sort_column, sort_direction: sort_direction } + end + end + resource :inbox do params do requires :cnt_only, type: Boolean, desc: 'return count number only' + optional :sort_column, type: String, desc: 'sort by creation time or name', + values: %w[created_at name], + default: 'name' end paginate per_page: 20, offset: 0, max_per_page: 50 @@ -20,7 +31,9 @@ class InboxAPI < Grape::API root: :inbox, only: [:inbox_count] else - scope = current_user.container.children.order(created_at: :desc) + sort_params = build_sort_params(params[:sort_column]) + + scope = current_user.container.children.order(:name) reset_pagination_page(scope) @@ -29,7 +42,7 @@ class InboxAPI < Grape::API end inbox_service = InboxService.new(current_user.container) - present inbox_service.to_hash(device_boxes) + present inbox_service.to_hash(device_boxes, sort_params, true) end end @@ -37,19 +50,42 @@ class InboxAPI < Grape::API params do requires :container_id, type: Integer, desc: 'subcontainer ID' optional :dataset_page, type: Integer, desc: 'Pagination number' + optional :sort_column, type: String, desc: 'sort by creation time or name', + values: %w[created_at name], + default: 'name' end get 'containers/:container_id' do if current_user.container.present? container = current_user.container.children.find params[:container_id] + dataset_sort_column = params[:sort_column].presence || 'name' + sort_params = build_sort_params(params[:sort_column]) + Entities::InboxEntity.represent(container, root_container: false, dataset_page: params[:dataset_page], + dataset_sort_column: dataset_sort_column, + sort_column: sort_params[:sort_column], + sort_direction: sort_params[:sort_direction], root: :inbox) end end + desc 'Returns unlinked attachments for inbox' + params do + optional :sort_column, type: String, desc: 'sort unlinked attachments by creation time or name', + values: %w[created_at name], + default: 'name' + end + + get 'unlinked_attachments' do + sort_params = build_sort_params(params[:sort_column]) + + inbox_service = InboxService.new(current_user.container) + present inbox_service.to_hash(nil, sort_params, false) + end + resource :samples do desc 'search samples from user by' params do diff --git a/app/api/chemotion/inventory_api.rb b/app/api/chemotion/inventory_api.rb new file mode 100644 index 0000000000..f5b3d29237 --- /dev/null +++ b/app/api/chemotion/inventory_api.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Chemotion + class InventoryAPI < Grape::API + resource :inventory do + namespace :update_inventory_label do + desc 'update inventory label for collection' + params do + requires :prefix, type: String + requires :name, type: String + requires :counter, type: Integer + requires :collection_ids, type: Array[Integer] + end + put do + collection_ids = params[:collection_ids] + unless params[:prefix].present? && params[:name].present? && + params[:counter].present? && collection_ids.present? + error!({ error: 'Missing required parameters' }, 400) + end + begin + Inventory.create_or_update_inventory_label( + params[:prefix], + params[:name], + params[:counter], + collection_ids, + current_user.id, + ) + rescue StandardError => e + error!({ error_type: e.class.name, error_message: e.message }, 500) + end + end + end + + namespace :user_inventory_collections do + desc 'get inventories and collections for user' + get do + { + inventory_collections: Collection.inventory_collections(current_user.id), + } + end + end + end + end +end diff --git a/app/api/chemotion/literature_api.rb b/app/api/chemotion/literature_api.rb index 995c43d71f..7fc62e31e8 100644 --- a/app/api/chemotion/literature_api.rb +++ b/app/api/chemotion/literature_api.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength, Rails/SkipsModelValidations, Style/MultilineIfThen + module Chemotion class LiteratureAPI < Grape::API helpers CollectionHelpers @@ -7,18 +9,28 @@ class LiteratureAPI < Grape::API helpers do def citation_for_elements(id = params[:element_id], type = @element_klass, cat = 'detail') - return Literature.none unless id.present? + return Literature.none if id.blank? + Literature.by_element_attributes_and_cat(id, type, cat).with_user_info end end resource :literatures do after_validation do - unless request.url =~ /doi\/metadata|ui_state|collection/ - @element_klass = params[:element_type].classify - @element = @element_klass.constantize.find_by(id: params[:element_id]) - @element_policy = ElementPolicy.new(current_user, @element) - allowed = if request.env['REQUEST_METHOD'] =~ /get/i + unless %r{doi/metadata|ui_state|collection}.match?(request.url) + if params[:element_type] == 'cell_line' + cell_line_sample = CelllineSample.find(params[:element_id]) + @element = CelllineMaterial.find(cell_line_sample.cellline_material_id) + @element_policy = ElementPolicy.new(current_user, cell_line_sample) + @element_klass = 'CelllineMaterial' + params[:element_id] = @element.id + else + @element_klass = params[:element_type].classify + @element = @element_klass.constantize.find_by(id: params[:element_id]) + @element_policy = ElementPolicy.new(current_user, @element) + end + + allowed = if /get/i.match?(request.env['REQUEST_METHOD']) @element_policy.read? else @element_policy.update? @@ -30,9 +42,9 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = desc 'Update type of literals by element' params do requires :element_id, type: Integer - requires :element_type, type: String, values: %w[sample reaction research_plan] + requires :element_type, type: String, values: %w[sample reaction research_plan cell_line] requires :id, type: Integer - requires :litype, type: String, values: %w[citedOwn citedRef referTo] + requires :litype, type: String, values: %w[citedOwn citedRef referTo literatureOfSource additionalLiterature] end put do Literal.find(params[:id])&.update(litype: params[:litype]) @@ -42,14 +54,14 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = with: Entities::LiteratureEntity, root: :literatures, with_element_count: false, - with_user_info: true + with_user_info: true, ) end desc 'Return the literature list for the given element' params do requires :element_id, type: Integer - requires :element_type, type: String, values: %w[sample reaction research_plan] + requires :element_type, type: String, values: %w[sample reaction research_plan cell_line] end get do @@ -58,14 +70,14 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = with: Entities::LiteratureEntity, root: :literatures, with_element_count: false, - with_user_info: true + with_user_info: true, ) end desc 'create a literature entry' params do requires :element_id, type: Integer - requires :element_type, type: String, values: %w[sample reaction research_plan] + requires :element_type, type: String, values: %w[sample reaction research_plan cell_line] requires :ref, type: Hash do optional :is_new, type: Boolean optional :id, types: [Integer, String] @@ -87,7 +99,7 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = doi: params[:ref][:doi], url: params[:ref][:url], title: params[:ref][:title], - isbn: params[:ref][:isbn] + isbn: params[:ref][:isbn], ) else Literature.find_by(id: params[:ref][:id]) @@ -100,7 +112,7 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = element_type: @element_klass, element_id: params[:element_id], litype: params[:ref][:litype], - category: 'detail' + category: 'detail', } unless Literal.find_by(attributes) Literal.create(attributes) @@ -112,27 +124,29 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = with: Entities::LiteratureEntity, root: :literatures, with_element_count: false, - with_user_info: true + with_user_info: true, ) - end params do requires :element_id, type: Integer - requires :element_type, type: String, values: %w[sample reaction research_plan] + requires :element_type, type: String, values: %w[sample reaction research_plan cell_line] requires :id, type: Integer end delete do - Literal.find_by( + literal = Literal.find_by( id: params[:id], # user_id: current_user.id, element_type: @element_klass, element_id: params[:element_id], - category: 'detail' - )&.destroy! + category: 'detail', + ) - {} + error!('Literal not found', 400) unless literal + + literal.destroy! + status 200 end namespace :collection do @@ -150,16 +164,19 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = sample_ids = @dl_s > 1 ? @c.sample_ids : [] reaction_ids = @dl_r > 1 ? @c.reaction_ids : [] research_plan_ids = @dl_rp > 1 ? @c.research_plan_ids : [] - collection_references = Literature.by_element_attributes_and_cat(@c_id, 'Collection', 'detail').group_by_element + collection_references = Literature.by_element_attributes_and_cat(@c_id, 'Collection', + 'detail').group_by_element sample_references = Literature.by_element_attributes_and_cat(sample_ids, 'Sample', 'detail').group_by_element - reaction_references = Literature.by_element_attributes_and_cat(reaction_ids, 'Reaction', 'detail').group_by_element - research_plan_references = Literature.by_element_attributes_and_cat(research_plan_ids, 'ResearchPlan', 'detail').group_by_element + reaction_references = Literature.by_element_attributes_and_cat(reaction_ids, 'Reaction', + 'detail').group_by_element + research_plan_references = Literature.by_element_attributes_and_cat(research_plan_ids, 'ResearchPlan', + 'detail').group_by_element { collectionRefs: Entities::LiteratureEntity.represent(collection_references, with_element_count: true), sampleRefs: Entities::LiteratureEntity.represent(sample_references, with_element_count: true), reactionRefs: Entities::LiteratureEntity.represent(reaction_references, with_element_count: true), - researchPlanRefs: Entities::LiteratureEntity.represent(research_plan_references, with_element_count: true) + researchPlanRefs: Entities::LiteratureEntity.represent(research_plan_references, with_element_count: true), } end end @@ -198,26 +215,26 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = post do if params[:ref] && @pl >= 1 lit = if params[:ref][:is_new] - Literature.find_or_create_by( - doi: params[:ref][:doi], - url: params[:ref][:url], - title: params[:ref][:title] - ) - else - Literature.find_by(id: params[:ref][:id]) - end + Literature.find_or_create_by( + doi: params[:ref][:doi], + url: params[:ref][:url], + title: params[:ref][:title], + ) + else + Literature.find_by(id: params[:ref][:id]) + end lit.update!(refs: (lit.refs || {}).merge(declared(params)[:ref][:refs])) if params[:ref][:refs] if lit - { 'Sample': @sids, 'Reaction': @rids }.each do |type, ids| + { Sample: @sids, Reaction: @rids }.each do |type, ids| ids.each do |id| - ltl = Literal.find_or_create_by( + Literal.find_or_create_by( literature_id: lit.id, user_id: current_user.id, element_type: type, element_id: id, litype: params[:ref][:litype], - category: 'detail' + category: 'detail', ) end end @@ -225,13 +242,14 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = end sample_references = Literature.by_element_attributes_and_cat(@sids, 'Sample', @cat).with_element_and_user_info - reaction_references = Literature.by_element_attributes_and_cat(@rids, 'Reaction', @cat).with_element_and_user_info + reaction_references = Literature.by_element_attributes_and_cat(@rids, 'Reaction', + @cat).with_element_and_user_info present( sample_references + reaction_references, with: Entities::LiteratureEntity, root: :selectedRefs, - with_element_and_user_info: true + with_element_and_user_info: true, ) end end @@ -243,10 +261,10 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = end after_validation do - params[:doi].match(/(?:\s*10\.)(\S+)\/(\S+)/) - @doi_prefix = $1 - @doi_suffix = $2 - error(400) unless (@doi_prefix.present? && @doi_suffix.present?) + params[:doi] =~ %r{(?:\s*10\.)(\S+)/(\S+)} + @doi_prefix = ::Regexp.last_match(1) + @doi_suffix = ::Regexp.last_match(2) + error(400) unless @doi_prefix.present? && @doi_suffix.present? end get :metadata do @@ -255,17 +273,17 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = faraday.headers = { 'Accept' => 'application/x-bibtex' } end resp = connection.get { |req| req.url("/10.#{@doi_prefix}/#{@doi_suffix}") } - unless resp.success? - error!({ error: reason_phrase }, resp.status) - end + error!({ error: reason_phrase }, resp.status) unless resp.success? resp_json = begin - JSON.parse(BibTeX.parse(resp.body).to_json) - rescue StandardError => e - error!(e, 503) - end + JSON.parse(BibTeX.parse(resp.body).to_json) + rescue StandardError => e + error!(e, 503) + end { bibtex: resp.body, BTjson: resp_json } end end end end end +# rubocop:enable Metrics/ClassLength +# rubocop:enable Rails/SkipsModelValidations, Style/MultilineIfThen diff --git a/app/api/chemotion/message_api.rb b/app/api/chemotion/message_api.rb index 34f6923f43..eb20f51eba 100644 --- a/app/api/chemotion/message_api.rb +++ b/app/api/chemotion/message_api.rb @@ -124,8 +124,7 @@ class MessageAPI < Grape::API message_from: current_user.id, message_to: params[:user_ids] ) - - present message, with: Entities::MessageEntity, root: :message + status 204 if message end end end diff --git a/app/api/chemotion/molecule_api.rb b/app/api/chemotion/molecule_api.rb index 9731a9be62..a1f11be5ba 100644 --- a/app/api/chemotion/molecule_api.rb +++ b/app/api/chemotion/molecule_api.rb @@ -35,7 +35,7 @@ class MoleculeAPI < Grape::API babel_info = OpenBabelService.molecule_info_from_structure(smiles, 'smi') inchikey = babel_info[:inchikey] - return {} unless inchikey + return {} if inchikey.blank? molecule = Molecule.find_by(inchikey: inchikey, is_partial: false) unless molecule @@ -60,6 +60,7 @@ class MoleculeAPI < Grape::API end return {} unless molfile molecule = Molecule.find_or_create_by_molfile(molfile, babel_info) + molecule = Molecule.find_or_create_dummy if molecule.blank? end return unless molecule @@ -151,6 +152,7 @@ class MoleculeAPI < Grape::API ob = '' else molecule = Molecule.find_or_create_by_molfile(molfile) + molecule = Molecule.find_or_create_dummy if molecule.blank? ob = molecule&.ob_log end molecule&.attributes&.merge(temp_svg: svg_name, ob_log: ob) @@ -164,21 +166,21 @@ class MoleculeAPI < Grape::API requires :molfile, type: String, desc: 'Molecule molfile' optional :svg_file, type: String, desc: 'Molecule svg file' optional :editor, type: String, desc: 'SVGProcessor' - optional :decoupled, type: Boolean, desc: 'decouple from molecule', default: false + optional :decoupled, type: Boolean, desc: 'Is decoupled sample?', default: false end post do svg = params[:svg_file] molfile = params[:molfile] decoupled = params[:decoupled] - molecule = Molecule.find_or_create_by_molfile(molfile) - molecule = Molecule.find_by(inchikey: 'DUMMY') if molecule.blank? && decoupled + molecule = decoupled ? Molecule.find_or_create_dummy : Molecule.find_or_create_by_molfile(molfile) + molecule = Molecule.find_or_create_dummy if molecule.blank? ob = molecule&.ob_log - if params[:svg_file].present? + if svg.present? svg_process = SVG::Processor.new.structure_svg(params[:editor], svg, molfile) else svg_file_src = Rails.public_path.join('images', 'molecules', molecule.molecule_svg_file) if File.exist?(svg_file_src) - mol = molecule.molfile.lines[0..1] + mol = molecule.molfile.lines.first(2) if mol[1]&.strip&.match?('OpenBabel') svg = File.read(svg_file_src) svg_process = SVG::Processor.new.structure_svg('openbabel', svg, molfile) @@ -189,7 +191,6 @@ class MoleculeAPI < Grape::API end end molecule&.attributes&.merge(temp_svg: svg_process[:svg_file_name], ob_log: ob) - Entities::MoleculeEntity.represent(molecule, temp_svg: svg_process[:svg_file_name], ob_log: ob) end diff --git a/app/api/chemotion/ols_terms_api.rb b/app/api/chemotion/ols_terms_api.rb index 94e421e64d..0da44101f4 100644 --- a/app/api/chemotion/ols_terms_api.rb +++ b/app/api/chemotion/ols_terms_api.rb @@ -6,19 +6,19 @@ class OlsTermsAPI < Grape::API namespace :ols_terms do desc 'Get List' params do - requires :name, type: String, desc: 'OLS Name', values: %w[chmo rxno] + requires :name, type: String, desc: 'OLS Name', values: %w[chmo rxno bao] optional :edited, type: Boolean, default: true, desc: 'Only list visible terms' end get 'list' do file = Rails.public_path.join( 'ontologies', - "#{params[:name]}#{params[:edited] ? '.edited.json' : '.json'}" + "#{params[:name]}#{params[:edited] ? '.edited.json' : '.json'}", ) unless File.exist?(file) file = Rails.public_path.join( 'ontologies_default', - "#{params[:name]}#{params[:edited] ? '.default.edited.json' : '.default.json'}" + "#{params[:name]}#{params[:edited] ? '.default.edited.json' : '.default.json'}", ) end result = JSON.parse(File.read(file, encoding: 'bom|utf-8')) if File.exist?(file) diff --git a/app/api/chemotion/permission_api.rb b/app/api/chemotion/permission_api.rb index 7481d0af8b..c89848d720 100644 --- a/app/api/chemotion/permission_api.rb +++ b/app/api/chemotion/permission_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Chemotion class PermissionAPI < Grape::API helpers CollectionHelpers @@ -6,49 +8,101 @@ class PermissionAPI < Grape::API def non_empty(filter) return true if filter.all return true if filter.included_ids.any? + false end end resource :permissions do - namespace :status do - desc "Returns if selected elements contain a top secret sample" + desc 'Returns if selected elements contain a top secret sample' params do use :main_ui_state_params end post do - cid = fetch_collection_id_w_current_user(params[:currentCollection][:id], params[:currentCollection][:is_sync_to_me]) - sel, has_sel = {}, {} + cid = fetch_collection_id_w_current_user(params[:currentCollection][:id], + params[:currentCollection][:is_sync_to_me]) + sel = {} + has_sel = {} + API::ELEMENTS.each do |element| ui_state = params[element] + if ui_state && (ui_state[:checkedAll] || ui_state[:checkedIds].present?) - element_klass = element.classify.constantize + element_klass = if element == 'cell_line' + CelllineSample + else + element.classify.constantize + end sel[element] = element_klass.by_collection_id(cid).by_ui_state(params[:sample]) - .for_user_n_groups(user_ids) + .for_user_n_groups(user_ids) end has_sel[element] = sel[element].present? end is_top_secret = has_sel['sample'] ? sel['sample'].pluck(:is_top_secret).any? : false - is_top_secret = is_top_secret || (has_sel['reaction'] ? sel['reaction'].lazy.flat_map(&:samples).map(&:is_top_secret).any? : false) - is_top_secret = is_top_secret || (has_sel['wellplate'] ? sel['wellplate'].lazy.flat_map(&:samples).map(&:is_top_secret).any? : false) - is_top_secret = is_top_secret || (has_sel['screen'] ? sel['screen'].lazy.flat_map(&:wellplates).flat_map(&:samples).map(&:is_top_secret).any? : false) + is_top_secret ||= if has_sel['reaction'] + sel['reaction'].lazy.flat_map(&:samples).map(&:is_top_secret).any? + else + false + end + + is_top_secret ||= if has_sel['wellplate'] + sel['wellplate'].lazy.flat_map(&:samples).map(&:is_top_secret).any? + else + false + end + + is_top_secret ||= if has_sel['screen'] + sel['screen'].lazy.flat_map(&:wellplates).flat_map(&:samples).map(&:is_top_secret).any? + else + false + end deletion_allowed = true sharing_allowed = true - if (params[:currentCollection][:is_sync_to_me] || params[:currentCollection][:is_shared]) + if params[:currentCollection][:is_sync_to_me] || params[:currentCollection][:is_shared] deletion_allowed = has_sel['sample'] ? ElementsPolicy.new(current_user, sel['sample']).destroy? : true - deletion_allowed = deletion_allowed && (has_sel['reaction'] ? ElementsPolicy.new(current_user, sel['reaction']).destroy? : true) - deletion_allowed = deletion_allowed && (has_sel['wellplate'] ? ElementsPolicy.new(current_user, sel['wellplate']).destroy? : true) - deletion_allowed = deletion_allowed && (has_sel['screen'] ? ElementsPolicy.new(current_user, sel['screen']).destroy? : true) + deletion_allowed &&= (if has_sel['reaction'] + ElementsPolicy.new(current_user, + sel['reaction']).destroy? + else + true + end) + deletion_allowed &&= (if has_sel['wellplate'] + ElementsPolicy.new(current_user, + sel['wellplate']).destroy? + else + true + end) + deletion_allowed &&= (if has_sel['screen'] + ElementsPolicy.new(current_user, + sel['screen']).destroy? + else + true + end) if deletion_allowed sharing_allowed = true else sharing_allowed = has_sel['sample'] ? ElementsPolicy.new(current_user, sel['sample']).share? : true - sharing_allowed = sharing_allowed && has_sel['reaction'] ? ElementsPolicy.new(current_user, sel['reaction']).share? : true - sharing_allowed = sharing_allowed && has_sel['wellplate'] ? ElementsPolicy.new(current_user, sel['wellplate']).share? : true - sharing_allowed = sharing_allowed && has_sel['screen'] ? ElementsPolicy.new(current_user, sel['screen']).share? : true + sharing_allowed = if sharing_allowed && has_sel['reaction'] + ElementsPolicy.new(current_user, + sel['reaction']).share? + else + true + end + sharing_allowed = if sharing_allowed && has_sel['wellplate'] + ElementsPolicy.new(current_user, + sel['wellplate']).share? + else + true + end + sharing_allowed = if sharing_allowed && has_sel['screen'] + ElementsPolicy.new(current_user, + sel['screen']).share? + else + true + end end end { deletion_allowed: deletion_allowed, sharing_allowed: sharing_allowed, is_top_secret: is_top_secret } diff --git a/app/api/chemotion/private_note_api.rb b/app/api/chemotion/private_note_api.rb index ce98cab470..a74c82a3d8 100644 --- a/app/api/chemotion/private_note_api.rb +++ b/app/api/chemotion/private_note_api.rb @@ -1,19 +1,50 @@ +# frozen_string_literal: true + module Chemotion class PrivateNoteAPI < Grape::API + rescue_from ActiveRecord::RecordNotFound do + error!('404 Private note with given id not found', 404) + end + + helpers do + def authorize_note_access!(note) + error!('401 Unauthorized', 401) unless note.created_by == current_user.id + end + end + resource :private_notes do - desc 'Return private note by id' params do requires :id, type: Integer, desc: 'Private note id' end route_param :id do + before do + @note = PrivateNote.find(params[:id]) + authorize_note_access!(@note) + end + + desc 'Return private note by id' get do - note = PrivateNote.find(params[:id]) - if note.created_by == current_user.id - present note, with: Entities::PrivateNoteEntity, root: 'note' - else - error!('401 Unauthorized', 401) - end + present @note, with: Entities::PrivateNoteEntity, root: 'note' + end + + desc 'Update a note' + params do + requires :content, type: String + optional :noteable_id, type: Integer + optional :noteable_type, type: String, values: %w[Sample Reaction Wellplate Screen ResearchPlan] + end + + put do + attributes = declared(params, include_missing: false) + @note.update!(attributes) + + present @note, with: Entities::PrivateNoteEntity, root: 'note' + end + + desc 'Delete a note' + delete do + @note.destroy end end @@ -25,69 +56,31 @@ class PrivateNoteAPI < Grape::API get do note = PrivateNote.find_by( - noteable_id: params[:noteable_id], noteable_type: params[:noteable_type], created_by: current_user.id + noteable_id: params[:noteable_id], + noteable_type: params[:noteable_type], + created_by: current_user.id, ) || PrivateNote.new present note, with: Entities::PrivateNoteEntity, root: 'note' end - resource :create do - desc 'Create a note' - params do - requires :content, type: String - requires :noteable_id, type: Integer - requires :noteable_type, type: String, values: %w[Sample Reaction Wellplate Screen ResearchPlan] - end - - post do - attributes = { - content: params[:content], - noteable_id: params[:noteable_id], - noteable_type: params[:noteable_type], - created_by: current_user.id - } - note = PrivateNote.new(attributes) - note.save! - - present note, with: Entities::PrivateNoteEntity, root: 'note' - end - end - - desc 'Update a note' + desc 'Create a note' params do - requires :id, type: Integer, desc: 'Private note id' requires :content, type: String - optional :noteable_id, type: Integer - optional :noteable_type, type: String, values: %w[Sample Reaction Wellplate Screen ResearchPlan] - end - route_param :id do - after_validation do - @note = PrivateNote.find(params[:id]) - error!('404 Private note with given id not found', 404) if @note.nil? - error!('401 Unauthorized', 401) unless @note.created_by == current_user.id - end - - put do - attributes = declared(params, include_missing: false) - @note.update!(attributes) - - present @note, with: Entities::PrivateNoteEntity, root: 'note' - end + requires :noteable_id, type: Integer + requires :noteable_type, type: String, values: %w[Sample Reaction Wellplate Screen ResearchPlan] end - desc 'Delete a note' - params do - requires :id, type: Integer, desc: 'Private note id' - end - route_param :id do - after_validation do - @note = PrivateNote.find(params[:id]) - error!('404 Private note with given id not found', 404) if @note.nil? - error!('401 Unauthorized', 401) unless @note.created_by == current_user.id - end + post do + attributes = { + content: params[:content], + noteable_id: params[:noteable_id], + noteable_type: params[:noteable_type], + created_by: current_user.id, + } + note = PrivateNote.new(attributes) + note.save! - delete do - @note.destroy - end + present note, with: Entities::PrivateNoteEntity, root: 'note' end end end diff --git a/app/api/chemotion/profile_api.rb b/app/api/chemotion/profile_api.rb index cb2b5c7daf..a54f0a18b1 100644 --- a/app/api/chemotion/profile_api.rb +++ b/app/api/chemotion/profile_api.rb @@ -1,12 +1,14 @@ -module Chemotion +# frozen_string_literal: true +# rubocop: disable Style/MultilineIfModifier +module Chemotion class ProfileLayoutHash < Grape::Validations::Validators::Base def validate_param!(attr_name, params) fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], - message: "has too many entries" if params[attr_name].keys.size > 30 + message: "has too many entries" if params[attr_name].keys.size > 100 params[attr_name].each do |key, val| fail(Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], - message: "has wrong structure") unless key.to_s =~ /\A[\w \-]+\Z/ + message: "has wrong structure") unless key.to_s =~ /\A[\w \(\)\-]+\Z/ fail(Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "has wrong structure") unless val.to_s =~ /\d+/ end @@ -26,9 +28,9 @@ class ProfileAPI < Grape::API end if current_user.matrix_check_by_name('genericElement') - available_elments = ElementKlass.where(is_active: true).pluck(:name) + available_elments = Labimotion::ElementKlass.where(is_active: true).pluck(:name) new_layout = data['layout'] || {} - ElementKlass.where(is_active: true).find_each do |el| + Labimotion::ElementKlass.where(is_active: true).find_each do |el| if data['layout'] && data['layout']["#{el.name}"].nil? new_layout["#{el.name}"] = new_layout&.values&.min < 0 ? new_layout&.values.min-1 : -1; end @@ -80,6 +82,7 @@ class ProfileAPI < Grape::API optional :cur_template_idx, type: Integer end optional :default_structure_editor, type: String + optional :filters, type: Hash end optional :show_external_name, type: Boolean optional :show_sample_name, type: Boolean @@ -89,9 +92,15 @@ class ProfileAPI < Grape::API put do declared_params = declared(params, include_missing: false) data = current_user.profile.data || {} - available_ements = API::ELEMENTS + ElementKlass.where(is_active: true).pluck(:name) - - data['layout'] = { 'sample' => 1, 'reaction' => 2, 'wellplate' => 3, 'screen' => 4, 'research_plan' => 5 } if data['layout'].nil? + available_ements = API::ELEMENTS + Labimotion::ElementKlass.where(is_active: true).pluck(:name) + data['layout'] = { + 'sample' => 1, + 'reaction' => 2, + 'wellplate' => 3, + 'screen' => 4, + 'research_plan' => 5, + 'cell_line' => -1000, + } if data['layout'].nil? layout = data['layout'].select { |e| available_ements.include?(e) } data['layout'] = layout.sort_by { |_k, v| v }.to_h @@ -110,3 +119,4 @@ class ProfileAPI < Grape::API end end end +# rubocop: enable Style/MultilineIfModifier diff --git a/app/api/chemotion/public_api.rb b/app/api/chemotion/public_api.rb index 6fb448e8d9..c830df5372 100644 --- a/app/api/chemotion/public_api.rb +++ b/app/api/chemotion/public_api.rb @@ -44,27 +44,17 @@ def send_notification(attachment, user, status, has_error = false) end end - namespace :element_klasses_name do - desc 'get klasses' - params do - optional :generic_only, type: Boolean, desc: 'list generic element only' - end - get do - list = ElementKlass.where(is_active: true) if params[:generic_only].present? && params[:generic_only] == true - unless params[:generic_only].present? && params[:generic_only] == true - list = ElementKlass.where(is_active: true) - end - list.pluck(:name) - end - end namespace :omniauth_providers do desc 'get omniauth providers' get do res = {} config = Devise.omniauth_configs - config.each { |k, _v| res[k] = { icon: File.basename(config[k].options[:icon] || '') } } - res + extra_rules = Matrice.extra_rules + config.each do |k, _v| + res[k] = { icon: File.basename(config[k].options[:icon] || ''), label: config[k].options[:label] } + end + { omniauth_providers: res, extra_rules: extra_rules } end end diff --git a/app/api/chemotion/reaction_api.rb b/app/api/chemotion/reaction_api.rb index 0220e01cc1..5c70814359 100644 --- a/app/api/chemotion/reaction_api.rb +++ b/app/api/chemotion/reaction_api.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength module Chemotion # Reaction API class ReactionAPI < Grape::API @@ -17,6 +18,11 @@ class ReactionAPI < Grape::API optional :from_date, type: Integer, desc: 'created_date from in ms' optional :to_date, type: Integer, desc: 'created_date to in ms' optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at' + optional :sort_column, type: String, desc: 'sort by created_at, updated_at, rinchi_short_key, or rxno', + values: %w[created_at updated_at rinchi_short_key rxno], + default: 'created_at' + optional :sort_direction, type: String, desc: 'sort direction', + values: %w[ASC DESC] end paginate per_page: 7, offset: 0 @@ -42,13 +48,17 @@ class ReactionAPI < Grape::API end else Reaction.joins(:collections).where(collections: { user_id: current_user.id }).distinct - end.order('created_at DESC') + end from = params[:from_date] to = params[:to_date] by_created_at = params[:filter_created_at] || false - scope = scope.includes_for_list_display + sort_column = params[:sort_column].presence || 'created_at' + sort_direction = params[:sort_direction].presence || + (%w[created_at updated_at].include?(sort_column) ? 'DESC' : 'ASC') + + scope = scope.includes_for_list_display.order("#{sort_column} #{sort_direction}") scope = scope.created_time_from(Time.at(from)) if from && by_created_at scope = scope.created_time_to(Time.at(to) + 1.day) if to && by_created_at scope = scope.updated_time_from(Time.at(from)) if from && !by_created_at @@ -72,13 +82,16 @@ class ReactionAPI < Grape::API requires :id, type: Integer, desc: 'Reaction id' end route_param :id do - before do + after_validation do @element_policy = ElementPolicy.new(current_user, Reaction.find(params[:id])) error!('401 Unauthorized', 401) unless @element_policy.read? + rescue ActiveRecord::RecordNotFound + error!('404 Not Found', 404) end get do reaction = Reaction.find(params[:id]) + class_name = reaction&.class&.name { reaction: Entities::ReactionEntity.represent( @@ -86,7 +99,7 @@ class ReactionAPI < Grape::API policy: @element_policy, detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: reaction).detail_levels, ), - literatures: Entities::LiteratureEntity.represent(citation_for_elements(params[:id], 'Reaction')), + literatures: Entities::LiteratureEntity.represent(citation_for_elements(params[:id], class_name)), } end end @@ -149,6 +162,7 @@ class ReactionAPI < Grape::API optional :duration, type: String optional :rxno, type: String optional :segments, type: Array + optional :variations, type: [Hash] end route_param :id do after_validation do @@ -214,6 +228,7 @@ class ReactionAPI < Grape::API requires :container, type: Hash optional :duration, type: String optional :rxno, type: String + optional :variations, type: [Hash] end post do @@ -308,3 +323,4 @@ class ReactionAPI < Grape::API end end end +# rubocop:enable Metrics/ClassLength diff --git a/app/api/chemotion/report_api.rb b/app/api/chemotion/report_api.rb index 4386b72fc8..9d2b736a3d 100644 --- a/app/api/chemotion/report_api.rb +++ b/app/api/chemotion/report_api.rb @@ -56,34 +56,24 @@ def is_int? end c_id = params[:uiState][:currentCollection] c_id = SyncCollectionsUser.find(c_id)&.collection_id if params[:uiState][:isSync] - %i[sample reaction wellplate].each do |table| - next unless (p_t = params[:uiState][table]) - ids = p_t[:checkedAll] ? p_t[:uncheckedIds] : p_t[:checkedIds] - next unless p_t[:checkedAll] || ids.present? + table_params = { + ui_state: params[:uiState], + c_id: c_id, + } - column_query = build_column_query(filter_column_selection(table), current_user.id) - sql_query = send("build_sql_#{table}_sample", column_query, c_id, ids, p_t[:checkedAll]) - next unless sql_query - - result = db_exec_query(sql_query) - export.generate_sheet_with_samples(table, result) + if params[:columns][:chemicals].blank? + generate_sheets_for_tables(%i[sample reaction wellplate], table_params, export) end if params[:exportType] == 1 && params[:columns][:analyses].present? - %i[sample].each do |table| - next unless (p_t = params[:uiState][table]) - - ids = p_t[:checkedAll] ? p_t[:uncheckedIds] : p_t[:checkedIds] - next unless p_t[:checkedAll] || ids - - column_query = build_column_query(filter_column_selection("#{table}_analyses".to_sym), current_user.id) - sql_query = send("build_sql_#{table}_analyses", column_query, c_id, ids, p_t[:checkedAll]) - next unless sql_query + generate_sheets_for_tables(%i[sample], table_params, export, params[:columns][:analyses], :analyses) + end - result = db_exec_query(sql_query) - export.generate_analyses_sheet_with_samples("#{table}_analyses".to_sym, result, params[:columns][:analyses]) - end + if params[:exportType] == 1 && params[:columns][:chemicals].present? + generate_sheets_for_tables(%i[sample], table_params, export, params[:columns][:chemicals], + :chemicals) + generate_sheets_for_tables(%i[reaction wellplate], table_params, export) end case export.file_extension diff --git a/app/api/chemotion/research_plan_api.rb b/app/api/chemotion/research_plan_api.rb index 599cc74813..740482bb3f 100644 --- a/app/api/chemotion/research_plan_api.rb +++ b/app/api/chemotion/research_plan_api.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + module Chemotion + # rubocop: disable Metrics/ClassLength + class ResearchPlanAPI < Grape::API include Grape::Kaminari helpers ParamsHelpers @@ -18,8 +22,8 @@ class ResearchPlanAPI < Grape::API get do scope = if params[:collection_id] begin - Collection.belongs_to_or_shared_by(current_user.id,current_user.group_ids). - find(params[:collection_id]).research_plans + Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids) + .find(params[:collection_id]).research_plans rescue ActiveRecord::RecordNotFound ResearchPlan.none end @@ -136,6 +140,17 @@ class ResearchPlanAPI < Grape::API end end + desc 'Return element linked to research plan' + params do + requires :id, type: Integer, desc: 'Research plan id' + requires :element, type: String, desc: 'Sample or Reaction' + end + + get 'linked' do + type = "#{params[:element]}_id" + ResearchPlan.where('body @> ?', [{ value: { type => params[:id] } }].to_json).select(:id, :name) + end + desc 'Return serialized research plan by id' params do requires :id, type: Integer, desc: 'Research plan id' @@ -238,7 +253,7 @@ class ResearchPlanAPI < Grape::API desc 'Export research plan by id' params do requires :id, type: Integer, desc: 'Research plan id' - optional :export_format, type: Symbol, desc: 'Export format', values: [:docx, :odt, :html, :markdown, :latex] + optional :export_format, type: Symbol, desc: 'Export format', values: %i[docx odt html markdown latex] end route_param :id do before do @@ -259,7 +274,7 @@ class ResearchPlanAPI < Grape::API content_type 'application/octet-stream' # init the export object - if [:html, :markdown, :latex].include? params[:export_format] + if %i[html markdown latex].include? params[:export_format] header['Content-Disposition'] = "attachment; filename=\"#{research_plan.name}.zip\"" present export.to_zip else @@ -366,4 +381,6 @@ class ResearchPlanAPI < Grape::API end end end + + # rubocop: enable Metrics/ClassLength end diff --git a/app/api/chemotion/sample_api.rb b/app/api/chemotion/sample_api.rb index 4d7d37d09a..2ab411638e 100644 --- a/app/api/chemotion/sample_api.rb +++ b/app/api/chemotion/sample_api.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/ClassLength, Lint/UselessAssignment require 'open-uri' -#require './helpers' module Chemotion - # rubocop:disable Metrics/ClassLength class SampleAPI < Grape::API include Grape::Kaminari helpers ContainerHelpers @@ -13,12 +14,11 @@ class SampleAPI < Grape::API helpers UserLabelHelpers resource :samples do - - # TODO Refactoring: Use Grape Entities + # TODO: Refactoring: Use Grape Entities namespace :ui_state do - desc "Get samples by UI state" + desc 'Get samples by UI state' params do - requires :ui_state, type: Hash, desc: "Selected samples from the UI" do + requires :ui_state, type: Hash, desc: 'Selected samples from the UI' do optional :all, type: Boolean optional :included_ids, type: Array optional :excluded_ids, type: Array @@ -27,7 +27,7 @@ class SampleAPI < Grape::API optional :collection_id, type: Integer optional :is_sync_to_me, type: Boolean, default: false end - optional :limit, type: Integer, desc: "Limit number of samples" + optional :limit, type: Integer, desc: 'Limit number of samples' end before do @@ -45,18 +45,17 @@ class SampleAPI < Grape::API end namespace :subsamples do - desc "Split Samples into Subsamples" + desc 'Split Samples into Subsamples' params do - requires :ui_state, type: Hash, desc: "Selected samples from the UI" + requires :ui_state, type: Hash, desc: 'Selected samples from the UI' end post do ui_state = params[:ui_state] col_id = ui_state[:currentCollectionId] - sample_ids = Sample.for_user(current_user.id).for_ui_state_with_collection(ui_state[:sample], CollectionsSample, col_id) + sample_ids = Sample.for_user(current_user.id) + .for_ui_state_with_collection(ui_state[:sample], CollectionsSample, col_id) Sample.where(id: sample_ids).each do |sample| - # rubocop:disable Lint/UselessAssignment subsample = sample.create_subsample(current_user, col_id, true, 'sample') - # rubocop:enable Lint/UselessAssignment end {} # JS layer does not use the reply @@ -64,65 +63,68 @@ class SampleAPI < Grape::API end namespace :import do - desc "Import Samples from a File" + desc 'Import Samples from a File' before do error!('401 Unauthorized', 401) unless current_user.collections.find(params[:currentCollectionId]) end post do + # Create a temp file in the tmp folder and sdf delayed job, and pass it to sdf delayed job extname = File.extname(params[:file][:filename]) - if extname.match(/\.(sdf?|mol)/i) - sdf_import = Import::ImportSdf.new(file_path: params[:file][:tempfile].path, + if /\.(sdf?|mol)/i.match?(extname) + sdf_import = Import::ImportSdf.new( + file_path: params[:file][:tempfile].path, collection_id: params[:currentCollectionId], - mapped_keys: { - description: {field: "description", displayName: "Description", multiple: true}, - location: {field: "location", displayName: "Location"}, - name: {field: "name", displayName: "Name"}, - external_label: {field: "external_label", displayName: "External label"}, - purity: {field: "purity", displayName: "Purity"}, - - molecule_name: { field: 'molecule_name', displayName: 'Molecule Name' }, - short_label: { field: 'short_label', displayName: 'Short Label' }, - real_amount: { field: 'real_amount', displayName: 'Real Amount' }, - real_amount_unit: { field: 'real_amount_unit', displayName: 'Real Amount Unit' }, - target_amount: { field: 'target_amount', displayName: 'Target Amount' }, - target_amount_unit: { field: 'target_amount_unit', displayName: 'Target Amount Unit' }, - molarity: { field: 'molarity', displayName: 'Molarity' }, - density: { field: 'density', displayName: 'Density' }, - melting_point: { field: 'melting_point', displayName: 'Melting Point' }, - boiling_point: { field: 'boiling_point', displayName: 'Boiling Point' }, - cas: { field: 'cas', displayName: 'Cas' }, - }, - current_user_id: current_user.id) + current_user_id: current_user.id, + ) sdf_import.find_or_create_mol_by_batch return { - sdf: true, message: sdf_import.message, - data: sdf_import.processed_mol, status: sdf_import.status, - custom_data_keys: sdf_import.custom_data_keys.keys, - mapped_keys: sdf_import.mapped_keys, - collection_id: sdf_import.collection_id - } + sdf: true, message: sdf_import.message, + data: sdf_import.processed_mol, status: sdf_import.status, + custom_data_keys: sdf_import.custom_data_keys.keys, + mapped_keys: sdf_import.mapped_keys, + collection_id: sdf_import.collection_id + } end # Creates the Samples from the XLS/CSV file. Empty Array if not successful - import_result = Import::ImportSamples.new.from_file( - params[:file][:tempfile].path, - params[:currentCollectionId], current_user.id - ).process - - if import_result[:status] == 'ok' - # the FE does not actually use the returned data, just the number of elements. - # see ElementStore.js handleImportSamplesFromFile or NotificationStore.js handleNotificationImportSamplesFromFile - import_result[:data] = import_result[:data].map(&:id) + file_size = params[:file][:tempfile].size + file = params[:file] + if file_size < 25_000 + import = Import::ImportSamples.new( + params[:file][:tempfile].path, + params[:currentCollectionId], current_user.id, file['filename'], params[:import_type] + ) + import_result = import.process + if import_result[:status] == 'ok' || import_result[:status] == 'warning' + # the FE does not actually use the returned data, just the number of elements. + # see ElementStore.js handleImportSamplesFromFile or NotificationStore.js + # handleNotificationImportSamplesFromFile ** + import_result[:data] = import_result[:data].map(&:id) + end + import_result + else + temp_filename = "#{SecureRandom.hex}-#{file['filename']}" + # Create a new file in the tmp folder + tmp_file_path = File.join('tmp', temp_filename) + # Write the contents of the uploaded file to the temporary file + File.binwrite(tmp_file_path, file[:tempfile].read) + parameters = { + collection_id: params[:currentCollectionId], + user_id: current_user.id, + file_name: file['filename'], + file_path: tmp_file_path, + import_type: params[:import_type], + } + ImportSamplesJob.perform_later(parameters) + { status: 'in progress', message: 'Importing samples in background' } end - - import_result end end namespace :confirm_import do - desc "Create Samples from an Array of inchikeys" + desc 'Create Samples from an Array of inchikeys' params do - requires :rows, type: Array, desc: "Selected Molecule from the UI" + requires :rows, type: Array, desc: 'Selected Molecule from the UI' requires :currentCollectionId, type: Integer requires :mapped_keys, type: Hash end @@ -132,17 +134,33 @@ class SampleAPI < Grape::API end post do - sdf_import = Import::ImportSdf.new( - collection_id: params[:currentCollectionId], - current_user_id: current_user.id, - rows: params[:rows], - mapped_keys: params[:mapped_keys] - ) - - sdf_import.create_samples - return { - sdf: true, message: sdf_import.message, status: sdf_import.status, error_messages: sdf_import.error_messages - } + rows = params[:rows] + if rows.length < 25 + sdf_import = Import::ImportSdf.new( + collection_id: params[:currentCollectionId], + current_user_id: current_user.id, + rows: rows, + mapped_keys: params[:mapped_keys], + ) + sdf_import.create_samples + return { + sdf: true, message: sdf_import.message, + status: sdf_import.status, + error_messages: sdf_import.error_messages + } + else + parameters = { + collection_id: params[:currentCollectionId], + user_id: current_user.id, + file_name: 'dummy.sdf', + sdf_rows: rows, + mapped_keys: params[:mapped_keys], + } + ImportSamplesJob.perform_later(parameters) + return { + message: 'importing samples in background', + } + end end end @@ -196,21 +214,20 @@ class SampleAPI < Grape::API sample_scope = sample_scope.includes_for_list_display prod_only = params[:product_only] || false sample_scope = if prod_only - sample_scope.product_only - else - sample_scope.distinct.sample_or_startmat_or_products - end + sample_scope.product_only + else + sample_scope.distinct.sample_or_startmat_or_products + end from = params[:from_date] to = params[:to_date] by_created_at = params[:filter_created_at] || false - sample_scope = sample_scope.samples_created_time_from(Time.at(from)) if from && by_created_at - sample_scope = sample_scope.samples_created_time_to(Time.at(to) + 1.day) if to && by_created_at - sample_scope = sample_scope.samples_updated_time_from(Time.at(from)) if from && !by_created_at - sample_scope = sample_scope.samples_updated_time_to(Time.at(to) + 1.day) if to && !by_created_at + sample_scope = sample_scope.created_time_from(Time.zone.at(from)) if from && by_created_at + sample_scope = sample_scope.created_time_to(Time.zone.at(to) + 1.day) if to && by_created_at + sample_scope = sample_scope.updated_time_from(Time.zone.at(from)) if from && !by_created_at + sample_scope = sample_scope.updated_time_to(Time.zone.at(to) + 1.day) if to && !by_created_at - reset_pagination_page(sample_scope) - samplelist = [] + sample_list = [] if params[:molecule_sort] == 1 molecule_scope = Molecule @@ -219,50 +236,53 @@ class SampleAPI < Grape::API .order(:sum_formular) reset_pagination_page(molecule_scope) paginate(molecule_scope).each do |molecule| - samplesGroup = sample_scope.select {|v| v.molecule_id == molecule.id} - samplesGroup = samplesGroup.sort { |x, y| y.updated_at <=> x.updated_at } - samplesGroup.each do |sample| + samples_group = sample_scope.select { |v| v.molecule_id == molecule.id } + samples_group = samples_group.sort { |x, y| y.updated_at <=> x.updated_at } + samples_group.each do |sample| detail_levels = ElementDetailLevelCalculator.new(user: current_user, element: sample).detail_levels - samplelist.push( - Entities::SampleEntity.represent(sample, detail_levels: detail_levels, displayed_in_list: true) + sample_list.push( + Entities::SampleEntity.represent(sample, detail_levels: detail_levels, displayed_in_list: true), ) end end else + reset_pagination_page(sample_scope) sample_scope = sample_scope.order('updated_at DESC') paginate(sample_scope).each do |sample| detail_levels = ElementDetailLevelCalculator.new(user: current_user, element: sample).detail_levels - samplelist.push( - Entities::SampleEntity.represent(sample, detail_levels: detail_levels, displayed_in_list: true) + sample_list.push( + Entities::SampleEntity.represent(sample, detail_levels: detail_levels, displayed_in_list: true), ) end end return { - samples: samplelist, - samples_count: sample_scope.count + samples: sample_list, + samples_count: sample_scope.count, } end - desc "Return serialized sample by id" + desc 'Return serialized sample by id' params do - requires :id, type: Integer, desc: "Sample id" + requires :id, type: Integer, desc: 'Sample id' end route_param :id do - before do + after_validation do @element_policy = ElementPolicy.new(current_user, Sample.find(params[:id])) error!('401 Unauthorized', 401) unless @element_policy.read? + rescue ActiveRecord::RecordNotFound + error!('404 Not Found', 404) end get do sample = Sample.includes(:molecule, :residues, :elemental_compositions, :container) - .find(params[:id]) + .find(params[:id]) present( sample, with: Entities::SampleEntity, detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: sample).detail_levels, policy: @element_policy, - root: :sample + root: :sample, ) end end @@ -281,31 +301,32 @@ class SampleAPI < Grape::API end end - desc "Update sample by id" + desc 'Update sample by id' params do - requires :id, type: Integer, desc: "Sample id" - optional :name, type: String, desc: "Sample name" - optional :external_label, type: String, desc: "Sample external label" - optional :imported_readout, type: String, desc: "Sample Imported Readout" - optional :target_amount_value, type: Float, desc: "Sample target amount_value" - optional :target_amount_unit, type: String, desc: "Sample target amount_unit" - optional :real_amount_value, type: Float, desc: "Sample real amount_value" - optional :real_amount_unit, type: String, desc: "Sample real amount_unit" - optional :molarity_value, type: Float, desc: "Sample molarity value" - optional :molarity_unit, type: String, desc: "Sample real amount_unit" - optional :description, type: String, desc: "Sample description" - optional :metrics, type: String, desc: "Sample metric units" - optional :purity, type: Float, desc: "Sample purity" - optional :solvent, type: Array[Hash], desc: "Sample solvent" - optional :location, type: String, desc: "Sample location" - optional :molfile, type: String, desc: "Sample molfile" - optional :sample_svg_file, type: String, desc: "Sample SVG file" + requires :id, type: Integer, desc: 'Sample id' + optional :name, type: String, desc: 'Sample name' + optional :external_label, type: String, desc: 'Sample external label' + optional :imported_readout, type: String, desc: 'Sample Imported Readout' + optional :target_amount_value, type: Float, desc: 'Sample target amount_value' + optional :target_amount_unit, type: String, desc: 'Sample target amount_unit' + optional :real_amount_value, type: Float, desc: 'Sample real amount_value' + optional :real_amount_unit, type: String, desc: 'Sample real amount_unit' + optional :molarity_value, type: Float, desc: 'Sample molarity value' + optional :molarity_unit, type: String, desc: 'Sample real amount_unit' + optional :description, type: String, desc: 'Sample description' + optional :metrics, type: String, desc: 'Sample metric units' + optional :purity, type: Float, desc: 'Sample purity' + optional :solvent, type: Array[Hash], desc: 'Sample solvent' + optional :location, type: String, desc: 'Sample location' + optional :molfile, type: String, desc: 'Sample molfile' + optional :sample_svg_file, type: String, desc: 'Sample SVG file' + optional :dry_solvent, default: false, type: Boolean, desc: 'Sample dry solvent' # optional :molecule, type: Hash, desc: "Sample molecule" do - # optional :id, type: Integer + # optional :id, type: Integer # end optional :molecule_id, type: Integer - optional :is_top_secret, type: Boolean, desc: "Sample is marked as top secret?" - optional :density, type: Float, desc: "Sample density" + optional :is_top_secret, type: Boolean, desc: 'Sample is marked as top secret?' + optional :density, type: Float, desc: 'Sample density' optional :boiling_point_upperbound, type: Float, desc: 'upper bound of sample boiling point' optional :boiling_point_lowerbound, type: Float, desc: 'lower bound of sample boiling point' optional :melting_point_upperbound, type: Float, desc: 'upper bound of sample melting point' @@ -325,7 +346,7 @@ class SampleAPI < Grape::API optional :inventory_sample, type: Boolean, default: false optional :molecular_mass, type: Float optional :sum_formula, type: String - #use :root_container_params + # use :root_container_params end route_param :id do @@ -342,27 +363,29 @@ class SampleAPI < Grape::API update_datamodel(attributes[:container]) attributes.delete(:container) - update_element_labels(@sample,attributes[:user_labels], current_user.id) + update_element_labels(@sample, attributes[:user_labels], current_user.id) attributes.delete(:user_labels) attributes.delete(:segments) # otherwise ActiveRecord::UnknownAttributeError appears - attributes[:elemental_compositions].each do |i| + attributes[:elemental_compositions]&.each do |i| i.delete :description - end if attributes[:elemental_compositions] + end # set nested attributes - %i(molecule residues elemental_compositions).each do |prop| + %i[molecule residues elemental_compositions].each do |prop| prop_value = attributes.delete(prop) + next if prop_value.blank? + attributes.merge!( - "#{prop}_attributes".to_sym => prop_value - ) unless prop_value.blank? + "#{prop}_attributes".to_sym => prop_value, + ) end - boiling_point_lowerbound = params['boiling_point_lowerbound'].blank? ? -Float::INFINITY : params['boiling_point_lowerbound'] - boiling_point_upperbound = params['boiling_point_upperbound'].blank? ? Float::INFINITY : params['boiling_point_upperbound'] - melting_point_lowerbound = params['melting_point_lowerbound'].blank? ? -Float::INFINITY : params['melting_point_lowerbound'] - melting_point_upperbound = params['melting_point_upperbound'].blank? ? Float::INFINITY : params['melting_point_upperbound'] + boiling_point_lowerbound = (params['boiling_point_lowerbound'].presence || -Float::INFINITY) + boiling_point_upperbound = (params['boiling_point_upperbound'].presence || Float::INFINITY) + melting_point_lowerbound = (params['melting_point_lowerbound'].presence || -Float::INFINITY) + melting_point_upperbound = (params['melting_point_upperbound'].presence || Float::INFINITY) attributes['boiling_point'] = Range.new(boiling_point_lowerbound, boiling_point_upperbound) attributes['melting_point'] = Range.new(melting_point_lowerbound, melting_point_upperbound) attributes.delete(:boiling_point_lowerbound) @@ -373,16 +396,7 @@ class SampleAPI < Grape::API @sample.update!(attributes) @sample.save_segments(segments: params[:segments], current_user_id: current_user.id) - # params[:segments].each do |seg| - # segment = Segment.find_by(element_type: Sample.name, element_id: @sample.id, segment_klass_id: seg["segment_klass_id"]) - # if segment.present? - # segment.update!(properties: seg["properties"]) - # else - # Segment.create!(segment_klass_id: seg["segment_klass_id"], element_type: Sample.name, element_id: @sample.id, properties: seg["properties"], created_by: current_user.id) - # end - # end - - #save to profile + # save to profile kinds = @sample.container&.analyses&.pluck(Arel.sql("extended_metadata->'kind'")) recent_ols_term_update('chmo', kinds) if kinds&.length&.positive? @@ -391,34 +405,35 @@ class SampleAPI < Grape::API with: Entities::SampleEntity, detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: @sample).detail_levels, policy: @element_policy, - root: :sample + root: :sample, ) end end - desc "Create a sample" + desc 'Create a sample' params do - optional :name, type: String, desc: "Sample name" - optional :short_label, type: String, desc: "Sample short label" - optional :external_label, type: String, desc: "Sample external label" - optional :imported_readout, type: String, desc: "Sample Imported Readout" - requires :target_amount_value, type: Float, desc: "Sample target amount_value" - requires :target_amount_unit, type: String, desc: "Sample target amount_unit" - optional :real_amount_value, type: Float, desc: "Sample real amount_value" - optional :real_amount_unit, type: String, desc: "Sample real amount_unit" - optional :molarity_value, type: Float, desc: "Sample molarity value" - optional :molarity_unit, type: String, desc: "Sample real amount_unit" - requires :description, type: String, desc: "Sample description" - requires :purity, type: Float, desc: "Sample purity" + optional :name, type: String, desc: 'Sample name' + optional :short_label, type: String, desc: 'Sample short label' + optional :external_label, type: String, desc: 'Sample external label' + optional :imported_readout, type: String, desc: 'Sample Imported Readout' + requires :target_amount_value, type: Float, desc: 'Sample target amount_value' + requires :target_amount_unit, type: String, desc: 'Sample target amount_unit' + optional :real_amount_value, type: Float, desc: 'Sample real amount_value' + optional :real_amount_unit, type: String, desc: 'Sample real amount_unit' + optional :molarity_value, type: Float, desc: 'Sample molarity value' + optional :molarity_unit, type: String, desc: 'Sample real amount_unit' + requires :description, type: String, desc: 'Sample description' + requires :purity, type: Float, desc: 'Sample purity' + optional :dry_solvent, default: false, type: Boolean, desc: 'Sample dry solvent' # requires :solvent, type: String, desc: "Sample solvent" - optional :solvent, type: Array[Hash], desc: "Sample solvent", default: [] - requires :location, type: String, desc: "Sample location" - optional :molfile, type: String, desc: "Sample molfile" - optional :sample_svg_file, type: String, desc: "Sample SVG file" - #optional :molecule, type: Hash, desc: "Sample molecule" - optional :collection_id, type: Integer, desc: "Collection id" - requires :is_top_secret, type: Boolean, desc: "Sample is marked as top secret?" - optional :density, type: Float, desc: "Sample density" + optional :solvent, type: Array[Hash], desc: 'Sample solvent', default: [] + requires :location, type: String, desc: 'Sample location' + optional :molfile, type: String, desc: 'Sample molfile' + optional :sample_svg_file, type: String, desc: 'Sample SVG file' + # optional :molecule, type: Hash, desc: "Sample molecule" + optional :collection_id, type: Integer, desc: 'Collection id' + requires :is_top_secret, type: Boolean, desc: 'Sample is marked as top secret?' + optional :density, type: Float, desc: 'Sample density' optional :boiling_point_upperbound, type: Float, desc: 'upper bound of sample boiling point' optional :boiling_point_lowerbound, type: Float, desc: 'lower bound of sample boiling point' optional :melting_point_upperbound, type: Float, desc: 'upper bound of sample melting point' @@ -440,7 +455,11 @@ class SampleAPI < Grape::API optional :sum_formula, type: String end post do - molecule_id = params[:decoupled] && params[:molfile].blank? ? Molecule.find_or_create_dummy&.id : params[:molecule_id] + molecule_id = if params[:decoupled] && params[:molfile].blank? + Molecule.find_or_create_dummy&.id + else + params[:molecule_id] + end attributes = { name: params[:name], short_label: params[:short_label], @@ -453,6 +472,7 @@ class SampleAPI < Grape::API molarity_unit: params[:molarity_unit], description: params[:description], purity: params[:purity], + dry_solvent: params[:dry_solvent], solvent: params[:solvent], location: params[:location], molfile: params[:molfile], @@ -469,33 +489,35 @@ class SampleAPI < Grape::API decoupled: params[:decoupled], inventory_sample: params[:inventory_sample], molecular_mass: params[:molecular_mass], - sum_formula: params[:sum_formula] + sum_formula: params[:sum_formula], } - boiling_point_lowerbound = params['boiling_point_lowerbound'].blank? ? -Float::INFINITY : params['boiling_point_lowerbound'] - boiling_point_upperbound = params['boiling_point_upperbound'].blank? ? Float::INFINITY : params['boiling_point_upperbound'] - melting_point_lowerbound = params['melting_point_lowerbound'].blank? ? -Float::INFINITY : params['melting_point_lowerbound'] - melting_point_upperbound = params['melting_point_upperbound'].blank? ? Float::INFINITY : params['melting_point_upperbound'] + boiling_point_lowerbound = (params['boiling_point_lowerbound'].presence || -Float::INFINITY) + boiling_point_upperbound = (params['boiling_point_upperbound'].presence || Float::INFINITY) + melting_point_lowerbound = (params['melting_point_lowerbound'].presence || -Float::INFINITY) + melting_point_upperbound = (params['melting_point_upperbound'].presence || Float::INFINITY) attributes['boiling_point'] = Range.new(boiling_point_lowerbound, boiling_point_upperbound) attributes['melting_point'] = Range.new(melting_point_lowerbound, melting_point_upperbound) # otherwise ActiveRecord::UnknownAttributeError appears # TODO should be in params validation - attributes[:elemental_compositions].each do |i| + attributes[:elemental_compositions]&.each do |i| i.delete :description i.delete :id - end if attributes[:elemental_compositions] + end - attributes[:residues].each do |i| + attributes[:residues]&.each do |i| i.delete :id - end if attributes[:residues] + end # set nested attributes - %i(molecule residues elemental_compositions).each do |prop| + %i[molecule residues elemental_compositions].each do |prop| prop_value = attributes.delete(prop) + next if prop_value.blank? + attributes.merge!( - "#{prop}_attributes".to_sym => prop_value - ) unless prop_value.blank? + "#{prop}_attributes".to_sym => prop_value, + ) end attributes.delete(:segments) @@ -507,7 +529,7 @@ class SampleAPI < Grape::API end is_shared_collection = false - unless collection.present? + if collection.blank? sync_collection = current_user.all_sync_in_collections_users.where(id: params[:collection_id]).take if sync_collection.present? is_shared_collection = true @@ -525,25 +547,17 @@ class SampleAPI < Grape::API sample.save! sample.save_segments(segments: params[:segments], current_user_id: current_user.id) - # params[:segments].each do |seg| - # segment = Segment.find_by(element_type: Sample.name, element_id: @sample.id, segment_klass_id: seg["segment_klass_id"]) - # if segment.present? - # segment.update!(properties: seg["properties"]) - # else - # Segment.create!(segment_klass_id: seg["segment_klass_id"], element_type: Sample.name, element_id: @sample.id, properties: seg["properties"], created_by: current_user.id) - # end - # end - #save to profile + # save to profile kinds = sample.container&.analyses&.pluck(Arel.sql("extended_metadata->'kind'")) recent_ols_term_update('chmo', kinds) if kinds&.length&.positive? present sample, with: Entities::SampleEntity, root: :sample end - desc "Delete a sample by id" + desc 'Delete a sample by id' params do - requires :id, type: Integer, desc: "Sample id" + requires :id, type: Integer, desc: 'Sample id' end route_param :id do before do @@ -552,15 +566,10 @@ class SampleAPI < Grape::API delete do sample = Sample.find(params[:id]) - # DevicesSample.find_by(sample_id: sample.id).destroy - # sample.devices_analyses.map{|d| - # d.analyses_experiments.destroy_all - # d.destroy - # } sample.destroy end end end end end -# rubocop:enable Metrics/ClassLength +# rubocop:enable Metrics/ClassLength, Lint/UselessAssignment diff --git a/app/api/chemotion/sample_task_api.rb b/app/api/chemotion/sample_task_api.rb index bbd77b7285..a55e206b06 100644 --- a/app/api/chemotion/sample_task_api.rb +++ b/app/api/chemotion/sample_task_api.rb @@ -20,7 +20,7 @@ class SampleTaskAPI < Grape::API optional :status, type: String, values: %w[open with_missing_scan_results done], default: 'open' end get do - tasks = SampleTask.for(current_user).public_send(params[:status]) + tasks = SampleTask.for(current_user).public_send(params[:status]).order(created_at: :desc) present tasks, with: Entities::SampleTaskEntity end @@ -60,6 +60,14 @@ class SampleTaskAPI < Grape::API present updater.sample_task, with: Entities::SampleTaskEntity end + # delete an open sample task + delete ':id' do + task = SampleTask.for(current_user).open.find_by(id: params[:id]) + error!('Task could not be deleted', 400) unless task.present? && task.destroy + + { deleted: task.id } + end + route_param :id do resource :scan_results do params do diff --git a/app/api/chemotion/screen_api.rb b/app/api/chemotion/screen_api.rb index f8db265e49..b488056807 100644 --- a/app/api/chemotion/screen_api.rb +++ b/app/api/chemotion/screen_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Chemotion class ScreenAPI < Grape::API include Grape::Kaminari @@ -21,22 +23,22 @@ class ScreenAPI < Grape::API end get do scope = if params[:collection_id] - begin - Collection.belongs_to_or_shared_by(current_user.id,current_user.group_ids). - find(params[:collection_id]).screens - rescue ActiveRecord::RecordNotFound - Screen.none - end - elsif params[:sync_collection_id] - begin - current_user.all_sync_in_collections_users.find(params[:sync_collection_id]).collection.screens - rescue ActiveRecord::RecordNotFound - Screen.none - end - else - # All collection of current_user - Screen.joins(:collections).where('collections.user_id = ?', current_user.id).distinct - end.includes(collections: :sync_collections_users).order("created_at DESC") + begin + Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids) + .find(params[:collection_id]).screens + rescue ActiveRecord::RecordNotFound + Screen.none + end + elsif params[:sync_collection_id] + begin + current_user.all_sync_in_collections_users.find(params[:sync_collection_id]).collection.screens + rescue ActiveRecord::RecordNotFound + Screen.none + end + else + # All collection of current_user + Screen.joins(:collections).where(collections: { user_id: current_user.id }).distinct + end.includes(:comments, collections: :sync_collections_users).order('created_at DESC') from = params[:from_date] to = params[:to_date] diff --git a/app/api/chemotion/search_api.rb b/app/api/chemotion/search_api.rb index fb29023fcd..88fe4610e0 100644 --- a/app/api/chemotion/search_api.rb +++ b/app/api/chemotion/search_api.rb @@ -1,26 +1,30 @@ # frozen_string_literal: true -# rubocop: disable Metrics/ClassLength +# rubocop:disable Metrics/MethodLength, Metrics/BlockLength, Metrics/AbcSize, Metrics/ClassLength + +# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Lint/SafeNavigationChain, Style/RedundantParentheses + +# rubocop:disable Naming/VariableName, Naming/MethodParameterName, Layout/LineLength module Chemotion class SearchAPI < Grape::API include Grape::Kaminari - # TODO implement search cache? + # TODO: implement search cache? helpers CollectionHelpers helpers do params :search_params do optional :page, type: Integer requires :selection, type: Hash do optional :search_by_method, type: String # , values: %w[ - # advanced substring structure - # screen_name wellplate_name reaction_name reaction_short_label - # sample_name sample_short_label - # sample_external_label sum_formula iupac_name inchistring cano_smiles - # polymer_type - #] + # advanced substring structure + # screen_name wellplate_name reaction_name reaction_short_label + # sample_name sample_short_label + # sample_external_label sum_formula iupac_name inchistring cano_smiles + # polymer_type + # ] optional :elementType, type: String, values: %w[ - All Samples Reactions Wellplates Screens all samples reactions wellplates screens elements + All Samples Reactions Wellplates Screens all samples reactions wellplates screens elements cell_lines by_ids advanced structure ] optional :molfile, type: String optional :search_type, type: String, values: %w[similar sub] @@ -30,9 +34,28 @@ class SearchAPI < Grape::API optional :name, type: String optional :advanced_params, type: Array do optional :link, type: String, values: ['', 'AND', 'OR'], default: '' - optional :match, type: String, values: ['=', 'LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE'], default: 'LIKE' + optional :match, type: String, values: ['=', 'LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE', '>', '<', '>=', '@>', '<@'], default: 'LIKE' + optional :table, type: String, values: %w[samples reactions wellplates screens research_plans elements segments literatures] + optional :element_id, type: Integer + optional :unit, type: String requires :field, type: Hash requires :value, type: String + optional :smiles, type: String + optional :sub_values, type: Array + end + optional :id_params, type: Hash do + requires :model_name, type: String, values: %w[ + sample reaction wellplate screen element research_plan + ] + requires :ids, type: Array + optional :total_elements, type: Integer + optional :with_filter, type: Boolean + end + optional :list_filter_params, type: Hash do + optional :filter_created_at, type: Boolean + optional :from_date, type: Date + optional :to_date, type: Date + optional :product_only, type: Boolean end end requires :collection_id, type: String @@ -58,109 +81,80 @@ def adv_params params[:selection][:advanced_params] end - def sample_structure_search(c_id = @c_id, not_permitted = @dl_s && @dl_s < 1 ) + def id_params + params[:selection][:id_params] + end + + def list_filter_params + params[:selection][:list_filter_params] + end + + def sample_structure_search(c_id = @c_id, not_permitted = @dl_s && @dl_s < 1) return Sample.none if not_permitted + molfile = Fingerprint.standardized_molfile(params[:selection][:molfile]) threshold = params[:selection][:tanimoto_threshold] - # TODO implement this: http://pubs.acs.org/doi/abs/10.1021/ci600358f - if params[:selection][:search_type] == 'similar' - Sample.by_collection_id(c_id).search_by_fingerprint_sim(molfile,threshold) - else - Sample.by_collection_id(c_id).search_by_fingerprint_sub(molfile) - end + # TODO: implement this: http://pubs.acs.org/doi/abs/10.1021/ci600358f + scope = + if params[:selection][:search_type] == 'similar' + Sample.by_collection_id(c_id).search_by_fingerprint_sim(molfile, threshold) + else + Sample.by_collection_id(c_id).search_by_fingerprint_sub(molfile) + end + order_by_molecule(scope) + end + + def order_by_molecule(scope) + scope.includes(:molecule) + .joins(:molecule) + .order(Arel.sql("LENGTH(SUBSTRING(molecules.sum_formular, 'C\\d+'))")) + .order('molecules.sum_formular') end def whitelisted_table(table:, column:, **_) - API::WL_TABLES.has_key?(table) && API::WL_TABLES[table].include?(column) + return true if %w[elements segments chemicals containers measurements molecules].include?(table) + + API::WL_TABLES.key?(table) && API::WL_TABLES[table].include?(column) end # desc: return true if the detail level allow to access the column - def filter_with_detail_level(table:, column:, sample_detail_level:, reaction_detail_level:, **_) - # TODO filter according to columns + def filter_with_detail_level(table:, column:, sample_detail_level:, reaction_detail_level:, **_) + # TODO: filter according to columns return true unless table.in?(%w[samples reactions]) - return true if table == 'samples' && (sample_detail_level > 0 || column == 'external_label') + return true if table == 'samples' && (sample_detail_level.positive? || column == 'external_label') return true if table == 'reactions' && reaction_detail_level > -1 false end - def advanced_search(c_id = @c_id, dl = @dl) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Naming/MethodParameterName - query = '' - cond_val = [] - tables = [] - - adv_params.each do |filter| - adv_field = filter['field'].to_h.merge(dl).symbolize_keys - next unless whitelisted_table(**adv_field) - next unless filter_with_detail_level(**adv_field) - table = filter['field']['table'] - tables.push(table: table, ext_key: filter['field']['ext_key']) - field = filter['field']['column'] - words = filter['value'].split(/(\r)?\n/).map!(&:strip) - words = words.map { |e| "%#{ActiveRecord::Base.send(:sanitize_sql_like, e)}%" } unless filter['match'] == '=' - field = "xref ->> 'cas'" if field == 'xref' && filter['field']['opt'] == 'cas' - conditions = words.collect { "#{table}.#{field} #{filter['match']} ? " }.join(' OR ') - query = "#{query} #{filter['link']} (#{conditions}) " - cond_val += words - end - - scope = Sample.by_collection_id(c_id.to_i) - tables.each do |table_info| - table = table_info[:table] - ext_key = table_info[:ext_key] - next if table.casecmp('samples').zero? - - scope = if ext_key.nil? - scope = scope.joins("INNER JOIN #{table} ON "\ - "#{table}.sample_id = samples.id") - else - scope = scope.joins("INNER JOIN #{table} ON "\ - "samples.#{ext_key} = #{table}.id") - end - end - scope = scope.where([query] + cond_val) + def advanced_search(c_id = @c_id, dl = @dl) + conditions = Usecases::Search::ConditionsForAdvancedSearch.new( + detail_levels: dl, + params: params[:selection][:advanced_params], + ).filter! + + query_cond = conditions[:value].present? ? [conditions[:query]] + conditions[:value] : conditions[:query] + scope = conditions[:model_name].by_collection_id(c_id.to_i) + .where(query_cond) + .joins(conditions[:joins].join(' ')) + scope = order_by_molecule(scope) if conditions[:model_name] == Sample + scope = scope.group("#{conditions[:model_name].table_name}.id") if %w[ResearchPlan Wellplate].include?(conditions[:model_name].to_s) scope end - def elements_search(c_id = @c_id, dl = @dl) - collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id) - element_scope = Element.joins(:collections_elements).where('collections_elements.collection_id = ? and collections_elements.element_type = (?)', collection.id, params[:selection][:genericElName]) - element_scope = element_scope.where("name like (?)", "%#{params[:selection][:searchName]}%") if params[:selection][:searchName].present? - element_scope = element_scope.where("short_label like (?)", "%#{params[:selection][:searchShowLabel]}%") if params[:selection][:searchShowLabel].present? - if params[:selection][:searchProperties].present? - params[:selection][:searchProperties] && params[:selection][:searchProperties][:layers] && params[:selection][:searchProperties][:layers].keys.each do |lk| - layer = params[:selection][:searchProperties][:layers][lk] - qs = layer[:fields].select{ |f| f[:value].present? || f[:type] == "input-group" } - qs.each do |f| - if f[:type] == "input-group" - sfs = f[:sub_fields].map{ |e| { "id": e[:id], "value": e[:value] } } - query = { "#{lk}": { "fields": [{ "field": f[:field].to_s, "sub_fields": sfs }] } } if sfs.length > 0 - elsif f[:type] == "checkbox" || f[:type] == "integer" || f[:type] == "system-defined" - query = { "#{lk}": { "fields": [{ "field": f[:field].to_s, "value": f[:value] }] } } - else - query = { "#{lk}": { "fields": [{ "field": f[:field].to_s, "value": f[:value].to_s }] } } - end - element_scope = element_scope.where("properties @> ?", query.to_json) - end - end - end - element_scope - end - - def serialize_samples sample_ids, page, search_method, molecule_sort + def serialize_samples(sample_ids, page, molecule_sort) return { data: [], size: 0 } if sample_ids.empty? samples_size = sample_ids.size samplelist = [] - if molecule_sort == true # Sorting by molecule for non-advanced search molecule_scope = - Molecule.joins(:samples).where('samples.id IN (?)', sample_ids) - .order("LENGTH(SUBSTRING(sum_formular, 'C\\d+'))") + Molecule.joins(:samples).where(samples: { id: sample_ids }) + .order(Arel.sql("LENGTH(SUBSTRING(sum_formular, 'C\\d+'))")) .order(:sum_formular) molecule_scope = molecule_scope.page(page).per(page_size) samples = Sample.includes_for_list_display.find(sample_ids) @@ -173,7 +167,7 @@ def serialize_samples sample_ids, page, search_method, molecule_sort serialized_sample = Entities::SampleEntity.represent( sample, detail_levels: detail_levels, - displayed_in_list: true + displayed_in_list: true, ).serializable_hash samplelist.push(serialized_sample) end @@ -189,25 +183,26 @@ def serialize_samples sample_ids, page, search_method, molecule_sort serialized_sample = Entities::SampleEntity.represent( sample, detail_levels: detail_levels, - displayed_in_list: true + displayed_in_list: true, ).serializable_hash samplelist.push(serialized_sample) end end - return { - data: samplelist, - size: samples_size - } + { data: samplelist, size: samples_size } end + # rubocop:disable Style/OptionalBooleanParameter + def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false) element_ids = elements.fetch(:element_ids, []) reaction_ids = elements.fetch(:reaction_ids, []) sample_ids = elements.fetch(:sample_ids, []) - samples_data = serialize_samples(sample_ids, page, search_by_method, molecule_sort) + samples_data = serialize_samples(sample_ids, page, molecule_sort) screen_ids = elements.fetch(:screen_ids, []) wellplate_ids = elements.fetch(:wellplate_ids, []) + cell_line_ids = elements.fetch(:cell_line_ids, []) + research_plan_ids = elements.fetch(:research_plan_ids, []) paginated_reaction_ids = Kaminari.paginate_array(reaction_ids).page(page).per(page_size) serialized_reactions = Reaction.find(paginated_reaction_ids).map do |reaction| @@ -224,6 +219,16 @@ def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false Entities::ScreenEntity.represent(screen, displayed_in_list: true).serializable_hash end + paginated_cell_line_ids = Kaminari.paginate_array(cell_line_ids).page(page).per(page_size) + serialized_cell_lines = CelllineSample.find(paginated_cell_line_ids).map do |cell_line| + Entities::CellLineSampleEntity.represent(cell_line, displayed_in_list: true).serializable_hash + end + + paginated_research_plan_ids = Kaminari.paginate_array(research_plan_ids).page(page).per(page_size) + serialized_research_plans = ResearchPlan.find(paginated_research_plan_ids).map do |research_plan| + Entities::ResearchPlanEntity.represent(research_plan, displayed_in_list: true).serializable_hash + end + result = { samples: { elements: samples_data[:data], @@ -231,7 +236,7 @@ def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false page: page, pages: pages(samples_data[:size]), perPage: page_size, - ids: sample_ids + ids: sample_ids, }, reactions: { elements: serialized_reactions, @@ -239,7 +244,7 @@ def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false page: page, pages: pages(reaction_ids.size), perPage: page_size, - ids: reaction_ids + ids: reaction_ids, }, wellplates: { elements: serialized_wellplates, @@ -247,7 +252,7 @@ def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false page: page, pages: pages(wellplate_ids.size), perPage: page_size, - ids: wellplate_ids + ids: wellplate_ids, }, screens: { elements: serialized_screens, @@ -255,16 +260,32 @@ def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false page: page, pages: pages(screen_ids.size), perPage: page_size, - ids: screen_ids - } + ids: screen_ids, + }, + cell_lines: { + elements: serialized_cell_lines, + totalElements: cell_line_ids.size, + page: page, + pages: pages(cell_line_ids.size), + perPage: page_size, + ids: cell_line_ids, + }, + research_plans: { + elements: serialized_research_plans, + totalElements: research_plan_ids.size, + page: page, + pages: pages(research_plan_ids.size), + perPage: page_size, + ids: research_plan_ids, + }, } - klasses = ElementKlass.where(is_active: true, is_generic: true) + klasses = Labimotion::ElementKlass.where(is_active: true, is_generic: true) klasses.each do |klass| - element_ids_for_klass = Element.where(id: element_ids, element_klass_id: klass.id).pluck(:id) + element_ids_for_klass = Labimotion::Element.where(id: element_ids, element_klass_id: klass.id).pluck(:id) paginated_element_ids = Kaminari.paginate_array(element_ids_for_klass).page(page).per(page_size) - serialized_elements = Element.find(paginated_element_ids).map do |element| - Entities::ElementEntity.represent(element, displayed_in_list: true).serializable_hash + serialized_elements = Labimotion::Element.find(paginated_element_ids).map do |element| + Labimotion::ElementEntity.represent(element, displayed_in_list: true).serializable_hash end result["#{klass.name}s"] = { @@ -273,45 +294,48 @@ def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false page: page, pages: pages(element_ids_for_klass.size), perPage: page_size, - ids: element_ids_for_klass + ids: element_ids_for_klass, } end result end + # rubocop:enable Style/OptionalBooleanParameter + # Generate search query def search_elements(c_id = @c_id, dl = @dl) search_method = search_by_method molecule_sort = params[:molecule_sort] arg = params[:selection][:name] - return if !(search_method =~ /advanced|structure/) && !arg.presence + return if (search_method !~ /advanced|structure/) && !arg.presence + dl_s = dl[:sample_detail_level] || 0 scope = case search_method when 'polymer_type' - if dl_s > 0 - Sample.by_collection_id(c_id).order("samples.updated_at DESC") + if dl_s.positive? + Sample.by_collection_id(c_id).order('samples.updated_at DESC') .by_residues_custom_info('polymer_type', arg) else Sample.none end when 'sum_formula', 'sample_external_label' if dl_s > -1 - Sample.by_collection_id(c_id).order("samples.updated_at DESC") + Sample.by_collection_id(c_id).order('samples.updated_at DESC') .search_by(search_method, arg) else Sample.none end when 'iupac_name', 'inchistring', 'inchikey', 'cano_smiles', 'sample_name', 'sample_short_label' - if dl_s > 0 - Sample.by_collection_id(c_id).order("samples.updated_at DESC") + if dl_s.positive? + Sample.by_collection_id(c_id).order('samples.updated_at DESC') .search_by(search_method, arg) else Sample.none end when 'cas' - if dl_s > 0 - Sample.by_collection_id(c_id).order("samples.updated_at DESC") + if dl_s.positive? + Sample.by_collection_id(c_id).order('samples.updated_at DESC') .by_sample_xref_cas(arg) else Sample.none @@ -326,7 +350,7 @@ def search_elements(c_id = @c_id, dl = @dl) # NB we'll have to split the content of the pg_search_document into # MW + external_label (dl_s = 0) and the other info only available # from dl_s > 0. For now one can use the suggested search instead. - if dl_s > 0 + if dl_s.positive? AllElementSearch.new(arg).search_by_substring.by_collection_id(c_id, current_user) else AllElementSearch::Results.new(Sample.none) @@ -335,26 +359,20 @@ def search_elements(c_id = @c_id, dl = @dl) sample_structure_search when 'advanced' advanced_search(c_id) - when 'elements' - elements_search(c_id) + when 'cell_line_material_name' + CelllineSample.by_material_name(arg, c_id) + when 'cell_line_sample_name' + CelllineSample.by_sample_name(arg, c_id) end - if search_method == 'advanced' && molecule_sort == false - arg_value_str = adv_params.first['value'].split(/(\r)?\n/).map(&:strip) - .select{ |s| !s.empty? }.join(',') - return scope.order(Arel.sql( - "position(','||(#{adv_params.first['field']['column']}::text)||',' in ','||(#{ActiveRecord::Base.connection.quote(arg_value_str)}::text)||',')" - )) - elsif search_method == 'advanced' && molecule_sort == true - return scope.order('samples.updated_at DESC') - elsif search_method != 'advanced' && molecule_sort == true - return scope.includes(:molecule) - .joins(:molecule) - .order(Arel.sql("LENGTH(SUBSTRING(molecules.sum_formular, 'C\\d+'))")) - .order('molecules.sum_formular') - elsif search_by_method.start_with?("element_short_label_") - klass = ElementKlass.find_by(name: search_by_method.sub("element_short_label_","")) - return Element.by_collection_id(c_id).by_klass_id_short_label(klass.id, arg) + if search_method != 'advanced' && search_method != 'structure' && molecule_sort == true + scope.includes(:molecule) + .joins(:molecule) + .order(Arel.sql("LENGTH(SUBSTRING(molecules.sum_formular, 'C\\d+'))")) + .order('molecules.sum_formular') + elsif search_by_method.start_with?('element_short_label_') + klass = Labimotion::ElementKlass.find_by(name: search_by_method.sub('element_short_label_', '')) + return Labimotion::Element.by_collection_id(c_id).by_klass_id_short_label(klass.id, arg) end scope end @@ -365,36 +383,50 @@ def elements_by_scope(scope, collection_id = @c_id) user_reactions = Reaction.by_collection_id(collection_id) user_wellplates = Wellplate.by_collection_id(collection_id) user_screens = Screen.by_collection_id(collection_id) - user_elements = Element.by_collection_id(collection_id) + user_research_plans = ResearchPlan.by_collection_id(collection_id) + user_elements = Labimotion::Element.by_collection_id(collection_id) case scope&.first when Sample elements[:sample_ids] = scope&.ids elements[:reaction_ids] = user_reactions.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq elements[:wellplate_ids] = user_wellplates.by_sample_ids(elements[:sample_ids]).uniq.pluck(:id) - elements[:screen_ids] = user_screens.by_wellplate_ids(elements[:wellplate_ids]).pluck(:id) + elements[:screen_ids] = user_screens.by_wellplate_ids(elements[:wellplate_ids]).pluck(:id).uniq + elements[:research_plan_ids] = user_research_plans.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq elements[:element_ids] = user_elements.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq when Reaction elements[:reaction_ids] = scope&.ids elements[:sample_ids] = user_samples.by_reaction_ids(elements[:reaction_ids]).pluck(:id).uniq elements[:wellplate_ids] = user_wellplates.by_sample_ids(elements[:sample_ids]).uniq.pluck(:id) - elements[:screen_ids] = user_screens.by_wellplate_ids(elements[:wellplate_ids]).pluck(:id) + elements[:screen_ids] = user_screens.by_wellplate_ids(elements[:wellplate_ids]).pluck(:id).uniq + elements[:research_plan_ids] = user_research_plans.by_reaction_ids(elements[:reaction_ids]).pluck(:id).uniq when Wellplate elements[:wellplate_ids] = scope&.ids elements[:screen_ids] = user_screens.by_wellplate_ids(elements[:wellplate_ids]).uniq.pluck(:id) elements[:sample_ids] = user_samples.by_wellplate_ids(elements[:wellplate_ids]).uniq.pluck(:id) elements[:reaction_ids] = user_reactions.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq + elements[:research_plan_ids] = ResearchPlansWellplate.get_research_plans(elements[:wellplate_ids]).uniq + # elements[:element_ids] = user_elements.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq when Screen elements[:screen_ids] = scope&.ids elements[:wellplate_ids] = user_wellplates.by_screen_ids(elements[:screen_ids]).uniq.pluck(:id) elements[:sample_ids] = user_samples.by_wellplate_ids(elements[:wellplate_ids]).uniq.pluck(:id) - elements[:reaction_ids] = user_reactions.by_sample_ids(elements[:sample_ids]).pluck(:id) - when Element + elements[:reaction_ids] = user_reactions.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq + elements[:research_plan_ids] = user_research_plans.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq + elements[:element_ids] = user_elements.by_sample_ids(elements[:sample_ids]).pluck(:id).uniq + when ResearchPlan + elements[:research_plan_ids] = scope&.ids + sample_ids = ResearchPlan.sample_ids_by_research_plan_ids(elements[:research_plan_ids]) + reaction_ids = ResearchPlan.reaction_ids_by_research_plan_ids(elements[:research_plan_ids]) + elements[:sample_ids] = sample_ids.map(&:sample_id).uniq + elements[:reaction_ids] = reaction_ids.map(&:reaction_id).uniq + elements[:wellplate_ids] = ResearchPlansWellplate.get_wellplates(elements[:research_plan_ids]).uniq + when Labimotion::Element elements[:element_ids] = scope&.ids - sample_ids = ElementsSample.where(element_id: elements[:element_ids]).pluck(:sample_id) + sids = Labimotion::ElementsSample.where(element_id: elements[:element_ids]).pluck(:sample_id) elements[:sample_ids] = Sample.by_collection_id(collection_id).where(id: sids).uniq.pluck(:id) when AllElementSearch::Results - # TODO check this samples_ids + molecules_ids ???? + # TODO: check this samples_ids + molecules_ids ???? elements[:sample_ids] = (scope&.samples_ids + scope&.molecules_ids) elements[:reaction_ids] = ( scope&.reactions_ids + @@ -410,15 +442,18 @@ def elements_by_scope(scope, collection_id = @c_id) scope&.screens_ids + user_screens.by_wellplate_ids(elements[:wellplate_ids]).pluck(:id) ).uniq + elements[:element_ids] = (scope&.element_ids).uniq + when CelllineSample + elements[:cell_line_ids] = scope&.ids end elements end end resource :search do - namespace :elements do - desc "Return all matched elements and associations for substring query" + namespace :cell_lines do + desc 'Return all matched cell lines and associations for substring query' params do use :search_params end @@ -428,20 +463,30 @@ def elements_by_scope(scope, collection_id = @c_id) end post do - scope = elements_search(@c_id) - return unless scope - elements_ids = elements_by_scope(scope) + query = @params[:selection][:name] + collection_id = @params[:collection_id] + cell_lines = + case search_by_method + when 'cell_line_material_name' + CelllineSample.by_material_name(query, collection_id) + when 'cell_line_sample_name' + CelllineSample.by_sample_name(query, collection_id) + end + + return unless cell_lines + + elements_ids = elements_by_scope(cell_lines) serialization_by_elements_and_page( elements_ids, params[:page], - params[:molecule_sort] + params[:molecule_sort], ) end end namespace :all do - desc "Return all matched elements and associations for substring query" + desc 'Return all matched elements and associations for substring query' params do use :search_params end @@ -453,18 +498,83 @@ def elements_by_scope(scope, collection_id = @c_id) post do scope = search_elements(@c_id, @dl) return unless scope - elements_ids = elements_by_scope(scope) + elements_ids = elements_by_scope(scope) serialization_by_elements_and_page( elements_ids, params[:page], - params[:molecule_sort] + params[:molecule_sort], ) end end + namespace :structure do + desc 'Return all matched elements and associations for structure search' + params do + use :search_params + end + + after_validation do + set_var + end + + post do + Usecases::Search::StructureSearch.new( + collection_id: @c_id, + params: params, + user: current_user, + detail_levels: @dl, + ).perform! + end + end + + namespace :advanced do + desc 'Return all matched elements and associations for advanced / detail search' + params do + use :search_params + end + + after_validation do + set_var + end + + post do + conditions = + Usecases::Search::ConditionsForAdvancedSearch.new( + detail_levels: @dl, + params: params[:selection][:advanced_params], + ).filter! + + Usecases::Search::AdvancedSearch.new( + collection_id: @c_id, + params: params, + user: current_user, + conditions: conditions, + ).perform! + end + end + + namespace :by_ids do + desc 'Return elements by ids' + params do + use :search_params + end + + after_validation do + set_var + end + + post do + Usecases::Search::ByIds.new( + collection_id: @c_id, + params: params, + user: current_user, + ).perform! + end + end + namespace :samples do - desc "Return samples and associated elements by search selection" + desc 'Return samples and associated elements by search selection' params do use :search_params end @@ -479,20 +589,20 @@ def elements_by_scope(scope, collection_id = @c_id) when 'structure' sample_structure_search when 'cas' - Sample.by_collection_id(@c_id).by_sample_xref_cas( params[:selection][:name]) + Sample.by_collection_id(@c_id).by_sample_xref_cas(params[:selection][:name]) else Sample.by_collection_id(@c_id).search_by(search_by_method, params[:selection][:name]) end serialization_by_elements_and_page( elements_by_scope(samples), - params[:page] + params[:page], ) end end namespace :reactions do - desc "Return reactions and associated elements by search selection" + desc 'Return reactions and associated elements by search selection' params do use :search_params end @@ -520,13 +630,13 @@ def elements_by_scope(scope, collection_id = @c_id) serialization_by_elements_and_page( elements_by_scope(reactions), - params[:page] + params[:page], ) end end namespace :wellplates do - desc "Return wellplates and associated elements by search selection" + desc 'Return wellplates and associated elements by search selection' params do use :search_params end @@ -547,13 +657,13 @@ def elements_by_scope(scope, collection_id = @c_id) serialization_by_elements_and_page( elements_by_scope(wellplates), - params[:page] + params[:page], ) end end namespace :screens do - desc "Return screens and associated elements by search selection" + desc 'Return screens and associated elements by search selection' params do use :search_params end @@ -575,7 +685,7 @@ def elements_by_scope(scope, collection_id = @c_id) serialization_by_elements_and_page( elements_by_scope(screens), - params[:page] + params[:page], ) end end @@ -583,4 +693,8 @@ def elements_by_scope(scope, collection_id = @c_id) end end -# rubocop:enable Metrics/ClassLength +# rubocop:enable Naming/VariableName, Naming/MethodParameterName, Layout/LineLength + +# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Lint/SafeNavigationChain, Style/RedundantParentheses + +# rubocop:enable Metrics/MethodLength, Metrics/BlockLength, Metrics/AbcSize, Metrics/ClassLength diff --git a/app/api/chemotion/segment_api.rb b/app/api/chemotion/segment_api.rb deleted file mode 100644 index 9a84c4453f..0000000000 --- a/app/api/chemotion/segment_api.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Chemotion - class SegmentAPI < Grape::API - include Grape::Kaminari - - resource :segments do - namespace :klasses do - desc "get segment klasses" - params do - optional :element, type: String, desc: "Klass Element, e.g. Sample, Reaction, Mof,..." - end - get do - list = SegmentKlass.joins(:element_klass).where(klass_element: params[:element], is_active: true) if params[:element].present? - list = SegmentKlass.joins(:element_klass).where(is_active: true) unless params[:element].present? - present list.sort_by(&:place), with: Entities::SegmentKlassEntity, root: 'klass' - end - end - end - end -end diff --git a/app/api/chemotion/suggestion_api.rb b/app/api/chemotion/suggestion_api.rb index a5a758b5bf..311a0a4e7e 100644 --- a/app/api/chemotion/suggestion_api.rb +++ b/app/api/chemotion/suggestion_api.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/ClassLength, Metrics/BlockLength + module Chemotion # Input suggestion for free text search class SuggestionAPI < Grape::API @@ -25,6 +27,19 @@ def search_possibilities_to_suggestions(search_possibilities) suggestions end + def search_for_celllines + collection_id = @params['collection_id'] + query = params[:query] + + material_ids = CelllineSample.by_material_name(query, collection_id).map(&:cellline_material_id).uniq + { + cell_line_material_name: CelllineMaterial.where(id: material_ids).pluck(:name).uniq, + cell_line_sample_name: CelllineSample.by_sample_name(query, collection_id).pluck(:name), + } + end + + # rubocop:disable Style/TrailingCommaInHashLiteral, Layout/LineLength + def search_possibilities_by_type_user_and_collection(type) collection_id = @c_id dl = @dl @@ -33,6 +48,7 @@ def search_possibilities_by_type_user_and_collection(type) dl_wp = dl[:wellplate_detail_level] dl_sc = dl[:screen_detail_level] dl_e = dl[:element_detail_level] + dl_cl = dl[:celllinesample_detail_level] d_for = proc do |klass| klass.by_collection_id(collection_id) @@ -51,26 +67,31 @@ def search_possibilities_by_type_user_and_collection(type) search_by_element_short_label = proc do |klass, qry| scope = d_for.call klass - scope.send("by_short_label", qry).page(1).per(page_size).map { |el| {klass: el.element_klass.name, icon: el.element_klass.icon_name, label: "#{el.element_klass.label} Short Label", name: el.short_label } } + scope.send(:by_short_label, qry).page(1).per(page_size).map do |el| + { klass: el.element_klass.name, + icon: el.element_klass.icon_name, + label: "#{el.element_klass.label} Short Label", + name: el.short_label } + end end qry = params[:query] case type when 'samples' - sample_short_label = dl_s.positive? && search_by_field.call(Sample, :short_label, qry) || [] - sample_external_label = dl_s > -1 && search_by_field.call(Sample, :external_label, qry) || [] - sample_name = dl_s.positive? && search_by_field.call(Sample, :name, qry) || [] - polymer_type = dl_s.positive? && d_for.call(Sample) + sample_short_label = (dl_s.positive? && search_by_field.call(Sample, :short_label, qry)) || [] + sample_external_label = (dl_s > -1 && search_by_field.call(Sample, :external_label, qry)) || [] + sample_name = (dl_s.positive? && search_by_field.call(Sample, :name, qry)) || [] + polymer_type = (dl_s.positive? && d_for.call(Sample) .by_residues_custom_info('polymer_type', qry) - .pluck(Arel.sql("residues.custom_info->'polymer_type'")).uniq || [] - sum_formula = dl_s.positive? && search_by_field.call(Sample, :molecule_sum_formular, qry) || [] - iupac_name = dl_s.positive? && search_by_field.call(Molecule, :iupac_name, qry) || [] + .pluck(Arel.sql("residues.custom_info->'polymer_type'")).uniq) || [] + sum_formula = (dl_s.positive? && search_by_field.call(Sample, :molecule_sum_formular, qry)) || [] + iupac_name = (dl_s.positive? && search_by_field.call(Molecule, :iupac_name, qry)) || [] # cas = dl_s.positive? && search_by_field.call(Molecule, :cas, qry) || [] - cas = dl_s.positive? && search_by_field.call(Sample, :sample_xref_cas, qry) || [] - inchistring = dl_s.positive? && search_by_field.call(Molecule, :inchistring, qry) || [] - inchikey = dl_s.positive? && search_by_field.call(Molecule, :inchikey, qry) || [] - cano_smiles = dl_s.positive? && search_by_field.call(Molecule, :cano_smiles, qry) || [] + cas = (dl_s.positive? && search_by_field.call(Sample, :sample_xref_cas, qry)) || [] + inchistring = (dl_s.positive? && search_by_field.call(Molecule, :inchistring, qry)) || [] + inchikey = (dl_s.positive? && search_by_field.call(Molecule, :inchikey, qry)) || [] + cano_smiles = (dl_s.positive? && search_by_field.call(Molecule, :cano_smiles, qry)) || [] { sample_short_label: sample_short_label, sample_external_label: sample_external_label, @@ -81,17 +102,30 @@ def search_possibilities_by_type_user_and_collection(type) cas: cas, inchistring: inchistring, inchikey: inchikey, - cano_smiles: cano_smiles + cano_smiles: cano_smiles, } when 'reactions' - reaction_name = dl_r > -1 && search_by_field.call(Reaction, :name, qry) || [] - reaction_short_label = dl_r > -1 && search_by_field.call(Reaction, :short_label, qry) || [] - reaction_status = dl_r > -1 && search_by_field.call(Reaction, :status, qry) || [] - reaction_rinchi_string = dl_r > -1 && search_by_field.call(Reaction, :rinchi_string, qry) || [] - sample_name = dl_s.positive? && d_for.call(Sample).with_reactions.by_name(qry).pluck(:name).uniq || [] - iupac_name = dl_s.positive? && d_for.call(Molecule).with_reactions.by_iupac_name(qry).pluck(:iupac_name).uniq || [] - inchistring = dl_s.positive? && d_for.call(Molecule).with_reactions.by_inchistring(qry).pluck(:inchistring).uniq || [] - cano_smiles = dl_s.positive? && d_for.call(Molecule).with_reactions.by_cano_smiles(qry).pluck(:cano_smiles).uniq || [] + reaction_name = (dl_r > -1 && search_by_field.call(Reaction, :name, qry)) || [] + reaction_short_label = (dl_r > -1 && search_by_field.call(Reaction, :short_label, qry)) || [] + reaction_status = (dl_r > -1 && search_by_field.call(Reaction, :status, qry)) || [] + reaction_rinchi_string = (dl_r > -1 && search_by_field.call(Reaction, :rinchi_string, qry)) || [] + sample_name = (dl_s.positive? && d_for.call(Sample) + .with_reactions + .by_name(qry) + .pluck(:name).uniq) || [] + iupac_name = (dl_s.positive? && d_for.call(Molecule) + .with_reactions.by_iupac_name(qry) + .pluck(:iupac_name) + .uniq) || [] + inchistring = (dl_s.positive? && d_for.call(Molecule) + .with_reactions + .by_inchistring(qry) + .pluck(:inchistring) + .uniq) || [] + cano_smiles = (dl_s.positive? && d_for.call(Molecule) + .with_reactions.by_cano_smiles(qry) + .pluck(:cano_smiles) + .uniq) || [] { reaction_name: reaction_name, reaction_short_label: reaction_short_label, @@ -100,53 +134,71 @@ def search_possibilities_by_type_user_and_collection(type) sample_name: sample_name, iupac_name: iupac_name, inchistring: inchistring, - cano_smiles: cano_smiles + cano_smiles: cano_smiles, } when 'wellplates' - wellplate_name = dl_wp > -1 && search_by_field.call(Wellplate, :name, qry) || [] - sample_name = dl_s.positive? && d_for.call(Sample).with_wellplates.by_name(qry).pluck(:name).uniq || [] - iupac_name = dl_s.positive? && d_for.call(Molecule).with_wellplates.by_iupac_name(qry).pluck(:iupac_name).uniq || [] - inchistring = dl_s.positive? && d_for.call(Molecule).with_wellplates.by_inchistring(qry).pluck(:inchistring).uniq || [] - cano_smiles = dl_s.positive? && d_for.call(Molecule).with_wellplates.by_cano_smiles(qry).pluck(:cano_smiles).uniq || [] + wellplate_name = (dl_wp > -1 && search_by_field.call(Wellplate, :name, qry)) || [] + sample_name = (dl_s.positive? && d_for.call(Sample) + .with_wellplates + .by_name(qry) + .pluck(:name).uniq) || [] + iupac_name = (dl_s.positive? && d_for.call(Molecule) + .with_wellplates + .by_iupac_name(qry) + .pluck(:iupac_name) + .uniq) || [] + inchistring = (dl_s.positive? && d_for.call(Molecule) + .with_wellplates + .by_inchistring(qry) + .pluck(:inchistring) + .uniq) || [] + cano_smiles = (dl_s.positive? && d_for.call(Molecule) + .with_wellplates + .by_cano_smiles(qry) + .pluck(:cano_smiles) + .uniq) || [] { wellplate_name: wellplate_name, sample_name: sample_name, iupac_name: iupac_name, inchistring: inchistring, - cano_smiles: cano_smiles + cano_smiles: cano_smiles, } when 'screens' - screen_name = dl_sc > -1 && search_by_field.call(Screen, :name, qry) || [] - conditions = dl_sc > -1 && search_by_field.call(Screen, :conditions, qry) || [] - requirements = dl_sc > -1 && search_by_field.call(Screen, :requirements, qry) || [] + screen_name = (dl_sc > -1 && search_by_field.call(Screen, :name, qry)) || [] + conditions = (dl_sc > -1 && search_by_field.call(Screen, :conditions, qry)) || [] + requirements = (dl_sc > -1 && search_by_field.call(Screen, :requirements, qry)) || [] { screen_name: screen_name, conditions: conditions, - requirements: requirements + requirements: requirements, } + when 'cell_lines' + dl_cl.positive? ? search_for_celllines : [] else - element_short_label = dl_e.positive? && search_by_element_short_label.call(Element, qry) || [] - sample_name = dl_s.positive? && search_by_field.call(Sample, :name, qry) || [] - sample_short_label = dl_s.positive? && search_by_field.call(Sample, :short_label, qry) || [] - sample_external_label = dl_s > -1 && search_by_field.call(Sample, :external_label, qry) || [] - polymer_type = dl_s.positive? && d_for.call(Sample) + element_short_label = (dl_e.positive? && search_by_element_short_label.call(Labimotion::Element, qry)) || [] + sample_name = (dl_s.positive? && search_by_field.call(Sample, :name, qry)) || [] + sample_short_label = (dl_s.positive? && search_by_field.call(Sample, :short_label, qry)) || [] + sample_external_label = (dl_s > -1 && search_by_field.call(Sample, :external_label, qry)) || [] + polymer_type = (dl_s.positive? && d_for.call(Sample) .by_residues_custom_info('polymer_type', qry) - .pluck(Arel.sql("residues.custom_info->'polymer_type'")).uniq || [] - sum_formula = dl_s.positive? && search_by_field.call(Sample, :molecule_sum_formular, qry) || [] - iupac_name = dl_s.positive? && search_by_field.call(Molecule, :iupac_name, qry) || [] + .pluck(Arel.sql("residues.custom_info->'polymer_type'")).uniq) || [] + sum_formula = (dl_s.positive? && search_by_field.call(Sample, :molecule_sum_formular, qry)) || [] + iupac_name = (dl_s.positive? && search_by_field.call(Molecule, :iupac_name, qry)) || [] # cas = dl_s.positive? && search_by_field.call(Molecule, :cas, qry) || [] - cas = dl_s.positive? && search_by_field.call(Sample, :sample_xref_cas, qry) || [] - inchistring = dl_s.positive? && search_by_field.call(Molecule, :inchistring, qry) || [] - inchikey = dl_s.positive? && search_by_field.call(Molecule, :inchikey, qry) || [] - cano_smiles = dl_s.positive? && search_by_field.call(Molecule, :cano_smiles, qry) || [] - reaction_name = dl_r > -1 && search_by_field.call(Reaction, :name, qry) || [] - reaction_status = dl_r > -1 && search_by_field.call(Reaction, :status, qry) || [] - reaction_short_label = dl_r > -1 && search_by_field.call(Reaction, :short_label, qry) || [] - reaction_rinchi_string = dl_r > -1 && search_by_field.call(Reaction, :rinchi_string, qry) || [] - wellplate_name = dl_wp > -1 && search_by_field.call(Wellplate, :name, qry) || [] - screen_name = dl_sc > -1 && search_by_field.call(Screen, :name, qry) || [] - conditions = dl_sc > -1 && search_by_field.call(Screen, :conditions, qry) || [] - requirements = dl_sc > -1 && search_by_field.call(Screen, :requirements, qry) || [] + cas = (dl_s.positive? && search_by_field.call(Sample, :sample_xref_cas, qry)) || [] + inchistring = (dl_s.positive? && search_by_field.call(Molecule, :inchistring, qry)) || [] + inchikey = (dl_s.positive? && search_by_field.call(Molecule, :inchikey, qry)) || [] + cano_smiles = (dl_s.positive? && search_by_field.call(Molecule, :cano_smiles, qry)) || [] + reaction_name = (dl_r > -1 && search_by_field.call(Reaction, :name, qry)) || [] + reaction_status = (dl_r > -1 && search_by_field.call(Reaction, :status, qry)) || [] + reaction_short_label = (dl_r > -1 && search_by_field.call(Reaction, :short_label, qry)) || [] + reaction_rinchi_string = (dl_r > -1 && search_by_field.call(Reaction, :rinchi_string, qry)) || [] + wellplate_name = (dl_wp > -1 && search_by_field.call(Wellplate, :name, qry)) || [] + screen_name = (dl_sc > -1 && search_by_field.call(Screen, :name, qry)) || [] + conditions = (dl_sc > -1 && search_by_field.call(Screen, :conditions, qry)) || [] + requirements = (dl_sc > -1 && search_by_field.call(Screen, :requirements, qry)) || [] + cell_line_infos = dl_cl.positive? ? search_for_celllines : [] { element_short_label: element_short_label, @@ -167,18 +219,19 @@ def search_possibilities_by_type_user_and_collection(type) wellplate_name: wellplate_name, screen_name: screen_name, conditions: conditions, - requirements: requirements - } + requirements: requirements, + }.merge(cell_line_infos) end end end + # rubocop:enable Style/TrailingCommaInHashLiteral, Layout/LineLength resource :suggestions do after_validation do set_var end - route_param :element_type, type: String, values: %w[all samples reactions wellplates screens] do + route_param :element_type, type: String, values: %w[all samples reactions wellplates screens cell_lines] do desc 'Return all suggestions for AutoCompleteInput' params do use :suggestion_params @@ -192,3 +245,4 @@ def search_possibilities_by_type_user_and_collection(type) end end end +# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/ClassLength, Metrics/BlockLength diff --git a/app/api/chemotion/third_party_app_api.rb b/app/api/chemotion/third_party_app_api.rb index b6d2885c82..ce1ec0adde 100644 --- a/app/api/chemotion/third_party_app_api.rb +++ b/app/api/chemotion/third_party_app_api.rb @@ -138,12 +138,12 @@ def cache_key_for_encoded_token(payload) desc 'create new third party app entry' params do - requires :IPAddress, type: String, desc: 'The IPAddress in order to redirect to the app.' + requires :url, type: String, desc: 'The url in order to redirect to the app.' requires :name, type: String, desc: 'name of third party app. User will chose correct app based on names.' end post '/new_third_party_app' do declared(params, include_missing: false) - ThirdPartyApp.create!(IPAddress: params[:IPAddress], name: params[:name]) + ThirdPartyApp.create!(url: params[:url], name: params[:name]) status 201 rescue ActiveRecord::RecordInvalid error!('Unauthorized. User has to be admin.', 401) @@ -152,13 +152,13 @@ def cache_key_for_encoded_token(payload) desc 'update a third party app entry' params do requires :id, type: String, desc: 'The id of the app which should be updated' - requires :IPAddress, type: String, desc: 'The IPAddress in order to redirect to the app.' + requires :url, type: String, desc: 'The url in order to redirect to the app.' requires :name, type: String, desc: 'name of third party app. User will chose correct app based on names.' end post '/update_third_party_app' do declared(params, include_missing: false) entry = ThirdPartyApp.find(params[:id]) - entry.update!(IPAddress: params[:IPAddress], name: params[:name]) + entry.update!(url: params[:url], name: params[:name]) status 201 rescue ActiveRecord::RecordInvalid error!('Unauthorized. User has to be admin.', 401) @@ -197,7 +197,7 @@ def cache_key_for_encoded_token(payload) end get 'IP' do tpa = ThirdPartyApp.find_by(name: params[:name]) - return tpa.IPAddress if tpa + return tpa.url if tpa error_msg = "Third party app with ID: #{id} not found" { error: error_msg } @@ -211,22 +211,23 @@ def cache_key_for_encoded_token(payload) desc 'create token for use in download public_api' params do requires :attID, type: String, desc: 'Attachment ID' - requires :userID, type: String, desc: 'User ID' requires :nameThirdPartyApp, type: String, desc: 'name of the third party app' end get 'Token' do - cache_key = "token/#{params[:attID]}/#{params[:userID]}/#{params[:nameThirdPartyApp]}" - payload = { attID: params[:attID], userID: params[:userID], nameThirdPartyApp: params[:nameThirdPartyApp] } + app = ThirdPartyApp.find_by(name: params[:nameThirdPartyApp]) + cache_key = "token/#{params[:attID]}/#{current_user.id}/#{app.id}" + payload = { attID: params[:attID], userID: current_user.id, nameThirdPartyApp: params[:nameThirdPartyApp] } cached_token = encode_token(payload, params[:nameThirdPartyApp]) Rails.cache.write(cache_key, cached_token, expires_in: 48.hours) - cached_token.token + address = app.url + "#{address}?token=#{cached_token.token}&url=#{CGI.escape(Rails.application.config.root_url)}" end end resource :names do desc 'Find all names of all third party app' get 'all' do - ThirdPartyApp.all_names + ThirdPartyApp.pluck :name end end end diff --git a/app/api/chemotion/ui_api.rb b/app/api/chemotion/ui_api.rb index 2b7eae9bc2..cc6866fb4f 100644 --- a/app/api/chemotion/ui_api.rb +++ b/app/api/chemotion/ui_api.rb @@ -14,16 +14,22 @@ class UiAPI < Grape::API sfn_config = Rails.configuration.try(:sfn_config).try(:provider) converter_config = Rails.configuration.try(:converter).try(:url) radar_config = Rails.configuration.try(:radar).try(:url) + collector_config = Rails.configuration.try(:datacollectors) + collector_address = collector_config.present? && ( + collector_config.dig(:mailcollector, :aliases, -1) || collector_config.dig(:mailcollector, :mail_address) + ) { has_chem_spectra: has_chem_spectra, has_nmrium_wrapper: has_nmrium_wrapper, matrices: File.exist?(m_config) ? JSON.parse(File.read(m_config)) : {}, - klasses: ElementKlass.where(is_active: true, is_generic: true)&.pluck(:name) || [], + klasses: Labimotion::ElementKlass.where(is_active: true, is_generic: true)&.pluck(:name) || [], structure_editors: Rails.configuration.structure_editors, has_sfn: sfn_config.present? && current_user.matrix_check_by_name('scifinderN'), has_converter: converter_config.present?, has_radar: radar_config.present?, + collector_address: collector_address.presence, + third_party_apps: ThirdPartyApp.all } end end diff --git a/app/api/chemotion/user_api.rb b/app/api/chemotion/user_api.rb index 678979c461..6828f4df88 100644 --- a/app/api/chemotion/user_api.rb +++ b/app/api/chemotion/user_api.rb @@ -1,18 +1,22 @@ # frozen_string_literal: true + module Chemotion + # rubocop:disable Metrics/ClassLength class UserAPI < Grape::API resource :users do desc 'Find top 3 matched user names' params do requires :name, type: String + optional :type, type: [String], desc: 'user types', + coerce_with: ->(val) { val.split(/[\s|,]+/) }, + values: %w[Group Person], + default: %w[Group Person] end get 'name' do - unless params[:name].nil? || params[:name].empty? - { users: User.where(type: %w[Person Group]).by_name(params[:name]).limit(3) - .select('first_name', 'last_name', 'name', 'id', 'name_abbreviation', 'name_abbreviation as abb', 'type as user_type') } - else - { users: [] } - end + return { users: [] } if params[:name].blank? + + users = User.where(type: params[:type]).by_name(params[:name]).limit(3) + present users, with: Entities::UserSimpleEntity, root: 'users' end desc 'Return current_user' @@ -30,12 +34,14 @@ class UserAPI < Grape::API desc 'list structure editors' get 'list_editors' do editors = [] - %w[chemdrawEditor marvinjsEditor ketcher2Editor].each { |str| editors.push(str) if current_user.matrix_check_by_name(str) } + %w[chemdrawEditor marvinjsEditor ketcher2Editor].each do |str| + editors.push(str) if current_user.matrix_check_by_name(str) + end present Matrice.where(name: editors).order('name'), with: Entities::MatriceEntity, root: 'matrices' end namespace :omniauth_providers do - desc "get omniauth providers" + desc 'get omniauth providers' get do { providers: Devise.omniauth_configs.keys, current_user: current_user } end @@ -57,7 +63,7 @@ class UserAPI < Grape::API access_level: params[:access_level] || 0, title: params[:title], description: params[:description], - color: params[:color] + color: params[:color], } label = nil if params[:id].present? @@ -88,7 +94,8 @@ class UserAPI < Grape::API namespace :scifinder do desc 'scifinder-n credential' get do - present(ScifinderNCredential.find_by(created_by: current_user.id) || {}, with: Entities::ScifinderNCredentialEntity) + present(ScifinderNCredential.find_by(created_by: current_user.id) || {}, + with: Entities::ScifinderNCredentialEntity) end end @@ -101,7 +108,7 @@ class UserAPI < Grape::API resource :groups do rescue_from ActiveRecord::RecordInvalid do |error| message = error.record.errors.messages.map do |attr, msg| - "%s %s" % [attr, msg.first] + format('%s %s', attr: attr, msg: msg.first) end error!(message.join(', '), 404) end @@ -121,7 +128,7 @@ class UserAPI < Grape::API after_validation do users = params[:group_param][:users] || [] @group_params = declared(params, include_missing: false).deep_symbolize_keys[:group_param] - @group_params[:email] ||= "%i@eln.edu" % [Time.now.getutc.to_i] + @group_params[:email] ||= format('%i@eln.edu', Time.now.getutc.to_i) @group_params[:password] = Devise.friendly_token.first(8) @group_params[:password_confirmation] = @group_params[:password] @group_params[:users] = User.where(id: [current_user.id] + users) @@ -157,7 +164,8 @@ class UserAPI < Grape::API end route_param :device_id do get do - present DeviceMetadata.find_by(device_id: params[:device_id]), with: Entities::DeviceMetadataEntity, root: 'device_metadata' + present DeviceMetadata.find_by(device_id: params[:device_id]), with: Entities::DeviceMetadataEntity, + root: 'device_metadata' end end end @@ -166,32 +174,43 @@ class UserAPI < Grape::API desc 'update a group of persons' params do requires :id, type: Integer - optional :rm_users, type: Array - optional :add_users, type: Array + optional :rm_users, type: [Integer], desc: 'remove users from group', default: [] + optional :add_users, type: [Integer], desc: 'add users to group', default: [] + optional :add_admin, type: [Integer], desc: 'add admin to group', default: [] + optional :rm_admin, type: [Integer], desc: 'remove admin from group', default: [] optional :destroy_group, type: Boolean, default: false end after_validation do - if current_user.administrated_accounts.where(id: params[:id]).empty? && - !params[:rm_users].nil? && current_user.id != params[:rm_users][0] - error!('401 Unauthorized', 401) - end + @group = Group.find_by(id: params[:id]) + @as_admin = @group.administrated_by?(current_user) + @rm_current_user_id = !@as_admin && params[:rm_users].delete(current_user.id) + error!('401 Unauthorized', 401) unless @group.administrated_by?(current_user) || @rm_current_user_id end put ':id' do - group = Group.find(params[:id]) - if params[:destroy_group] - User.find_by(id: params[:id])&.remove_from_matrices - { destroyed_id: params[:id] } if group.destroy! + if @rm_current_user_id + @group.users.delete(User.where(id: rm_user_id)) + User.gen_matrix([@rm_current_user_id]) + present @group, with: Entities::GroupEntity, root: 'group' + elsif params[:destroy_group] + @group.destroy! && { destroyed_id: params[:id] } else - new_users = - (params[:add_users] || []).map(&:to_i) - group.users.pluck(:id) - rm_users = (params[:rm_users] || []).map(&:to_i) - group.users << Person.where(id: new_users) - group.save! - group.users.delete(User.where(id: rm_users)) - User.gen_matrix(rm_users) if rm_users&.length&.positive? - present group, with: Entities::GroupEntity, root: 'group' + # add new admins + params[:add_admin].delete(@group.admins.pluck(:id)) # ensure that admins are not added twice + @group.admins << User.where(id: params[:add_admin]) + # remove admins + # ensure that current_user is not removed from admins when being last admin + params[:rm_admin].delete(current_user.id) if Group.last.admins.count == 1 + @group.users_admins.where(admin_id: params[:rm_admin]).destroy_all + # add new users + params[:add_users].delete(@group.users.pluck(:id)) + @group.users << Person.where(id: params[:add_users]) + # remove users + @group.users.delete(User.where(id: params[:rm_users])) + User.gen_matrix(params[:rm_users]) if params[:rm_users].length.positive? + + present @group, with: Entities::GroupEntity, root: 'group' end end end @@ -204,19 +223,21 @@ class UserAPI < Grape::API end get :novnc do - if params[:id] != '0' - devices = Device.by_user_ids(user_ids).novnc.where(id: params[:id]).includes(:profile) - else - devices = Device.by_user_ids(user_ids).novnc.includes(:profile) - end + devices = if params[:id] == '0' + Device.by_user_ids(user_ids).novnc.includes(:profile) + else + Device.by_user_ids(user_ids).novnc.where(id: params[:id]).includes(:profile) + end present devices, with: Entities::DeviceNovncEntity, root: 'devices' end get 'current_connection' do path = Rails.root.join('tmp/novnc_devices', params[:id]) - cmd = "echo '#{current_user.id},#{params[:status] == 'true' ? 1 : 0}' >> #{path};LINES=$(tail -n 8 #{path});echo \"$LINES\" | tee #{path}" - { result: Open3.popen3(cmd) { |i, o, e, t| o.read.split(/\s/) } } + cmd = "echo '#{current_user.id},#{params[:status] == 'true' ? 1 : 0}' >> #{path};" + cmd += "LINES=$(tail -n 8 #{path});echo \"$LINES\" | tee #{path}" + { result: Open3.popen3(cmd) { |_i, o, _e, _t| o.read.split(/\s/) } } end end end + # rubocop:enable Metrics/ClassLength end diff --git a/app/api/entities/application_entity.rb b/app/api/entities/application_entity.rb index a862374bb8..05e788ee0c 100644 --- a/app/api/entities/application_entity.rb +++ b/app/api/entities/application_entity.rb @@ -5,6 +5,7 @@ class ApplicationEntity < Grape::Entity CUSTOM_ENTITY_OPTIONS = %i[anonymize_below anonymize_with].freeze format_with(:eln_timestamp) do |datetime| + # datetime.present? ? I18n.l(datetime, format: :eln_iso8601) : nil datetime.present? ? I18n.l(datetime, format: :eln_timestamp) : nil end diff --git a/app/api/entities/attachment_entity.rb b/app/api/entities/attachment_entity.rb index 4963bef5fb..b3c1ca24d1 100644 --- a/app/api/entities/attachment_entity.rb +++ b/app/api/entities/attachment_entity.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true module Entities - class AttachmentEntity < Grape::Entity + class AttachmentEntity < ApplicationEntity expose :id, documentation: { type: 'Integer', desc: "Attachment's unique id" } - expose :filename, :identifier, :content_type, :thumb, :aasm_state, :filesize + expose :filename, :identifier, :content_type, :thumb, :aasm_state, :filesize, :thumbnail + expose_timestamps + + def thumbnail + object.thumb ? Base64.encode64(object.read_thumbnail) : nil + end end end diff --git a/app/api/entities/cell_line_material_name_entity.rb b/app/api/entities/cell_line_material_name_entity.rb new file mode 100644 index 0000000000..44ef9cb7c8 --- /dev/null +++ b/app/api/entities/cell_line_material_name_entity.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Entities + # Publish-Subscription Entities + class CellLineMaterialNameEntity < Grape::Entity + expose :name + expose :source + expose :id + end +end diff --git a/app/api/entities/cell_line_sample_entity.rb b/app/api/entities/cell_line_sample_entity.rb new file mode 100644 index 0000000000..aa865e102a --- /dev/null +++ b/app/api/entities/cell_line_sample_entity.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Entities + class CellLineSampleEntity < Grape::Entity + expose :id + expose :amount + expose :passage + expose :contamination + expose :name + expose :short_label + expose :description + expose :unit + expose :cellline_material + expose :tag + expose :container, using: 'Entities::ContainerEntity' + end +end diff --git a/app/api/entities/channel_entity.rb b/app/api/entities/channel_entity.rb index c8a944d392..6b7fd956b0 100644 --- a/app/api/entities/channel_entity.rb +++ b/app/api/entities/channel_entity.rb @@ -2,18 +2,11 @@ module Entities # Publish-Subscription Entities - class ChannelEntity < Grape::Entity + class ChannelEntity < ApplicationEntity expose :id, :subject, :channel_type - expose :created_at, :updated_at expose :msg_template, if: ->(obj, _opts) { obj.respond_to? :msg_template } expose :user_id, if: ->(obj, _opts) { obj.respond_to? :user_id } - def created_at - object.created_at.strftime('%d.%m.%Y, %H:%M:%S') - end - - def updated_at - object.updated_at.strftime('%d.%m.%Y, %H:%M:%S') - end + expose_timestamps end end diff --git a/app/api/entities/collection_entity.rb b/app/api/entities/collection_entity.rb index a56169b1ea..5f577457f6 100644 --- a/app/api/entities/collection_entity.rb +++ b/app/api/entities/collection_entity.rb @@ -16,6 +16,7 @@ class CollectionEntity < ApplicationEntity :screen_detail_level, :shared_by_id, :wellplate_detail_level, + :tabs_segment ) expose :children, using: 'Entities::CollectionEntity' diff --git a/app/api/entities/collection_root_entity.rb b/app/api/entities/collection_root_entity.rb index c576ee76e5..8a59d6af98 100644 --- a/app/api/entities/collection_root_entity.rb +++ b/app/api/entities/collection_root_entity.rb @@ -45,6 +45,12 @@ class CollectionRootEntity < Grape::Entity expose :shared_by do |obj| obj['shared_by'] end + expose :tabs_segment do |obj| + obj['tabs_segment'] + end + expose :ancestry do |obj| + obj['ancestry'] + end expose :children, as: 'children', using: 'Entities::CollectionRootEntity' end end diff --git a/app/api/entities/collection_sync_entity.rb b/app/api/entities/collection_sync_entity.rb index b4480887dd..6e85bb1a7f 100644 --- a/app/api/entities/collection_sync_entity.rb +++ b/app/api/entities/collection_sync_entity.rb @@ -45,6 +45,9 @@ class CollectionSyncEntity < Grape::Entity expose :sharer do |obj| obj['temp_sharer'] end + expose :tabs_segment do |obj| + obj['tabs_segment'] + end expose :children, as: 'children', using: Entities::CollectionSyncEntity expose :is_sync_to_me do |obj| diff --git a/app/api/entities/comment_entity.rb b/app/api/entities/comment_entity.rb new file mode 100644 index 0000000000..3b63501f18 --- /dev/null +++ b/app/api/entities/comment_entity.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Entities + class CommentEntity < ApplicationEntity + expose :id + expose :content + expose :created_by + expose :section + expose :status + expose :submitter + expose :resolver_name + expose :commentable_id, :commentable_type + + expose_timestamps + end +end diff --git a/app/api/entities/container_entity.rb b/app/api/entities/container_entity.rb index 29658c907f..018de84e49 100644 --- a/app/api/entities/container_entity.rb +++ b/app/api/entities/container_entity.rb @@ -15,7 +15,7 @@ class ContainerEntity < ApplicationEntity expose :attachments, using: 'Entities::AttachmentEntity' expose :code_log, using: 'Entities::CodeLogEntity' expose :children, using: 'Entities::ContainerEntity' - expose :dataset, using: 'Entities::DatasetEntity' + expose :dataset, using: 'Labimotion::DatasetEntity' def extended_metadata return unless object.extended_metadata @@ -55,12 +55,16 @@ def preview_img ) return no_preview_image_available unless attachments_with_thumbnail.exists? - latest_image_attachment = attachments_with_thumbnail.where( + atts_with_thumbnail = attachments_with_thumbnail.where( "attachment_data -> 'metadata' ->> 'mime_type' in (:value)", value: THUMBNAIL_CONTENT_TYPES, - ).order(updated_at: :desc).first + ).order(updated_at: :desc) - attachment = latest_image_attachment || attachments_with_thumbnail.first + combined_image_attachment = atts_with_thumbnail.where('filename LIKE ?', '%combined%').first + + latest_image_attachment = atts_with_thumbnail.first + + attachment = combined_image_attachment || latest_image_attachment || attachments_with_thumbnail.first preview_image = attachment.read_thumbnail return no_preview_image_available unless preview_image diff --git a/app/api/entities/dataset_entity.rb b/app/api/entities/dataset_entity.rb deleted file mode 100644 index 4abc618972..0000000000 --- a/app/api/entities/dataset_entity.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Entity module -module Entities - # Dataset entity - class DatasetEntity < Grape::Entity - expose :id, :dataset_klass_id, :properties, :element_id, :element_type, :klass_ols, :klass_label - def klass_ols - object&.dataset_klass&.ols_term_id - end - def klass_label - object&.dataset_klass&.label - end - end -end diff --git a/app/api/entities/dataset_klass_entity.rb b/app/api/entities/dataset_klass_entity.rb deleted file mode 100644 index 27296364b3..0000000000 --- a/app/api/entities/dataset_klass_entity.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Entities - class DatasetKlassEntity < ApplicationEntity - expose( - :desc, - :id, - :is_active, - :label, - :ols_term_id, - :place, - :properties_release, - :properties_template, - :uuid, - ) - expose_timestamps(timestamp_fields: [:released_at]) - end -end diff --git a/app/api/entities/device_metadata_entity.rb b/app/api/entities/device_metadata_entity.rb index d21642409e..d49e4e75c5 100644 --- a/app/api/entities/device_metadata_entity.rb +++ b/app/api/entities/device_metadata_entity.rb @@ -20,5 +20,13 @@ class DeviceMetadataEntity < Grape::Entity expose :data_cite_created_at, documentation: { type: 'DateTime', desc: 'created_at DataCite ' } expose :data_cite_updated_at, documentation: { type: 'DateTime', desc: 'updated_at DataCite' } expose :data_cite_version, documentation: { type: 'Integer', desc: 'version at DataCite' } + + def data_cite_created_at + object.data_cite_created_at.present? ? I18n.l(object.data_cite_created_at, format: :eln_timestamp) : nil + end + + def data_cite_updated_at + object.data_cite_updated_at.present? ? I18n.l(object.data_cite_updated_at, format: :eln_timestamp) : nil + end end end diff --git a/app/api/entities/element_entity.rb b/app/api/entities/element_entity.rb deleted file mode 100644 index e910937535..0000000000 --- a/app/api/entities/element_entity.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -module Entities - class ElementEntity < ApplicationEntity - with_options(anonymize_below: 0) do - expose! :can_copy - expose! :container, using: 'Entities::ContainerEntity' - expose! :created_by - expose! :id - expose! :is_restricted - expose! :klass_uuid - expose! :name - expose! :properties - expose! :short_label - expose! :type - expose! :uuid - end - - with_options(anonymize_below: 10) do - expose! :element_klass, anonymize_with: nil, using: 'Entities::ElementKlassEntity' - expose! :segments, anonymize_with: [], using: 'Entities::SegmentEntity' - expose! :tag, anonymize_with: nil, using: 'Entities::ElementTagEntity' - end - - expose_timestamps - - - private - - def is_restricted - detail_levels[Element] < 10 - end - - # TODO: Refactor this method to something more readable/understandable - def properties - (object.properties['layers']&.keys || []).each do |key| - # layer = object.properties[key] - field_sample_molecules = object.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'drag_sample' || ss['type'] == 'drag_molecule' } - field_sample_molecules.each do |field| - idx = object.properties['layers'][key]['fields'].index(field) - sid = field.dig('value', 'el_id') - next unless sid.present? - - el = field['type'] == 'drag_sample' ? Sample.find_by(id: sid) : Molecule.find_by(id: sid) - next unless el.present? - next unless object.properties.dig('layers', key, 'fields', idx, 'value').present? - - object.properties['layers'][key]['fields'][idx]['value']['el_label'] = el.short_label if field['type'] == 'drag_sample' - object.properties['layers'][key]['fields'][idx]['value']['el_tip'] = el.short_label if field['type'] == 'drag_sample' - object.properties['layers'][key]['fields'][idx]['value']['el_svg'] = field['type'] == 'drag_sample' ? el.get_svg_path : File.join('/images', 'molecules', el.molecule_svg_file) - end - - field_tables = object.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'table' } - field_tables.each do |field| - idx = object.properties['layers'][key]['fields'].index(field) - next unless field['sub_values'].present? && field['sub_fields'].present? - - field_table_molecules = field['sub_fields'].select { |ss| ss['type'] == 'drag_molecule' } - object.properties['layers'][key]['fields'][idx] = set_table(field, field_table_molecules, 'Molecule') if field_table_molecules.present? - - field_table_samples = field['sub_fields'].select { |ss| ss['type'] == 'drag_sample' } - object.properties['layers'][key]['fields'][idx] = set_table(field, field_table_samples, 'Sample') if field_table_samples.present? - end - end - object.properties - end - - def type - object.element_klass.name # 'genericEl' #object.type - end - - def set_table(field, field_table_objs, obj) - col_ids = field_table_objs.map { |x| x.values[0] } - col_ids.each do |col_id| - field['sub_values'].each do |sub_value| - next unless sub_value[col_id].present? && sub_value[col_id]['value'].present? && sub_value[col_id]['value']['el_id'].present? - - find_obj = obj.constantize.find_by(id: sub_value[col_id]['value']['el_id']) - next unless find_obj.present? - - case obj - when 'Molecule' - sub_value[col_id]['value']['el_svg'] = File.join('/images', 'molecules', find_obj.molecule_svg_file) - sub_value[col_id]['value']['el_inchikey'] = find_obj.inchikey - sub_value[col_id]['value']['el_smiles'] = find_obj.cano_smiles - sub_value[col_id]['value']['el_iupac'] = find_obj.iupac_name - sub_value[col_id]['value']['el_molecular_weight'] = find_obj.molecular_weight - when 'Sample' - sub_value[col_id]['value']['el_svg'] = find_obj.get_svg_path - sub_value[col_id]['value']['el_label'] = find_obj.short_label - sub_value[col_id]['value']['el_short_label'] = find_obj.short_label - sub_value[col_id]['value']['el_name'] = find_obj.name - sub_value[col_id]['value']['el_external_label'] = find_obj.external_label - sub_value[col_id]['value']['el_molecular_weight'] = find_obj.decoupled ? find_obj.molecular_mass : find_obj.molecule.molecular_weight - end - end - end - field - end - - def can_copy - true - end - end -end diff --git a/app/api/entities/element_klass_entity.rb b/app/api/entities/element_klass_entity.rb deleted file mode 100644 index 952d72f35e..0000000000 --- a/app/api/entities/element_klass_entity.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Entities - class ElementKlassEntity < ApplicationEntity - expose( - :desc, - :icon_name, - :id, - :is_active, - :is_generic, - :klass_prefix, - :label, - :name, - :place, - :properties_release, - :properties_template, - :uuid, - ) - - expose_timestamps(timestamp_fields: [:released_at]) - end -end diff --git a/app/api/entities/element_revision_entity.rb b/app/api/entities/element_revision_entity.rb deleted file mode 100644 index 0250131e5a..0000000000 --- a/app/api/entities/element_revision_entity.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module Entities - # ElementRevisionEntity - class ElementRevisionEntity < Grape::Entity - expose :id, :element_id, :uuid, :name, :klass_uuid, :properties, :created_at - def created_at - object.created_at.strftime('%d.%m.%Y, %H:%M') - end - - def properties - object.properties['layers']&.keys.each do |key| - field_sample_molecules = object.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'drag_sample' || ss['type'] == 'drag_molecule' } - field_sample_molecules.each do |field| - idx = object.properties['layers'][key]['fields'].index(field) - sid = field.dig('value', 'el_id') - next unless sid.present? - - el = field['type'] == 'drag_sample' ? Sample.find_by(id: sid) : Molecule.find_by(id: sid) - next unless el.present? - next unless object.properties.dig('layers', key, 'fields', idx, 'value').present? - - object.properties['layers'][key]['fields'][idx]['value']['el_label'] = el.short_label if field['type'] == 'drag_sample' - object.properties['layers'][key]['fields'][idx]['value']['el_tip'] = el.short_label if field['type'] == 'drag_sample' - object.properties['layers'][key]['fields'][idx]['value']['el_svg'] = field['type'] == 'drag_sample' ? el.get_svg_path : File.join('/images', 'molecules', el.molecule_svg_file) - end - - field_tables = object.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'table' } - field_tables.each do |field| - next unless field['sub_values'].present? && field['sub_fields'].present? - - field_table_molecules = field['sub_fields'].select { |ss| ss['type'] == 'drag_molecule' } - next unless field_table_molecules.present? - - col_ids = field_table_molecules.map { |x| x.values[0] } - col_ids.each do |col_id| - field_table_values = field['sub_values'].each do |sub_value| - next unless sub_value[col_id].present? && sub_value[col_id]['value'].present? && sub_value[col_id]['value']['el_id'].present? - - find_mol = Molecule.find_by(id: sub_value[col_id]['value']['el_id']) - next unless find_mol.present? - - sub_value[col_id]['value']['el_svg'] = File.join('/images', 'molecules', find_mol.molecule_svg_file) - sub_value[col_id]['value']['el_inchikey'] = find_mol.inchikey - sub_value[col_id]['value']['el_smiles'] = find_mol.cano_smiles - sub_value[col_id]['value']['el_iupac'] = find_mol.iupac_name - sub_value[col_id]['value']['el_molecular_weight'] = find_mol.molecular_weight - end - end - end - end - object.properties - end - end -end diff --git a/app/api/entities/inbox_entity.rb b/app/api/entities/inbox_entity.rb index dc855b40fb..5dc2d0c5c8 100644 --- a/app/api/entities/inbox_entity.rb +++ b/app/api/entities/inbox_entity.rb @@ -19,10 +19,28 @@ class InboxEntity < ApplicationEntity def children depth = options[:root_container] ? 1 : 2 - dataset_page = options[:dataset_page].to_i || 1 + start_index, end_index = calculate_indices + + parent_objects = object.hash_tree(limit_depth: depth)[object].to_a + sorted_parents = sort_parent_objects(parent_objects) + + parents_slice = sorted_parents[start_index..end_index] + + serialize_children(parents_slice.to_h) + end + + def calculate_indices + dataset_page = (options[:dataset_page] || 1).to_i start_index = (dataset_page - 1) * DATASETS_PER_PAGE end_index = start_index + DATASETS_PER_PAGE - 1 - serialize_children(object.hash_tree(limit_depth: depth)[object].to_a.slice(start_index..end_index).to_h) + [start_index, end_index] + end + + # here the datasets within the deviceBoxes are being sorted + + def sort_parent_objects(parent_objects) + sorted_parents = parent_objects.sort_by { |container, _| container.send(options[:dataset_sort_column]) } + options[:dataset_sort_column].eql?('created_at') ? sorted_parents.reverse : sorted_parents end def serialize_children(container_tree_hash) @@ -34,7 +52,7 @@ def serialize_children(container_tree_hash) name: container.name, container_type: container.container_type, attachments: current_attachments, - created_at: container.created_at, + created_at: I18n.l(container.created_at, format: :eln_timestamp), children: serialize_children(subcontainers).compact, } end @@ -49,7 +67,7 @@ def unlinked_attachments attachable_type: 'Container', attachable_id: nil, created_for: object&.containable&.id, - ) + ).order("#{options[:sort_column]} #{options[:sort_direction]}") end def all_descendants_attachments @@ -59,7 +77,7 @@ def all_descendants_attachments FROM attachments AS sub_attachments WHERE sub_attachments.attachable_id = attachments.attachable_id LIMIT 50 - )") + )").order("#{options[:sort_column]} #{options[:sort_direction]}") end def inbox_count diff --git a/app/api/entities/inventory_entity.rb b/app/api/entities/inventory_entity.rb new file mode 100644 index 0000000000..87a7d51b7a --- /dev/null +++ b/app/api/entities/inventory_entity.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Entities + class InventoryEntity < Grape::Entity + expose :id, documentation: { type: 'Integer', desc: "Inventory's id" } + expose :prefix, documentation: { type: 'String', desc: "Inventory's prefix" } + expose :name, documentation: { type: 'String', desc: "Inventory's name" } + expose :counter, documentation: { type: 'Integer', desc: "Inventory's counter" } + end +end diff --git a/app/api/entities/matrice_entity.rb b/app/api/entities/matrice_entity.rb index 9ca87b1f47..ea1cc40881 100644 --- a/app/api/entities/matrice_entity.rb +++ b/app/api/entities/matrice_entity.rb @@ -1,6 +1,8 @@ module Entities - class MatriceEntity < Grape::Entity - expose :id, :enabled, :name, :label, :include_ids, :exclude_ids, :created_at, :updated_at, :include_users, :exclude_users, :configs + class MatriceEntity < ApplicationEntity + expose :id, :enabled, :name, :label, :include_ids, :exclude_ids, :include_users, :exclude_users, :configs + + expose_timestamps def include_users [] if object&.include_ids.nil? diff --git a/app/api/entities/message_entity.rb b/app/api/entities/message_entity.rb index 6565a51fcd..b9a729b733 100644 --- a/app/api/entities/message_entity.rb +++ b/app/api/entities/message_entity.rb @@ -2,18 +2,11 @@ module Entities # Publish-Subscription Entities - class MessageEntity < Grape::Entity + class MessageEntity < ApplicationEntity expose :id, :message_id expose :subject, :content, :channel_type expose :sender_id, :sender_name, :receiver_id, :is_ack - expose :created_at, :updated_at - def created_at - object.created_at.strftime('%d.%m.%Y, %H:%M:%S') - end - - def updated_at - object.updated_at.strftime('%d.%m.%Y, %H:%M:%S') - end + expose_timestamps end end diff --git a/app/api/entities/private_note_entity.rb b/app/api/entities/private_note_entity.rb index 322a4468d0..f92ceced3f 100644 --- a/app/api/entities/private_note_entity.rb +++ b/app/api/entities/private_note_entity.rb @@ -3,19 +3,12 @@ module Entities # Publish-Subscription Entities - class PrivateNoteEntity < Grape::Entity + class PrivateNoteEntity < ApplicationEntity expose :id expose :content expose :created_by - expose :created_at, :updated_at expose :noteable_id, :noteable_type - def created_at - object.created_at&.strftime('%d.%m.%Y, %H:%M') - end - - def updated_at - object.updated_at&.strftime('%d.%m.%Y, %H:%M') - end + expose_timestamps end end diff --git a/app/api/entities/reaction_entity.rb b/app/api/entities/reaction_entity.rb index dc080e449d..6fe11e51fb 100644 --- a/app/api/entities/reaction_entity.rb +++ b/app/api/entities/reaction_entity.rb @@ -17,6 +17,7 @@ class ReactionEntity < ApplicationEntity expose! :solvents, using: 'Entities::ReactionMaterialEntity' expose! :starting_materials, using: 'Entities::ReactionMaterialEntity' expose! :type + expose :comment_count end with_options(anonymize_below: 10) do @@ -34,7 +35,7 @@ class ReactionEntity < ApplicationEntity expose! :rinchi_short_key expose! :rinchi_web_key expose! :rxno - expose! :segments, anonymize_with: [], using: 'Entities::SegmentEntity' + expose! :segments, anonymize_with: [], using: 'Labimotion::SegmentEntity' expose! :short_label expose! :solvent, unless: :displayed_in_list expose! :status @@ -44,6 +45,7 @@ class ReactionEntity < ApplicationEntity expose! :timestamp_stop, unless: :displayed_in_list expose! :tlc_description, unless: :displayed_in_list expose! :tlc_solvents, unless: :displayed_in_list + expose! :variations, anonymize_with: [], using: 'Entities::ReactionVariationEntity' end expose_timestamps @@ -97,6 +99,14 @@ def starting_materials def type 'reaction' end + + def comment_count + object.comments.count + end + + def variations + object.variations.map(&:deep_symbolize_keys) + end end end # rubocop:enable Layout/ExtraSpacing, Layout/LineLength diff --git a/app/api/entities/reaction_report_entity.rb b/app/api/entities/reaction_report_entity.rb index 753525b15a..c3b466bc10 100644 --- a/app/api/entities/reaction_report_entity.rb +++ b/app/api/entities/reaction_report_entity.rb @@ -3,7 +3,7 @@ module Entities class ReactionReportEntity < ReactionEntity with_options(anonymize_below: 0) do - expose! :collections, using: 'Entities::CollectionEntity' + expose! :collections expose! :literatures expose! :products, using: 'Entities::ReactionMaterialReportEntity' expose! :purification_solvents, using: 'Entities::ReactionMaterialReportEntity' @@ -21,5 +21,12 @@ def literatures with_user_info: true ) end + + def collections + Entities::CollectionEntity.represent( + object.collections, + current_user: current_user, + ) + end end end diff --git a/app/api/entities/reaction_variation_entity.rb b/app/api/entities/reaction_variation_entity.rb new file mode 100644 index 0000000000..58f0da0fe8 --- /dev/null +++ b/app/api/entities/reaction_variation_entity.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Entities + class ReactionVariationEntity < ApplicationEntity + expose( + :id, + :properties, + :reactants, + :products, + :solvents, + ) + expose :starting_materials, as: :startingMaterials + + def properties + {}.tap do |properties| + properties[:temperature] = ReactionVariationPropertyEntity.represent(object[:properties][:temperature]) + properties[:duration] = ReactionVariationPropertyEntity.represent(object[:properties][:duration]) + end + end + + def materials(material_type) + {}.tap do |materials| + object[material_type].each do |k, v| + materials[k] = ReactionVariationMaterialEntity.represent(v) + end + end + end + + def starting_materials + materials(:startingMaterials) + end + + def reactants + materials(:reactants) + end + + def products + materials(:products) + end + + def solvents + materials(:solvents) + end + end + + class ReactionVariationPropertyEntity < ApplicationEntity + expose( + :value, + :unit, + ) + end + + class ReactionVariationMaterialEntity < ApplicationEntity + expose( + :value, + :unit, + ) + expose :aux, using: 'Entities::ReactionVariationMaterialAuxEntity' + end + + class ReactionVariationMaterialAuxEntity < ApplicationEntity + expose( + :coefficient, + :isReference, + :loading, + :purity, + :molarity, + :molecularWeight, + :sumFormula, + :yield, + :equivalent, + ) + end +end diff --git a/app/api/entities/research_plan_entity.rb b/app/api/entities/research_plan_entity.rb index c6f09df6c6..a5694a5762 100644 --- a/app/api/entities/research_plan_entity.rb +++ b/app/api/entities/research_plan_entity.rb @@ -11,6 +11,7 @@ class ResearchPlanEntity < ApplicationEntity expose! :name expose! :thumb_svg expose! :type + expose! :comment_count end with_options(anonymize_below: 10) do @@ -18,7 +19,7 @@ class ResearchPlanEntity < ApplicationEntity expose! :research_plan_metadata, anonymize_with: nil, using: 'Entities::ResearchPlanMetadataEntity' expose! :tag, anonymize_with: nil, using: 'Entities::ElementTagEntity' expose! :wellplates, anonymize_with: [], using: 'Entities::WellplateEntity' - expose! :segments, anonymize_with: [], using: 'Entities::SegmentEntity' + expose! :segments, anonymize_with: [], using: 'Labimotion::SegmentEntity' end # rubocop:enable Layout/ExtraSpacing @@ -49,5 +50,9 @@ def type def wellplates displayed_in_list? ? [] : object.wellplates end + + def comment_count + object.comments.count + end end end diff --git a/app/api/entities/sample_entity.rb b/app/api/entities/sample_entity.rb index 2b5aeac929..ac01d08f3a 100644 --- a/app/api/entities/sample_entity.rb +++ b/app/api/entities/sample_entity.rb @@ -19,6 +19,9 @@ class SampleEntity < ApplicationEntity expose! :molecule_computed_props, using: 'Entities::ComputedPropEntity' expose! :sum_formula expose! :type + expose :comments, using: 'Entities::CommentEntity' + expose :comment_count + expose :dry_solvent end # Level 1 attributes @@ -58,7 +61,7 @@ class SampleEntity < ApplicationEntity expose! :real_amount_value, unless: :displayed_in_list expose! :residues, unless: :displayed_in_list, anonymize_with: [], using: 'Entities::ResidueEntity' expose! :sample_svg_file - expose! :segments, unless: :displayed_in_list, anonymize_with: [], using: 'Entities::SegmentEntity' + expose! :segments, unless: :displayed_in_list, anonymize_with: [], using: 'Labimotion::SegmentEntity' expose! :short_label expose! :showed_name expose! :solvent, unless: :displayed_in_list, anonymize_with: [] @@ -130,5 +133,9 @@ def parent_id def type 'sample' end + + def comment_count + object.comments.count + end end end diff --git a/app/api/entities/sample_report_entity.rb b/app/api/entities/sample_report_entity.rb index 52a94b5d29..6f8bf2b6c1 100644 --- a/app/api/entities/sample_report_entity.rb +++ b/app/api/entities/sample_report_entity.rb @@ -11,7 +11,7 @@ class SampleReportEntity < SampleEntity end with_options(anonymize_below: 10) do - expose! :reactions, anonymize_with: [], using: 'Entities::ReactionReportEntity' + expose! :reactions, anonymize_with: [] expose! :molecule_iupac_name, anonymize_with: nil expose! :get_svg_path, anonymize_with: nil expose! :literatures, anonymize_with: [] @@ -26,5 +26,13 @@ def literatures with_user_info: true, ) end + + def reaction + Entities::ReactionReportEntity.represent( + object, + current_user: current_user, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: object).detail_levels, + ).serializable_hash + end end end diff --git a/app/api/entities/sample_task_entity.rb b/app/api/entities/sample_task_entity.rb index 4a0c16f629..a87bf86722 100644 --- a/app/api/entities/sample_task_entity.rb +++ b/app/api/entities/sample_task_entity.rb @@ -14,16 +14,25 @@ class SampleTaskEntity < ApplicationEntity expose :sample_svg_file expose :short_label expose :scan_results, using: 'Entities::ScanResultEntity' + expose :target_amount_value + expose :target_amount_unit expose :done expose_timestamps private - delegate(:short_label, :sample_svg_file, to: :'object.sample', allow_nil: true) + delegate( + :short_label, + :sample_svg_file, + :target_amount_value, + :target_amount_unit, + to: :'object.sample', + allow_nil: true, + ) def display_name - object.sample&.showed_name + object.sample&.name || object.sample&.showed_name end def scan_results diff --git a/app/api/entities/screen_entity.rb b/app/api/entities/screen_entity.rb index d15cdf7f0c..6ac20babf9 100644 --- a/app/api/entities/screen_entity.rb +++ b/app/api/entities/screen_entity.rb @@ -12,6 +12,7 @@ class ScreenEntity < ApplicationEntity expose! :conditions expose! :requirements expose! :wellplates, using: 'Entities::WellplateEntity' + expose! :comment_count end with_options(anonymize_below: 10) do @@ -21,7 +22,7 @@ class ScreenEntity < ApplicationEntity expose! :container, anonymize_with: nil, using: 'Entities::ContainerEntity' expose! :research_plans, anonymize_with: [], using: 'Entities::ResearchPlanEntity' expose! :component_graph_data, anonymize_with: {} - expose! :segments, anonymize_with: [], using: 'Entities::SegmentEntity' + expose! :segments, anonymize_with: [], using: 'Labimotion::SegmentEntity' expose! :tag, anonymize_with: nil, using: 'Entities::ElementTagEntity' end # rubocop:enable Layout/ExtraSpacing @@ -57,5 +58,9 @@ def type def wellplates displayed_in_list? ? [] : object.wellplates end + + def comment_count + object.comments.count + end end end diff --git a/app/api/entities/segment_entity.rb b/app/api/entities/segment_entity.rb deleted file mode 100644 index 69d2eeaf93..0000000000 --- a/app/api/entities/segment_entity.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -module Entities - class SegmentEntity < ApplicationEntity - expose( - :element_id, - :element_type, - :id, - :klass_uuid, - :properties, - :segment_klass_id, - :uuid, - ) - - def properties - return unless object.respond_to?(:properties) - return unless object&.properties.dig('layers').present? - - object&.properties['layers'].keys.each do |key| - next unless object&.properties.dig('layers', key, 'fields').present? - - field_sample_molecules = object&.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'drag_molecule' } - field_sample_molecules.each do |field| - idx = object&.properties['layers'][key]['fields'].index(field) - sid = field.dig('value', 'el_id') - next unless sid.present? - - el = Molecule.find_by(id: sid) - next unless el.present? - next unless object&.properties.dig('layers', key, 'fields', idx, 'value').present? - - object&.properties['layers'][key]['fields'][idx]['value']['el_svg'] = File.join('/images', 'molecules', el.molecule_svg_file) - end - - field_tables = object.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'table' } - field_tables.each do |field| - next unless field['sub_values'].present? && field['sub_fields'].present? - - field_table_molecules = field['sub_fields'].select { |ss| ss['type'] == 'drag_molecule' } - next unless field_table_molecules.present? - - col_ids = field_table_molecules.map { |x| x.values[0] } - col_ids.each do |col_id| - field_table_values = field['sub_values'].each do |sub_value| - next unless sub_value[col_id].present? && sub_value[col_id]['value'].present? && sub_value[col_id]['value']['el_id'].present? - - find_mol = Molecule.find_by(id: sub_value[col_id]['value']['el_id']) - next unless find_mol.present? - - sub_value[col_id]['value']['el_svg'] = File.join('/images', 'molecules', find_mol.molecule_svg_file) - sub_value[col_id]['value']['el_inchikey'] = find_mol.inchikey - sub_value[col_id]['value']['el_smiles'] = find_mol.cano_smiles - sub_value[col_id]['value']['el_iupac'] = find_mol.iupac_name - sub_value[col_id]['value']['el_molecular_weight'] = find_mol.molecular_weight - end - end - end - end - object&.properties - end - end -end diff --git a/app/api/entities/segment_klass_entity.rb b/app/api/entities/segment_klass_entity.rb deleted file mode 100644 index 181fee49be..0000000000 --- a/app/api/entities/segment_klass_entity.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Entities - class SegmentKlassEntity < ApplicationEntity - expose( - :desc, - :id, - :is_active, - :label, - :place, - :properties_release, - :properties_template, - :uuid, - ) - expose :element_klass, using: Entities::ElementKlassEntity - expose_timestamps(timestamp_fields: [:released_at]) - end -end diff --git a/app/api/entities/segment_revision_entity.rb b/app/api/entities/segment_revision_entity.rb deleted file mode 100644 index 1f068a22a6..0000000000 --- a/app/api/entities/segment_revision_entity.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module Entities - class SegmentRevisionEntity < Grape::Entity - expose :id, :segment_id, :uuid, :klass_uuid, :properties, :created_at - def created_at - object.created_at.strftime('%d.%m.%Y, %H:%M') - end - - def properties - object.properties['layers']&.keys.each do |key| - field_sample_molecules = object.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'drag_sample' || ss['type'] == 'drag_molecule' } - field_sample_molecules.each do |field| - idx = object.properties['layers'][key]['fields'].index(field) - sid = field.dig('value', 'el_id') - next unless sid.present? - - el = field['type'] == 'drag_sample' ? Sample.find_by(id: sid) : Molecule.find_by(id: sid) - next unless el.present? - next unless object.properties.dig('layers', key, 'fields', idx, 'value').present? - - object.properties['layers'][key]['fields'][idx]['value']['el_label'] = el.short_label if field['type'] == 'drag_sample' - object.properties['layers'][key]['fields'][idx]['value']['el_tip'] = el.short_label if field['type'] == 'drag_sample' - object.properties['layers'][key]['fields'][idx]['value']['el_svg'] = field['type'] == 'drag_sample' ? el.get_svg_path : File.join('/images', 'molecules', el.molecule_svg_file) - end - - field_tables = object.properties['layers'][key]['fields'].select { |ss| ss['type'] == 'table' } - field_tables.each do |field| - next unless field['sub_values'].present? && field['sub_fields'].present? - - field_table_molecules = field['sub_fields'].select { |ss| ss['type'] == 'drag_molecule' } - next unless field_table_molecules.present? - - col_ids = field_table_molecules.map { |x| x.values[0] } - col_ids.each do |col_id| - field_table_values = field['sub_values'].each do |sub_value| - next unless sub_value[col_id].present? && sub_value[col_id]['value'].present? && sub_value[col_id]['value']['el_id'].present? - - find_mol = Molecule.find_by(id: sub_value[col_id]['value']['el_id']) - next unless find_mol.present? - - sub_value[col_id]['value']['el_svg'] = File.join('/images', 'molecules', find_mol.molecule_svg_file) - sub_value[col_id]['value']['el_inchikey'] = find_mol.inchikey - sub_value[col_id]['value']['el_smiles'] = find_mol.cano_smiles - sub_value[col_id]['value']['el_iupac'] = find_mol.iupac_name - sub_value[col_id]['value']['el_molecular_weight'] = find_mol.molecular_weight - end - end - end - end - object.properties - end - end -end diff --git a/app/api/entities/user_entity.rb b/app/api/entities/user_entity.rb index 0aa6623916..3a15ef41b4 100644 --- a/app/api/entities/user_entity.rb +++ b/app/api/entities/user_entity.rb @@ -1,37 +1,47 @@ +# frozen_string_literal: true + module Entities class UserEntity < Grape::Entity - expose :id, documentation: { type: "Integer", desc: "User's unique id"} - expose :name, documentation: { type: "String", desc: "User's name" } - expose :first_name, documentation: { type: "String", desc: "User's name" } - expose :last_name, documentation: { type: "String", desc: "User's name" } - expose :initials, documentation: { type: "String", desc: "initials" } - expose :samples_count, documentation: { type: "Integer", desc: "Sample count"} - expose :reactions_count, documentation: { type: "Integer", desc: "Reactions count"} - expose :type, if: -> (obj, opts) { obj.respond_to? :type} - expose :reaction_name_prefix, if: -> (obj, opts) { obj.respond_to? :reaction_name_prefix} - expose :layout, if: -> (obj, opts) { obj.respond_to? :layout} - expose :email, if: -> (obj, opts) { obj.respond_to? :email} - expose :unconfirmed_email, if: -> (obj, opts) { obj.respond_to? :unconfirmed_email} - expose :confirmed_at, if: -> (obj, opts) { obj.respond_to? :confirmed_at} - expose :current_sign_in_at, if: -> (obj, opts) { obj.respond_to? :current_sign_in_at} - expose :locked_at, if: -> (obj, opts) { obj.respond_to? :locked_at} - expose :is_templates_moderator, documentation: { type: "Boolean", desc: "ketcherails template administrator" } + expose :id, documentation: { type: 'Integer', desc: "User's unique id" } + expose :name, documentation: { type: 'String', desc: "User's name" } + expose :first_name, documentation: { type: 'String', desc: "User's name" } + expose :last_name, documentation: { type: 'String', desc: "User's name" } + expose :initials, documentation: { type: 'String', desc: 'initials' } + expose :samples_count, documentation: { type: 'Integer', desc: 'Sample count' } + expose :reactions_count, documentation: { type: 'Integer', desc: 'Reactions count' } + expose :cell_lines_count, documentation: { type: 'Integer', desc: 'Cellline Samples count' } + expose :type, if: ->(obj, _opts) { obj.respond_to? :type } + expose :reaction_name_prefix, if: ->(obj, _opts) { obj.respond_to? :reaction_name_prefix } + expose :layout, if: ->(obj, _opts) { obj.respond_to? :layout } + expose :email, if: ->(obj, _opts) { obj.respond_to? :email } + expose :unconfirmed_email, if: ->(obj, _opts) { obj.respond_to? :unconfirmed_email } + expose :confirmed_at, if: ->(obj, _opts) { obj.respond_to? :confirmed_at } + expose :current_sign_in_at, if: ->(obj, _opts) { obj.respond_to? :current_sign_in_at } + expose :locked_at, if: ->(obj, _opts) { obj.respond_to? :locked_at } + expose :is_templates_moderator, documentation: { type: 'Boolean', desc: 'ketcherails template administrator' } expose :molecule_editor, documentation: { type: 'Boolean', desc: 'molecule administrator' } expose :converter_admin, documentation: { type: 'Boolean', desc: 'converter administrator' } expose :account_active, documentation: { type: 'Boolean', desc: 'User Account Active or Inactive' } expose :matrix, documentation: { type: 'Integer', desc: "User's matrix" } expose :counters + expose :generic_admin, documentation: { type: 'Hash', desc: 'Generic administrator' } def samples_count object.counters['samples'].to_i end + def reactions_count object.counters['reactions'].to_i end + def cell_lines_count + object.counters['celllines'].to_i + end + expose :current_sign_in_at do |obj| return nil unless obj.respond_to? :current_sign_in_at - obj.current_sign_in_at.strftime('%d.%m.%Y, %H:%M') unless obj.current_sign_in_at.nil? + + obj.current_sign_in_at&.strftime('%d.%m.%Y, %H:%M') end end end diff --git a/app/api/entities/wellplate_entity.rb b/app/api/entities/wellplate_entity.rb index ef7a6c3a5f..00876a4aad 100644 --- a/app/api/entities/wellplate_entity.rb +++ b/app/api/entities/wellplate_entity.rb @@ -10,6 +10,7 @@ class WellplateEntity < ApplicationEntity expose! :size expose! :type expose! :wells, using: 'Entities::WellEntity' + expose! :comment_count end with_options(anonymize_below: 10) do @@ -18,7 +19,7 @@ class WellplateEntity < ApplicationEntity expose! :description expose! :name expose! :readout_titles - expose! :segments, anonymize_with: [], using: 'Entities::SegmentEntity' + expose! :segments, anonymize_with: [], using: 'Labimotion::SegmentEntity' expose! :short_label expose! :tag, anonymize_with: nil, using: 'Entities::ElementTagEntity' end @@ -51,5 +52,9 @@ def wells def type 'wellplate' end + + def comment_count + object.comments.count + end end end diff --git a/app/api/helpers/collection_helpers.rb b/app/api/helpers/collection_helpers.rb index d392eac438..ce222b2c14 100644 --- a/app/api/helpers/collection_helpers.rb +++ b/app/api/helpers/collection_helpers.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/ModuleLength, Style/OptionalBooleanParameter, Naming/MethodParameterName, Layout/LineLength + module CollectionHelpers extend Grape::API::Helpers @@ -8,7 +12,7 @@ module CollectionHelpers def fetch_collection_id_w_current_user(id, is_sync = false) if is_sync SyncCollectionsUser.find_by( - id: id.to_i, user_id: user_ids + id: id.to_i, user_id: user_ids, )&.collection_id else (Collection.find_by(id: id.to_i, user_id: user_ids) || @@ -27,13 +31,14 @@ def fetch_collection_w_current_user(id, is_sync = false) # desc: given an id of coll or sync coll return detail levels as array def detail_level_for_collection(id, is_sync = false) - dl = (is_sync && SyncCollectionsUser || Collection).find_by( - id: id.to_i, user_id: user_ids + dl = ((is_sync && SyncCollectionsUser) || Collection).find_by( + id: id.to_i, user_id: user_ids, )&.slice( :permission_level, :sample_detail_level, :reaction_detail_level, :wellplate_detail_level, :screen_detail_level, - :researchplan_detail_level, :element_detail_level + :researchplan_detail_level, :element_detail_level, + :celllinesample_detail_level )&.symbolize_keys { permission_level: 0, @@ -43,6 +48,7 @@ def detail_level_for_collection(id, is_sync = false) screen_detail_level: 0, researchplan_detail_level: 0, element_detail_level: 0, + celllinesample_detail_level: 0, }.merge(dl || {}) end @@ -76,21 +82,21 @@ def fetch_collection_id_for_assign(prms = params, pl = 1) def fetch_collection_by_ui_state_params_and_pl(pl = 2) current_collection = params['ui_state']['currentCollection'] @collection = if current_collection['is_sync_to_me'] - Collection.joins(:sync_collections_users).where( - 'sync_collections_users.id = ? and sync_collections_users.user_id in (?) and sync_collections_users.permission_level >= ?', - current_collection['id'], - user_ids, - pl - ).first - else - Collection.where( - 'id = ? AND ((user_id in (?) AND (is_shared IS NOT TRUE OR permission_level >= ?)) OR shared_by_id = ?)', - current_collection['id'], - user_ids, - pl, - current_user - ).first - end + Collection.joins(:sync_collections_users).where( + 'sync_collections_users.id = ? and sync_collections_users.user_id in (?) and sync_collections_users.permission_level >= ?', + current_collection['id'], + user_ids, + pl, + ).first + else + Collection.where( + 'id = ? AND ((user_id in (?) AND (is_shared IS NOT TRUE OR permission_level >= ?)) OR shared_by_id = ?)', + current_collection['id'], + user_ids, + pl, + current_user, + ).first + end @collection end @@ -116,6 +122,7 @@ def set_var(c_id = params[:collection_id], is_sync = params[:is_sync]) screen_detail_level: 10, researchplan_detail_level: 10, element_detail_level: 10, + celllinesample_detail_level: 10, } @dl = detail_level_for_collection(c_id, is_sync) unless @is_owned @@ -126,5 +133,18 @@ def set_var(c_id = params[:collection_id], is_sync = params[:is_sync]) @dl_sc = @dl[:screen_detail_level] @dl_rp = @dl[:researchplan_detail_level] @dl_e = @dl[:element_detail_level] + @dl_cl = @dl[:celllinesample_detail_level] + end + + def create_classes_of_element(element) + if element == 'cell_line' + element_klass = CelllineSample + collections_element_klass = CollectionsCellline + else + collections_element_klass = "collections_#{element}".classify.constantize + element_klass = element.classify.constantize + end + [element_klass, collections_element_klass] end end +# rubocop:enable Metrics/ModuleLength, Style/OptionalBooleanParameter, Naming/MethodParameterName, Layout/LineLength diff --git a/app/api/helpers/comment_helpers.rb b/app/api/helpers/comment_helpers.rb new file mode 100644 index 0000000000..c6081cfa76 --- /dev/null +++ b/app/api/helpers/comment_helpers.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module CommentHelpers + extend Grape::API::Helpers + + def authorized_users(collections) + sync_collections = collections.synchronized + shared_collections = collections.where(is_shared: true) + + sync_collection_users = SyncCollectionsUser.includes(:user) + .where(shared_by_id: sync_collections.pluck(:user_id), + collection_id: sync_collections.ids) + user_ids = sync_collection_users&.flat_map do |sync_collection_user| + sync_collection_user.user&.send(:user_ids).to_a + end + + shared_collections&.flat_map do |collection| + user_ids += collection.user&.send(:user_ids).to_a + end + + (collections.unshared.pluck(:user_id) + user_ids.compact).uniq + end + + def element_name(element) + if element.respond_to?(:short_label) + element&.short_label + elsif element.respond_to?(:name) + element.name + else + '' + end + end + + def notify_synchronized_collection_users(collections, current_user, element) + sync_collections = collections.synchronized + sync_collection_users = SyncCollectionsUser.includes(:user) + .where(shared_by_id: sync_collections.pluck(:user_id), + collection_id: sync_collections.ids) + + message_to = sync_collection_users&.flat_map do |sync_collection_user| + sync_collection_user.user&.send(:user_ids) + end + + message_to = (message_to + collections.unshared.pluck(:user_id) - [current_user.id]).uniq + return if message_to.blank? + + data_args = { + commented_by: current_user.name, + element_type: element.class.to_s, + element_name: element_name(element), + } + + Message.create_msg_notification( + channel_subject: Channel::COMMENT_ON_MY_COLLECTION, + message_from: current_user.id, + message_to: message_to, + data_args: data_args, + level: 'info', + ) + end + + def notify_shared_collection_users(shared_collections, current_user, element) + shared_collections&.each do |collection| + message_to = collection.user.send(:user_ids) - [current_user.id] + next if message_to.blank? + + data_args = { + commented_by: current_user.name, + element_type: element.class.to_s, + element_name: element_name(element), + } + + Message.create_msg_notification( + channel_subject: Channel::COMMENT_ON_MY_COLLECTION, + message_from: current_user.id, + message_to: collection.user.send(:user_ids) - [current_user.id], + data_args: data_args, + level: 'info', + ) + end + end + + def create_message_notification(collections, current_user, element) + notify_synchronized_collection_users(collections, current_user, element) + notify_shared_collection_users(collections.where(is_shared: true), current_user, element) + end + + def notify_comment_resolved(comment, current_user) + commentable_type = comment.commentable_type + commentable = commentable_type.classify.constantize.find comment.commentable_id + + Message.create_msg_notification( + channel_subject: Channel::COMMENT_RESOLVED, + message_from: current_user.id, message_to: [comment.created_by], + data_args: { resolved_by: current_user.name, + element_type: commentable_type, + element_name: element_name(commentable) }, + level: 'info' + ) + end +end diff --git a/app/api/helpers/container_helpers.rb b/app/api/helpers/container_helpers.rb index 744c7e5fdf..db104fbe9b 100644 --- a/app/api/helpers/container_helpers.rb +++ b/app/api/helpers/container_helpers.rb @@ -1,30 +1,36 @@ +require 'labimotion' module ContainerHelpers extend Grape::API::Helpers - def update_datamodel(container) -#TODO check this logic, not sure this is still needed + containable_type should not be null - if container[:is_new] - root_container = Container.create( - #name: "root", - container_type: container[:containable_type] #should be 'root' - ) - else - root_container = Container.find_by id: container[:id] - #root_container.name = "root" #if it is created from client.side + def update_datamodel(container, _current_user = {}) + # TODO: check this logic, not sure this is still needed + containable_type should not be null + root_container = if container[:is_new] + Container.create( + # name: "root", + container_type: container[:containable_type], # should be 'root' + ) + else + Container.find_by id: container[:id] + # root_container.name = "root" #if it is created from client.side + end + # root_container.save! + # ODOT + unless container[:description].nil? || root_container.nil? + root_container[:description] = container[:description] + root_container.save! end - #root_container.save! -#ODOT if container[:children] != nil && !container[:children].empty? - create_or_update_containers(container[:children], root_container) + create_or_update_containers(container[:children], root_container, current_user) end root_container end private - def create_or_update_containers(children, parent_container) + def create_or_update_containers(children, parent_container, current_user={}) return unless children return unless can_update_container(parent_container) + children.each do |child| if child[:is_deleted] delete_containers_and_attachments(child) unless child[:is_new] @@ -32,51 +38,52 @@ def create_or_update_containers(children, parent_container) end extended_metadata = child[:extended_metadata] - if child[:container_type] == "analysis" - extended_metadata["content"] = if extended_metadata.key?("content") - extended_metadata["content"].to_json - else - "{\"ops\":[{\"insert\":\"\"}]}" - end + if child[:container_type] == 'analysis' + extended_metadata['content'] = if extended_metadata.key?('content') + extended_metadata['content'].to_json + else + '{"ops":[{"insert":""}]}' + end end if child[:is_new] - #Create container + # Create container container = parent_container.children.create( name: child[:name], container_type: child[:container_type], description: child[:description], - extended_metadata: extended_metadata + extended_metadata: extended_metadata, ) else - #Update container + # Update container next unless container = Container.find_by(id: child[:id]) + container.update!( name: child[:name], container_type: child[:container_type], description: child[:description], - extended_metadata: extended_metadata + extended_metadata: extended_metadata, ) end create_or_update_attachments(container, child[:attachments]) if child[:attachments] - if child[:container_type] == 'dataset' && child[:dataset].present? && child[:dataset]["changed"] + if child[:container_type] == 'dataset' && child[:dataset].present? && child[:dataset]['changed'] klass_id = child[:dataset]['dataset_klass_id'] properties = child[:dataset]['properties'] container.save_dataset(dataset_klass_id: klass_id, properties: properties) end - container.destroy_datasetable if child[:container_type] == 'dataset' && child[:dataset].blank? - create_or_update_containers(child[:children], container) end end def create_or_update_attachments(container, attachments) return if attachments.empty? + can_update = can_update_container(container) can_edit = true return unless can_update + attachments.each do |att| if att[:is_new] attachment = Attachment.where(key: att[:id], attachable: nil).last @@ -88,13 +95,13 @@ def create_or_update_attachments(container, attachments) can_edit = can_update_container(att_container) end end - if attachment - if att[:is_deleted] && can_edit - attachment.destroy! - next - end - attachment.update!(attachable: container) + next unless attachment + + if att[:is_deleted] && can_edit + attachment.destroy! + next end + attachment.update!(attachable: container) end end @@ -115,5 +122,4 @@ def can_update_container(container) true end end - -end #module +end # module diff --git a/app/api/helpers/generic_helpers.rb b/app/api/helpers/generic_helpers.rb deleted file mode 100644 index e3563dd41a..0000000000 --- a/app/api/helpers/generic_helpers.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -# Helper for associated sample -module GenericHelpers - extend Grape::API::Helpers - - def fetch_properties_uploads(properties) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength - uploads = [] - properties['layers'].each_key do |key| - layer = properties['layers'][key] - field_uploads = layer['fields'].select { |ss| ss['type'] == 'upload' } - field_uploads.each do |field| - ((field['value'] && field['value']['files']) || []).each do |file| - uploads.push({ layer: key, field: field['field'], uid: file['uid'], filename: file['filename'] }) - end - end - end - uploads - end - - def update_properties_upload(element, properties, att, pa) # rubocop:disable Metrics/AbcSize,Naming/MethodParameterName - return if pa.nil? - - idx = properties['layers'][pa[:layer]]['fields'].index { |fl| fl['field'] == pa[:field] } - fidx = properties['layers'][pa[:layer]]['fields'][idx]['value']['files'].index { |fi| fi['uid'] == pa[:uid] } - properties['layers'][pa[:layer]]['fields'][idx]['value']['files'][fidx]['aid'] = att.id - properties['layers'][pa[:layer]]['fields'][idx]['value']['files'][fidx]['uid'] = att.identifier - element.update_columns(properties: properties) # rubocop:disable Rails/SkipsModelValidations - end - - def create_uploads(type, id, files, param_info, user_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity - return if files.nil? || param_info.nil? || files.empty? || param_info.empty? - - attach_ary = [] - map_info = JSON.parse(param_info) - map_info&.keys&.each do |key| # rubocop:disable Metrics/BlockLength - next if map_info[key]['files'].empty? - - case type - when 'Segment' - element = Segment.find_by(element_id: id, segment_klass_id: key) - when 'Element' - element = Element.find_by(id: id) - end - next if element.nil? - - uploads = fetch_properties_uploads(element.properties) - - map_info[key]['files'].each do |fobj| - file = (files || []).select { |ff| ff['filename'] == fobj['uid'] }&.first - pa = uploads.select { |ss| ss[:uid] == file[:filename] }&.first || nil - next unless (tempfile = file[:tempfile]) - - att = Attachment.new( - bucket: file[:container_id], - filename: fobj['filename'], - file_path: file[:tempfile].path, - created_by: user_id, - created_for: user_id, - content_type: file[:type], - attachable_type: map_info[key]['type'], - attachable_id: element.id, - ) - - ActiveRecord::Base.transaction do - att.save! - - update_properties_upload(element, element.properties, att, pa) - attach_ary.push(att.id) - ensure - tempfile.close - tempfile.unlink - end - end - element.send("#{type.downcase}s_revisions")&.last&.destroy! - element.save! - end - attach_ary - end - - def create_attachments(files, del_files, type, id, identifier, user_id) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists - attach_ary = [] - - (files || []).each_with_index do |file, index| - next unless (tempfile = file[:tempfile]) - - att = Attachment.new( - bucket: file[:container_id], - filename: file[:filename], - file_path: file[:tempfile].path, - created_by: user_id, - created_for: user_id, - content_type: file[:type], - identifier: identifier[index], - attachable_type: type, - attachable_id: id, - ) - - ActiveRecord::Base.transaction do - att.save! - attach_ary.push(att.id) - - ensure - tempfile.close - tempfile.unlink - end - end - unless (del_files || []).empty? - Attachment.where('id IN (?) AND attachable_type = (?)', del_files.map!(&:to_i), - type).update_all(attachable_id: nil) # rubocop:disable Rails/SkipsModelValidations - end - attach_ary - end -end diff --git a/app/api/helpers/reflection_helpers.rb b/app/api/helpers/reflection_helpers.rb new file mode 100644 index 0000000000..dd640ec609 --- /dev/null +++ b/app/api/helpers/reflection_helpers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ReflectionHelpers + extend Grape::API::Helpers + + def get_assoziation_name_in_collections(element) + assoziation_name = "#{element}s" + assoziation_name = 'cellline_samples' if element == 'cell_line' + assoziation_name + end +end diff --git a/app/api/helpers/report_helpers.rb b/app/api/helpers/report_helpers.rb index 7403d65c09..4a4cac2133 100644 --- a/app/api/helpers/report_helpers.rb +++ b/app/api/helpers/report_helpers.rb @@ -192,6 +192,76 @@ def build_sql(table, columns, c_id, ids, checkedAll = false) # from collections sync_colls assigned to user # - selected columns from samples, molecules table # + + def generate_sheet(table, result, columns_params, export, type) + case type + when :analyses + sheet_name = "#{table}_analyses".to_sym + export.generate_analyses_sheet_with_samples(sheet_name, result, columns_params) + when :chemicals + sheet_name = "#{table}_chemicals" + format_result = Export::ExportChemicals.format_chemical_results(result) + export.generate_sheet_with_samples(sheet_name, format_result, columns_params) + else + export.generate_sheet_with_samples(table, result) + end + end + + def build_sql_query(table, current_user, sql_params, type) + tables = %i[sample reaction wellplate] + filter_parameter = if tables.include?(table) && type.nil? + table + else + "#{table}_#{type}".to_sym + end + type ||= :sample + filter_selections = filter_column_selection(filter_parameter) + column_query = build_column_query(filter_selections, current_user.id) + send("build_sql_#{table}_#{type}", column_query, sql_params[:c_id], sql_params[:ids], sql_params[:checked_all]) + end + + def generate_sheets_for_tables(tables, table_params, export, columns_params = nil, type = nil) + tables.each do |table| + next unless (p_t = table_params[:ui_state][table]) + + checked_all = p_t[:checkedAll] + + ids = checked_all ? p_t[:uncheckedIds] : p_t[:checkedIds] + next unless checked_all || ids.present? + + sql_params = { + c_id: table_params[:c_id], ids: ids, checked_all: checked_all + } + sql_query = build_sql_query(table, current_user, sql_params, type) + next unless sql_query + + result = db_exec_query(sql_query) + generate_sheet(table, result, columns_params, export, type) + end + end + + def sample_details_subquery(u_ids, selection) + # Extract sample details subquery + <<~SQL.squish + select + s.id as s_id + , s.is_top_secret as ts + , min(co.id) as co_id + , min(scu.id) as scu_id + , bool_and(co.is_shared) as shared_sync + , max(GREATEST(co.permission_level, scu.permission_level)) as pl + , max(GREATEST(co.sample_detail_level,scu.sample_detail_level)) dl_s + from samples s + inner join collections_samples c_s on s.id = c_s.sample_id and c_s.deleted_at is null + left join collections co on (co.id = c_s.collection_id and co.user_id in (#{u_ids})) + left join collections sco on (sco.id = c_s.collection_id and sco.user_id not in (#{u_ids})) + left join sync_collections_users scu on (sco.id = scu.collection_id and scu.user_id in (#{u_ids})) + where #{selection} s.deleted_at isnull and c_s.deleted_at isnull + and (co.id is not null or scu.id is not null) + group by s_id + SQL + end + def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) s_ids = [ids].flatten.join(',') u_ids = [user_ids].flatten.join(',') @@ -200,6 +270,7 @@ def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) if checkedAll return unless c_id + collection_join = " inner join collections_samples c_s on s_id = c_s.sample_id and c_s.deleted_at is null and c_s.collection_id = #{c_id} " order = 's_id asc' selection = s_ids.empty? && '' || "s.id not in (#{s_ids}) and" @@ -208,35 +279,49 @@ def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) selection = "s.id in (#{s_ids}) and" end - <<~SQL + rest_of_selections = if columns[0].is_a?(Array) + columns[0][0] + else + columns + end + s_subquery = sample_details_subquery(u_ids, selection) + + <<~SQL.squish select s_id, ts, co_id, scu_id, shared_sync, pl, dl_s , res.residue_type, s.molfile_version, s.decoupled, s.molecular_mass as "molecular mass (decoupled)", s.sum_formula as "sum formula (decoupled)" , s.stereo->>'abs' as "stereo_abs", s.stereo->>'rel' as "stereo_rel" - , #{columns} - from ( - select - s.id as s_id - , s.is_top_secret as ts - , min(co.id) as co_id - , min(scu.id) as scu_id - , bool_and(co.is_shared) as shared_sync - , max(GREATEST(co.permission_level, scu.permission_level)) as pl - , max(GREATEST(co.sample_detail_level,scu.sample_detail_level)) dl_s - from samples s - inner join collections_samples c_s on s.id = c_s.sample_id and c_s.deleted_at is null - left join collections co on (co.id = c_s.collection_id and co.user_id in (#{u_ids})) - left join collections sco on (sco.id = c_s.collection_id and sco.user_id not in (#{u_ids})) - left join sync_collections_users scu on (sco.id = scu.collection_id and scu.user_id in (#{u_ids})) - where #{selection} s.deleted_at isnull and c_s.deleted_at isnull - and (co.id is not null or scu.id is not null) - group by s_id - ) as s_dl + , #{rest_of_selections} + from (#{s_subquery}) as s_dl inner join samples s on s_dl.s_id = s.id #{collection_join} left join molecules m on s.molecule_id = m.id left join molecule_names mn on s.molecule_name_id = mn.id left join residues res on res.sample_id = s.id - order by #{order}; + order by #{order} + SQL + end + + def chemical_query(chemical_columns, sample_ids) + individual_queries = sample_ids.map do |s_id| + <<~SQL.squish + SELECT #{s_id} AS chemical_sample_id, #{chemical_columns} + FROM chemicals c + WHERE c.sample_id = #{s_id} + SQL + end + individual_queries.join(' UNION ALL ') + end + + def build_sql_sample_chemicals(columns, c_id, ids, checked_all) + sample_query = build_sql_sample_sample(columns[0].join(','), c_id, ids, checked_all) + return nil if sample_query.blank? + + chemical_query = chemical_query(columns[1].join(','), ids) + <<~SQL.squish + SELECT * + FROM (#{sample_query}) AS sample_results + JOIN (#{chemical_query}) AS chemical_results + ON sample_results.s_id = chemical_results.chemical_sample_id SQL end @@ -245,6 +330,7 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) u_ids = [user_ids].flatten.join(',') return if columns.empty? || u_ids.empty? return if !checkedAll && s_ids.empty? + t = 's' # table samples cont_type = 'Sample' # containable_type if checkedAll @@ -256,8 +342,9 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) order = "position(','||s_id::text||',' in '(,#{s_ids},)')" selection = "s.id in (#{s_ids}) and" end + s_subquery = sample_details_subquery(u_ids, selection) - <<~SQL + <<~SQL.squish select s_id, ts, co_id, scu_id, shared_sync, pl, dl_s , #{columns} @@ -292,24 +379,7 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) where cont.containable_type = '#{cont_type}' and cont.containable_id = #{t}.id ) analysis ) as analyses - from ( - select - s.id as s_id - , s.is_top_secret as ts - , min(co.id) as co_id - , min(scu.id) as scu_id - , bool_and(co.is_shared) as shared_sync - , max(GREATEST(co.permission_level, scu.permission_level)) as pl - , max(GREATEST(co.sample_detail_level,scu.sample_detail_level)) dl_s - from samples s - inner join collections_samples c_s on s.id = c_s.sample_id and c_s.deleted_at is null - left join collections co on (co.id = c_s.collection_id and co.user_id in (#{u_ids})) - left join collections sco on (sco.id = c_s.collection_id and sco.user_id not in (#{u_ids})) - left join sync_collections_users scu on (sco.id = scu.collection_id and scu.user_id in (#{u_ids})) - where #{selection} s.deleted_at isnull and c_s.deleted_at isnull - and (co.id is not null or scu.id is not null) - group by s_id - ) as s_dl + from (#{s_subquery}) as s_dl inner join samples s on s_dl.s_id = s.id #{collection_join} order by #{order}; SQL @@ -460,17 +530,17 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) sample: { external_label: ['s.external_label', '"sample external label"', 0], name: ['s."name"', '"sample name"', 0], - cas: ['s.xref', nil, 0], + cas: ['s.xref', '"cas"', 0], target_amount_value: ['s.target_amount_value', '"target amount"', 0], target_amount_unit: ['s.target_amount_unit', '"target unit"', 0], real_amount_value: ['s.real_amount_value', '"real amount"', 0], real_amount_unit: ['s.real_amount_unit', '"real unit"', 0], - description: ['s.description', nil, 0], + description: ['s.description', '"description"', 0], molfile: ["encode(s.molfile, 'escape')", 'molfile', 1], - purity: ['s.purity', nil, 0], - solvent: ['s.solvent', nil, 0], + purity: ['s.purity', '"purity"', 0], + solvent: ['s.solvent', '"solvent"', 0], # impurities: ['s.impurities', nil, 0], - location: ['s.location', nil, 0], + location: ['s.location', '"location"', 0], is_top_secret: ['s.is_top_secret', '"secret"', 10], # ancestry: ['s.ancestry', nil, 10], short_label: ['s.short_label', '"short label"', 0], @@ -478,27 +548,28 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) sample_svg_file: ['s.sample_svg_file', 'image', 1], molecule_svg_file: ['m.molecule_svg_file', 'm_image', 1], identifier: ['s.identifier', nil, 1], - density: ['s.density', nil, 0], + density: ['s.density', '"density"', 0], melting_point: ['s.melting_point', '"melting pt"', 0], boiling_point: ['s.boiling_point', '"boiling pt"', 0], - created_at: ['s.created_at', nil, 0], - updated_at: ['s.updated_at', nil, 0], + created_at: ['s.created_at', '"created at"', 0], + updated_at: ['s.updated_at', '"updated_at"', 0], # deleted_at: ['wp.deleted_at', nil, 10], molecule_name: ['mn."name"', '"molecule name"', 1], - molarity_value: ['s."molarity_value"', '"molarity_value"', 0] + molarity_value: ['s."molarity_value"', '"molarity_value"', 0], + dry_solvent: ['s."dry_solvent"', '"dry_solvent"', 0], }, sample_id: { external_label: ['s.external_label', '"sample external label"', 0], name: ['s."name"', '"sample name"', 0], short_label: ['s.short_label', '"short label"', 0], - #molecule_name: ['mn."name"', '"molecule name"', 1] + # molecule_name: ['mn."name"', '"molecule name"', 1] }, molecule: { cano_smiles: ['m.cano_smiles', '"canonical smiles"', 10], sum_formular: ['m.sum_formular', '"sum formula"', 10], inchistring: ['m.inchistring', 'inchistring', 10], molecular_weight: ['m.molecular_weight', '"MW"', 0], - inchikey: ['m.inchikey', '"InChI"', 10] + inchikey: ['m.inchikey', '"InChI"', 10], }, wellplate: { name: ['wp."name"', '"wellplate name"', 10], @@ -516,7 +587,7 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) # created_at: ['w.created_at', nil, 10], # updated_at: ['w.updated_at', nil, 10], readouts: ['w.readouts', '"well readouts"', 10], - additive: ['w.additive', nil, 10] + additive: ['w.additive', nil, 10], # deleted_at: ['w.deleted_at', nil, 10], }, reaction: { @@ -542,47 +613,54 @@ def build_sql_reaction_sample(columns, c_id, ids, checkedAll = false) # created_by: ['r.created', ni, 10l] # reactions_sample: equivalent: ['r_s.equivalent', '"r eq"', 10], - reference: ['r_s.reference', '"r ref"', 10] + reference: ['r_s.reference', '"r ref"', 10], }, analysis: { name: ['anac."name"', '"name"', 10], description: ['anac.description', '"description"', 10], kind: ['anac.extended_metadata->\'kind\'', '"kind"', 10], content: ['anac.extended_metadata->\'content\'', '"content"', 10], - status: ['anac.extended_metadata->\'status\'', '"status"', 10] + status: ['anac.extended_metadata->\'status\'', '"status"', 10], }, dataset: { name: ['datc."name"', '"dataset name"', 10], description: ['datc.description', '"dataset description"', 10], - instrument: ['datc.extended_metadata->\'instrument\'', '"instrument"', 10] + instrument: ['datc.extended_metadata->\'instrument\'', '"instrument"', 10], }, attachment: { filename: ['att.filename', '"filename"', 10], checksum: ['att.checksum', '"checksum"', 10], - } + }, }.freeze - # desc: concatenate columns to be queried + def custom_column_query(table, col, selection, user_id, attrs) + if col == 'user_labels' + selection << "labels_by_user_sample(#{user_id}, s_id) as user_labels" + elsif col == 'literature' + selection << "literatures_by_element('Sample', s_id) as literatures" + elsif col == 'cas' + selection << "s.xref->>'cas' as cas" + elsif (s = attrs[table][col.to_sym]) + selection << ("#{s[1] && s[0]} as #{s[1] || s[0]}") + end + end + def build_column_query(sel, user_id = 0, attrs = EXP_MAP_ATTR) selection = [] - attrs.keys.each do |table| + attrs.each_key do |table| sel.symbolize_keys.fetch(table, []).each do |col| - if col == 'user_labels' - selection << "labels_by_user_sample(#{user_id}, s_id) as user_labels" - elsif col == 'literature' - selection << "literatures_by_element('Sample', s_id) as literatures" - elsif col == 'cas' - selection << "s.xref->>'cas' as cas" - elsif (s = attrs[table][col.to_sym]) - selection << (s[1] && s[0] + ' as ' + s[1] || s[0]) - end + custom_column_query(table, col, selection, user_id, attrs) end end - selection.join(', ') + selection = if sel[:chemicals].present? + Export::ExportChemicals.build_chemical_column_query(selection, sel) + else + selection.join(',') + end end - def filter_column_selection(type, columns = params[:columns]) - case type.to_sym + def filter_column_selection(table, columns = params[:columns]) + case table.to_sym when :sample columns.slice(:sample, :molecule) when :reaction @@ -590,11 +668,13 @@ def filter_column_selection(type, columns = params[:columns]) when :wellplate columns.slice(:sample, :molecule, :wellplate) when :sample_analyses - # FIXME: slice analyses + process properly + # FIXME: slice analyses + process properly columns.slice(:analyses).merge(sample_id: params[:columns][:sample]) # TODO: reaction analyses data # when :reaction_analyses # columns.slice(:analysis).merge(reaction_id: params[:columns][:reaction]) + when :sample_chemicals + columns.slice(:chemicals, :sample, :molecule) else {} end @@ -633,7 +713,7 @@ def force_molfile_selection position_x position_y readouts - ] + ], }.freeze DEFAULT_COLUMNS_REACTION = { @@ -663,12 +743,13 @@ def force_molfile_selection sum_formular inchistring molecular_weight - ] + ], }.freeze def default_columns_reaction DEFAULT_COLUMNS_REACTION end + def default_columns_wellplate DEFAULT_COLUMNS_WELLPLATE end diff --git a/app/api/helpers/sample_association_helpers.rb b/app/api/helpers/sample_association_helpers.rb deleted file mode 100644 index 13cb4ea9cd..0000000000 --- a/app/api/helpers/sample_association_helpers.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -# Helper for associated sample -module SampleAssociationHelpers - extend Grape::API::Helpers - - def build_sample(sid, cols, current_user, cr_opt) - parent_sample = Sample.find(sid) - - case cr_opt - when 0 - subsample = parent_sample - collections = Collection.where(id: cols).where.not(id: subsample.collections.pluck(:id)) - subsample.collections << collections unless collections.empty? - when 1 - subsample = parent_sample.create_subsample(current_user, cols, true, 'sample') - when 2 - subsample = parent_sample.dup - subsample.parent = nil - collections = (Collection.where(id: cols) | Collection.where(user_id: current_user.id, label: 'All', is_locked: true)) - subsample.collections << collections - subsample.container = Container.create_root_container - else - return nil - end - - return nil if subsample.nil? - - subsample.save! - subsample.reload - subsample - end - - def build_table_sample(element, field_tables) - sds = [] - field_tables.each do |field| - next unless field['sub_values'].present? && field['sub_fields'].present? - - field_table_samples = field['sub_fields'].select { |ss| ss['type'] == 'drag_sample' } - next unless field_table_samples.present? - - col_ids = field_table_samples.map { |x| x.values[0] } - col_ids.each do |col_id| - field['sub_values'].each do |sub_value| - next unless sub_value[col_id].present? && sub_value[col_id]['value'].present? && sub_value[col_id]['value']['el_id'].present? - - svalue = sub_value[col_id]['value'] - sid = svalue['el_id'] - next unless sid.present? - - sds << sid unless svalue['is_new'] - next unless svalue['is_new'] - - cr_opt = svalue['cr_opt'] - - subsample = build_sample(sid, element.collections, current_user, cr_opt) unless sid.nil? || cr_opt.nil? - next if subsample.nil? - - sds << subsample.id - sub_value[col_id]['value']['el_id'] = subsample.id - sub_value[col_id]['value']['is_new'] = false - ElementsSample.find_or_create_by(element_id: element.id, sample_id: subsample.id) - end - end - end - sds - end - - def update_sample_association(element, properties, current_user) - sds = [] - properties['layers'].keys.each do |key| - layer = properties['layers'][key] - field_samples = layer['fields'].select { |ss| ss['type'] == 'drag_sample' } - field_samples.each do |field| - idx = properties['layers'][key]['fields'].index(field) - sid = field.dig('value', 'el_id') - next if sid.blank? - - sds << sid unless properties.dig('layers', key, 'fields', idx, 'value', 'is_new') == true - next unless properties.dig('layers', key, 'fields', idx, 'value', 'is_new') == true - - cr_opt = field.dig('value', 'cr_opt') - - subsample = build_sample(sid, element.collections, current_user, cr_opt) unless sid.nil? || cr_opt.nil? - next if subsample.nil? - - sds << subsample.id - properties['layers'][key]['fields'][idx]['value']['el_id'] = subsample.id - properties['layers'][key]['fields'][idx]['value']['el_label'] = subsample.short_label - properties['layers'][key]['fields'][idx]['value']['el_tip'] = subsample.short_label - properties['layers'][key]['fields'][idx]['value']['is_new'] = false - ElementsSample.find_or_create_by(element_id: element.id, sample_id: subsample.id) - end - field_tables = properties['layers'][key]['fields'].select { |ss| ss['type'] == 'table' } - sds << build_table_sample(element, field_tables) - end - ElementsSample.where(element_id: element.id).where.not(sample_id: sds)&.destroy_all - properties - end -end diff --git a/app/assets/javascripts/pages.js b/app/assets/javascripts/pages.js index 1289cd116f..dac61a1143 100644 --- a/app/assets/javascripts/pages.js +++ b/app/assets/javascripts/pages.js @@ -1,15 +1,14 @@ - -function showExampleLabel() { - var prefix = document.querySelector('input#reaction-name-prefix')?.value; - var counter = parseInt(document.querySelector('input#reactions-count')?.value); - var user_name_abbr = document.querySelector('input#name_abbreviation')?.value; - var reaction_label = user_name_abbr + '-' + prefix + (counter + 1) - let label = document.querySelector('span#reaction-label-example') +const showExampleLabel = () => { + const prefix = document.querySelector('input#reaction-name-prefix')?.value; + const counter = parseInt(document.querySelector('input#reactions-count')?.value, 10); + const usernameAbbr = document.querySelector('input#name_abbreviation')?.value; + const reactionLabel = `${usernameAbbr}-${prefix}${counter + 1}`; + const label = document.querySelector('span#reaction-label-example'); if (label) { - label.textContent = reaction_label; + label.textContent = reactionLabel; } -} +}; -(function () { +(function showExampleLabelWrapper() { showExampleLabel(); -})(); \ No newline at end of file +}()); diff --git a/app/assets/stylesheets/_icons.scss b/app/assets/stylesheets/_icons.scss index 332dbf1e3b..e58ee1e41a 100644 --- a/app/assets/stylesheets/_icons.scss +++ b/app/assets/stylesheets/_icons.scss @@ -91,6 +91,7 @@ .icon-resin-solvent:before { content: "\f108"; } .icon-resin-solvent-reagent:before { content: "\f109"; } .icon-sample:before { content: "\f101"; } +.icon-cell_line:before { content: "\f102"; } .icon-screen:before { content: "\f103"; } .icon-tlc-control:before { content: "\f112"; } .icon-uv:before { content: "\f11c"; } diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index 6998fc1128..c248a9a062 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -14,9 +14,9 @@ *= require_self */ -@import 'ag-grid-community/dist/styles/ag-grid.css'; -@import "ag-grid-community/dist/styles/ag-theme-alpine.css"; -@import "ag-grid-community/dist/styles/ag-theme-balham.css"; +@import "ag-grid-community/styles/ag-grid.css"; +@import "ag-grid-community/styles/ag-theme-alpine.css"; +@import "ag-grid-community/styles/ag-theme-balham.css"; @import "antd/dist/antd.css"; @@ -41,10 +41,6 @@ @import "react-datetime-picker/dist/DateTimePicker.css"; -<%Bundler.load.current_dependencies.select{|dep| dep.groups.include?(:plugins)}.map(&:name).each do |plugin|%> -@import "<%=plugin%>"; -<%end%> - html, body, @@ -195,9 +191,17 @@ $font-icons-research_plan: "\f0f6"; content: "\f0f6"; } +.icon-cell_line { + font-style: inherit; +} +.icon-cell_line:before { + font-family: FontAwesome; + content: "\f0a3"; +} + .exportModal { width: 60%; - height: auto; + height: 100%; } .importChemDrawModal { diff --git a/app/assets/stylesheets/attachment-list.scss b/app/assets/stylesheets/attachment-list.scss new file mode 100644 index 0000000000..89d95c270a --- /dev/null +++ b/app/assets/stylesheets/attachment-list.scss @@ -0,0 +1,132 @@ +.attachment-dropzone { + width: 100%; + padding: 20px; + border: 4px dashed #cccccc; + text-align: center; + margin-bottom: 20px; + background-color: #ffffff; + border-radius: 10px; + box-shadow: 0px 2px 4px rgba(0,0,0,0.1); + transition: all 0.3s ease; + height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +.attachment-dropzone:hover { + border-color: #a0a0a0; + box-shadow: 0px 4px 8px rgba(0,0,0,0.15); +} + +.sorting-row-style { + padding: 5px; + border-radius: 5px; + border: 1px solid #ccc; + height: 35px; +} + +.sort-icon-style { + margin-left: 5px; + cursor: auto; + font-size: 20px !important; + color: #000; + border-radius: 50%; + padding: 5px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s; + background: none !important; + border: none !important; + outline: inherit !important; +} + +.sort-icon-style:focus { + box-shadow: none !important; +} + +.sort-icon-style:hover { + color: #797979 !important; + transform: scale(1.1); + transition: color 0.3s, transform 0.3s +} + +.attachment-row { + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + transition: box-shadow 0.3s ease, transform 0.2s ease-out; + transform-origin: center; + position: relative; +} + +.attachment-row:hover { + box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5); +} + +.attachment-row-image { + flex: 0.05; + display: flex; + margin-left: 10px; + align-items: center; + transition: transform 0.3s ease-in-out; +} +.attachment-row-image:hover { + transform: scale(6) translateX(20px); + z-index: 1000; + transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + + +.attachment-row-text { + flex: 0.7; + margin-left: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #333; + font-size: 16px; +} + +.attachment-row-subtext { + font-size: 12px; + color: #777; + display: flex; +} + +.attachment-row-actions { + flex: 0.4; + display: flex; + margin-right: 5px; + justify-content: flex-end; +} + +.attachment-button-size { + width: 30px !important; + height: 30px !important; +} + +.attachment-gray-button { + background-color: #c0c0c0 !important; + border-color: #c0c0c0 !important; +} + +.attachment-main-container { + padding: 10px; + background-color: #ffffff; + border-radius: 5px; +} + +.no-attachments-text { + text-align: center; + font-size: 16px; + color: #888; +} + +.sort-container { + flex-direction: row; + justify-content: space-between; +} + diff --git a/app/assets/stylesheets/cellLines.scss b/app/assets/stylesheets/cellLines.scss new file mode 100644 index 0000000000..88297a6d10 --- /dev/null +++ b/app/assets/stylesheets/cellLines.scss @@ -0,0 +1,355 @@ +.invalid-input { + border-color: red !important; + border-width: 1px; + border-style: solid; + border-radius: 4px; + .css-1pahdxg-control{ + border-style: none; + } + .css-1pahdxg-control{ + box-shadow: 0 0 0 0px #2684FF; + border-style: none; + } +} + +.amount{ + width: 90%; + input{ + height:38px; + } +} + +.scientific-button{ + button{ + margin-top: 2px; + margin-left: 3px; + width: 32px; + height: 32px; + } +} + +.cell-line-group{ + .group-entry{ + .elements{ + margin-bottom: 0px; + border-left-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-top-width: 0px; + } + .select-checkBox{ + border-left-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-top-width: 1px; + padding-top: 13px; + width:30px; + } + .short_label{ + border-left-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-top-width: 1px; + padding-top: 13px; + width:120px; + cursor: pointer; + } + .arrow{ + border-left-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + margin-top: -1px; + border-top-width: 1px; + float:right; + } + .item-text{ + border-left-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-top-width: 1px; + cursor: pointer; + + } + .item-property-name{ + margin-left:20px; + width:60px; + } + .starting{ + margin-left: 25px; + } + .item-property-value{ + width:200px; + } + .item-name-header{ + font-weight: bold; + } + .item-properties{ + font-size: 90%; + } + .item-colon{ + width:15px; + } + td{ + border-width: 0px; + } + .white-background{ + background:white + } + .blue-background{ + background:rgb(51, 122, 183) + } + .top-border{ + border-top: 1px solid #ddd; + } + } + + .title-panel{ + border-top: 1px solid #ddd; + background: rgb(245, 245, 245); + min-height: 50px; + .quick-sample{ + margin-top: 5px; + background-color: #337ab7; + border-color: #2e6da4; + margin-top: 14px; + color: white; + + &:hover{ + background-color: #2c6ba3; + color: rgb(233, 233, 233); + border-color: #2e6da4; + } + } + .detailed-info-off{ + margin: 5px; + background-color: #5bc0de; + margin-top: 14px; + + .fa-info-circle{ + color: #ffffff; + } + + &:hover{ + background-color: #5bc0de; + color: rgb(233, 233, 233); + } + } + .detailed-info-on{ + margin: 5px; + background-color: #40adce; + border-color: rgb(44, 44, 44); + margin-top: 14px; + + .fa-info-circle{ + color: #ffffff; + } + + &:hover{ + background-color: #40adce; + color: rgb(233, 233, 233); + border-color: rgb(44, 44, 44); + } + } + } +} + +.property-row{ + margin-top: 2px; +} + +.cell-line-name{ + input{ + font-size: 14px !important; + margin-bottom: 8px; + color: #555555 !important; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + font-family:arial, sans-serif !important; + } + .clear-icon{ + visibility: hidden; + } + .css-g1d714-ValueContainer{ + height:36px; + } + .css-1hb7zxy-IndicatorsContainer{ + height:36px; + } + + .invalid{ + .css-yk16xz-control{ + border-color: red !important; + } + .css-1pahdxg-control{ + box-shadow: 0 0 0 0px #ff2626; + border-color: red !important; + border-style: solid; + } + } + .cell-line-name-autocomplete{ + height:36px; + z-index: 999; + margin-bottom: 2px; + + .wrapper{ + z-index: 999; + height:36px; + border-radius: 4px; + + } + .invalid{ + .wrapper{ + border-color: red !important; + } + } + + li{ + z-index: 999; + background-color:white; + color:rgb(58, 58, 58); + } + .selected{ + background-color:rgb(204, 204, 204); + } + ul{ + margin-top: -13px; + font-size: 10px; + background-color:rgb(211, 211, 211) !important; + padding-bottom: 0px !important; + border-width: 1px !important; + border-style:solid; + border-color: #6d6d6d !important; + } + } +} + +.cell-line-group-bottom-border { + border-bottom-width: 1px; + border-bottom-style: solid; + border-color: #bdbdbd; +} + +.cell-line-group-arrow{ + margin-left: 3px; + font-size: 15px; + color: rgb(51, 122, 183); + line-height: 10px; + margin-right:5px; + margin-top:16px; +} + +.missing-property{ + margin-right: 5px; + + i { + color:red; + } +} + +#cell-line-details-tab{ + .tab-content{ + margin-top: 20px; + } +} + +.tab-content{ + .eln-panel-detail{ + .header{ + margin-top: 3px; + margin-right: 10px; + color: white; + } + .panel-heading{ + min-height: 41px; + } + .blue-background{ + background:rgb(51, 122, 183) + } + .collection-label{ + margin-top: 2px; + margin-right: 10px; + } + } +} + +.floating{ + float:left; +} + +.floating-right{ + float:right; +} + +.property-block{ + width:20%; +} + +.cell-line-group-header-name{ + font-size: large; + margin-left: 30px; + margin-top: 13px; + font-weight: bold; +} +.cell-line-group-header-property{ + margin-left: 35px; + + .property-key{ + width: 170px; + } + .property-key-minus{ + width: 20px; + } + .property-value{ + width: 500px; + } +} + +.order-mode-button{ + margin-left: 10px; + margin-top: 10px; + float:left +} +.add-button{ + margin-right: 10px; + margin-top: 10px; + float:right +} +.analyses{ + margin-top: 10px; +} +.no-analyses-panel{ + padding-top: 50px; + margin-bottom: 25px; +} +.analysis-container{ + .chosen-element{ + opacity: 0.1; + } + .last-hovered-element{ + border-width: 1px; + border: #337ab7; + border-style: dashed; + } + + .order-mode-button{ + .btn-orderMode{ + color: #fff; + background-color: #5cb85c + + } + .btn-editMode{ + color: #fff; + background-color: #337ab7 + } + .btn{ + width:100px; + i{ + margin-right: 6px; + } + } + } +} +.analyses{ + padding-top: 50px; + margin-bottom: 25px; +} + diff --git a/app/assets/stylesheets/chemical-tab.scss b/app/assets/stylesheets/chemical-tab.scss index 757c7a4ca6..e07e537ab9 100644 --- a/app/assets/stylesheets/chemical-tab.scss +++ b/app/assets/stylesheets/chemical-tab.scss @@ -142,18 +142,22 @@ table.table-borderless { justify-content: space-between; margin-top: 10px; } + + .text-input-price { + width: 22%; + } .text-input-person { - width: 29%; + width: 22%; } .text-input-date { margin-top: -17px; - width: 40%; + width: 35%; } .text-input-required-by { - width: 29%; + width: 22%; } } diff --git a/app/assets/stylesheets/collection_management.scss b/app/assets/stylesheets/collection_management.scss index 7615b340dc..a0a315c28d 100644 --- a/app/assets/stylesheets/collection_management.scss +++ b/app/assets/stylesheets/collection_management.scss @@ -26,6 +26,7 @@ position: relative; overflow: hidden; @extend .f-no-select; + margin-left: 5px; } .m-draggable { @@ -91,7 +92,11 @@ margin-left: 15px; float: left; padding-top: 4px; + padding-right: 0px; height: 26px; + border-width: 0px; + width:94%; + box-shadow: 0px 0px 0px rgb(104, 104, 104); } .form-group { @@ -99,7 +104,11 @@ } &.is-active { - background-color: #D3D3D3 + background-color: #D3D3D3; + + .root-label{ + background-color: #D3D3D3 + } } } @@ -130,3 +139,7 @@ font-size: 15px; } } + +.collection-tab-edit-btn { + vertical-align: inherit !important; +} diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 87330233e3..459bccdf82 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -129,6 +129,7 @@ .adv-search-row { display: flex; overflow: visible !important; + margin-left: 10px; } button { @@ -142,17 +143,31 @@ .link-select { margin: 5px; - flex: 0 0 117px + flex: 0 0 117px; } .match-select { margin: 5px; - flex: 0 0 150px + flex: 0 0 150px; + } + + .value-field-select { + margin: 5px; + flex: 0 0 205px; + + .ant-select { + width: 100%; + } + .ant-select-selection--single { + height: 34px; + } + .value-select-unit { + height: 34px; + } } .value-select { margin: 5px; - height: 60px; } .footer { @@ -181,6 +196,17 @@ } } +.collection-tab-modal { + .modal-dialog { + width: 1200px; + overflow-y: scroll; + } +} + +.collection-tab-modal-body { + position: sticky; +} + .status-select { .Select-menu-outer { z-index: 30; @@ -318,9 +344,19 @@ cursor: pointer; } +.btn-inbox-sort { + border: none; + background: none; + padding: 0; + cursor: pointer; + margin-left: 10px; + margin-right: 10px; +} + .dataset-pagination { text-align: center; margin-top: 5px; + margin-bottom: 10px; } .page-count { diff --git a/app/assets/stylesheets/components/CommentButton/CommentButton.scss b/app/assets/stylesheets/components/CommentButton/CommentButton.scss new file mode 100644 index 0000000000..bdc014d7d5 --- /dev/null +++ b/app/assets/stylesheets/components/CommentButton/CommentButton.scss @@ -0,0 +1,31 @@ +#commentBtn { + margin-top: 10px; + margin-bottom: 10px; +} + +.commentIcon { + margin-left: 5px; + margin-right: 5px; + border: 1px solid grey; + background-color: white; + color: black; + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: 700; + line-height: 1; + border-radius: 0.25em; + + &:hover{ + background-color: #19B5FE; + } +} + +.comments-header-btn { + margin-left: 10px; + white-space: nowrap; +} + +.comments-header-btn button { + display: inline-block; + margin-right: 1px; +} diff --git a/app/assets/stylesheets/components/CommentModal/CommentModal.scss b/app/assets/stylesheets/components/CommentModal/CommentModal.scss new file mode 100644 index 0000000000..66214146b4 --- /dev/null +++ b/app/assets/stylesheets/components/CommentModal/CommentModal.scss @@ -0,0 +1,57 @@ +.comment-modal { + width: 90vw !important; +} + +.table-responsive { + overflow-x: auto; +} + +.modal-body { + #detailsBtn { + margin-bottom: 10px; + } + + .comment-details { + font-size: 20px; + cursor: pointer; + color: #337ab7; + vertical-align: middle; + top: 0; + } + + .btn-toolbar { + position: relative; + + #editCommentBtn { + position: absolute; + left: 90px; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); + } + + #deleteCommentBtn{ + position: absolute; + left: 118px; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); + margin-left: 5px; + } + } + + .commentList { + overflow: auto; + max-height: 60vh; + min-height: 10vh; + + table { + table-layout: fixed; + + td { + word-wrap: break-word; + white-space: pre-line; + } + } + } +} diff --git a/app/assets/stylesheets/components/inbox/inbox.scss b/app/assets/stylesheets/components/inbox/inbox.scss new file mode 100644 index 0000000000..e20731ea2e --- /dev/null +++ b/app/assets/stylesheets/components/inbox/inbox.scss @@ -0,0 +1,7 @@ +.draggable { + cursor: grab; +} + +.draggable:active { + cursor: grabbing; +} diff --git a/app/assets/stylesheets/components/select.scss b/app/assets/stylesheets/components/select.scss new file mode 100644 index 0000000000..812ec9214d --- /dev/null +++ b/app/assets/stylesheets/components/select.scss @@ -0,0 +1,17 @@ +.header-group-select { + .Select-control { + width: 200px; + height: 27px; + cursor: pointer; + box-shadow: inset 0 2px 2px #e9e9e9; + border: 1px solid #aeaeae; + } + + .Select-value { + line-height: 2 !important; + } + + .Select-input { + height: 27px !important; + } +} diff --git a/app/assets/stylesheets/elements_details.css b/app/assets/stylesheets/elements_details.css index c265dd267d..93213f499f 100644 --- a/app/assets/stylesheets/elements_details.css +++ b/app/assets/stylesheets/elements_details.css @@ -7,6 +7,7 @@ right: 0px; z-index: 1; margin-left: 2px; + text-align: center; } .no-margin { @@ -124,3 +125,14 @@ width: 100%; padding: 0 !important; } + +.header-button { + padding: 6px 12px; + font-size: 14px; + border-radius: 4px; + border: 1px solid transparent; + margin-left: 10px; + min-width: 25px; + height: 25px; + font-weight: bold; +} diff --git a/app/assets/stylesheets/elements_table.scss b/app/assets/stylesheets/elements_table.scss index 0afd6c8db6..20ebb73026 100644 --- a/app/assets/stylesheets/elements_table.scss +++ b/app/assets/stylesheets/elements_table.scss @@ -60,6 +60,7 @@ th.drag { } .collection-overlay > .popover-content { + overflow-y: scroll; padding: 0px !important; } @@ -78,9 +79,6 @@ th.drag { } .sample-list-from-date, .sample-list-to-date { - float: right; - margin-right: 2px; - input { font-size: 13px; border-radius: 4px; @@ -92,22 +90,6 @@ th.drag { } } -/* .panel-title div:empty:after { - content: "."; - visibility: hidden; -} */ - - -/*.number-shown-select { - float: right !important; - text-align: right; - width: 55% !important; -}*/ - -/*.element-svg-checkbox { - margin: 0px 0 0; -}*/ - .list-container-bottom { width: 100%; } @@ -139,17 +121,17 @@ th.drag { display: flex; align-items: center; justify-content: space-between; - height: 40px; + min-height: 40px; background-color: #ddd; padding: 5px; } -.header-right { - display: flex; - margin-right: 12px; -} - -.select-all { +@media (max-width: 600px) { + .table-header { + height: auto; + flex-wrap: wrap; + overflow: auto; + } } #tabList .active a { diff --git a/app/assets/stylesheets/format-container.scss b/app/assets/stylesheets/format-container.scss index 54df1c5e6b..aece4c8fe1 100644 --- a/app/assets/stylesheets/format-container.scss +++ b/app/assets/stylesheets/format-container.scss @@ -7,33 +7,59 @@ max-height: calc(100vh - 250px); } -.attachment-dataset-modal { +.attachment-annotation-modal { height: 95%; top: 52% !important; margin: unset !important; width: calc(80vw) !important; .modal-content { height: 95%; - display: grid; - grid-template-rows: auto 1fr auto; - .modal-header .modal-title .btn-toolbar { - float: right; + .modal-title{ + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; } - .modal-body { + .modal-body { + height: 90%; overflow: auto; - .row { - height: 100%; - .col-base .desc { - min-height: 200px; - } - .col-full { - height: 100%; - .list { - overflow: auto; - // max-height: calc(100% - 70px) !important; - } - } - } + max-height: calc(100% - 130px) !important; } } } + +.attachment-modal { + top: 55% !important; + height: 100%; + margin: unset !important; + width: 70vw !important; + .modal-content { + height: 90%; + .modal-title{ + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } + .modal-body { + height: 90%; + overflow: auto; + max-height: calc(100% - 130px) !important; + } + } +} + +.attachment-name-input-div { + display: flex; + flex-grow: 1; + align-items: center; +} + +.attachment-name-input { + font-weight: bold; + color: #495057; + width: 100%; + border: none; + border-bottom: 1px solid #ccc; + padding: 2px 0; +} diff --git a/app/assets/stylesheets/generic_element.scss b/app/assets/stylesheets/generic_element.scss index 18cd2295de..340ee1ba32 100644 --- a/app/assets/stylesheets/generic_element.scss +++ b/app/assets/stylesheets/generic_element.scss @@ -404,3 +404,92 @@ $color-default: #777; opacity: 0.2; } } + + +.generic_layer_modal { + > .layer_header { + background-color: white; + color: $colo-bs-primary; + font-size: 20px; + } +} + +.generic_layer_column { + @include generic_grid_cell; + border: 1px solid $colo-bs-primary; + background-color: white; + margin: 5px; + padding: 10px; + color: $colo-bs-primary; + border-radius: 4px; +} + +.generic_layer_column > div:first-child { + display: flex; +} + +.generic_layer_column > div:first-child > button { + float: right; + border-radius: 50%; +} + +.generic_layer_column > div:first-child > div { + width: 100%; +} + +.generic_wf_modal { + max-height: 100%; + overflow: auto; +} + +.generic_wf_modal > div:first-child { + height: 85vh; + width: 100%; +} + +.flow_view_draggable { + z-index: 100; + position: absolute; + top: 12%; + left: 10%; + .panel-body { + padding: 5px; + > .body_bg { + max-height: 100%; + overflow: auto; + > .body_canvas { + resize: both; + overflow: auto; + height: 70vh; + width: 40vw; + min-height: 250px; + min-width: 300px; + max-width: 1600px; + max-height: 840px; + } + } + } +} + +.generic-ds-panel { + border: none !important; + > .panel-body { + position: relative; + min-height: 260px; + overflow-y: unset !important; + padding: 0px !important; + } +} + +.generic_segments_repo { + padding: 10px; + background-color: white; + margin-bottom: 6px; +} + +.generic-add-analysis { + background-color: unset; + position: sticky; + top: 0px; + z-index: 10; +} diff --git a/app/assets/stylesheets/inventory-label-settings.scss b/app/assets/stylesheets/inventory-label-settings.scss new file mode 100644 index 0000000000..e8b8fea01f --- /dev/null +++ b/app/assets/stylesheets/inventory-label-settings.scss @@ -0,0 +1,37 @@ +.inventory-label-settings { + .select-collection-id { + padding-top: 15px; + padding-right: 10px; + display: flex; + justify-content: space-between; + + & b, .select-collection-name, .inventory-counter-prefix-name, .inventory-counter-starts-at { + padding-top: 7px; + padding-right: 6px; + padding-left: 6px; + } + } + + .update-user-button { + margin-left: 190px; + margin-top: 20px; + + .inventory-label-next-counter { + padding-top: 8px; + width: 50%; + } + + .update-inventory-user-button { + margin-bottom : 20px; + .text-center { + width: 155px; + margin-left: 80px; + + .fa-spinner { + display: inline-block; + width: 120px; + } + } + } + } +} diff --git a/app/assets/stylesheets/molecules.scss b/app/assets/stylesheets/molecules.scss index 0c7931e56d..bfb203026e 100644 --- a/app/assets/stylesheets/molecules.scss +++ b/app/assets/stylesheets/molecules.scss @@ -3,6 +3,12 @@ height: 90px; } +.reaction-header svg { + width: 100%; + max-width: 500px; + height: auto; +} + .molecule rect { fill: none; fill-opacity: 0.0; diff --git a/app/assets/stylesheets/omniauth.scss b/app/assets/stylesheets/omniauth.scss index 9118844276..02511e0bf4 100644 --- a/app/assets/stylesheets/omniauth.scss +++ b/app/assets/stylesheets/omniauth.scss @@ -1,5 +1,5 @@ .omniauth-btn { - width: 17vh; + width: 18vh; text-align: left; img { position: absolute; @@ -20,4 +20,4 @@ height: 3vh; } } - } \ No newline at end of file + } diff --git a/app/assets/stylesheets/research-plan.scss b/app/assets/stylesheets/research-plan.scss index 481fb1fbb6..bcfbc7b71c 100644 --- a/app/assets/stylesheets/research-plan.scss +++ b/app/assets/stylesheets/research-plan.scss @@ -164,20 +164,33 @@ } } -.research-plan-attachments-annotation-download{ - button{ +.research-plan-attachments-annotation-download{ + button{ margin-right: 10px; - } -} + } +} .imageEditedWarning{ + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + white-space: nowrap; + div{ - background-color: rgba(252, 252, 138, 0.952); - border-color: #d0d0d0; - color: rgba(102, 102, 60, 0.95); - margin-top: 3px; - } -} + background-color: #ffcc00; + border: 1px solid #d0d0d0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + color: #333; + padding: 10px; + margin: 0; + border-radius: 4px; + transition: all 0.3s ease; + overflow: hidden; + text-overflow: ellipsis; + } +} .research-plan-table-grid.grid-with-collapsed-rows { .ag-body-viewport { diff --git a/app/assets/stylesheets/sample.scss b/app/assets/stylesheets/sample.scss index 401fa5fec4..a4a7ae1494 100644 --- a/app/assets/stylesheets/sample.scss +++ b/app/assets/stylesheets/sample.scss @@ -141,7 +141,6 @@ .label--bold { font-weight: bold; - margin-right: 10px; } .noAnalyses-warning { diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss index 7d4dbd2403..6ddbb16453 100644 --- a/app/assets/stylesheets/search.scss +++ b/app/assets/stylesheets/search.scss @@ -25,4 +25,4 @@ .list-group-item-heading { overflow-wrap: break-word; } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/search_modal.scss b/app/assets/stylesheets/search_modal.scss new file mode 100644 index 0000000000..608dc502eb --- /dev/null +++ b/app/assets/stylesheets/search_modal.scss @@ -0,0 +1,460 @@ +[dialogas=full-search].modal { + width: fit-content; + height: fit-content; + top: calc(100% - 95vh); + left: calc((100% - 90vw) / 2); +} + +[dialogas=full-search] .modal-dialog { + width: 90vw !important; + position: relative !important; + top: 0; + left: 0; + transform: none !important; + margin: 5px 20px 22px 15px; +} + +[dialogas=full-search] .modal-content { + border: 1px solid rgba(0, 0, 0, 0.4); + overflow: hidden; + height: fit-content; +} + +[dialogas=full-search] .modal-header { + background: #ddd; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + padding: 10px 15px 10px 0; +} + +[dialogas=full-search] .modal-header .close { + opacity: 0.4; + font-size: 25px; + line-height: 1.3; +} + +[dialogas=full-search] .modal-header .form-group { + margin-bottom: 0; + font-size: 16px; +} + +[dialogas=full-search] .modal-header .move { + margin-right: 15px; +} + +[dialogas=full-search] .modal-header .modal-title { + padding: 5px 0; +} + +[dialogas=full-search] .modal-header .window-minimize { + color: rgba(0, 0, 0, 0.4); + float: right; + margin-top: -62px; + margin-right: 15px; +} + +[dialogas=full-search] .modal-header .window-minimize:hover { + cursor: pointer; +} + +[dialogas=full-search] .modal-body { + padding: 0; +} + +@media screen and (min-width: 768px) { + [dialogas=full-search] .modal-header .window-minimize { + margin-top: -22px; + } +} + +.search-selection.btn-group { + float: right; +} + +.search-selection .btn { + height: 35px; + color: #337ab7; + border: 1px solid rgba(0, 0, 0, 0.4); + margin-right: 12px; +} + +.search-selection .btn:hover { + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.4); +} + +.search-selection .btn.active, .search-selection .btn.active:hover, .search-selection .btn.active:focus { + background-color: #5bc0de75; + box-shadow: none; + border: 1px solid rgba(0, 0, 0, 0.4); + outline: none; +} + +.search-icon { + float: left; + margin-right: 5px; +} +.search-icon i { + font-size: 1.5em; + padding-top: 1px; +} + +.form-container { + display: block; +} + +.form-container.minimized { + display: none; +} + +.collapsible-search-result.panel.panel-default { + border: none !important; + box-shadow: none !important; + margin-bottom: 0 !important; +} + +.collapsible-search-result.panel.panel-default .btn-toolbar { + margin-bottom: 8px; +} + +.collapsible-search-result.inactive { + display: none; +} + +.collapsible-search-result.panel-default > .panel-heading { + background: #5bc0de75; + border: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); +} + +.collapsible-search-result.panel-default > .panel-heading.inactive { + background: #a2bdc6; + border: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); +} + +.collapsible-search-result .panel-title a:hover, .collapsible-search-result .panel-title a:link { + text-decoration: none; + display: block; +} + +.collapsible-search-result #tabList { + height: 46.8vh; + @media screen and (min-width: 1600px) { + height: 51vh; + } +} + +.no-selected-search { + padding: 15px; +} + +.search-spinner, .tab-spinner { + color: #ddd; + position: absolute; + top: 0; + left: 50%; +} + +i.fa.icon-right { + float: right; +} + +.advanced-search { + margin-bottom: 39px; + padding-left: 4px; + height: 58.5vh; + position: relative; + + @media screen and (min-width: 1600px) { + height: 65vh; + } + + .scrollable-content { + clear: both; + overflow-y: auto; + height: calc(58.5vh - 65px); + margin-top: 23px; + .alert { + margin-left: 15px; + } + @media screen and (min-width: 1600px) { + height: calc(65vh - 65px); + } + } + + .Select-menu, .css-4ljt47-MenuList { + max-height: 250px; + } + .toggle-elements { + width: calc(100% - 5px); + } + .btn-group { + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + } + .btn-group .btn { + padding: 0 25px; + border: 1px solid rgba(0, 0, 0, 0.4); + border-bottom: none; + border-radius: 0; + color: rgba(0, 0, 0, 0.5); + height: 45px; + } + .btn-group .btn:hover { + background-color: #fff; + } + .btn-group .btn:first-child { + border-top-left-radius: 4px; + } + .btn-group .btn:last-child { + border-top-right-radius: 4px; + } + .btn-group .btn.active { + background-color: #5bc0de75; + box-shadow: none; + } + .btn-group i { + font-size: 2.2em; + color: #337ab7; + } + .btn-group .btn:hover i { + color: rgba(0, 0, 0, 0.6); + } + .btn-group i.icon-research_plan { + font-size: 1.6em; + padding: 6px 0 5px 0; + display: inline-block; + } + .btn-group i.icon_generic_nav { + font-size: 1.8em; + padding: 10px 0 8px 0; + } + .css-1okebmr-indicatorSeparator { + background-color: #fff; + } + .css-18ng2q5-group { + color: #5bc0de; + font-size: 13px; + font-weight: bold; + } + .css-syji7d-Group .css-yt9ioa-option, .css-syji7d-Group .css-1n7v3ny-option { + padding-left: 30px; + } + .vertical-buttons { + -webkit-transform: rotate(-90deg) translate(-100%, 0); + -moz-transform: rotate(-90deg) translate(-100%, 0); + -o-transform: rotate(-90deg) translate(-100%, 0); + transform: rotate(-90deg) translate(-100%, 0); + -webkit-transform-origin: -17px 15px; + -moz-transform-origin: -17px 15px; + -o-transform-origin: -17px 15px; + transform-origin: -17px 15px; + width: 43.8vh; + border-bottom: none; + } + + .vertical-buttons .btn { + padding: 3px 25px; + width: 50%; + height: 33px; + font-size: 1.2em; + border-bottom: 1px solid rgba(0,0,0, 0.4); + background-color: #ddd; + } + .vertical-buttons .btn.active { + background-color: #fff; + color: #347ab8; + border-bottom: none; + } + + .advanced-detail-switch { + height: 0; + width: 0; + visibility: hidden; + } + + .advanced-detail-switch-label { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + width: 115px; + height: 30px; + position: relative; + float: right; + margin-top: -40px; + margin-bottom: 25px; + margin-right: 6px; + padding: 0 34px 0 17px; + color: #347ab8; + background-color: #b7e2ef; + font-weight: normal; + border-radius: 100px; + } + + .advanced-detail-switch-label.active { + padding: 0 17px 0 36px; + } + + .advanced-detail-switch-label .advanced-detail-switch-button { + content: ''; + position: absolute; + top: 2px; + right: 2px; + width: 26px; + height: 26px; + border-radius: 26px; + transition: right 2s; + background: #fff; + box-shadow: 0 0 2px 0 rgba(10, 10, 10, 0.29); + } + + .advanced-detail-switch-label.active .advanced-detail-switch-button { + right: calc(115px - 28px); + } + + .detail-search { + overflow: visible !important; + margin-left: 15px; + } + .detail-search .form-group, .detail-search .checkbox, .detail-search .ant-select { + width: 49%; + margin-right: 1%; + } + .detail-search .sub-group-with-addon-2col { + width: 100%; + } + .detail-search .sub-group-with-addon-2col:last-child { + margin-right: 0; + } + @media screen and (min-width: 1023px) { + .detail-search .form-group, .detail-search .checkbox, .detail-search .ant-select { + width: 24%; + margin-right: 1%; + } + .detail-search .sub-group-with-addon-2col { + width: 49%; + } + } + .detail-search .ant-select { + width: 100%; + } + .detail-search .checkbox, .detail-search .radio + .radio, .detail-search .checkbox + .checkbox, .detail-search-headline + .checkbox { + padding-top: 5px; + margin-top: -5px; + } + .detail-search .form-group + .checkbox, .detail-search .form-group + .checkbox + .checkbox, .detail-search .detail-search-headline + .checkbox { + padding-top: 17px; + margin-top: 10px; + } + .detail-search-headline, .detail-search-segment-headline { + flex-basis: 100%; + font-weight: 700; + font-size: 1.15em; + color: rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(0, 0, 0, 0.5); + padding-bottom: 3px; + margin-bottom: 15px; + margin-top: 3px; + } + .detail-search-headline { + background-color: #f5f5f5; + color: #333; + padding: 5px; + padding-left: 10px; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + } + .detail-search-headline + .detail-search-headline { + color: rgba(0, 0, 0, 0.5); + background-color: transparent; + border-top: none; + padding: 0; + padding-bottom: 3px; + border-bottom: 1px solid rgba(0, 0, 0, 0.5); + } + .detail-search-segment-headline { + font-size: 1.5em; + color: rgba(0, 0, 0, 0.6); + margin-top: 20px; + } + .css-yk16xz-control, .css-1pahdxg-control { + min-height: 34px; + height: 34px; + } + .css-1wa3eu0-placeholder, .css-1uccc91-singleValue { + top: 42%; + } + .css-26l3qy-menu { + margin-top: 0; + } + ul.ant-select-tree li .ant-select-tree-node-content-wrapper, .react-select-4-option-0 { + display: block; + height: 17px; + } + .detail-search ul.ant-select-tree li:first-child span.ant-select-tree-node-content-wrapper { + height: 17px; + } + .detail-search .css-26l3qy-menu { + z-index: 10000; + } + .detail-search hr.generic-spacer { + border-top: 1px solid rgba(0, 0, 0, 0.5); + margin-top: 3px; + margin-bottom: 15px; + flex-basis: 100%; + } + .detail-search .grey-field { + background-color: #555; + border: 1px solid #ccc; + } + .detail-search .grouped-sub-fields { + display: flex; + justify-content: space-between; + width: 100%; + } + .detail-search .subfields-with-addon-left-3 { + width: 32%; + margin-right: 1%; + } + .detail-search .subfields-with-addon-left-2 { + width: 49%; + margin-right: 1%; + } +} + +#detail-search-form-element-tabs, #detail-search-form-element-tabs .nav-tabs { + width: 100%; +} +#detail-search-form-element-tabs [id^=detail-search-form-element-tabs-pane-] { + display: none; + width: 100%; + padding-top: 15px; +} +#detail-search-form-element-tabs [id^=detail-search-form-element-tabs-pane-].active { + display: flex; + flex-wrap: wrap; +} +#detail-search-form-element-tabs .nav-tabs { + border-bottom: 1px solid rgba(0,0,0, 0.4); + margin-top: 2px; +} +#detail-search-form-element-tabs .nav-tabs li a { + margin-right: 2px; + padding-top: 8px; + padding-bottom: 8px; + color: #555; + border-bottom: 1px; +} +#detail-search-form-element-tabs .nav-tabs li.active a { + color: #337ab7; + border: 1px solid rgba(0,0,0, 0.4); + border-bottom-color: transparent; +} +.search-info-button { + display: inline-block; + margin-left: 5px; +} + +.result-error-message { + margin-top: 20px; +} diff --git a/app/assets/stylesheets/search_results.scss b/app/assets/stylesheets/search_results.scss new file mode 100644 index 0000000000..4e77a30f08 --- /dev/null +++ b/app/assets/stylesheets/search_results.scss @@ -0,0 +1,173 @@ +.search-result-tab-navbar.navbar-default { + background-color: #fff; + margin-bottom: 0; + border: none; +} + +.search-result-tab-navbar.navbar-default .container { + padding: 0; + margin: 0; + width: 100%; +} + +.search-result-tab-navbar.navbar-default .navbar-nav { + border: 1px solid rgba(0, 0, 0, 0.4); + border-bottom: none; + border-radius: 4px 4px 0 0; +} + +.search-result-tab-navbar.navbar-default .navbar-nav li a { + border: none; + border-right: 1px solid rgba(0, 0, 0, 0.4); + border-radius: 0; + padding: 10px 30px; + line-height: 27px; + color: #337ab7; +} + +.search-result-tab-navbar.navbar-default .navbar-nav li:last-child a { + border-right: none; +} + +.search-result-tab-navbar.navbar-default .navbar-nav li.active a { + border: none; + border-right: 1px solid rgba(0, 0, 0, 0.4); + color: #337ab7; + background-color: #5bc0de75; +} + +.search-result-tab-navbar.navbar-default .navbar-nav a i { + font-size: 2.2em; +} + +.search-result-tab-navbar.navbar-default .navbar-nav a i.icon-research_plan { + font-size: 1.6em; +} + +.search-result-tab-navbar.navbar-default .navbar-nav a i.icon_generic_nav { + font-size: 1.8em; + padding-top: 2px; +} + +.search-result-tab-navbar.navbar-default .navbar-nav a span { + font-size: 1.4em; + padding-top: 2px; +} + +.search-result-tab-navbar.navbar-default .elements-list-tab.no-result a { + color: rgba(0, 0, 0, 0.2); +} + +.search-result-tab-content.tab-content { + border: none; +} + +.search-result-tab-navbar.navbar-default .elements-list-tab.no-result a i { + font-size: 1.8em; +} + +.search-result-tab-navbar.navbar-default .elements-list-tab.no-result a i.icon-research_plan { + font-size: 1.4em; + padding-top: 2px; +} + +.search-result-tab-navbar.navbar-default .elements-list-tab.no-result a i.icon_generic_nav { + font-size: 1.5em; + padding-top: 4px; +} + +.search-result-tab-navbar.navbar-default .elements-list-tab.no-result a span { + font-size: 1.2em; +} + +.search-result-tab-content-list { + background-color: rgb(245, 245, 245); + padding: 0 15px 15px 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + border-top: 1px solid rgba(0, 0, 0, 0.1); +} +.search-result-tab-content-list:first-child { + border-top: none; +} + +.search-result-tab-content-list p { + font-weight: bold; + margin-bottom: 5px; +} + +.search-result-tab-content-list p:first-child { + margin-top: 15px; +} + +.search-result-tab-content-list-white { + background-color: #fff; + padding: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + +.search-result-tab-content-list-white:last-child { + border-bottom: none; +} + +.search-result-molecule { + display: flex; +} + +.search-result-molecule .preview-table { + max-width: 100px; + min-height: 100px; + margin-right: 10px; + margin-bottom: 10px; +} + +.search-result-molecule.reaction .preview-table { + max-width: 300px; +} + +.search-result-tab-content-list-name { + display: block; + margin: -16px -15px -15px -15px; + padding: 9px 15px; + background-color: #fff; +} + +.search-result-pagination { + margin-top: 20px; + margin-bottom: -6px; +} + +.search-result-pagination .pagination { + margin-bottom: 0; +} + +.result-button-toolbar { + border-top: 1px solid #ddd; + margin-top: 20px; + padding-top: 15px; +} + +.search-result-tab-content-container { + overflow-x: auto; + min-height: 80px; + max-height: 34.3vh; + position: relative; + border: 1px solid rgba(0, 0, 0, 0.4); + @media screen and (min-width: 1600px) { + max-height: 40vh; + } +} + +.tab-spinner { + color: #ddd; + position: absolute; + top: 20%; + left: 2%; +} + +.search-value-list { + position: relative; + max-height: 12vh; + min-height: 92px; + overflow-y: auto; +} diff --git a/app/assets/stylesheets/spectra.scss b/app/assets/stylesheets/spectra.scss index 7f1a182ddf..47ca620b6c 100644 --- a/app/assets/stylesheets/spectra.scss +++ b/app/assets/stylesheets/spectra.scss @@ -8,7 +8,7 @@ top: auto; left: 1vw; width: 98vw; - text-align: center; + text-align: left; } } @@ -47,7 +47,6 @@ text-align: center; } - .txt-sv-panel-title, .txt-sv-panel-head { font-size: 12px !important; font-family: 'Helvetica'!important; @@ -99,6 +98,11 @@ font-family: 'Helvetica'!important; } +.txt-sv-panel-title { + font-size: 14px !important; + font-family: 'Helvetica'!important; +} + .svg-file-zoom-pan { width: 100%; height: 100%; diff --git a/app/assets/stylesheets/tab_layout_container.scss b/app/assets/stylesheets/tab_layout_container.scss index f556e5688b..bcb4529b21 100644 --- a/app/assets/stylesheets/tab_layout_container.scss +++ b/app/assets/stylesheets/tab_layout_container.scss @@ -14,3 +14,18 @@ table.layout-container { color: #555555; } } + +.collection-tag-element { + font-size: 11px; + font-weight: bold; + margin-top: 10px; +} + +.tab-layout-cell { + font-size: 12px; + color: #000000; + text-align: center; + word-wrap: break-word; + width: 85px; + margin-top: 10px; +} diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 3e0e37f78d..77d64830ba 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -33,8 +33,8 @@ def sfn_cb def update_user @user = current_user - @user.counters['reactions'] = params[:reactions_count].to_i - @user.reaction_name_prefix = params[:reaction_name_prefix] + @user.counters['reactions'] = params[:reactions_count].to_i if params[:reactions_count].present? + @user.reaction_name_prefix = params[:reaction_name_prefix] if params[:reactions_count].present? if @user.save flash['success'] = 'User settings is successfully saved!' redirect_to root_path diff --git a/app/jobs/gate_transfer_job.rb b/app/jobs/gate_transfer_job.rb deleted file mode 100644 index 2f0c37e789..0000000000 --- a/app/jobs/gate_transfer_job.rb +++ /dev/null @@ -1,154 +0,0 @@ -# frozen_string_literal: true - -# Job to update molecule info for molecules with no CID -# associated CID (molecule tag) and iupac names (molecule_names) are updated if -# inchikey found in PC db -# -class GateTransferJob < ApplicationJob - queue_as :gate_transfer - # queue_as :gate_transfer - # job_options retry: false - SAMPLE = 'Sample' - REACTION = 'Reaction' - STATE_BEFORE_TRANSFER = 'before transfer' - STATE_TRANSFER = 'transferring' - STATE_TRANSFERRED = 'transferred' - STATE_FAILED_TRANSFER = 'unable to transfer' - - def perform(id, url, req_headers) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - # ping remote - @url = url - @req_headers = req_headers - @no_error = true - - connection = Faraday.new(url: @url) do |faraday| - faraday.response :follow_redirects - faraday.headers = @req_headers - end - - @resp = connection.get { |req| req.url('/api/v1/gate/ping') } - raise @resp.reason_phrase unless @resp.success? - - @reactions = [] - @samples = [] - - @collection = Collection.find(id) - all_reaction_ids = CollectionsReaction.where(collection_id: id).pluck(:reaction_id) - reaction_sample_ids = Reaction.get_associated_samples(all_reaction_ids) - dec_ids = ReactionsSample.where(reaction_id: all_reaction_ids) - .joins(:sample).where('samples.decoupled = true').pluck(:reaction_id) - all_reaction_ids -= dec_ids - all_sample_ids = CollectionsSample.where(collection_id: id) - .joins(:sample) - .where('samples.decoupled = false') - .pluck(:sample_id) - reaction_sample_ids - - return true if all_reaction_ids.empty? && all_sample_ids.empty? - - if all_reaction_ids.present? || all_sample_ids.present? - all_reaction_ids.each do |reaction_id| - transfer_data(type: GateTransferJob::REACTION, id: reaction_id, state: GateTransferJob::STATE_BEFORE_TRANSFER, - msg: '') - end - - begin - all_sample_ids.each do |sample_id| - transfer_data(type: GateTransferJob::SAMPLE, id: sample_id, state: GateTransferJob::STATE_BEFORE_TRANSFER, - msg: '') - end - ensure - if @no_error && (@reactions.present? || @samples.present?) - MoveToCollectionJob.set(queue: "move_to_collection_#{id}").perform_later(id, @reactions, @samples) - end - end - end - true - end - - def transfer_data(**element) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - sample_ids = [element[:id]] if element[:type] == GateTransferJob::SAMPLE - reaction_ids = [element[:id]] if element[:type] == GateTransferJob::REACTION - exp = Export::ExportJson.new( - collection_id: @collection.id, sample_ids: sample_ids, reaction_ids: reaction_ids, - ).export - attachment_ids = exp.data.delete('attachments') - attachments = Attachment.where(id: attachment_ids) - data_file = Tempfile.new - data_file.write(exp.to_json) - data_file.rewind - - req_payload = {} - req_payload[:data] = Faraday::UploadIO.new( - data_file.path, 'application/json', 'data.json' - ) - - tmp_files = [] - attachments.each do |att| - cont_type = att.content_type || MimeMagic.by_path(att.filename)&.type - tmp_files << Tempfile.new(encoding: 'ascii-8bit') - file_stream = att.read_file - file_checksum = Digest::SHA256.hexdigest(file_stream) - file_checksum_md5 = Digest::MD5.hexdigest(file_stream) - if att.checksum != file_checksum && att.checksum != file_checksum_md5 - raise 'The file checksum does not mach, unable to transfer, please try again later!' - end - - tmp_files[-1].write(file_stream) - tmp_files[-1].rewind - req_payload[att.identifier] = Faraday::UploadIO.new( - tmp_files[-1].path, cont_type, att.filename - ) - end - - payload_connection = Faraday.new(url: @url) do |faraday| - faraday.response :follow_redirects - faraday.request :multipart - faraday.headers = @req_headers.merge('Accept' => 'application/json') - end - - @resp = payload_connection.post do |req| - req.url('/api/v1/gate/receiving') - req.body = req_payload - end - - if @resp.success? - element[:state] = GateTransferJob::STATE_TRANSFERRED - else - Delayed::Worker.logger.error <<~TXT - --------- gate transfer FAIL message.BEGIN ------------ - resp status: #{@resp.status} - #{element[:type]} - #{element[:id]} - --------- gate transfer FAIL message.END --------------- - TXT - - element[:state] = GateTransferJob::STATE_TRANSFER - element[:msg] = 'response is not successful' - end - rescue StandardError => e - Delayed::Worker.logger.error <<~TXT - --------- gate transfer FAIL error message.BEGIN ------------ - message: #{e.message} - --------- gate transfer FAIL error message.END --------------- - TXT - - element[:state] = GateTransferJob::STATE_FAILED_TRANSFER - element[:msg] = e.message - @no_error = false - Message.create_msg_notification( - channel_subject: Channel::GATE_TRANSFER_NOTIFICATION, - data_args: { comment: e.message }, - level: 'error', - message_from: @collection.user_id, - ) - ensure - @samples.push(element) if element[:type] == GateTransferJob::SAMPLE - @reactions.push(element) if element[:type] == GateTransferJob::REACTION - - data_file&.close - data_file&.unlink - tmp_files&.each do |tf| - tf.close - tf.unlink - end - end -end diff --git a/app/jobs/import_samples_job.rb b/app/jobs/import_samples_job.rb new file mode 100644 index 0000000000..a0b1d53fdf --- /dev/null +++ b/app/jobs/import_samples_job.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ImportSamplesJob < ApplicationJob + include ActiveJob::Status + + queue_as :import_samples + + after_perform do + Message.create_msg_notification( + channel_subject: Channel::IMPORT_SAMPLES_NOTIFICATION, + message_from: @user_id, + message_to: [@user_id], + data_args: { message: @result[:message] }, + level: 'info', + autoDismiss: 5, + ) + rescue StandardError => e + Delayed::Worker.logger.error e + end + + def perform(params) + @user_id = params[:user_id] + file_path = params[:file_path] + file_format = File.extname(params[:file_name]) + begin + case file_format + when '.xlsx' + import = Import::ImportSamples.new( + file_path, + params[:collection_id], + @user_id, params[:file_name], + params[:import_type] + ) + @result = import.process + when '.sdf' + sdf_import = Import::ImportSdf.new( + collection_id: params[:collection_id], + current_user_id: @user_id, + rows: params[:sdf_rows], + mapped_keys: params[:mapped_keys], + ) + sdf_import.create_samples + @result = {} + @result[:message] = sdf_import.message + end + rescue StandardError => e + Delayed::Worker.logger.error e + ensure + # Clean up the temporary file after processing + FileUtils.rm(file_path) if file_path && File.exist?(file_path) + end + end +end diff --git a/app/jobs/refresh_element_tag_job.rb b/app/jobs/refresh_element_tag_job.rb index 5a7ea14c8f..1c921f06e4 100644 --- a/app/jobs/refresh_element_tag_job.rb +++ b/app/jobs/refresh_element_tag_job.rb @@ -8,7 +8,7 @@ def perform Reaction.all.find_each(batch_size: 30) do |reaction| reaction.update_tag!(collection_tag: true) end - Element.all.find_each(batch_size: 30) do |el| + Labimotion::Element.all.find_each(batch_size: 30) do |el| el.update_tag!(collection_tag: true, analyses_tag: true) end end diff --git a/app/jobs/send_calendar_entry_notification_job.rb b/app/jobs/send_calendar_entry_notification_job.rb index f121d2dc12..1130ef30f2 100644 --- a/app/jobs/send_calendar_entry_notification_job.rb +++ b/app/jobs/send_calendar_entry_notification_job.rb @@ -3,11 +3,16 @@ class SendCalendarEntryNotificationJob < ApplicationJob queue_as :send_calendar_entry_notification + def max_attempts + 1 + end + def perform(calendar_entry_id, user_ids, type) entry = CalendarEntry.find_by(id: calendar_entry_id) return true if entry.nil? || user_ids&.none? entry.create_messages(user_ids, type) entry.send_emails(user_ids, type) + rescue Net::SMTPError end end diff --git a/app/jobs/transfer_repo_job.rb b/app/jobs/transfer_repo_job.rb index dbd2f728d5..bd7cdffef9 100644 --- a/app/jobs/transfer_repo_job.rb +++ b/app/jobs/transfer_repo_job.rb @@ -22,7 +22,7 @@ def perform(collection_id, _user_id, url, req_headers) @collection = Collection.find(collection_id) resp = transfer_data(collection_id, url, req_headers) if resp.status == 200 - MoveToCollectionJob.set(queue: "move_to_collection_#{collection_id}").perform_now(collection_id) + MoveToCollectionJob.perform_later(collection_id) end rescue StandardError => e Rails.logger.debug(e.backtrace) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index e64ec2e76d..81d695fc84 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -34,7 +34,7 @@ class Attachment < ApplicationRecord include AttachmentJcampAasm include AttachmentJcampProcess - include AttachmentConverter + include Labimotion::AttachmentConverter include AttachmentUploader::Attachment(:attachment) attr_accessor :file_data, :file_path, :thumb_path, :thumb_data, :duplicated, :transferred diff --git a/app/models/cellline_material.rb b/app/models/cellline_material.rb new file mode 100644 index 0000000000..01cefb783c --- /dev/null +++ b/app/models/cellline_material.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: cellline_materials +# +# id :bigint not null, primary key +# name :string +# source :string +# cell_type :string +# organism :jsonb +# tissue :jsonb +# disease :jsonb +# growth_medium :string +# biosafety_level :string +# variant :string +# mutation :string +# optimal_growth_temp :float +# cryo_pres_medium :string +# gender :string +# description :string +# deleted_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# +class CelllineMaterial < ApplicationRecord + acts_as_paranoid + + has_many :literals, as: :element, dependent: :destroy + has_many :literatures, through: :literals +end diff --git a/app/models/cellline_sample.rb b/app/models/cellline_sample.rb new file mode 100644 index 0000000000..30a98daf92 --- /dev/null +++ b/app/models/cellline_sample.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: cellline_samples +# +# id :bigint not null, primary key +# cellline_material_id :bigint +# cellline_sample_id :bigint +# amount :bigint +# unit :string +# passage :integer +# contamination :string +# name :string +# description :string +# user_id :bigint +# deleted_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# short_label :string +# +# rubocop:disable Rails/InverseOf, Rails/HasManyOrHasOneDependent +class CelllineSample < ApplicationRecord + acts_as_paranoid + + include ElementUIStateScopes + include Taggable + include Collectable + + has_one :container, as: :containable + has_many :collections_celllines, inverse_of: :cellline_sample, dependent: :destroy + has_many :collections, through: :collections_celllines + + belongs_to :cell_line_sample, optional: true + belongs_to :cellline_material + belongs_to :creator, class_name: 'User', foreign_key: 'user_id' + + scope :by_sample_name, lambda { |query, collection_id| + joins(:collections).where(collections: { id: collection_id }) + .where('name ILIKE ?', "%#{sanitize_sql_like(query)}%") + } + + scope :by_material_name, lambda { |query, collection_id| + joins(:cellline_material) + .joins(:collections) + .where('collections.id=?', collection_id) + .where('cellline_materials.name ILIKE ?', "%#{sanitize_sql_like(query)}%") + } +end +# rubocop:enable Rails/InverseOf, Rails/HasManyOrHasOneDependent diff --git a/app/models/channel.rb b/app/models/channel.rb index c2963dfc98..93387bd1ae 100644 --- a/app/models/channel.rb +++ b/app/models/channel.rb @@ -36,6 +36,9 @@ class Channel < ApplicationRecord DOWNLOAD_ANALYSES_ZIP = 'Download Analyses' DOWNLOAD_ANALYSES_ZIP_FAIL = 'Download Analyses Failure' CALENDAR_ENTRY = 'Calender Entry Notification' + IMPORT_SAMPLES_NOTIFICATION = 'Import Samples Completed' + COMMENT_ON_MY_COLLECTION = 'New comment on synchronized collection' + COMMENT_RESOLVED = 'Comment resolved in synchronized collection' class << self def build_message(**args) @@ -43,6 +46,7 @@ def build_message(**args) channel_subject = args[:channel_subject] # args.delete(:channel_subject) channel = channel_id ? find_by(id: channel_id) : find_by(subject: channel_subject) return unless channel + data_args = args.delete(:data_args) message = channel.msg_template if message.present? diff --git a/app/models/collection.rb b/app/models/collection.rb index 79110a7e8e..37b9559b56 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/AbcSize, Rails/HasManyOrHasOneDependent, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity + # == Schema Information # # Table name: collections @@ -21,6 +25,7 @@ # is_synchronized :boolean default(FALSE), not null # researchplan_detail_level :integer default(10) # element_detail_level :integer default(10) +# tabs_segment :jsonb # # Indexes # @@ -32,6 +37,7 @@ class Collection < ApplicationRecord acts_as_paranoid belongs_to :user, optional: true + belongs_to :inventory, optional: true has_ancestry has_many :collections_samples, dependent: :destroy @@ -39,16 +45,19 @@ 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_elements, dependent: :destroy - + has_many :collections_elements, dependent: :destroy, class_name: 'Labimotion::CollectionsElement' + has_many :collections_vessels, dependent: :destroy + has_many :collections_celllines, dependent: :destroy has_many :samples, through: :collections_samples has_many :reactions, through: :collections_reactions has_many :wellplates, through: :collections_wellplates has_many :screens, through: :collections_screens has_many :research_plans, through: :collections_research_plans + has_many :vessels, through: :collections_vessels has_many :elements, through: :collections_elements + has_many :cellline_samples, through: :collections_celllines - has_many :sync_collections_users, foreign_key: :collection_id, dependent: :destroy + has_many :sync_collections_users, dependent: :destroy, inverse_of: :collection has_many :shared_users, through: :sync_collections_users, source: :user has_one :metadata @@ -57,11 +66,12 @@ class Collection < ApplicationRecord scope :unlocked, -> { where(is_locked: false) } scope :locked, -> { where(is_locked: true) } - scope :ordered, -> { order("position ASC") } + scope :ordered, -> { order('position ASC') } scope :unshared, -> { where(is_shared: false) } + scope :synchronized, -> { where(is_synchronized: true) } scope :shared, ->(user_id) { where('shared_by_id = ? AND is_shared = ?', user_id, true) } scope :remote, ->(user_id) { where('is_shared = ? AND NOT shared_by_id = ?', true, user_id) } - scope :belongs_to_or_shared_by, ->(user_id, with_group = false) do + scope :belongs_to_or_shared_by, lambda { |user_id, with_group = false| if with_group.present? where( 'user_id = ? OR shared_by_id = ? OR (user_id IN (?) AND is_locked = false)', @@ -70,7 +80,7 @@ class Collection < ApplicationRecord else where('user_id = ? OR shared_by_id = ?', user_id, user_id) end - end + } default_scope { ordered } @@ -87,13 +97,13 @@ def self.bulk_update(user_id, collection_attributes, deleted_ids) end def self.filter_collection_attributes(user_id, collection_attributes) - c_ids = collection_attributes.map { |ca| !ca['isNew'] && ca['id'].to_i || nil }.compact - filtered_cids = Collection.where(id: c_ids).map do |c| + c_ids = collection_attributes.filter_map { |ca| (!ca['isNew'] && ca['id'].to_i) || nil } + filtered_cids = Collection.where(id: c_ids).filter_map do |c| if (c.user_id == user_id && !c.is_shared) || (c.is_shared && (c.shared_by_id == user_id || (c.user_id == user_id && c.permission_level == 10))) c.id end - end.compact + end collection_attributes.select { |ca| ca['isNew'] || filtered_cids.include?(ca['id'].to_i) } end @@ -141,6 +151,33 @@ def self.delete_set(user_id, deleted_ids) def self.reject_shared(user_id, collection_id) Collection.where(id: collection_id, user_id: user_id, is_shared: true) - .each(&:destroy) + .find_each(&:destroy) + end + + def self.collections_for_user(user_id) + Collection.where(user_id: user_id, shared_by_id: nil) + end + + def self.collections_group_by_inventory(collections, inventory) + { + collections: collections, + inventory: { + id: inventory&.id, + prefix: inventory&.prefix, + name: inventory&.name, + counter: inventory&.counter, + }, + } + end + + def self.inventory_collections(user_id) + collections = collections_for_user(user_id).reject { |c| c.label == 'All' } + grouped_collections = collections.group_by { |c| c.inventory&.id } + grouped_collections.values.map do |collections_group| + collections = collections_group.map { |c| { id: c.id, label: c.label } } + inventory = collections_group.first&.inventory + collections_group_by_inventory(collections, inventory) + end end end +# rubocop:enable Metrics/AbcSize, Rails/HasManyOrHasOneDependent,Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity diff --git a/app/models/collections_cellline.rb b/app/models/collections_cellline.rb new file mode 100644 index 0000000000..c15ffd84df --- /dev/null +++ b/app/models/collections_cellline.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: collections_celllines +# +# id :bigint not null, primary key +# collection_id :integer +# cellline_sample_id :integer +# deleted_at :datetime +# +class CollectionsCellline < ApplicationRecord + acts_as_paranoid + belongs_to :collection + belongs_to :cellline_sample + + include Tagging + include Collecting + + # Remove from collection and process associated elements (and update collection info tag) + def self.remove_in_collection(cellline_id, collection_id) + CollectionsCellline.find_by( + collection_id: collection_id, + cellline_sample_id: cellline_id, + deleted_at: nil, + )&.destroy + end + + def self.move_to_collection(cellline_ids, from_collection_id, to_colllection_id) + raise "could not find collection with #{to_colllection_id}" unless Collection.find_by(id: to_colllection_id) + + Array(cellline_ids).each do |cell_line_id| + next if to_colllection_id == from_collection_id + + CollectionsCellline.save_to_collection(cell_line_id, to_colllection_id) + CollectionsCellline.remove_in_collection(cell_line_id, from_collection_id) + end + + CollectionsCellline.update_collection_tag(cellline_ids) + end + + def self.create_in_collection(cellline_ids, to_col_id) + raise "could not find collection with #{to_col_id}" unless Collection.find_by(id: to_col_id) + + Array(cellline_ids).each do |cell_line_id| + CollectionsCellline.save_to_collection(cell_line_id, to_col_id) unless CollectionsCellline.find_by( + collection_id: to_col_id, + cellline_sample_id: cell_line_id, + deleted_at: nil, + ) + end + CollectionsCellline.update_collection_tag(cellline_ids) + end + + def self.save_to_collection(cell_line_id, to_col_id) + entry_already_there = CollectionsCellline.find_by( + cellline_sample_id: cell_line_id, + collection_id: to_col_id, + ) + + return if entry_already_there + + CollectionsCellline.new( + cellline_sample_id: cell_line_id, + collection_id: to_col_id, + ).save + end + + def self.update_collection_tag(element_ids) + CelllineSample.includes(:tag).where(id: element_ids).select(:id) + .each { |el| el.update_tag!(collection_tag: true) } + end +end diff --git a/app/models/collections_element.rb b/app/models/collections_element.rb deleted file mode 100644 index 7f19cd54d0..0000000000 --- a/app/models/collections_element.rb +++ /dev/null @@ -1,52 +0,0 @@ -# == Schema Information -# -# Table name: collections_elements -# -# id :integer not null, primary key -# collection_id :integer -# element_id :integer -# deleted_at :datetime -# element_type :string -# -# Indexes -# -# index_collections_elements_on_collection_id (collection_id) -# index_collections_elements_on_deleted_at (deleted_at) -# index_collections_elements_on_element_id (element_id) -# index_collections_elements_on_element_id_and_collection_id (element_id,collection_id) UNIQUE -# - -class CollectionsElement < ApplicationRecord - acts_as_paranoid - belongs_to :collection - belongs_to :element - - include Tagging - include Collecting - - def self.get_elements_by_collection_type(collection_ids, type) - self.where(collection_id: collection_ids, element_type: type).pluck(:element_id).compact.uniq - end - - def self.remove_in_collection(element_ids, from_col_ids) - sample_ids = Element.get_associated_samples(element_ids) - delete_in_collection(element_ids, from_col_ids) - update_tag_by_element_ids(element_ids) - CollectionsSample.remove_in_collection(sample_ids, from_col_ids) - end - - def self.move_to_collection(element_ids, from_col_ids, to_col_ids, element_type='') - sample_ids = Element.get_associated_samples(element_ids) - delete_in_collection(element_ids, from_col_ids) - static_create_in_collection(element_ids, to_col_ids) - CollectionsSample.move_to_collection(sample_ids, from_col_ids, to_col_ids) - CollectionsElement.where(collection_id: to_col_ids, element_id: element_ids)&.find_each { |ce| ce.update_columns(element_type: element_type) } - end - - def self.create_in_collection(element_ids, to_col_ids, element_type='') - sample_ids = Element.get_associated_samples(element_ids) - static_create_in_collection(element_ids, to_col_ids) - CollectionsSample.create_in_collection(sample_ids, to_col_ids) - CollectionsElement.where(collection_id: to_col_ids, element_id: element_ids)&.find_each { |ce| ce.update_columns(element_type: element_type) } - end - end diff --git a/app/models/collections_vessel.rb b/app/models/collections_vessel.rb new file mode 100644 index 0000000000..29f05779f7 --- /dev/null +++ b/app/models/collections_vessel.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: collections_vessels +# +# id :uuid not null, primary key +# collection_id :bigint +# vessel_id :uuid +# created_at :datetime not null +# updated_at :datetime not null +# deleted_at :datetime +# +# Indexes +# +# index_collections_vessels_on_collection_id (collection_id) +# index_collections_vessels_on_deleted_at (deleted_at) +# index_collections_vessels_on_vessel_id (vessel_id) +# index_collections_vessels_on_vessel_id_and_collection_id (vessel_id,collection_id) UNIQUE +# +class CollectionsVessel < ApplicationRecord + acts_as_paranoid + + belongs_to :collection + belongs_to :vessel +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000000..df7b34ad56 --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: comments +# +# id :bigint not null, primary key +# content :string +# created_by :integer not null +# section :string +# status :string default("Pending") +# submitter :string +# resolver_name :string +# commentable_id :integer +# commentable_type :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_comments_on_commentable_type_and_commentable_id (commentable_type,commentable_id) +# index_comments_on_section (section) +# index_comments_on_user (created_by) +# + +class Comment < ApplicationRecord + COMMENTABLE_TYPE = %w[Sample Reaction Screen Wellplate ResearchPlan].freeze + + enum sample_section: { + properties: 'sample_properties', + analyses: 'sample_analyses', + qc_curation: 'sample_qc_curation', + results: 'sample_results', + references: 'sample_references', + inventory: 'sample_inventory', + }, _prefix: true + + enum reaction_section: { + scheme: 'reaction_scheme', + properties: 'reaction_properties', + references: 'reaction_references', + analyses: 'reaction_analyses', + green_chemistry: 'reaction_green_chemistry', + }, _prefix: true + + enum wellplate_section: { + properties: 'wellplate_properties', + analyses: 'wellplate_analyses', + designer: 'wellplate_designer', + list: 'wellplate_list', + }, _prefix: true + + enum screen_section: { + properties: 'screen_properties', + analyses: 'screen_analyses', + }, _prefix: true + + enum research_plan_section: { + properties: 'research_plan_research_plan', + analyses: 'research_plan_analyses', + attachments: 'research_plan_attachments', + references: 'research_plan_references', + metadata: 'research_plan_metadata', + }, _prefix: true + + enum header_section: { + sample: 'sample_header', + reaction: 'reaction_header', + wellplate: 'wellplate_header', + screen: 'screen_header', + research_plan: 'research_plan_header', + }, _prefix: true + + belongs_to :commentable, polymorphic: true + belongs_to :creator, foreign_key: :created_by, class_name: 'User', inverse_of: :comments + + validates :section, :status, presence: true + + scope :pending, -> { where(status: 'Pending') } + scope :resolved, -> { where(status: 'Resolved') } + + def resolved? + status.eql? 'Resolved' + end +end diff --git a/app/models/concerns/attachment_converter.rb b/app/models/concerns/attachment_converter.rb deleted file mode 100644 index b6ada38dcf..0000000000 --- a/app/models/concerns/attachment_converter.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -# rubocop: disable Metrics/AbcSize - -module AttachmentConverter - ACCEPTED_FORMATS = (Rails.configuration.try(:converter).try(:ext) || []).freeze - extend ActiveSupport::Concern - - included do - before_create :init_converter - after_update :exec_converter - def init_converter - self.aasm_state = 'queueing' if Rails.configuration.try(:converter).try(:url) && ACCEPTED_FORMATS.include?(File.extname(filename&.downcase)) # rubocop:disable Layout/LineLength - end - - def exec_converter - return if attachable_id.nil? - - return if !Rails.configuration.try(:converter).try(:url) || ACCEPTED_FORMATS.exclude?(File.extname(filename&.downcase)) || aasm_state != 'queueing' # rubocop:disable Layout/LineLength - - state = Analyses::Converter.jcamp_converter(id) - self.aasm_state = state if %w[done failure].include?(state) - end - end -end - -# rubocop: enable Metrics/AbcSize diff --git a/app/models/concerns/attachment_jcamp_aasm.rb b/app/models/concerns/attachment_jcamp_aasm.rb index 69048d0218..a72069b1ca 100644 --- a/app/models/concerns/attachment_jcamp_aasm.rb +++ b/app/models/concerns/attachment_jcamp_aasm.rb @@ -122,6 +122,7 @@ def belong_to_analysis? # - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Process for attachment Jcamp handle +# rubocop:disable Metrics/ModuleLength module AttachmentJcampProcess extend ActiveSupport::Concern @@ -134,6 +135,7 @@ def generate_att(meta_tmp, addon, to_edit = false, ext = nil) if att.nil? att = Attachment.children_of(self[:id]).new( filename: meta_filename, + con_state: Labimotion::ConState::READ, file_path: meta_tmp.path, created_by: created_by, created_for: created_for, @@ -253,9 +255,9 @@ def create_process(is_regen) elsif arr_jcamp.length > 1 read_processed_data(arr_jcamp, arr_img, spc_type, is_regen) else + img_att = generate_img_att(tmp_img, 'peak') jcamp_att = generate_jcamp_att(tmp_jcamp, 'peak') jcamp_att.auto_infer_n_clear_json(spc_type, is_regen) - img_att = generate_img_att(tmp_img, 'peak') tmp_files_to_be_deleted = [tmp_jcamp, tmp_img] tmp_files_to_be_deleted.push(*arr_img) @@ -331,6 +333,8 @@ def generate_spectrum_data(params, is_regen) end def generate_spectrum(is_create = false, is_regen = false, params = {}) + return if is_create && !is_regen && jcamp_files_already_present? + is_create ? create_process(is_regen) : edit_process(is_regen, params) rescue StandardError => e set_failure @@ -338,6 +342,28 @@ def generate_spectrum(is_create = false, is_regen = false, params = {}) Rails.logger.error(e) end + def jcamp_files_already_present? + first_part, extname = extension_parts + return false if (extname.casecmp('nmrium').zero? || first_part['processed_']) # ignore when file is nmrium or preprocessed from Bruker NMR + + attachments = Attachment.where(attachable_id: self[:attachable_id]) + num = filename.match(/\.(\d+)_/)&.[](1)&.to_i + jcamp_attachments = file_match(attachments, num) + jcamp_attachments.any? + end + + def file_match(attachments, num) + attachments.select do |att| + if num + att.filename == filename || att.filename == "#{filename[0..-2]}#{num}_bagit.peak.jdx" || + att.filename == "#{filename[0..-2]}#{num}_bagit.edit.jdx" + else + att.extension_parts[-1] == 'jdx' || att.extension_parts[0] == 'peak' || + att.extension_parts[0] == 'edit' + end + end + end + def read_processed_data(arr_jcamp, arr_img, spc_type, is_regen) jcamp_att = nil tmp_to_be_deleted = [] @@ -380,7 +406,6 @@ def read_bagit_data(arr_jcamp, arr_img, arr_csv, spc_type, is_regen, params) generate_csv_att(curr_tmp_csv, "#{idx + 1}_bagit", false, params) tmp_to_be_deleted.push(curr_tmp_csv) end - jcamp_att = curr_jcamp_att if idx == 0 end @@ -649,3 +674,4 @@ def auto_infer_n_clear_json(spc_type, is_regen) end end end +# rubocop:enable Metrics/ModuleLength diff --git a/app/models/concerns/collectable.rb b/app/models/concerns/collectable.rb index 67a1fddfbd..1789c502ac 100644 --- a/app/models/concerns/collectable.rb +++ b/app/models/concerns/collectable.rb @@ -6,14 +6,21 @@ module Collectable scope :for_user_n_groups, ->(user_ids) { joins(:collections).where('collections.user_id IN (?)', user_ids).references(:collections) } scope :by_collection_id, ->(id) { joins(:collections).where('collections.id = ?', id) } scope :search_by, ->(search_by_method, arg) { public_send("search_by_#{search_by_method}", arg) } - scope :created_time_to, ->(time) { where('created_at <= ?', time) } - scope :created_time_from, ->(time) { where('created_at >= ?', time) } - scope :updated_time_to, ->(time) { where('updated_at <= ?', time) } - scope :updated_time_from, ->(time) { where('updated_at >= ?', time) } - scope :samples_created_time_from, ->(time) { where('samples.created_at >= ?', time) } - scope :samples_created_time_to, ->(time) { where('samples.created_at <= ?', time) } - scope :samples_updated_time_from, ->(time) { where('samples.updated_at >= ?', time) } - scope :samples_updated_time_to, ->(time) { where('samples.updated_at <= ?', time) } + + # TODO: Filters are not working properly + # the following scopes are not working as I would expect + # in the ui the selection is a date but in the api we are getting a timestamp, + # which we are parsing whiteout timezone information, so we lose some hours + # dayjs('2019-01-25').unix() // 1548381600 is Time.at(1548381600) 2019-01-25 03:00:00 +0100 + # I would suggest the following: + # send date from frontend and use the psql date method: where("date(#{table_name}.created_at) <= ?", date) or + # remove + 1.day part from created_time_to and updated_time_to scopes and use .end_of_day and + # use beginning_of_day for the from scopes: where("#{table_name}.created_at >= ?", time.beginning_of_day) + scope :created_time_to, ->(time) { where("#{table_name}.created_at <= ?", time) } + scope :created_time_from, ->(time) { where("#{table_name}.created_at >= ?", time) } + scope :updated_time_to, ->(time) { where("#{table_name}.updated_at <= ?", time) } + scope :updated_time_from, ->(time) { where("#{table_name}.updated_at >= ?", time) } + scope :join_collections_element, ->{ tb = name.underscore joins("inner join collections_#{tb}s on #{tb}s.id = collections_#{tb}s.sample_id") diff --git a/app/models/concerns/collecting.rb b/app/models/concerns/collecting.rb index 71dd9050a3..1c9a54a6df 100644 --- a/app/models/concerns/collecting.rb +++ b/app/models/concerns/collecting.rb @@ -47,8 +47,8 @@ def static_create_in_collection(element_ids, collection_ids) end def update_tag_by_element_ids(element_ids) - name[11..-1].constantize.includes(:tag).where(id: element_ids).select(:id) - .each { |el| el.update_tag!(collection_tag: true) } + klass = Labimotion::Utils.klass_by_collection(name).constantize + klass.includes(:tag).where(id: element_ids).select(:id).each { |el| el.update_tag!(collection_tag: true) } end handle_asynchronously :update_tag_by_element_ids @@ -67,7 +67,7 @@ def delete_in_collection_by_ui_state(**args) end def update_tag_by_ui_state(**args) - element_klass = name[11..-1].constantize + element_klass = Labimotion::Utils.klass_by_collection(name).constantize statement = "WHERE collection_id in (?) AND #{table_name}.#{table_name[12..-2]}_id" if args[:checkedAll] statement += ' NOT IN (?)' diff --git a/app/models/concerns/datasetable.rb b/app/models/concerns/datasetable.rb deleted file mode 100644 index af9c98d807..0000000000 --- a/app/models/concerns/datasetable.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -# Datasetable concern -module Datasetable - extend ActiveSupport::Concern - - included do - has_one :dataset, as: :element - end - - def not_dataset? - self.class.name == 'Container' && container_type != 'dataset' - end - - def save_dataset(**args) - return if not_dataset? - - klass = DatasetKlass.find_by(id: args[:dataset_klass_id]) - uuid = SecureRandom.uuid - props = args[:properties] - props['eln'] = Chemotion::Application.config.version if props['eln'] != Chemotion::Application.config.version - ds = Dataset.find_by(element_type: self.class.name, element_id: id) - if ds.present? && (ds.klass_uuid != props['klass_uuid'] || ds.properties != props) - props['uuid'] = uuid - props['eln'] = Chemotion::Application.config.version - props['klass'] = 'Dataset' - ds.update!(uuid: uuid, dataset_klass_id: args[:dataset_klass_id], properties: props, klass_uuid: props['klass_uuid']) - end - return if ds.present? - - props['uuid'] = uuid - props['klass_uuid'] = klass.uuid - props['eln'] = Chemotion::Application.config.version - props['klass'] = 'Dataset' - Dataset.create!(uuid: uuid, dataset_klass_id: args[:dataset_klass_id], element_type: self.class.name, element_id: id, properties: props, klass_uuid: klass.uuid) - end - - def destroy_datasetable - return if not_dataset? - - Dataset.where(element_type: self.class.name, element_id: id).destroy_all - end -end diff --git a/app/models/concerns/element_codes.rb b/app/models/concerns/element_codes.rb index 2e7aab6fc8..3ceda5f43a 100644 --- a/app/models/concerns/element_codes.rb +++ b/app/models/concerns/element_codes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ElementCodes extend ActiveSupport::Concern @@ -8,8 +10,9 @@ module ElementCodes def source_class() @source_class||=self.class.name.demodulize.underscore end def code_logs - CodeLog.where(source: source_class). - where(source_id: id).order(created_at: 'DESC') + return [] if source_class == 'container' && containable_type == 'Labimotion::Element' + + CodeLog.where(source: source_class).where(source_id: id).order(created_at: 'DESC') end def code_log() code_logs.first end diff --git a/app/models/concerns/element_ui_state_scopes.rb b/app/models/concerns/element_ui_state_scopes.rb index 5b5f0ff831..b4683b0e26 100644 --- a/app/models/concerns/element_ui_state_scopes.rb +++ b/app/models/concerns/element_ui_state_scopes.rb @@ -1,32 +1,42 @@ +# frozen_string_literal: true + +# rubocop:disable Performance/StringInclude + module ElementUIStateScopes extend ActiveSupport::Concern included do - scope :by_ui_state, ->(ui_state) { + scope :by_ui_state, lambda { |ui_state| # see ui_state_params in api/helpers/params_helpers.rb # map legacy params return none if ui_state.nil? checked_all = ui_state[:checkedAll] || ui_state[:all] + checked_all = false if checked_all == 'false' checked_ids = ui_state[:checkedIds].presence || ui_state[:included_ids] return none unless checked_all || checked_ids.present? unchecked_ids = ui_state[:uncheckedIds].presence || ui_state[:excluded_ids] + checked_all ? where.not(id: unchecked_ids) : where(id: checked_ids) } end module ClassMethods def for_ui_state(ui_state) - return self.none unless ui_state + return none unless ui_state all = coerce_all_to_boolean(ui_state.fetch(:all, false)) collection_id = ui_state.fetch(:collection_id, 'all') - if (all) + if all excluded_ids = ui_state.fetch(:excluded_ids, []) - collection_id == 'all' ? where.not(id: excluded_ids).distinct : by_collection_id(collection_id.to_i).where.not(id: excluded_ids).distinct + if collection_id == 'all' + where.not(id: excluded_ids).distinct + else + by_collection_id(collection_id.to_i).where.not(id: excluded_ids).distinct + end else included_ids = ui_state.fetch(:included_ids, []) where(id: included_ids).distinct @@ -35,24 +45,25 @@ def for_ui_state(ui_state) def for_ui_state_with_collection(ui_state, collection_class, collection_id) all = coerce_all_to_boolean(ui_state.fetch(:all, false)) - attributes = collection_class.column_names - ["collection_id"] + attributes = collection_class.column_names - ['collection_id'] element_label = attributes.find { |e| /_id/ =~ e } collection_elements = collection_class.where(collection_id: collection_id) - if (all) + if all excluded_ids = ui_state.fetch(:excluded_ids, []) - result = collection_elements.where.not({element_label => excluded_ids}) + result = collection_elements.where.not({ element_label => excluded_ids }) else included_ids = ui_state.fetch(:included_ids, []) - result = collection_elements.where({element_label => included_ids}) + result = collection_elements.where({ element_label => included_ids }) end result.pluck(element_label).uniq end - # TODO cleanup coercion in API + # TODO: cleanup coercion in API def coerce_all_to_boolean(all) return all unless all.is_a? String - all == "false" ? false : true + all != 'false' end end end +# rubocop:enable Performance/StringInclude diff --git a/app/models/concerns/generic_klass_revisions.rb b/app/models/concerns/generic_klass_revisions.rb deleted file mode 100644 index 48d90f57a6..0000000000 --- a/app/models/concerns/generic_klass_revisions.rb +++ /dev/null @@ -1,20 +0,0 @@ - -module GenericKlassRevisions - extend ActiveSupport::Concern - included do - # has_many :element_klasses_revisions, dependent: :destroy - end - - def create_klasses_revision(user_id=0) - self.update!({ uuid: properties_template['uuid'], properties_release: properties_template, released_at: DateTime.now }) - reload - attributes = { - released_by: user_id, - uuid: uuid, - properties_release: properties_template, - released_at: released_at - } - attributes["#{self.class.name.underscore}_id"] = id - "#{self.class.name}esRevision".constantize.create(attributes) - end -end \ No newline at end of file diff --git a/app/models/concerns/generic_revisions.rb b/app/models/concerns/generic_revisions.rb deleted file mode 100644 index b0ac5862b6..0000000000 --- a/app/models/concerns/generic_revisions.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# GenericRevisions concern -module GenericRevisions - extend ActiveSupport::Concern - included do - after_create :create_vault - after_update :save_to_vault - before_destroy :delete_attachments - end - - def create_vault - save_to_vault unless self.class.name == 'Element' - end - - def save_to_vault - attributes = { - uuid: uuid, - klass_uuid: klass_uuid, - properties: properties - } - attributes["#{self.class.name.downcase}_id"] = id - attributes['name'] = name if self.class.name == 'Element' - "#{self.class.name}sRevision".constantize.create(attributes) - end - - def delete_attachments - att_ids = [] - properties['layers'].keys.each do |key| - layer = properties['layers'][key] - field_uploads = layer['fields'].select { |ss| ss['type'] == 'upload' } - field_uploads.each do |field| - (field['value'] && field['value']['files'] || []).each do |file| - att_ids.push(file['aid']) unless file['aid'].nil? - end - end - end - Attachment.where(id: att_ids, attachable_id: id, attachable_type: %w[ElementProps SegmentProps]).destroy_all - end -end diff --git a/app/models/concerns/segmentable.rb b/app/models/concerns/segmentable.rb deleted file mode 100644 index a77c95ec9f..0000000000 --- a/app/models/concerns/segmentable.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -# Segmentable concern -module Segmentable - extend ActiveSupport::Concern - included do - has_many :segments, as: :element, dependent: :destroy - end - - def save_segments(**args) - return if args[:segments].nil? - - args[:segments].each do |seg| - klass = SegmentKlass.find_by(id: seg['segment_klass_id']) - uuid = SecureRandom.uuid - props = seg['properties'] - props['eln'] = Chemotion::Application.config.version if props['eln'] != Chemotion::Application.config.version - segment = Segment.find_by(element_type: self.class.name, element_id: self.id, segment_klass_id: seg['segment_klass_id']) - if segment.present? && (segment.klass_uuid != props['klass_uuid'] || segment.properties != props) - props['uuid'] = uuid - props['eln'] = Chemotion::Application.config.version - props['klass'] = 'Segment' - - segment.update!(properties: props, uuid: uuid, klass_uuid: props['klass_uuid']) - end - next if segment.present? - - props['uuid'] = uuid - props['klass_uuid'] = klass.uuid - props['eln'] = Chemotion::Application.config.version - props['klass'] = 'Segment' - Segment.create!(segment_klass_id: seg['segment_klass_id'], element_type: self.class.name, element_id: self.id, properties: props, created_by: args[:current_user_id], uuid: uuid, klass_uuid: klass.uuid) - end - end -end diff --git a/app/models/concerns/taggable.rb b/app/models/concerns/taggable.rb index 94fc5abf54..1a5e281d20 100644 --- a/app/models/concerns/taggable.rb +++ b/app/models/concerns/taggable.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop: disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize, Naming/MethodParameterName, Lint/AssignmentInCondition + # Module for tag behaviour module Taggable extend ActiveSupport::Concern @@ -10,7 +12,7 @@ module Taggable end def update_tag_callback - args = is_a?(Molecule) && { pubchem_tag: true } || { + args = (is_a?(Molecule) && { pubchem_tag: true }) || { analyses_tag: true, collection_tag: new_record? } update_tag(**args) @@ -19,6 +21,7 @@ def update_tag_callback def update_tag(**args) build_tag(taggable_data: {}) if new_record? || !tag return if tag.destroyed? + data = tag.taggable_data || {} data['reaction_id'] = args[:reaction_tag] if args[:reaction_tag] data['wellplate_id'] = args[:wellplate_tag] if args[:wellplate_tag] @@ -35,7 +38,7 @@ def update_tag!(**args) end def remove_blank_value(hash) - hash.delete_if do |_, value| value.blank? end + hash.compact_blank! end def inchikey? @@ -56,25 +59,28 @@ def collection_id(c) # Populate Collections tag def collection_tag - klass = "collections_#{self.class.name.underscore.pluralize}" + klass = Labimotion::Utils.col_by_element(self.class.name).underscore.pluralize + klass = 'collections_celllines' if klass == 'collections_cellline_samples' return unless respond_to?(klass) + cols = [] send(klass).each do |cc| next unless c = cc.collection next if c.label == 'All' && c.is_locked + cols.push({ - name: c.label, is_shared: c.is_shared, user_id: c.user_id, - id: c.id, shared_by_id: c.shared_by_id, - is_synchronized: false - }) - if c.is_synchronized - c.sync_collections_users&.each do |syn| - cols.push({ - name: c.label, is_shared: c.is_shared, user_id: syn.user_id, - id: syn.id, shared_by_id: syn.shared_by_id, - is_synchronized: c.is_synchronized - }) - end + name: c.label, is_shared: c.is_shared, user_id: c.user_id, + id: c.id, shared_by_id: c.shared_by_id, + is_synchronized: false + }) + next unless c.is_synchronized + + c.sync_collections_users&.each do |syn| + cols.push({ + name: c.label, is_shared: c.is_shared, user_id: syn.user_id, + id: syn.id, shared_by_id: syn.shared_by_id, + is_synchronized: c.is_synchronized + }) end end cols @@ -86,21 +92,24 @@ def grouped_analyses end def count_by_kind(analyses) - analyses.group_by { |x| x['kind'] }.map { |k, v| [k, v.length] }.to_h + analyses.group_by { |x| x['kind'] }.transform_values(&:length) end def analyses_tag return nil unless is_a?(Sample) && analyses.count.positive? - grouped_analyses.map { |key, val| + + grouped_analyses.to_h do |key, val| vv = count_by_kind(val) kk = key.to_s.downcase [kk, vv] - }.to_h + end end def pubchem_tag return nil unless is_a?(Molecule) return tag.taggable_data['pubchem_cid'] if pubchem_check - self.pcid.presence || PubChem.get_cid_from_inchikey(inchikey) + + pcid.presence || PubChem.get_cid_from_inchikey(inchikey) end end +# rubocop: enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize, Naming/MethodParameterName, Lint/AssignmentInCondition diff --git a/app/models/concerns/tagging.rb b/app/models/concerns/tagging.rb index 22155abd5d..7ff7274d3d 100644 --- a/app/models/concerns/tagging.rb +++ b/app/models/concerns/tagging.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + # update ElementTag when Element joint table association is updated +# rubocop: disable Metrics/CyclomaticComplexity module Tagging extend ActiveSupport::Concern @@ -18,18 +21,27 @@ def update_tag when 'Well' args = { wellplate_tag: wellplate_id } element = 'sample' - when 'ElementsSample' - el = Element.find_by(id: element_id) + when 'Labimotion::ElementsSample' + el = Labimotion::Element.find_by(id: element_id) return if el.nil? - args = deleted_at.nil? ? { element_tag: { "type": el.element_klass.name, "id": element_id } } : { element_tag: {} } + args = if deleted_at.nil? + { element_tag: { type: el.element_klass.name, + id: element_id } } + else + { element_tag: {} } + end element = 'sample' - when 'CollectionsReaction', 'CollectionsWellplate', 'CollectionsSample', 'CollectionsElement', + when 'CollectionsReaction', 'CollectionsWellplate', 'CollectionsSample', 'Labimotion::CollectionsElement', 'CollectionsScreen', 'CollectionsResearchPlan' args = { collection_tag: true } - element = klass[11..-1].underscore + element = Labimotion::Utils.elname_by_collection(klass) + when 'CollectionsCellline' + args = { collection_tag: true } + element = 'cellline_sample' end + element && send(element)&.update_tag!(args) end - # handle_asynchronously :update_tag end +# rubocop: enable Metrics/CyclomaticComplexity diff --git a/app/models/container.rb b/app/models/container.rb index 906a4fd281..a7db6497e6 100644 --- a/app/models/container.rb +++ b/app/models/container.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: containers @@ -21,29 +23,31 @@ class Container < ApplicationRecord include ElementCodes - include Datasetable + include Labimotion::Datasetable belongs_to :containable, polymorphic: true, optional: true has_many :attachments, as: :attachable + before_save :content_to_plain_text # TODO: dependent destroy for attachments should be implemented when attachment get paranoidized instead of this DJ before_destroy :delete_attachment before_destroy :destroy_datasetable has_closure_tree - scope :analyses_for_root, ->(root_id) { + scope :analyses_for_root, lambda { |root_id| where(container_type: 'analysis').joins( - "inner join container_hierarchies ch on ch.generations = 2 and ch.ancestor_id = #{root_id} and ch.descendant_id = containers.id " + "inner join container_hierarchies ch on ch.generations = 2 + and ch.ancestor_id = #{root_id} and ch.descendant_id = containers.id ", ) } def analyses - Container.analyses_for_root(self.id) + Container.analyses_for_root(id) end def root_element - self.root.containable + root.containable end def self.create_root_container(**args) @@ -56,11 +60,22 @@ def self.create_root_container(**args) def delete_attachment if Rails.env.production? - attachments.each { |attachment| + attachments.each do |attachment| attachment.delay(run_at: 96.hours.from_now, queue: 'attachment_deletion').destroy! - } + end else attachments.each(&:destroy!) end end + + # rubocop:disable Style/StringLiterals + + def content_to_plain_text + return unless extended_metadata_changed? + return if extended_metadata.blank? || (extended_metadata.present? && extended_metadata['content'].blank?) + return if extended_metadata['content'] == "{\"ops\":[{\"insert\":\"\"}]}" + + self.plain_text_content = Chemotion::QuillToPlainText.new.convert(extended_metadata['content']) + end + # rubocop:enable Style/StringLiterals end diff --git a/app/models/dataset.rb b/app/models/dataset.rb deleted file mode 100644 index db52040c0e..0000000000 --- a/app/models/dataset.rb +++ /dev/null @@ -1,22 +0,0 @@ -# == Schema Information -# -# Table name: datasets -# -# id :integer not null, primary key -# dataset_klass_id :integer -# element_type :string -# element_id :integer -# properties :jsonb -# created_at :datetime not null -# updated_at :datetime -# uuid :string -# klass_uuid :string -# deleted_at :datetime -# - -class Dataset < ApplicationRecord - acts_as_paranoid - include GenericRevisions - belongs_to :dataset_klass - belongs_to :element, polymorphic: true -end diff --git a/app/models/dataset_klass.rb b/app/models/dataset_klass.rb deleted file mode 100644 index 2ecebd6b36..0000000000 --- a/app/models/dataset_klass.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# == Schema Information -# -# Table name: dataset_klasses -# -# id :integer not null, primary key -# ols_term_id :string not null -# label :string not null -# desc :string -# properties_template :jsonb not null -# is_active :boolean default(FALSE), not null -# place :integer default(100), not null -# created_by :integer not null -# created_at :datetime not null -# updated_at :datetime -# deleted_at :datetime -# uuid :string -# properties_release :jsonb -# released_at :datetime -# -class DatasetKlass < ApplicationRecord - acts_as_paranoid - include GenericKlassRevisions - has_many :datasets, dependent: :destroy - has_many :dataset_klasses_revisions, dependent: :destroy - - def self.init_seeds - seeds_path = File.join(Rails.root, 'db', 'seeds', 'json', 'dataset_klasses.json') - seeds = JSON.parse(File.read(seeds_path)) - - seeds['chmo'].each do |term| - next if DatasetKlass.where(ols_term_id: term['id']).count.positive? - - attributes = { ols_term_id: term['id'], label: "#{term['label']} (#{term['synonym']})", desc: "#{term['label']} (#{term['synonym']})", place: term['position'], created_by: Admin.first&.id || 0 } - DatasetKlass.create!(attributes) - end - true - end -end diff --git a/app/models/dataset_klasses_revision.rb b/app/models/dataset_klasses_revision.rb deleted file mode 100644 index 4e083b2d7c..0000000000 --- a/app/models/dataset_klasses_revision.rb +++ /dev/null @@ -1,25 +0,0 @@ -# == Schema Information -# -# Table name: dataset_klasses_revisions -# -# id :integer not null, primary key -# dataset_klass_id :integer -# uuid :string -# properties_release :jsonb -# released_at :datetime -# released_by :integer -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# -# Indexes -# -# index_dataset_klasses_revisions_on_dataset_klass_id (dataset_klass_id) -# - -class DatasetKlassesRevision < ApplicationRecord - acts_as_paranoid - has_one :dataset_klass - -end diff --git a/app/models/datasets_revision.rb b/app/models/datasets_revision.rb deleted file mode 100644 index 5c9c68fe57..0000000000 --- a/app/models/datasets_revision.rb +++ /dev/null @@ -1,24 +0,0 @@ -# == Schema Information -# -# Table name: datasets_revisions -# -# id :integer not null, primary key -# dataset_id :integer -# uuid :string -# klass_uuid :string -# properties :jsonb -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# -# Indexes -# -# index_datasets_revisions_on_dataset_id (dataset_id) -# - -class DatasetsRevision < ApplicationRecord - acts_as_paranoid - has_one :dataset - -end diff --git a/app/models/element.rb b/app/models/element.rb deleted file mode 100644 index e6f061c07c..0000000000 --- a/app/models/element.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -# == Schema Information -# -# Table name: elements -# -# id :integer not null, primary key -# name :string -# element_klass_id :integer -# properties :jsonb -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# short_label :string -# uuid :string -# klass_uuid :string -# - -# Generic Element -class Element < ApplicationRecord - acts_as_paranoid - include PgSearch::Model - include ElementUIStateScopes - include Collectable - include AnalysisCodes - include Taggable - include Segmentable - include GenericRevisions - - multisearchable against: %i[name short_label] - - pg_search_scope :search_by_substring, against: %i[name short_label], using: { trigram: { threshold: 0.0001 } } - - attr_accessor :can_copy - - scope :by_name, ->(query) { where('name ILIKE ?', "%#{sanitize_sql_like(query)}%") } - scope :by_short_label, ->(query) { where('short_label ILIKE ?', "%#{sanitize_sql_like(query)}%") } - scope :by_klass_id_short_label, ->(klass_id, short_label) { where('element_klass_id = ? and short_label ILIKE ?', klass_id, "%#{sanitize_sql_like(short_label)}%") } - scope :by_sample_ids, ->(ids) { joins(:elements_samples).where('sample_id IN (?)', ids) } - scope :by_klass_id, ->(klass_id) { where('element_klass_id = ? ', klass_id) } - scope :elements_created_time_from, ->(time) { where('elements.created_at >= ?', time) } - scope :elements_created_time_to, ->(time) { where('elements.created_at <= ?', time) } - scope :elements_updated_time_from, ->(time) { where('elements.updated_at >= ?', time) } - scope :elements_updated_time_to, ->(time) { where('elements.updated_at <= ?', time) } - - belongs_to :element_klass - - has_many :collections_elements, inverse_of: :element, dependent: :destroy - has_many :collections, through: :collections_elements - has_many :attachments, as: :attachable - has_many :elements_samples, dependent: :destroy - has_many :samples, through: :elements_samples, source: :sample - has_one :container, :as => :containable - has_many :elements_revisions, dependent: :destroy - - accepts_nested_attributes_for :collections_elements - - belongs_to :creator, foreign_key: :created_by, class_name: 'User' - validates :creator, presence: true - - before_create :auto_set_short_label - after_create :update_counter - before_destroy :delete_attachment - - def attachments - Attachment.where(attachable_id: self.id, attachable_type: 'Element') - end - - - def self.get_associated_samples(element_ids) - ElementsSample.where(element_id: element_ids).pluck(:sample_id) - end - - def analyses - container ? container.analyses : [] - end - - def auto_set_short_label - prefix = element_klass.klass_prefix - if creator.counters[element_klass.name].nil? - creator.counters[element_klass.name] = '0' - creator.update_columns(counters: creator.counters) - creator.reload - end - counter = creator.counters[element_klass.name].to_i.succ - self.short_label = "#{creator.initials}-#{prefix}#{counter}" - end - - def update_counter - creator.increment_counter element_klass.name - end - - private - - def delete_attachment - if Rails.env.production? - attachments.each do |attachment| - attachment.delay(run_at: 96.hours.from_now, queue: 'attachment_deletion').destroy! - end - else - attachments.each(&:destroy!) - end - end -end diff --git a/app/models/element_klass.rb b/app/models/element_klass.rb deleted file mode 100644 index 74503f7ce4..0000000000 --- a/app/models/element_klass.rb +++ /dev/null @@ -1,42 +0,0 @@ -# == Schema Information -# -# Table name: element_klasses -# -# id :integer not null, primary key -# name :string -# label :string -# desc :string -# icon_name :string -# properties_template :jsonb -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# is_active :boolean default(TRUE), not null -# klass_prefix :string default("E"), not null -# is_generic :boolean default(TRUE), not null -# place :integer default(100), not null -# uuid :string -# properties_release :jsonb -# released_at :datetime -# - -class ElementKlass < ApplicationRecord - acts_as_paranoid - include GenericKlassRevisions - has_many :elements, dependent: :destroy - has_many :segment_klasses, dependent: :destroy - has_many :element_klasses_revisions, dependent: :destroy - - def self.gen_klasses_json - klasses = where(is_active: true, is_generic: true).order('place')&.pluck(:name) || [] - rescue ActiveRecord::StatementInvalid, PG::ConnectionBad, PG::UndefinedTable - klasses = [] - ensure - File.write( - Rails.root.join('config', 'klasses.json'), - klasses&.to_json || [] - ) - end - -end diff --git a/app/models/element_klasses_revision.rb b/app/models/element_klasses_revision.rb deleted file mode 100644 index 3f9b15ce5b..0000000000 --- a/app/models/element_klasses_revision.rb +++ /dev/null @@ -1,24 +0,0 @@ -# == Schema Information -# -# Table name: element_klasses_revisions -# -# id :integer not null, primary key -# element_klass_id :integer -# uuid :string -# properties_release :jsonb -# released_at :datetime -# released_by :integer -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# -# Indexes -# -# index_element_klasses_revisions_on_element_klass_id (element_klass_id) -# - -class ElementKlassesRevision < ApplicationRecord - acts_as_paranoid - has_one :element_klass -end diff --git a/app/models/elements_revision.rb b/app/models/elements_revision.rb deleted file mode 100644 index 4184887349..0000000000 --- a/app/models/elements_revision.rb +++ /dev/null @@ -1,25 +0,0 @@ -# == Schema Information -# -# Table name: elements_revisions -# -# id :integer not null, primary key -# element_id :integer -# uuid :string -# klass_uuid :string -# name :string -# properties :jsonb -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# -# Indexes -# -# index_elements_revisions_on_element_id (element_id) -# - -class ElementsRevision < ApplicationRecord - acts_as_paranoid - has_one :element - -end diff --git a/app/models/elements_sample.rb b/app/models/elements_sample.rb deleted file mode 100644 index 2e11e7151b..0000000000 --- a/app/models/elements_sample.rb +++ /dev/null @@ -1,26 +0,0 @@ -# == Schema Information -# -# Table name: elements_samples -# -# id :integer not null, primary key -# element_id :integer -# sample_id :integer -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# -# Indexes -# -# index_elements_samples_on_element_id (element_id) -# index_elements_samples_on_sample_id (sample_id) -# - - -class ElementsSample < ApplicationRecord - acts_as_paranoid - belongs_to :element - belongs_to :sample - - include Tagging -end diff --git a/app/models/inventory.rb b/app/models/inventory.rb new file mode 100644 index 0000000000..4f1b8cf360 --- /dev/null +++ b/app/models/inventory.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class Inventory < ApplicationRecord + has_many :collections, dependent: :nullify + + def self.compare_associations(collection_ids) + inventory_collection_ids = [] + collection_ids.map do |collection_id| + inventory = Collection.find(collection_id).inventory + next unless inventory + + ids = inventory.collections.pluck(:id) + inventory_collection_ids |= ids + end + inventory_collection_ids.sort == collection_ids.sort + end + + def self.update_inventory_label(prefix, name, counter, collection_id) + collection = Collection.find_by(id: collection_id) + return nil unless collection + + inventory = collection&.inventory || Inventory.new + inventory.collections << collection unless inventory.collections.include?(collection) + inventory['prefix'] = prefix + inventory['name'] = name + inventory['counter'] = counter + inventory.save! + inventory + end + + def self.create_or_update_inventory_label(prefix, name, counter, collection_ids, user_id) + associations = compare_associations(collection_ids) + ActiveRecord::Base.transaction do + if associations + inventory = update_inventory_label( + prefix, + name, + counter, + collection_ids.first, + ) + else + inventory = Inventory.new(prefix: prefix, name: name, counter: counter) + inventory.save! + end + collection_ids.map do |id| + collection = Collection.find(id) + collection.update(inventory_id: inventory.id) + end + { inventory_collections: Collection.inventory_collections(user_id) } + end + end + + def increment_inventory_label_counter(collection_ids) + inventory = Collection.find_by(id: collection_ids)&.inventory + return if inventory.nil? || inventory['counter'].nil? + + inventory['counter'] = inventory['counter'].succ + inventory.save! + inventory + end + + def self.fetch_inventories(user_id) + joins(collections: :user).where(users: { id: user_id }) + end +end diff --git a/app/models/matrice.rb b/app/models/matrice.rb index 95bf2abe80..dbc5398574 100644 --- a/app/models/matrice.rb +++ b/app/models/matrice.rb @@ -32,6 +32,11 @@ def self.gen_matrices_json ) end + def self.extra_rules + configs = find_by(name: 'userProvider')&.configs || {} + configs.dig('extra_rules', 'enable') == true ? configs['extra_rules'] : {} + end + private def gen_json diff --git a/app/models/message.rb b/app/models/message.rb index b729d10f90..91900f8ed7 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -25,6 +25,7 @@ def self.create_msg_notification(**args) content = args[:message_content].presence || Channel.build_message(args) channel_id ||= content&.fetch('channel_id', false) return unless channel_id + message = Message.create( content: content.as_json, channel_id: channel_id, diff --git a/app/models/molecule.rb b/app/models/molecule.rb index a972d0df3e..ef12a1d0ad 100644 --- a/app/models/molecule.rb +++ b/app/models/molecule.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: molecules @@ -29,6 +31,8 @@ # index_molecules_on_inchikey_and_is_partial (inchikey,is_partial) UNIQUE # +# rubocop:disable Metrics/ClassLength + class Molecule < ApplicationRecord acts_as_paranoid @@ -70,12 +74,12 @@ class Molecule < ApplicationRecord where('cano_smiles ILIKE ?', "%#{sanitize_sql_like(query)}%") } - scope :with_reactions, -> { - joins(:samples).joins("inner join reactions_samples rs on rs.sample_id = samples.id" ).uniq + scope :with_reactions, lambda { + joins(:samples).joins('inner join reactions_samples rs on rs.sample_id = samples.id') } - scope :with_wellplates, -> { - joins(:samples).joins("inner join wells w on w.sample_id = samples.id" ).uniq + scope :with_wellplates, lambda { + joins(:samples).joins('inner join wells w on w.sample_id = samples.id') } def self.find_or_create_dummy @@ -174,9 +178,14 @@ def attach_svg(svg_data) "#{SecureRandom.hex(64)}.svg" end Loofah::HTML5::SafeList::ALLOWED_ATTRIBUTES.add('overflow') + # NB: successiv gsub seems to be faster than a single gsub with a regexp with multiple matches + scrubbed_data = Loofah.scrub_fragment(svg_data.encode('UTF-8'), :strip).to_s + .gsub('viewbox', 'viewBox') + .gsub('lineargradient', 'linearGradient') + .gsub('radialgradient', 'radialGradient') File.write( full_svg_path(svg_file_name), - Loofah.scrub_fragment(svg_data.encode('UTF-8'), :strip).to_s.gsub('viewbox', 'viewBox'), + scrubbed_data, ) self.molecule_svg_file = svg_file_name @@ -206,9 +215,9 @@ def check_sum_formular end.join end - def load_cas(force = false) - return unless inchikey.present? - return unless force || cas.blank? + def load_cas + return if inchikey.blank? + self.cas = PubChem.get_cas_from_cid(cid) save end @@ -234,15 +243,18 @@ def get_lcss end end - def create_molecule_name_by_user(new_name, user_id) - return unless unique_molecule_name(new_name) - molecule_names - .create(name: new_name, description: "defined by user #{user_id}") + def create_molecule_name_by_user(new_names, user_id) + new_names.split(';').each do |new_name| + next unless unique_molecule_name(new_name) + + molecule_names + .create(name: new_name, description: "defined by user #{user_id}") + end end def unique_molecule_name(new_name) mns = molecule_names.map(&:name) - !mns.include?(new_name) + mns.exclude?(new_name) end def self.svg_reprocess(svg, molfile) @@ -286,3 +298,4 @@ def full_svg_path(svg_file_name = molecule_svg_file) Rails.public_path.join('images', 'molecules', svg_file_name) end end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/reaction.rb b/app/models/reaction.rb index 31ccfbb81a..dac00efbfc 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -41,6 +41,7 @@ # index_reactions_on_role (role) # +# rubocop:disable Metrics/ClassLength class Reaction < ApplicationRecord acts_as_paranoid include ElementUIStateScopes @@ -49,7 +50,7 @@ class Reaction < ApplicationRecord include ElementCodes include Taggable include ReactionRinchi - include Segmentable + include Labimotion::Segmentable serialize :description, Hash serialize :observation, Hash @@ -88,15 +89,16 @@ class Reaction < ApplicationRecord scope :by_name, ->(query) { where('name ILIKE ?', "%#{sanitize_sql_like(query)}%") } scope :by_short_label, ->(query) { where('short_label ILIKE ?', "%#{sanitize_sql_like(query)}%") } scope :by_rinchi_string, ->(query) { where('rinchi_string ILIKE ?', "%#{sanitize_sql_like(query)}%") } - scope :by_material_ids, ->(ids) { joins(:starting_materials).where('samples.id IN (?)', ids) } - scope :by_solvent_ids, ->(ids) { joins(:solvents).where('samples.id IN (?)', ids) } - scope :by_reactant_ids, ->(ids) { joins(:reactants).where('samples.id IN (?)', ids) } - scope :by_product_ids, ->(ids) { joins(:products).where('samples.id IN (?)', ids) } + scope :by_material_ids, ->(ids) { joins(:starting_materials).where(samples: { id: ids }) } + scope :by_solvent_ids, ->(ids) { joins(:solvents).where(samples: { id: ids }) } + scope :by_reactant_ids, ->(ids) { joins(:reactants).where(samples: { id: ids }) } + scope :by_product_ids, ->(ids) { joins(:products).where(samples: { id: ids }) } scope :by_sample_ids, ->(ids) { joins(:reactions_samples).where(reactions_samples: { sample_id: ids }) } + scope :by_literature_ids, ->(ids) { joins(:literals).where(literals: { literature_id: ids }) } scope :by_status, ->(query) { where('reactions.status ILIKE ?', "%#{sanitize_sql_like(query)}%") } scope :search_by_reaction_status, ->(query) { where(status: query) } scope :search_by_reaction_rinchi_string, ->(query) { where(rinchi_string: query) } - scope :includes_for_list_display, -> { includes(:tag) } + scope :includes_for_list_display, -> { includes(:tag, :comments) } has_many :collections_reactions, dependent: :destroy has_many :collections, through: :collections_reactions @@ -137,6 +139,7 @@ class Reaction < ApplicationRecord has_many :sync_collections_users, through: :collections has_many :private_notes, as: :noteable, dependent: :destroy + has_many :comments, as: :commentable, dependent: :destroy belongs_to :creator, foreign_key: :created_by, class_name: 'User' validates :creator, presence: true @@ -145,6 +148,8 @@ class Reaction < ApplicationRecord before_save :cleanup_array_fields before_save :scrub before_save :auto_format_temperature! + before_save :description_to_plain_text + before_save :observation_to_plain_text before_create :auto_set_short_label after_create :update_counter @@ -286,4 +291,17 @@ def scrubber(value) Loofah::HTML5::SafeList::ALLOWED_ATTRIBUTES.add('overflow') Loofah.scrub_fragment(value, :strip).to_s end + + def description_to_plain_text + return unless description_changed? + + self.plain_text_description = Chemotion::QuillToPlainText.new.convert(description) + end + + def observation_to_plain_text + return unless observation_changed? + + self.plain_text_observation = Chemotion::QuillToPlainText.new.convert(observation) + end end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/report.rb b/app/models/report.rb index 15d14a9d40..a2427993d7 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -20,7 +20,8 @@ # updated_at :datetime not null # template :string default("standard") # mol_serials :text default([]) -# si_reaction_settings :text default({"Name"=>true, "CAS"=>true, "Formula"=>true, "Smiles"=>true, "InCHI"=>true, "Molecular Mass"=>true, "Exact Mass"=>true, "EA"=>true}) +# si_reaction_settings :text default({"Name"=>true, "CAS"=>true, "Formula"=>true, "Smiles"=>true, +# "InCHI"=>true, "Molecular Mass"=>true, "Exact Mass"=>true, "EA"=>true}) # prd_atts :text default([]) # report_templates_id :integer # @@ -56,43 +57,41 @@ def create_docx if ReportTemplate.where(id: report_templates_id).present? report_template = ReportTemplate.includes(:attachment).find(report_templates_id) template = report_template.report_type -# tpl_path = if report_template.attachment -# report_template.attachment.attachment_url -# else -# report_template.report_type -# end - tpl_path = self.class.template_path(template) - else - tpl_path = self.class.template_path(template) + # tpl_path = if report_template.attachment + # report_template.attachment.attachment_url + # else + # report_template.report_type + # end end + tpl_path = self.class.template_path(template) case template when 'spectrum' Reporter::WorkerSpectrum.new( - report: self, template_path: tpl_path + report: self, template_path: tpl_path, ).process when 'supporting_information' Reporter::WorkerSi.new( - report: self, template_path: tpl_path, std_rxn: false + report: self, template_path: tpl_path, std_rxn: false, ).process when 'supporting_information_std_rxn' Reporter::WorkerSi.new( - report: self, template_path: tpl_path, std_rxn: true + report: self, template_path: tpl_path, std_rxn: true, ).process when 'rxn_list_xlsx' Reporter::WorkerRxnList.new( - report: self, ext: 'xlsx' + report: self, ext: 'xlsx', ).process when 'rxn_list_csv' Reporter::WorkerRxnList.new( - report: self, ext: 'csv' + report: self, ext: 'csv', ).process when 'rxn_list_html' Reporter::WorkerRxnList.new( - report: self, template_path: tpl_path, ext: 'html' + report: self, template_path: tpl_path, ext: 'html', ).process else Reporter::Worker.new( - report: self, template_path: tpl_path + report: self, template_path: tpl_path, ).process end end @@ -112,16 +111,17 @@ def self.docx_file(current_user, user_ids, params) reaction = Reaction.find(params[:id]) serialized_reaction = Entities::ReactionReportEntity.represent( reaction, - detail_levels: ElementDetailLevelCalculator.new(user: current_user, element:reaction).detail_levels + current_user: current_user, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: reaction).detail_levels, ).serializable_hash content = Reporter::Docx::Document.new(objs: [serialized_reaction]).convert tpl_path = template_path(params[:template]) - file = Sablon.template(tpl_path) - .render_to_string(merge(current_user, - content, - all_spl_settings, - all_rxn_settings, - all_configs)) + Sablon.template(tpl_path) + .render_to_string(merge(current_user, + content, + all_spl_settings, + all_rxn_settings, + all_configs)) end def self.docx_file_name(template) @@ -162,7 +162,7 @@ def self.merge(current_user, contents, spl_settings, rxn_settings, configs) spl_settings: spl_settings, rxn_settings: rxn_settings, configs: configs, - objs: contents + objs: contents, } end @@ -171,7 +171,7 @@ def self.all_spl_settings diagram: true, collection: true, analyses: true, - reaction_description: true + reaction_description: true, } end @@ -185,14 +185,15 @@ def self.all_rxn_settings tlc: true, observation: true, analysis: true, - literature: true + literature: true, + variations: true, } end def self.all_configs { page_break: true, - whole_diagram: true + whole_diagram: true, } end diff --git a/app/models/research_plan.rb b/app/models/research_plan.rb index 4a2ffd72fe..39d5fd2fb6 100644 --- a/app/models/research_plan.rb +++ b/app/models/research_plan.rb @@ -16,13 +16,34 @@ class ResearchPlan < ApplicationRecord include ElementUIStateScopes include Collectable include Taggable - include Segmentable + include Labimotion::Segmentable belongs_to :creator, foreign_key: :created_by, class_name: 'User' validates :creator, :name, presence: true scope :by_name, ->(query) { where('name ILIKE ?', "%#{sanitize_sql_like(query)}%") } - scope :includes_for_list_display, ->() { includes(:attachments) } + scope :includes_for_list_display, -> { includes(:attachments, :comments) } + scope :by_sample_ids, lambda { |ids| + joins('CROSS JOIN jsonb_array_elements(body) AS element') + .where("(element -> 'value'->> 'sample_id')::INT = ANY(array[?])", ids) + } + scope :by_reaction_ids, lambda { |ids| + joins('CROSS JOIN jsonb_array_elements(body) AS element') + .where("(element -> 'value'->> 'reaction_id')::INT = ANY(array[?])", ids) + } + scope :sample_ids_by_research_plan_ids, lambda { |ids| + select("(element -> 'value'->> 'sample_id') AS sample_id") + .joins('CROSS JOIN jsonb_array_elements(body) AS element') + .where(id: ids) + .where("(element -> 'value'->> 'sample_id')::INT IS NOT NULL") + } + scope :reaction_ids_by_research_plan_ids, lambda { |ids| + select("(element -> 'value'->> 'reaction_id') AS reaction_id") + .joins('CROSS JOIN jsonb_array_elements(body) AS element') + .where(id: ids) + .where("(element -> 'value'->> 'reaction_id')::INT IS NOT NULL") + } + scope :by_literature_ids, ->(ids) { joins(:literals).where(literals: { literature_id: ids }) } after_create :create_root_container @@ -31,6 +52,7 @@ class ResearchPlan < ApplicationRecord has_many :collections_research_plans, inverse_of: :research_plan, dependent: :destroy has_many :collections, through: :collections_research_plans has_many :attachments, as: :attachable + has_many :comments, as: :commentable, dependent: :destroy has_many :research_plans_wellplates, dependent: :destroy has_many :wellplates, through: :research_plans_wellplates @@ -38,6 +60,9 @@ class ResearchPlan < ApplicationRecord has_many :research_plans_screens, dependent: :destroy has_many :screens, through: :research_plans_screens + has_many :literals, as: :element, dependent: :destroy + has_many :literatures, through: :literals + before_destroy :delete_attachment accepts_nested_attributes_for :collections_research_plans diff --git a/app/models/research_plans_wellplate.rb b/app/models/research_plans_wellplate.rb index a44101adb6..4354e9668c 100644 --- a/app/models/research_plans_wellplate.rb +++ b/app/models/research_plans_wellplate.rb @@ -15,13 +15,20 @@ # index_research_plans_wellplates_on_wellplate_id (wellplate_id) # +# frozen_string_literal: true + class ResearchPlansWellplate < ApplicationRecord acts_as_paranoid belongs_to :research_plan belongs_to :wellplate - def self.get_wellplates research_plan_ids - self.where(research_plan_id: research_plan_ids).pluck(:wellplate_id).compact.uniq - end + scope :get_wellplates, lambda { |research_plan_ids| + where(research_plan_id: research_plan_ids) + .pluck(:wellplate_id).compact.uniq + } + scope :get_research_plans, lambda { |wellplate_ids| + where(wellplate_id: wellplate_ids) + .pluck(:research_plan_id).compact.uniq + } end diff --git a/app/models/sample.rb b/app/models/sample.rb index 5f3f43b0b8..11fb1afada 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -42,6 +42,7 @@ # molecular_mass :float # sum_formula :string # solvent :jsonb +# dry_solvent :boolean default(FALSE) # inventory_sample :boolean default(FALSE) # # Indexes @@ -64,7 +65,7 @@ class Sample < ApplicationRecord include AnalysisCodes include UnitConvertable include Taggable - include Segmentable + include Labimotion::Segmentable STEREO_ABS = ['any', 'rac', 'meso', 'delta', 'lambda', '(S)', '(R)', '(Sp)', '(Rp)', '(Sa)', '(Ra)'].freeze STEREO_REL = ['any', 'syn', 'anti', 'p-geminal', 'p-ortho', 'p-meta', 'p-para', 'cis', 'trans', 'fac', 'mer'].freeze @@ -133,7 +134,8 @@ class Sample < ApplicationRecord scope :by_reaction_material_ids, ->(ids) { joins(:reactions_starting_material_samples).where('reactions_samples.reaction_id in (?)', ids) } scope :by_reaction_solvent_ids, ->(ids) { joins(:reactions_solvent_samples).where('reactions_samples.reaction_id in (?)', ids) } scope :by_reaction_ids, ->(ids) { joins(:reactions_samples).where('reactions_samples.reaction_id in (?)', ids) } - scope :includes_for_list_display, -> { includes(:molecule_name, :tag, molecule: :tag) } + scope :by_literature_ids, ->(ids) { joins(:literals).where(literals: { literature_id: ids }) } + scope :includes_for_list_display, -> { includes(:molecule_name, :tag, :comments, molecule: :tag) } scope :product_only, -> { joins(:reactions_samples).where("reactions_samples.type = 'ReactionsProductSample'") } scope :sample_or_startmat_or_products, -> { @@ -167,6 +169,7 @@ class Sample < ApplicationRecord before_save :attach_svg, :init_elemental_compositions, :set_loading_from_ea before_save :auto_set_short_label + before_save :update_inventory_label, if: :new_record? before_create :check_molecule_name before_create :set_boiling_melting_points after_save :update_counter @@ -182,7 +185,7 @@ class Sample < ApplicationRecord has_many :reactions_reactant_samples, dependent: :destroy has_many :reactions_solvent_samples, dependent: :destroy has_many :reactions_product_samples, dependent: :destroy - has_many :elements_samples, dependent: :destroy + has_many :elements_samples, dependent: :destroy, class_name: 'Labimotion::ElementsSample' has_many :reactions, through: :reactions_samples has_many :reactions_as_starting_material, through: :reactions_starting_material_samples, source: :reaction @@ -196,6 +199,7 @@ class Sample < ApplicationRecord has_many :devices_samples has_many :analyses_experiments has_many :private_notes, as: :noteable, dependent: :destroy + has_many :comments, as: :commentable, dependent: :destroy belongs_to :fingerprint, optional: true belongs_to :user, optional: true @@ -345,7 +349,7 @@ def create_chemical_entry_for_subsample(sample_id, subsample_id, type) # rubocop:disable Style/MethodDefParentheses # rubocop:disable Style/OptionalBooleanParameter # rubocop:disable Layout/TrailingWhitespace - def create_subsample user, collection_ids, copy_ea = false, type = nil + def create_subsample user, collection_ids, copy_ea = false, type = nil subsample = self.dup subsample.name = self.name if self.name.present? subsample.external_label = self.external_label if self.external_label.present? @@ -648,6 +652,27 @@ def auto_set_short_label end end + def find_collection_id + collection_ids = collections_samples.map(&:collection_id) + all_collection_id = Collection.where(id: collection_ids, label: 'All').pick(:id) + collection_ids.delete(all_collection_id) + # on sample create, sample is assigned only to the collection in which it will be created along with All collection + collection_ids.first + end + + def update_inventory_label + collection_id = find_collection_id + return if collection_id.blank? + + collection = Collection.find_by(id: collection_id) + inventory = collection.inventory + return if inventory.blank? + + inventory = inventory.increment_inventory_label_counter(collection_id.to_s) + self['xref']['inventory_label'] = + "#{inventory['prefix']}-#{inventory['counter']}" + end + # rubocop: enable Metrics/AbcSize # rubocop: enable Metrics/CyclomaticComplexity # rubocop: enable Metrics/PerceivedComplexity @@ -702,7 +727,11 @@ def has_density def scrub(value) Loofah::HTML5::SafeList::ALLOWED_ATTRIBUTES.add('overflow') - Loofah.scrub_fragment(value, :strip).to_s.gsub('viewbox', 'viewBox') + # NB: successiv gsub seems to be faster than a single gsub with a regexp with multiple matches + Loofah.scrub_fragment(value, :strip).to_s + .gsub('viewbox', 'viewBox') + .gsub('lineargradient', 'linearGradient') + .gsub('radialgradient', 'radialGradient') # value end diff --git a/app/models/screen.rb b/app/models/screen.rb index ddf958b99a..761fe94fc0 100644 --- a/app/models/screen.rb +++ b/app/models/screen.rb @@ -26,7 +26,7 @@ class Screen < ApplicationRecord include Collectable include ElementCodes include Taggable - include Segmentable + include Labimotion::Segmentable serialize :description, Hash @@ -54,12 +54,23 @@ class Screen < ApplicationRecord has_many :research_plans_screens, dependent: :destroy has_many :research_plans, through: :research_plans_screens + has_many :comments, as: :commentable, dependent: :destroy + has_one :container, :as => :containable + before_save :description_to_plain_text + accepts_nested_attributes_for :collections_screens def analyses self.container ? self.container.analyses : [] end + private + + def description_to_plain_text + return unless description_changed? + + self.plain_text_description = Chemotion::QuillToPlainText.new.convert(description) + end end diff --git a/app/models/segment.rb b/app/models/segment.rb deleted file mode 100644 index 27f18464b5..0000000000 --- a/app/models/segment.rb +++ /dev/null @@ -1,25 +0,0 @@ -# == Schema Information -# -# Table name: segments -# -# id :integer not null, primary key -# segment_klass_id :integer -# element_type :string -# element_id :integer -# properties :jsonb -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# uuid :string -# klass_uuid :string -# - -class Segment < ApplicationRecord - acts_as_paranoid - include GenericRevisions - - belongs_to :segment_klass - belongs_to :element, polymorphic: true - has_many :segments_revisions, dependent: :destroy -end diff --git a/app/models/segment_klass.rb b/app/models/segment_klass.rb deleted file mode 100644 index 7e3c0643b4..0000000000 --- a/app/models/segment_klass.rb +++ /dev/null @@ -1,39 +0,0 @@ -# == Schema Information -# -# Table name: segment_klasses -# -# id :integer not null, primary key -# element_klass_id :integer -# label :string not null -# desc :string -# properties_template :jsonb -# is_active :boolean default(TRUE), not null -# place :integer default(100), not null -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# uuid :string -# properties_release :jsonb -# released_at :datetime -# - -class SegmentKlass < ApplicationRecord - acts_as_paranoid - include GenericKlassRevisions - belongs_to :element_klass - has_many :segments, dependent: :destroy - has_many :segment_klasses_revisions, dependent: :destroy - - def self.gen_klasses_json - klasses = where(is_active: true)&.pluck(:name) || [] - rescue ActiveRecord::StatementInvalid, PG::ConnectionBad, PG::UndefinedTable - klasses = [] - ensure - File.write( - Rails.root.join('config', 'segment_klass.json'), - klasses&.to_json || [] - ) - end - -end diff --git a/app/models/segment_klasses_revision.rb b/app/models/segment_klasses_revision.rb deleted file mode 100644 index 7014ea026b..0000000000 --- a/app/models/segment_klasses_revision.rb +++ /dev/null @@ -1,25 +0,0 @@ -# == Schema Information -# -# Table name: segment_klasses_revisions -# -# id :integer not null, primary key -# segment_klass_id :integer -# uuid :string -# properties_release :jsonb -# released_at :datetime -# released_by :integer -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# -# Indexes -# -# index_segment_klasses_revisions_on_segment_klass_id (segment_klass_id) -# - -class SegmentKlassesRevision < ApplicationRecord - acts_as_paranoid - has_one :segment_klass - -end diff --git a/app/models/segments_revision.rb b/app/models/segments_revision.rb deleted file mode 100644 index bb0450ab87..0000000000 --- a/app/models/segments_revision.rb +++ /dev/null @@ -1,24 +0,0 @@ -# == Schema Information -# -# Table name: segments_revisions -# -# id :integer not null, primary key -# segment_id :integer -# uuid :string -# klass_uuid :string -# properties :jsonb -# created_by :integer -# created_at :datetime -# updated_at :datetime -# deleted_at :datetime -# -# Indexes -# -# index_segments_revisions_on_segment_id (segment_id) -# - -class SegmentsRevision < ApplicationRecord - acts_as_paranoid - has_one :segment - -end diff --git a/app/models/sync_collections_user.rb b/app/models/sync_collections_user.rb index 703cd209db..8fc597af75 100644 --- a/app/models/sync_collections_user.rb +++ b/app/models/sync_collections_user.rb @@ -2,21 +2,22 @@ # # Table name: sync_collections_users # -# id :integer not null, primary key -# user_id :integer -# collection_id :integer -# shared_by_id :integer -# permission_level :integer default(0) -# sample_detail_level :integer default(0) -# reaction_detail_level :integer default(0) -# wellplate_detail_level :integer default(0) -# screen_detail_level :integer default(0) -# fake_ancestry :string -# researchplan_detail_level :integer default(10) -# label :string -# created_at :datetime -# updated_at :datetime -# element_detail_level :integer default(10) +# id :integer not null, primary key +# user_id :integer +# collection_id :integer +# shared_by_id :integer +# permission_level :integer default(0) +# sample_detail_level :integer default(0) +# reaction_detail_level :integer default(0) +# wellplate_detail_level :integer default(0) +# screen_detail_level :integer default(0) +# fake_ancestry :string +# researchplan_detail_level :integer default(10) +# label :string +# created_at :datetime +# updated_at :datetime +# element_detail_level :integer default(10) +# celllinesample_detail_level :integer default(10) # # Indexes # @@ -35,6 +36,7 @@ class SyncCollectionsUser < ApplicationRecord has_many :wellplates, through: :collection has_many :screens, through: :collection has_many :research_plans, through: :collection + has_many :cellline_samples, through: :collection before_create :auto_set_synchronized_flag after_destroy :check_collection_if_synced diff --git a/app/models/third_party_app.rb b/app/models/third_party_app.rb index 8e8ece876a..9b219e9b42 100644 --- a/app/models/third_party_app.rb +++ b/app/models/third_party_app.rb @@ -1,14 +1,5 @@ # frozen_string_literal: true class ThirdPartyApp < ApplicationRecord - def self.all_names - return nil if ThirdPartyApp.count.zero? - - entries = ThirdPartyApp.all - names = [] - entries.each do |e| - names << e.name - end - names - end + validates :name, presence: true, uniqueness: true, length: { maximum: 100 } end diff --git a/app/models/user.rb b/app/models/user.rb index 5b5fd086a7..cd9901c8ef 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,19 +25,18 @@ # name_abbreviation :string(12) # type :string default("Person") # reaction_name_prefix :string(3) default("R") +# layout :hstore not null # confirmation_token :string # confirmed_at :datetime # confirmation_sent_at :datetime # unconfirmed_email :string -# layout :hstore not null # selected_device_id :integer # failed_attempts :integer default(0), not null # unlock_token :string # locked_at :datetime # account_active :boolean # matrix :integer default(0) -# omniauth_provider :string -# omniauth_uid :string +# providers :jsonb # # Indexes # @@ -49,9 +48,12 @@ # index_users_on_unlock_token (unlock_token) UNIQUE # -# rubocop: disable Metrics/ClassLength + +# rubocop: disable Metrics/ClassLength, Metrics/CyclomaticComplexity, Performance/RedundantMerge, Style/MultilineIfModifier # rubocop: disable Metrics/MethodLength # rubocop: disable Metrics/AbcSize +# rubocop: disable Metrics/CyclicComplexity +# rubocop: disable Metrics/PerceivedComplexity class User < ApplicationRecord attr_writer :login @@ -70,14 +72,18 @@ class User < ApplicationRecord has_many :wellplates, through: :collections has_many :screens, through: :collections has_many :research_plans, through: :collections + has_many :vessels, through: :collections + # 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 :samples_created, foreign_key: :created_by, class_name: 'Sample' has_many :sync_out_collections_users, foreign_key: :shared_by_id, class_name: 'SyncCollectionsUser' - has_many :sync_in_collections_users, foreign_key: :user_id, class_name: 'SyncCollectionsUser' + has_many :sync_in_collections_users, class_name: 'SyncCollectionsUser' has_many :sharing_collections, through: :sync_out_collections_users, source: :collection has_many :shared_collections, through: :sync_in_collections_users, source: :collection - has_many :users_devices, dependent: :destroy, foreign_key: :user_id + has_many :users_devices, dependent: :destroy has_many :devices, through: :users_devices # belongs_to :selected_device, class_name: 'Device' @@ -98,10 +104,11 @@ class User < ApplicationRecord has_one :research_plan_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 accepts_nested_attributes_for :affiliations, :profile - validates_presence_of :first_name, :last_name, allow_blank: false + validates :first_name, :last_name, presence: { allow_blank: false } validates :name_abbreviation, uniqueness: { message: 'is already in use.' } validate :name_abbreviation_reserved_list, on: :create @@ -110,9 +117,14 @@ class User < ApplicationRecord validate :mail_checker # NB: only Persons and Admins can get a confirmation email and confirm their email. - before_create :skip_confirmation_notification!, unless: proc { |user| %w[Person Admin].include?(user.type) } + before_create :skip_confirmation_notification!, unless: proc { |user| + %w[Person Admin].include?(user.type) + } # NB: option to skip devise confirmable for Admins and Persons - before_create :skip_confirmation!, if: proc { |user| %w[Person Admin].include?(user.type) && self.class.allow_unconfirmed_access_for.nil? } + before_create :skip_confirmation!, if: proc { |user| + %w[Person Admin].include?(user.type) && + self.class.allow_unconfirmed_access_for.nil? + } before_create :set_account_active, if: proc { |user| %w[Person].include?(user.type) } after_create :create_chemotion_public_collection @@ -122,10 +134,13 @@ class User < ApplicationRecord after_create :send_welcome_email, if: proc { |user| %w[Person].include?(user.type) } before_destroy :delete_data - scope :by_name, ->(query) { + scope :by_name, lambda { |query| where("LOWER(first_name) ILIKE ? OR LOWER(last_name) ILIKE ? OR LOWER(first_name || ' ' || last_name) ILIKE ?", - "#{sanitize_sql_like(query.downcase)}%", "#{sanitize_sql_like(query.downcase)}%", "#{sanitize_sql_like(query.downcase)}%") + "#{sanitize_sql_like(query.downcase)}%", + "#{sanitize_sql_like(query.downcase)}%", + "#{sanitize_sql_like(query.downcase)}%") } + scope :persons, -> { where(type: 'Person') } scope :by_exact_name_abbreviation, lambda { |query, case_insensitive = false| if case_insensitive @@ -147,13 +162,13 @@ def self.try_find_by_name_abbreviation(name_abbreviation) end def login - @login || self.name_abbreviation || self.email + @login || name_abbreviation || email end def self.find_first_by_auth_conditions(warden_conditions) conditions = warden_conditions.dup if (login = conditions.delete(:login)) - where(conditions).where(["name_abbreviation = :value OR lower(email) = lower(:value)", { value: login }]).first + where(conditions).where(['name_abbreviation = :value OR lower(email) = lower(:value)', { value: login }]).first else where(conditions).first end @@ -164,7 +179,8 @@ def active_for_authentication? end def name_abbr_config - @name_abbr_config ||= Rails.configuration.respond_to?(:user_props) ? (Rails.configuration.user_props&.name_abbr || {}) : {} + @name_abbr_config ||= + Rails.configuration.respond_to?(:user_props) ? (Rails.configuration.user_props&.name_abbr || {}) : {} end def name_abbreviation_reserved_list @@ -175,7 +191,8 @@ def name_abbreviation_reserved_list def name_abbreviation_format format_abbr_default = /\A[a-zA-Z][a-zA-Z0-9\-_]*[a-zA-Z0-9]\Z/ - format_err_msg_default = "can be alphanumeric, middle '_' and '-' are allowed, but leading digit, or trailing '-' and '_' are not." + format_err_msg_default = + "can be alphanumeric, middle '_' and '-' are allowed, but leading digit, or trailing '-' and '_' are not." format_abbr = name_abbr_config[:format_abbr].presence || format_abbr_default.presence format_err_msg = name_abbr_config[:format_abbr_err_msg].presence || format_err_msg_default.presence @@ -186,7 +203,6 @@ def name_abbreviation_format end def name_abbreviation_length - na = name_abbreviation case type when 'Group' min_val = name_abbr_config[:length_group]&.first || 2 @@ -199,8 +215,9 @@ def name_abbreviation_length max_val = name_abbr_config[:length_default]&.last || 3 end - na.blank? || !na.length.between?(min_val, max_val) && - errors.add(:name_abbreviation, "has to be #{min_val} to #{max_val} characters long") + return if name_abbreviation.to_s.length.between?(min_val, max_val) + + errors.add(:name_abbreviation, "has to be #{min_val} to #{max_val} characters long") end def mail_checker @@ -226,21 +243,21 @@ def initials end def restore_counters_data - samples_number = self.samples_created.pluck(:short_label).map do |l| + samples_number = samples_created.pluck(:short_label).map do |l| l.split('-').map(&:to_i) end.flatten.max || 0 - reactions_number = self.reactions.pluck(:name).map do |l| + reactions_number = reactions.pluck(:name).map do |l| l.split('#').last.to_i end.max || 0 self.counters = { samples: samples_number, reactions: reactions_number, - wellplates: self.wellplates.count + self.wellplates.deleted.count + wellplates: wellplates.count + wellplates.deleted.count, } - self.save! + save! end def increment_counter(key) @@ -253,30 +270,33 @@ def increment_counter(key) end def has_profile - self.create_profile if !self.profile - if self.type == 'Person' - profile = self.profile - data = profile.data || {} - file = Rails.root.join('db', 'chmo.default.profile.json') - result = JSON.parse(File.read(file, encoding: 'bom|utf-8')) if File.exist?(file) - unless result.nil? || result['ols_terms'].nil? - data['chmo'] = result['ols_terms'] - data['is_templates_moderator'] = false - data['molecule_editor'] = false - data['converter_admin'] = false - data.merge!(layout: { - 'sample' => 1, - 'reaction' => 2, - 'wellplate' => 3, - 'screen' => 4, - 'research_plan' => 5 - }) if (data['layout'].nil?) - self.profile.update_columns(data: data) - end + create_profile unless profile + return unless type == 'Person' + + profile = self.profile + data = profile.data || {} + file = Rails.root.join('db', 'chmo.default.profile.json') + result = JSON.parse(File.read(file, encoding: 'bom|utf-8')) if File.file?(file) + return if result.nil? || result['ols_terms'].nil? + + data['chmo'] = result['ols_terms'] + data['is_templates_moderator'] = false + data['molecule_editor'] = false + data['converter_admin'] = false + if data['layout'].nil? + data.merge!(layout: { + 'sample' => 1, + 'reaction' => 2, + 'wellplate' => 3, + 'screen' => 4, + 'research_plan' => 5, + 'cell_line' => -1000, + }) end + self.profile.update_columns(data: data) end - has_many :users_groups, dependent: :destroy, foreign_key: :user_id + has_many :users_groups, dependent: :destroy has_many :groups, through: :users_groups def group_ids @@ -297,7 +317,7 @@ def all_sync_in_collections_users def current_affiliations Affiliation.joins( - 'INNER JOIN user_affiliations ua ON ua.affiliation_id = affiliations.id' + 'INNER JOIN user_affiliations ua ON ua.affiliation_id = affiliations.id', ).where( '(ua.user_id = ?) and (ua.deleted_at ISNULL) and (ua.to ISNULL or ua.to > ?)', id, Time.now @@ -312,6 +332,10 @@ def molecule_editor profile&.data&.fetch('molecule_editor', false) end + def generic_admin + profile&.data&.fetch('generic_admin', {}) + end + def converter_admin profile&.data&.fetch('converter_admin', false) end @@ -331,7 +355,8 @@ def matrix_check(id) end def update_matrix - check_sql = ApplicationRecord.send(:sanitize_sql_array, ["SELECT to_regproc('generate_users_matrix') IS NOT null as rs"]) + check_sql = ApplicationRecord.send(:sanitize_sql_array, + ["SELECT to_regproc('generate_users_matrix') IS NOT null as rs"]) result = ApplicationRecord.connection.exec_query(check_sql) if result.presence&.first&.fetch('rs', false) @@ -343,12 +368,17 @@ def update_matrix end def remove_from_matrices - Matrice.where('include_ids @> ARRAY[?]', [id]).each { |ma| ma.update_columns(include_ids: ma.include_ids -= [id]) } - Matrice.where('exclude_ids @> ARRAY[?]', [id]).each { |ma| ma.update_columns(exclude_ids: ma.exclude_ids -= [id]) } + Matrice.where('include_ids @> ARRAY[?]', [id]).find_each do |ma| + ma.update_columns(include_ids: ma.include_ids -= [id]) + end + Matrice.where('exclude_ids @> ARRAY[?]', [id]).find_each do |ma| + ma.update_columns(exclude_ids: ma.exclude_ids -= [id]) + end end def self.gen_matrix(user_ids = nil) - check_sql = ApplicationRecord.send(:sanitize_sql_array, ["SELECT to_regproc('generate_users_matrix') IS NOT null as rs"]) + check_sql = ApplicationRecord.send(:sanitize_sql_array, + ["SELECT to_regproc('generate_users_matrix') IS NOT null as rs"]) result = ApplicationRecord.connection.exec_query(check_sql) if result.presence&.first&.fetch('rs', false) sql = if user_ids.present? @@ -373,7 +403,7 @@ def create_text_template end def self.from_omniauth(provider, uid, email, first_name, last_name) - user = find_by(email: email) + user = find_by(email: email&.downcase) if user.present? providers = user.providers || {} providers[provider] = uid @@ -400,6 +430,10 @@ def password_required? super && provider.blank? end + def extra_rules + Matrice.extra_rules || {} + end + private # These user collections are locked, i.e., the user is not allowed to: @@ -417,7 +451,7 @@ def new_user_text_template end def create_chemotion_public_collection - return unless self.type == 'Person' + return unless type == 'Person' Collection.create(user: self, label: 'chemotion-repository.net', is_locked: true, position: 1) end @@ -427,11 +461,11 @@ def set_account_active end def send_welcome_email - file_path = Rails.public_path.join('welcome-message.md') + file_path = Rails.public_path.join('welcome-message.md') if File.exist?(file_path) SendWelcomeEmailJob.perform_later(id) else - #do nothing + # do nothing end end @@ -446,6 +480,10 @@ def delete_data update_columns(name_abbreviation: nil) if count.zero? update_columns(providers: nil) end + + def user_ids + [id] + end end class Person < User @@ -463,7 +501,7 @@ class Device < User has_many :users_admins, dependent: :destroy, foreign_key: :user_id has_many :admins, through: :users_admins, source: :admin - has_one :device_metadata, dependent: :destroy, foreign_key: :device_id + has_one :device_metadata, dependent: :destroy scope :by_user_ids, ->(ids) { joins(:users_devices).merge(UsersDevice.by_user_ids(ids)) } scope :novnc, -> { joins(:profile).merge(Profile.novnc) } @@ -478,9 +516,24 @@ class Group < User has_many :users, class_name: 'User', through: :users_groups has_many :users_admins, dependent: :destroy, foreign_key: :user_id - has_many :admins, through: :users_admins, source: :admin # , foreign_key: association_foreign_key: :admin_id + has_many :admins, through: :users_admins, source: :admin # , foreign_key: association_foreign_key: :admin_id + + before_destroy :remove_from_matrices + + def administrated_by?(user) + users_admins.where(admin: user).present? + end + + private + + def user_ids + # Override method to return an array of user IDs in the group + users.ids + end end -# rubocop: enable Metrics/ClassLength +# rubocop: enable Metrics/ClassLength, Metrics/CyclomaticComplexity, Performance/RedundantMerge, Style/MultilineIfModifier # rubocop: enable Metrics/MethodLength # rubocop: enable Metrics/AbcSize +# rubocop: enable Metrics/CyclicComplexity +# rubocop: enable Metrics/PerceivedComplexity diff --git a/app/models/vessel.rb b/app/models/vessel.rb new file mode 100644 index 0000000000..2791907686 --- /dev/null +++ b/app/models/vessel.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: vessels +# +# id :uuid not null, primary key +# vessel_template_id :uuid +# user_id :bigint +# name :string +# description :string +# short_label :string +# created_at :datetime not null +# updated_at :datetime not null +# deleted_at :datetime +# +# Indexes +# +# index_vessels_on_deleted_at (deleted_at) +# index_vessels_on_user_id (user_id) +# index_vessels_on_vessel_template_id (vessel_template_id) +# +class Vessel < ApplicationRecord + acts_as_paranoid + + belongs_to :vessel_template + belongs_to :creator, class_name: 'User', foreign_key: :user_id, inverse_of: :created_vessels + + has_many :collections_vessels, dependent: :destroy + has_many :collections, through: :collections_vessels + + delegate :details, :material_details, :material_type, :vessel_type, :volume_amount, :volume_unit, + :weight_amount, :weight_unit, to: :vessel_template +end diff --git a/app/models/vessel_template.rb b/app/models/vessel_template.rb new file mode 100644 index 0000000000..62fdecbf4d --- /dev/null +++ b/app/models/vessel_template.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: vessel_templates +# +# id :uuid not null, primary key +# name :string +# details :string +# material_details :string +# material_type :string +# vessel_type :string +# volume_amount :integer +# volume_unit :string +# created_at :datetime not null +# updated_at :datetime not null +# deleted_at :datetime +# +# Indexes +# +# index_vessel_templates_on_deleted_at (deleted_at) +# +class VesselTemplate < ApplicationRecord + acts_as_paranoid + + has_many :vessels, dependent: :destroy +end diff --git a/app/models/wellplate.rb b/app/models/wellplate.rb index b85641b4d3..8431d27d3c 100644 --- a/app/models/wellplate.rb +++ b/app/models/wellplate.rb @@ -26,7 +26,7 @@ class Wellplate < ApplicationRecord include Collectable include ElementCodes include Taggable - include Segmentable + include Labimotion::Segmentable serialize :description, Hash @@ -64,7 +64,7 @@ class Wellplate < ApplicationRecord scope :by_name, ->(query) { where('name ILIKE ?', "%#{sanitize_sql_like(query)}%") } scope :by_sample_ids, ->(ids) { joins(:samples).where('samples.id in (?)', ids) } scope :by_screen_ids, ->(ids) { joins(:screens).where('screens.id in (?)', ids) } - scope :includes_for_list_display, ->() { includes(:tag) } + scope :includes_for_list_display, -> { includes(:tag, :comments) } has_many :collections_wellplates, dependent: :destroy has_many :collections, through: :collections_wellplates @@ -83,8 +83,12 @@ class Wellplate < ApplicationRecord has_many :sync_collections_users, through: :collections + has_many :comments, as: :commentable, dependent: :destroy + has_one :container, as: :containable + before_save :description_to_plain_text + accepts_nested_attributes_for :collections_wellplates def self.associated_by_user_id_and_screen_ids(user_id, screen_ids) @@ -151,4 +155,12 @@ def set_short_label(user:) # rubocop:disable Naming/AccessorMethodName update(short_label: "#{user_label}-#{prefix}#{counter}") end + + private + + def description_to_plain_text + return unless description_changed? + + self.plain_text_description = Chemotion::QuillToPlainText.new.convert(description) + end end diff --git a/app/packs/entrypoints/application.js b/app/packs/entrypoints/application.js index 3b15fb5c19..aec8a7477b 100644 --- a/app/packs/entrypoints/application.js +++ b/app/packs/entrypoints/application.js @@ -13,5 +13,9 @@ var UserCounter = require('src/apps/userCounter'); var ScifinderCredential = require('src/apps/scifinderCredential'); var StructureEditorUserSetting = require('src/components/structureEditor/UserSetting'); var LoginOptions = require('src/apps/omniauthCredential/LoginOptions'); -var ConverterAdmin = require('../src/apps/converter/ConverterAdmin'); +var ConverterAdmin = require('src/apps/converter/ConverterAdmin'); +var GenericElementsAdmin = require('src/apps/generic/GenericElementsAdmin'); +var GenericSegmentsAdmin = require('src/apps/generic/GenericSegmentsAdmin'); +var GenericDatasetsAdmin = require('src/apps/generic/GenericDatasetsAdmin'); +var InventoryLabelSettings = require('src/apps/settings/InventoryLabelSettings'); var mydb = require('src/apps/mydb'); diff --git a/app/packs/src/apps/admin/AdminHome.js b/app/packs/src/apps/admin/AdminHome.js index 0926ef3263..16059e6bd7 100644 --- a/app/packs/src/apps/admin/AdminHome.js +++ b/app/packs/src/apps/admin/AdminHome.js @@ -11,10 +11,8 @@ import OlsTerms from 'src/apps/admin/OlsTerms'; import NovncSettings from 'src/apps/admin/NovncSettings'; import MatrixManagement from 'src/apps/admin/MatrixManagement'; import TextTemplateContainer from 'src/apps/admin/textTemplates/TextTemplateContainer'; -import GenericElementAdmin from 'src/apps/admin/GenericElementAdmin'; -import SegmentElementAdmin from 'src/apps/admin/SegmentElementAdmin'; -import DatasetElementAdmin from 'src/apps/admin/DatasetElementAdmin'; import DelayedJobs from 'src/apps/admin/DelayedJobs'; +import ChemSpectraLayouts from 'src/apps/admin/ChemSpectraLayouts'; // import TemplateManagement from 'src/apps/admin/TemplateManagement'; import ThirdPartyApp from 'src/apps/admin/ThirdPartyApp'; @@ -65,18 +63,12 @@ class AdminHome extends React.Component { return this.renderContent(); } else if (pageIndex === 8) { return this.renderTextTemplates(); - } else if (pageIndex === 9) { - return this.renderContent(); - } else if (pageIndex === 10) { - return this.renderContent(); - } else if (pageIndex === 11) { - return this.renderContent(); } else if (pageIndex === 12) { return this.renderTemplateManagement(); } else if (pageIndex === 13) { return this.renderDelayedJobs(); } else if (pageIndex === 14) { - return this.renderConverterAdmin(); + return this.renderChemSpectraLayouts(); } else if (pageIndex === 15) { return this.renderThirdPartyApp(); } @@ -100,13 +92,11 @@ class AdminHome extends React.Component { NoVNC Settings UI features Text Templates - Generic Elements (BETA) - Generic Segment (BETA) - Generic Dataset (BETA) Message Publish Load OLS Terms {/* Report-template Management */} Delayed Jobs + ChemSpectra Layouts Third Party Apps @@ -222,6 +212,15 @@ class AdminHome extends React.Component { ); } + renderChemSpectraLayouts() { + const { contentClassName } = this.state; + return ( + + + + ); + } + render() { return (
diff --git a/app/packs/src/apps/admin/AdminNavigation.js b/app/packs/src/apps/admin/AdminNavigation.js index e55df02d8c..099bdc1ce9 100644 --- a/app/packs/src/apps/admin/AdminNavigation.js +++ b/app/packs/src/apps/admin/AdminNavigation.js @@ -7,18 +7,7 @@ import UserActions from 'src/stores/alt/actions/UserActions'; import NavNewSession from 'src/components/navigation/NavNewSession'; import DocumentHelper from 'src/utilities/DocumentHelper'; - -const NavHead = () => ( - - - Chemotion repository - Complat - Complat on Github - - ELN - - -); +import NavHead from 'src/components/navigation/NavHead'; export default class AdminNavigation extends React.Component { constructor(props) { diff --git a/app/packs/src/apps/admin/ChemSpectraLayouts.js b/app/packs/src/apps/admin/ChemSpectraLayouts.js new file mode 100644 index 0000000000..b0db3276cd --- /dev/null +++ b/app/packs/src/apps/admin/ChemSpectraLayouts.js @@ -0,0 +1,311 @@ +/* eslint-disable no-param-reassign */ +import React, { Component } from 'react'; +import ChemSpectraFetcher from 'src/fetchers/ChemSpectraFetcher'; +import { + Table, Button, Form, FormControl, Modal, Panel, FormGroup, ControlLabel, Popover, OverlayTrigger, ButtonGroup, Alert +} from 'react-bootstrap'; +import Select from 'react-select'; + +export default class ChemSpectraLayouts extends Component { + constructor(props) { + super(props); + this.state = { + layouts: [], + newDataType: { + layout: '', + dataType: '', + }, + defaultLayouts: [], + showNewTypeLayoutModal: false, + alertMessage: null + }; + + this.fetchSpectraLayouts = this.fetchSpectraLayouts.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleAddDataType = this.handleAddDataType.bind(this); + this.handleDeleteDataType = this.handleDeleteDataType.bind(this); + this.handleShowNewTypeLayoutModal = this.handleShowNewTypeLayoutModal.bind(this); + this.handleCloseNewTypeLayoutModal = this.handleCloseNewTypeLayoutModal.bind(this); + this.getLayoutOptionsAndMapping = this.getLayoutOptionsAndMapping.bind(this); + } + + componentDidMount() { + this.fetchSpectraLayouts(); + } + + handleInputChange(event) { + const { name, value } = event.target; + this.setState((prevState) => ({ + newDataType: { + ...prevState.newDataType, + [name]: value, + }, + })); + } + + handleSelectLayout(selectedOption) { + if (selectedOption) { + this.setState((prevState) => ({ + newDataType: { + ...prevState.newDataType, + layout: selectedOption.value, + }, + })); + } + } + + handleShowNewTypeLayoutModal() { + this.setState({ showNewTypeLayoutModal: true }); + } + + handleCloseNewTypeLayoutModal() { + this.setState({ + showNewTypeLayoutModal: false, + alertMessage: null, + newDataType: { + layout: '', + dataType: '', + } + }); + } + + handleAddDataType() { + const { newDataType, layouts } = this.state; + if (newDataType.dataType.length === 0) { + this.setState({ alertMessage: 'Please enter a data type' }); + } else if (newDataType.layout.length === 0) { + this.setState({ alertMessage: 'Please select a layout' }); + } else { + const existingLayout = layouts.find(([layout]) => layout === newDataType.layout); + + if (existingLayout) { + const [, dataTypeArray] = existingLayout; + + if (dataTypeArray.includes(newDataType.dataType.trimEnd())) { + this.setState({ alertMessage: 'Data type already exists' }); + } else { + const updatedLayouts = layouts.map(([layout, dataType]) => { + if (layout === newDataType.layout) { + return [layout, [...dataType, newDataType.dataType]]; + } + return [layout, dataType]; + }); + const transformedData = updatedLayouts.reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); + + ChemSpectraFetcher.updateDataTypes(transformedData) + .then((message) => { + console.log(message); + this.handleCloseNewTypeLayoutModal(); + this.fetchUpdatedSpectraLayouts(); + }) + .catch((error) => console.error(error)); + } + } + } + } + + handleDeleteDataType(dataTypeToDelete) { + const { layouts } = this.state; + + const updatedLayouts = layouts.map((entry) => { + if (entry[0] === dataTypeToDelete.layout) { + entry[1] = entry[1].filter((dataType) => dataType !== dataTypeToDelete.dataType); + } + return entry; + }); + + const transformedData = updatedLayouts.reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); + + ChemSpectraFetcher.updateDataTypes(transformedData) + .then((message) => { + console.log(message); + this.fetchUpdatedSpectraLayouts(); + }) + .catch((error) => console.error(error)); + } + + fetchSpectraLayouts() { + ChemSpectraFetcher.fetchSpectraLayouts() + .then((layouts) => { + if (layouts) { + this.setState({ layouts: Object.entries(layouts.current_data_types), + defaultLayouts: Object.entries(layouts.default_data_types) + }); + } + }) + .catch((error) => console.error(error)); + } + + fetchUpdatedSpectraLayouts() { + ChemSpectraFetcher.fetchUpdatedSpectraLayouts() + .then((layouts) => { + if (layouts) { + this.setState({ layouts }); + } + }) + .catch((error) => console.error(error)); + } + + getLayoutOptionsAndMapping() { + const { layouts } = this.state; + const layoutsMapping = layouts.reduce((acc, [layout, dataTypes]) => { + dataTypes.forEach((dataType) => { + acc.push({ layout, dataType }); + }); + return acc; + }, []); + + const allLayouts = Array.from(new Set(layoutsMapping.map(({ layout }) => layout))).sort(); + + const layoutsOptions = allLayouts.map((layout) => ({ + value: layout, + label: layout + })); + + return { layoutsOptions, layoutsMapping }; + } + + render() { + const { + newDataType, showNewTypeLayoutModal, alertMessage, defaultLayouts + } = this.state; + + const { layoutsOptions, layoutsMapping } = this.getLayoutOptionsAndMapping(); + + return ( +
+ + + + + + {alertMessage && ( + + {alertMessage} + + )} + + + + New Data Type + + + +
+ + Data Type + + + + Layout + { cellLineDetailsStore.changeBioSafetyLevel(item.id, e.value); }} + /> + + +
+ ); + } + + renderAmount(item) { + const { cellLineDetailsStore } = this.context; + const { readOnly } = this.props; + const styleClassUnit = item.unit === '' ? 'invalid-input' : ''; + const options = [ + { value: 'g', label: 'g' }, + { value: 'units/cm²', label: 'units/cm²' }, + ]; + + const unitComponent = readOnly ? ( + {}} + /> + ) : ( + { cellLineDetailsStore.changeUnit(item.id, e.value); }} + onInputChange={(e, action) => { + if (action.action === 'input-change') { cellLineDetailsStore.changeUnit(item.id, e); } + }} + options={options} + placeholder="choose/enter unit" + defaultInputValue={item.unit} + /> + ); + + return ( +
+ + Amount * + + + + + {unitComponent} + + +
+ ); + } + + renderPanelHeaderIcon(panelName) { + const { openPanel } = this.state; + const arrowType = openPanel === panelName + ? 'fa fa-angle-double-down' + : 'fa fa-angle-double-right'; + return ( +
+