From f976908d7b671b517060853cc82b3251e0ef206b Mon Sep 17 00:00:00 2001 From: PiTrem Date: Tue, 6 Feb 2024 08:36:47 +0100 Subject: [PATCH] merge squash main / fix btn conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit 97cf66f0ecd7427a4960d6b2d5c078a5a3e3b6e0 Author: Mehreen Mansur Date: Thu Feb 1 15:21:06 2024 +0100 fix: export research plan error on docx format (#1718) arg not accepted since gem upd (ComPlat/chemotion_ELN@f0d6f7b) also fix error on table input commit d78f48e89cc94cdd20c88f4bc47a347e46c8b094 Author: Mehreen Mansur Date: Thu Feb 1 11:28:37 2024 +0100 feat: add helpdesk link in header (#1713) commit 6a6c3aa6a16a849ed0fd748df704f3b814df4fb4 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Thu Feb 1 11:08:29 2024 +0100 fix: nmrium missing 'close with save' button in research plan Refs: #1715 commit 8a257e2489a230406c57bf82faab347465c61d31 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Thu Feb 1 11:07:07 2024 +0100 fix: focus lost on input bug for melting & boiling points fields Refs: #1716 commit dc3203f04edfd2eb565ac9ba836aba70afa719cd Author: PiTrem Date: Wed Jan 31 15:08:22 2024 +0100 fix: dfg logo resource in README.md (#1710) * fix: dfg logo resource in README.md * Update README.md commit 3097ba2006352109b247d6301ae8a2258060cc15 Author: PiTrem Date: Wed Jan 31 13:28:09 2024 +0100 feat: Inbox device folders named with the device fullname (previously only firstname was used) db migration to rename Inbox-device folders Refs: #1709 commit 1f3d0b52353eb4a2f0cbf7c4c51ddd1eceb6e067 Author: Pei Chi Huang Date: Wed Jan 31 12:13:53 2024 +0100 feat: filter jdx files to be processed by converter-app prevent chemspectra generated jdx files to be processed by chemotion-converter-app jdx reader Co-authored-by: Chia-Lin Lin Refs: #1712 commit 5daa9eb5e84fa244e32de29615316739233f9724 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Wed Jan 31 09:28:55 2024 +0100 feat: enable nmrium in read only collection (#1708) commit 587ee7e1248c10ea83a35bfb7a6c89eb3a75f32e Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Wed Jan 31 08:46:50 2024 +0100 feat: input field for general remarks on all sample analyses Refs: #1696 commit affe293585209b7ff0bdac024f2a4990a23e693b Author: Pei Chi Huang Date: Tue Jan 30 16:46:19 2024 +0100 fix: data cannot be removed from segment of element https://github.com/LabIMotion/labimotion/releases/tag/v1.1.2 Co-authored-by: Chia-Lin Lin Refs: #1711 commit d6f881903c5300129b6a44dfbd7d41acce183b29 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Tue Jan 30 16:22:56 2024 +0100 feat: label detector in SEC spectra * label detector in SEC spectra * update chemspectra to v1.1.1 Refs: #1691 commit c83ed704d6eb3232bc0a841a91b69672e7256042 Author: Christian Buggle Date: Tue Jan 30 08:39:41 2024 +0100 feat: Add models VesselTemplate, Vessel, CollectionsVessel * Add models VesselTemplate, Vessel, CollectionsVessel, Minimal models as required as a common base for the upcoming ReactionProcessEditor and upcoming features in ELN * Add migration and models for VesselTemplates, Vessel, CollectionsVessel. * Add gem ‘shoulda-matchers’ in environment :test. * Add shared_example :acts_as_paranoid_soft_deletable_model, include in the specs of all affected models. * Enable extension `pg_crypto` in schema.rb as this should be there from earlier migration 20220712100010_add_segment_klass_identifier.rb. * Use well defined ORD constants for vessel_type, material_type. * Add created vessel to user’s collection. * Make idempotent, do not run if vessel name exists. * Add weight, barcode, qrcode to Vessels * Add weight_amount, weight_unit to VesselTemplate, delegate in Vessel. * seeds for VesselTemplates & Vessels --------- Co-authored-by: nh9378 Refs: #1548 commit 318fd345d60a2d0419bcf0faf5c2ce058f4ad8af Author: TasnimMehzabin Date: Tue Jan 30 08:35:06 2024 +0100 feat: sort the device list by name in command_n_control (#1707) Co-authored-by: Tasnim Mehzabin commit 4ac32c73d2d004962afca7e4fe113ff12e667b9d Author: Pei Chi Huang Date: Thu Jan 25 17:32:34 2024 +0100 feat: upgrade converter to v1.2.0 (#1704) commit 89db312b00f4e3cd3b532c2347411a220f2e56c5 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Jan 24 12:36:22 2024 +0100 chore: Bump puma from 5.6.7 to 5.6.8 (#1679) Bumps [puma](https://github.com/puma/puma) from 5.6.7 to 5.6.8. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.6.7...v5.6.8) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit f0d6f7b90e114eb999d81918fa350ea65e9eaadd Author: Christian Buggle Date: Wed Jan 24 11:22:17 2024 +0100 chore: missing constant MIME::Types * Fix missing constant MIME::Types * Add gem ‘mime-types’ to Gemfile: in preparation to ketcherails update: latest ketcherrails which no longer carries a dependency to `paperclip -> mime-types`. * patch farraday Refs: #1660 commit 29a861853b3313d096f46613f410bf9a76a65ad9 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Wed Jan 24 10:30:07 2024 +0100 fix(UI): sample entry label alignment (#1693) commit d60d9f7958b14bca76266e763b94d8663c7e4e49 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Wed Jan 24 10:27:08 2024 +0100 fix: amount change of a reaction product from the sample properties tab * fix: bug for amount change of a reaction product sample from sample properties tab, does not render the change in the reaction scheme after saving sample change * assignAmountType when sample benlongs to reaction Refs: #1692 commit 5eedcef0bafff684abda425d9a297beac5968bb1 Author: Lan Le Date: Wed Jan 24 09:21:27 2024 +0100 feat: update react-spectra-editor to display theoretical mass value Co-authored-by: Lan Le Refs: #1675, https://github.com/ComPlat/react-spectra-editor/pull/186 commit c69bbf00bb5a8c9bd6614eb979f1293a172ecbc8 Author: Jan C. Brammer Date: Wed Jan 24 09:03:36 2024 +0100 feat: Add `variations` attribute to reaction report settings (#1697) commit e51e89d8d3f960aee5bcfdc989cb061198063af3 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Tue Jan 23 08:56:54 2024 +0100 feat: add sample inventory label counter add inventory model and inventory label feature Refs: #1581 commit dffea5bb8219ed49ce2c6cc4f2b82c5e1e1d28c1 Author: Jan C. Brammer Date: Tue Jan 23 08:52:55 2024 +0100 refactor: extract` SpectraEditorButton` to dedicated component * Extract` SpectraEditorButton` to dedicated component * Pass missing `element` parameter to `SpectraEditorButton` Refs: #1664 commit 41928e8fe5fbd4b62b169a31e63106c639682ea2 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Tue Jan 23 08:51:29 2024 +0100 feat: group analysis attachments attachments are grouped according to their original/processed status added better thumbnail preview Refs: #1674 commit f8362ac9aa69fc49f4a19ab8e8bd0f25e5ab6d57 Author: Jan C. Brammer Date: Tue Jan 23 08:48:39 2024 +0100 fix(UI): show Tooltip on + button in reaction-variations tab (#1694) commit 16f75929f91b3f233b4c3253db927ad7940eaef0 Author: Pei Chi Huang Date: Thu Jan 18 15:07:30 2024 +0100 fix(int): zip upload Co-authored-by: Chia-Lin Lin Refs: #1690, #1688 commit 9a765180670bd9c737630ac31668786adf6026aa Author: Lan Le Date: Thu Jan 18 09:52:35 2024 +0100 test: fix stub request in spectra jdx test Co-authored-by: Lan Le Refs: #1689, #1596 commit 1a93cf52ceac1eed19d2ffc50c62c4528531df0e Author: Pei Chi Huang Date: Mon Jan 15 15:51:43 2024 +0100 feat: converter metadata added to dataset download (#1688) Dataset-xls improvement - more information is provided in the Description sheet. See the documentation https://www.chemotion.net/docs/labimotion/guides/user/datasets/download Co-authored-by: Chia-Lin Lin commit 69ad0a9ad183a9d614489a1ea38a47f82895b6b1 Author: PiTrem Date: Mon Jan 15 13:13:29 2024 +0100 refactor: dry schmooze tools (#1684) common input parser for quill-to-html and quill-to-plain-text now handle empty delta to prevent (db/migration) errors ``` Schmooze::JavaScript::TypeError: only `insert` operations can be transformed! ``` commit 7097d40324fe40145ddaca8819e9e135b9eff236 Author: Lan Le Date: Mon Jan 15 11:12:20 2024 +0100 chore: update runner - fix text (#1683) * test: fix mock api for jcamp process * test: test rubocop action to use ruby 3.3 * chore: npx browserslist@latest --update-db * chore: yarn-audit-fix * chore: upd gitignore * chore: update CHANGELOG v1.8.1 * chore: update db/schema.rb --------- Co-authored-by: Lan Le Co-authored-by: PiTrem commit 5bee5403e369abd63a2fcb64ff7e46dcef98a9fd Author: Jan C. Brammer Date: Wed Jan 10 15:47:09 2024 +0100 fix: temperature conversion in reaction Refs: #1680 commit 4c7b2ff43a2025c7a92f4319b3d1181366e94010 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Wed Jan 10 15:45:24 2024 +0100 fix: remove duplicate user label and center the share button (#1682) commit 234fb88b0b6a28d7656b828ffa673764b18daf43 Author: Fabian Mauz Date: Wed Jan 10 09:08:50 2024 +0100 fix: tests for searching cell lines (#1678) commit a0c1b34399cc31fccd0263d1c7d194fb8dad4189 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Wed Jan 10 09:01:01 2024 +0100 feat: Table of data types and chemspectra layouts in the ELN Admin * feat: Add chemspectra admin for updating data type * datatype list managed on the ELN with default values from chemspectra is passed to chemspectra as arg. Refs: #1574 commit f8edcd6a61cae163d04a129f91ffe76ca6dc4fa3 Author: Fabian Mauz Date: Mon Jan 8 10:28:13 2024 +0100 feat: add new element cell line cell-line as a stand-alone element : user can manage cell-line samples in their collections. add 2 models: CelllineSample and CelllineMaterial (belongs to CelllineSample) export/import collection with cell-lines bioassay ontology for cell line analysis search of cell line by material/sample name Refs: #1582 commit 0be3cd98440476b86db207021f5675f74da3ca60 Author: Beate Quednau Date: Fri Jan 5 15:20:40 2024 +0100 feat: extend search * Add search modal * Add toggable panel for search and result at search modal forms * Add search result tab lists * Add search result tab content with pagination * Add advanced search form fields * Add element list filter to search result * Add handle safe for advanced search and ketcher * Add handle refind, adopt result * Add clear search and tab results, Set tab index for tab with results * Add order and group by molecule to search results * Clear search when clicking on collection * Add search by ids for search result tab pages * Add alert to remove search result * Add basic element selection for advanced search * Add search queries for reactions, wellplates and screens, and research plans * Add publication search * Add basics for generic element search * Add hr to generic element search for testing deployment * Add more search fields for reactions and screens * Add temperature and duration to reaction search fields * Add readout titles at wellplate search * Add quill to plain text and additional plain text field for description fields * Add description fields to search * Remove old advanced and structure search, Remove old generic search, cleanup * Add use cases for advanced search, search by ids, structure search * Add input-group, formula, table fields to detail search * Add better error messages * Add search results store and simple search results * Simplify visibility of search modal with mobx store * Add plain text content to containers, refactore fieldsByTabs * Add solvent fields to sample search * Add unit tests for advanced, structure and by_ids search * Add cypress test and identificator for search modal * Add klasses.json to gitignore * Add more descriptions for readme-dev * Fix ketcher rails search * Fix advanced search with multi search fields * Fixes for generic elements * Fix base fetcher for generic elements commit d7144d433d62287702f6bd83ae765a2f261d6b9f Author: TasnimMehzabin Date: Thu Dec 21 10:35:36 2023 +0100 fix: attached research_plans in screens not being imported from collection * fix: attached research_plans in screens not being imported from collection wellplates not being imported from collection * style: rubocop --------- Co-authored-by: Tasnim Mehzabin Refs: #1671 commit cc3930d079d7e958f158edc860d0626df5876660 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Wed Dec 20 11:36:15 2023 +0100 feat: Unified attachment list enhancing attachment list: applying new attachment list to Datasets, research plan and well-plate attachments * feat: sorting feat by name, date ( hide sorting functions if no attachments) * enabling style prop on component * refactor: Dataset Modal Redesign * refactor: converting inline styles to scss * Style: cleaner code and linking to new css classes * dataset modal discarding logic * unifying edited image warning text * style: eslint fixes and css enhancements * feat: strike attachment name when deleted * Removed discard btn for 1.9.0 --------- Refs: #1608 commit 324bc372f2acc34712a441ed045f4f3bda77994a Author: Lan Le Date: Wed Dec 20 10:53:30 2023 +0100 fix: remove original data from nmrium data before storing it Co-authored-by: Lan Le Refs: #1661 commit 63c4e32e0441656081e13088be6ce63b5af101d0 Author: PiTrem Date: Wed Dec 20 06:21:01 2023 +0100 fix: camelcasing attributes for proper display of SVGs * fix: camelcasing attributes for proper display of svgs due to the scrubber library lowercasing all attribute names some properties are not rendered in the browser. In this case beads with a gradient in molecule, sample and reaction were not displayed in browser and were also missing after conversion in png in doc report. (successiv gsub benchmarked as faster than gsub with regex and a dict) * upd yarn.lock Refs: #1670 commit 15768e0b30894011397285e350bcd3ad4c8bf902 Author: Lan Le Date: Wed Dec 20 06:00:40 2023 +0100 feat: add chemspectra with ref peaks (#1596) Co-authored-by: Lan Le commit 589e8b8bc46bc1d17c19f2af978b5368dcef0ad5 Author: Lan Le Date: Wed Dec 20 05:52:53 2023 +0100 fix(spectra): react-spectra-editor upd to correct molecule display with svg zoom pan Co-authored-by: Lan Le Refs: #1656, https://github.com/ComPlat/react-svg-file-zoom-pan/pull/44 commit df1a75b92b4f2ad3786a63810720dc81f152d080 Author: Johannes Haubold Date: Wed Dec 13 12:36:05 2023 +0100 chore: Improve Dev Setup by autorecognizing the installed tool versions (#1665) commit a1fdf19339ff1583779152af3e9f641b666e4452 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Tue Dec 12 08:21:01 2023 +0100 fix(UX): molecule title layout and element table header responsiveness * spacing at sample header * responsive element table header * eslint * remove gray color * remove unneeded margin right Refs: #1650, #1646 commit 1d259fadb8a17f34087dcc131adc774959a212c8 Author: Mehreen Mansur Date: Tue Dec 12 08:09:39 2023 +0100 fix: si-spectra report generation to work even without preview * serialize data for spectra si report on backend * add spectra report thumbnail image preview * update spectra worker method * update attachment api --------- Co-authored-by: Mehreen Refs: #1654, #1642 commit 016eab42b1ce350322285e1d8b3879678eb64d35 Author: TasnimMehzabin Date: Tue Dec 12 07:58:20 2023 +0100 feat: add the option to change the inbox sizing * feat: add the option to change the inbox sizing make the default inbox sizing smaller to col-md-4 * feat: hide the datetime when inbox size is set to 'Small' change small inbox size to col-md-2 change css for sort button in inbox modal modify the info message in the inbox for the sort button --------- Co-authored-by: Tasnim Mehzabin Co-authored-by: mekkyz Refs: #1645 commit b8cde5288c6829bce1f3accccf6d0f6dd8b8b1b1 Author: TasnimMehzabin Date: Thu Dec 7 08:46:27 2023 +0100 test: fix test with deviceBox sorting in inbox Co-authored-by: Tasnim Mehzabin Refs: #1657 , #1446 commit fed53fa6568642af069377ebe4ce4b78d70c37b9 Author: TasnimMehzabin Date: Tue Dec 5 15:55:56 2023 +0100 feat: sorting option for datasets and attachments in the inbox by creation-time or name * feat: modify the sorting in the inbox based on user selection (name/creation time) add test codes prevent the deviceBox from being closed when ordering is changed deviceBox not to change with the sorting options. They will always be fixed and sorted by the name unsorted files are sorted with the selected sorting attachments within the datasets are sorted with the selected sorting the sorting icon is modified the default inbox sizing is made smaller to col-md-4 tooltip texts are directed downward --------- Co-authored-by: Tasnim Mehzabin Refs: #1446 commit 3752367b14fadb8f090b62e0c63a97bb3990c53e Author: Lan Le Date: Tue Dec 5 15:28:23 2023 +0100 fix(spectra): order of J value Co-authored-by: Lan Le Ref: #1649, https://github.com/ComPlat/react-spectra-editor/pull/179 commit a87201224d7a2cc88ab9f75342c308ad3295dd8c Author: Lan Le Date: Tue Dec 5 15:26:15 2023 +0100 fix(spectra): correctly trigger action spinner when saving peaks to avoid race condition Co-authored-by: Lan Le Refs: #1651 commit d503a4c5fa135b808bb80c77ef82d310cb40d2e8 Author: TasnimMehzabin Date: Wed Nov 29 14:34:32 2023 +0100 fix: reaction sort column value not being persistent for updated_at column Co-authored-by: Tasnim Mehzabin Refs: #1643 commit 97fcdd69aea585f94ac6ff62103968f8c519f181 Author: Mehreen Mansur Date: Wed Nov 29 12:47:59 2023 +0100 feat: show research plan links in reaction (#1575) * add research plans linked to reaction * update research plan api --------- Co-authored-by: Mehreen commit baadf31dc2aea0407de05cb188d7e2a15ccd306d Author: Lan Le Date: Wed Nov 29 11:10:24 2023 +0100 fix: update chemspectra client to prevent crash on CV layout Co-authored-by: Lan Le Refs: #1637, https://github.com/ComPlat/react-spectra-editor/pull/177 commit 93f8dce78cd647cec3adb653fb7093cadecc1e74 Author: Fabian Mauz Date: Wed Nov 29 11:07:12 2023 +0100 fix: collection management right click on the add button to not drag things around * fix: made root collection undraggable in MyCollections * style: changed css style of root collections * fix: made root collection in MySharedCollections undraggable * fix: made root collection in Shared-and SyncronizedWithMe undraggable * fix: made root collection in CollectionTabs undraggable * style: add some space left to the tree * style: add active marking also to root collection * fix: suppress drag with root buttons at MyCollections * fix: suppress drag with root buttons at MySharedCollections refs: #1639, #670 commit 5e3d9814e0761b6617307f961c9f5244033fb4f4 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Fri Nov 24 10:40:14 2023 +0100 feat: add volume field in inventory tab (#1613) * add volume field in inventory tab * fix: apply conditional check for p-statements in chemicalTab * fix: verify fetched pictograms from merck commit cf7e0b077ab652a3f02ea7c7969b6c6b629baed1 Author: StarmanMartin Date: Tue Nov 21 22:32:04 2023 +0100 feat: Changed Mail collector rules * Changed Mail collector rules: Attachment e-mails... - can be sent to multiple chemotion instances - can be sent to chemotion user in 'cc' or 'to'. The e-mail addresses in to and cc are treated equally - can be sent to different e-mails. All emails in "cc" or "to" belonging to a registered user will trigger the creation of an attachment. All non-registered e-mails are ignored. Additionally, if an e-mail throws an error the collector keeps running. -> mailcollector line 25 * style: rubocop -A Refs: #1566 commit fa19ed25769df52c273037f4eb291c41a4322076 Author: PiTrem Date: Tue Nov 21 21:53:06 2023 +0100 chore: upd node engine for dev container (#1635) * chore: upd node engine for dev container * feat: enable sentry monitoring for delayed_job * chore: upd dockerignore * ci: update runner image * ci: move faker to global group of Gems to allow user seedings in stage env commit 9876a63639436999eb9975e5a10541acb9a72b78 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Mon Nov 20 17:05:02 2023 +0100 fix: allow import of molecule_name on sample import for xslx format * fix: allow import of molecule_name on sample import for xslx format * add spec test for importing molecule names on sample import Refs: #1598 commit b7f13f4179ad0cf5e31ada00780faa59e59f427c Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Nov 20 16:32:30 2023 +0100 chore: Bump rmagick from 5.0.0 to 5.3.0 Bumps [rmagick](https://github.com/rmagick/rmagick) from 5.0.0 to 5.3.0. - [Changelog](https://github.com/rmagick/rmagick/blob/main/CHANGELOG.md) - [Commits](https://github.com/rmagick/rmagick/commits) --- updated-dependencies: - dependency-name: rmagick dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Refs: #1609 commit e3b90c636b0b1cd419f31b59ea591abafb9c9c50 Author: Lan Le Date: Mon Nov 20 16:30:21 2023 +0100 fix(spectra): Add/remove multiplicity peak buttons(#1630) update react-spectra-editor to v1.0.0-rc20 https://github.com/ComPlat/react-spectra-editor/pull/175 Co-authored-by: Lan Le Refs: 1630 commit 8b8690d8646a9707ee8e40026b213cebbbbfa1f7 Author: Lan Le Date: Mon Nov 20 16:17:20 2023 +0100 fix(spectra): remove blank line when saving peak Co-authored-by: Lan Le Refs: #1629 commit e559183511e644df48ec363da9186dcc36f6eb69 Author: Pei Chi Huang Date: Mon Nov 20 15:56:32 2023 +0100 feat: upgrade-converter-to-v1.1.1 https://github.com/ComPlat/chemotion-converter-app/releases/tag/v1.1.1 Refs: #1634 commit 1e43e364e7c4a4d79ac6b3ce0231ccb1890ca13f Author: TasnimMehzabin Date: Fri Nov 17 12:33:07 2023 +0100 fix: the attachment to be removed from the inbox when assigned to a sample (#1631) Co-authored-by: Tasnim Mehzabin commit a31a0ffdf84c81e6674b942f4272410431876073 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Thu Nov 16 08:54:43 2023 +0100 feat: include chemicals with import and export of collections (#1604) commit d6c6c9609544343e3d326c086d9c4078567a3778 Author: Pei Chi Huang Date: Thu Nov 16 06:41:31 2023 +0100 feat: drag samples and elements to segment (#1623) commit c1e26a2fb8970fbf3ac54c1e40170e98f89effe2 Author: PiTrem Date: Thu Nov 16 06:40:34 2023 +0100 feat: display mail collector address as info in the Inbox (#1529) send mail collector address to client if mail collector is configured (the last registered alias over the ui api) add info button for collecting file per email in inbox modal click to copy collector address to clipboard --------- Co-authored-by: nh9378 commit 09ec1599c0a17ec153b0fdc3941178f85c79a7ee Author: Lan Le Date: Tue Nov 14 12:29:32 2023 +0100 feat(spectra): update chemspectra backend to read some jcamp v6 (#1603) Co-authored-by: Lan Le commit 8203debbcfa84f7efb7d06021da63421b03e737a Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Tue Nov 14 12:28:20 2023 +0100 feat: report peaks from XRD (#1614) commit da0074b4e3a585baf37698f8e6015cf624de7091 Author: Pei Chi Huang Date: Tue Nov 7 12:30:53 2023 +0100 feat: better grouping of decoupled samples in list (#1612) commit ab9f67a5c4ea265edcf5ae7396a160676d067fdc Author: Lan Le Date: Mon Nov 6 17:36:08 2023 +0100 fix: polymer bead not visible in reaction svg update react-svg-file-zoom-pan to version 1.1.2 so that the polymer bead representation is visible on the reaction preview and detail images. Co-authored-by: Lan Le refs: #1607 commit be8e927e95f7f7383638e688ed3fa4e742a19986 Author: TasnimMehzabin Date: Thu Nov 2 10:15:17 2023 +0100 feat: add the option to sort reaction list by updated time (#1461) * feat: add the option to toggle reaction sort direction * fix: update the filter message in the elements table list --------- Co-authored-by: Tasnim Mehzabin commit c61c86a162270ff0c3a2ac5d4806d23ed40bed18 Author: TasnimMehzabin Date: Fri Oct 27 10:51:03 2023 +0200 feat(UI): file size is listed in the analyses tab Refs: #1601 Co-authored-by: Tasnim Mehzabin commit 6f1afba3a0861363e18962df96128d478647989a Author: TasnimMehzabin Date: Thu Oct 26 13:34:19 2023 +0200 feat(UI): remove redundant inbox section from the collection listing on the left panel - leave the left panel for collection browsing - Inbox is now only, but clearly, called from the top menu bar Refs: #1593 Co-authored-by: Tasnim Mehzabin commit 84c02bced038ef307abb67393a86a3fe71f8bb35 Author: Pei Chi Huang Date: Wed Oct 25 09:48:20 2023 +0200 feat: converter trigger on attachment inbox items Refs: #1583 commit 75fde46a36618f7c887408ff11628bf865a5150f Author: PiTrem Date: Tue Oct 24 22:20:11 2023 +0200 chore: update VERSION 1.8.0 chore: update ci image chore: minor node upd chore: upd changelog chore: .gitignore do not track doc/ Refs: #1576 commit 7bd6a5d811aa6cce2d702418fa502dc3e90f9509 Author: Pei Chi Huang Date: Tue Oct 24 11:52:45 2023 +0200 fix(UI): reaction list display break when reaction status not standard Rendering of Overlay component break UI when a reaction status goes to the switch default case. For example on shared reaction with limited permission (reaction.status = "***") Refs: #1592 commit 2659436039ac49e87720a09833efdeb330c60c9b Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Oct 24 10:53:22 2023 +0200 chore: Bump @babel/traverse from 7.16.10 to 7.23.2 (#1580) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.16.10 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 48aef3b04ae3aca429755d4da6f9550237f12e00 Author: Pei Chi Huang Date: Tue Oct 24 10:46:19 2023 +0200 feat: expand calendar function to generic element Refs: #1585 commit 3ef1129914dfb73b32d54b1a52cbd7b65009d7e7 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Tue Oct 24 06:58:59 2023 +0200 fix: reaction calculation when no reference material present fix break introduced with 40bc206fb0 Ref: #1589 commit 5f6d6ad50a698903825fa398f842286020dd78eb Author: Pei Chi Huang Date: Mon Oct 16 09:27:08 2023 +0200 chore: update labimotion - converter ui (#1578) fix: converter app call not triggered commit 6b1438478040eb356be4dcc80152d3c7b3f965a4 Author: Lan Le Date: Tue Oct 10 10:11:06 2023 +0200 fix: hide spectra button when only uploading an image (#1568) Co-authored-by: Lan Le commit 10ef1f11cd39750ccdb72df4152dfedf6af56c44 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Tue Oct 10 10:10:05 2023 +0200 fix: assign only boolean values for decoupled column when importing samples (#1571) commit 894306a126f6f1546825235eee256dc51ffd8316 Author: PiTrem Date: Tue Oct 10 08:49:55 2023 +0200 fix: Admin seed: ensure exisiting Admins have a profile (#1572) - fix: navigation/call to user-profile depending page/action - fix: README links to docs (trailing '/' routing) commit e2ee14b1d824a03a203920947965da8498980e4b Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Oct 10 08:48:02 2023 +0200 chore: Bump @adobe/css-tools from 4.2.0 to 4.3.1 (#1511) Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.2.0 to 4.3.1. - [Changelog](https://github.com/adobe/css-tools/blob/main/History.md) - [Commits](https://github.com/adobe/css-tools/commits) --- updated-dependencies: - dependency-name: "@adobe/css-tools" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 3de16fa33e1bdb7eaba46c3f793fd346aca7ac83 Author: PiTrem Date: Mon Oct 9 16:08:16 2023 +0200 fix: rollback labi version up (#1570) commit 0781e5cda2d2b25a430193018ea5cf46d22799e3 Author: PiTrem Date: Mon Oct 9 13:59:49 2023 +0200 prep v1.8.0-rc2 commit c4200dffad7d30f20aa0b7cb1654ba21cc11c63b Author: PiTrem Date: Mon Oct 9 13:53:52 2023 +0200 chore: minor dep updates (#1569) * chore: minor update sentry * fix: typo env var for sentry monitoring * chore: update service dep * fix: downgrade labimotion to prevent converter process early exit * chore: yarn audit fix commit d55520d512d40502b455b54d39c912ef54c0facb Author: Lan Le Date: Mon Oct 9 13:48:18 2023 +0200 feat: update chemspectra to change value of referent solvent (#1557) Co-authored-by: Lan Le commit bebdec0eea8170f21a36e97f64756b0aaf84a279 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Mon Oct 9 13:05:19 2023 +0200 fix: sample properties tab (#1503) * Fix: style issue for boxes in sample * Enhancement: hide labels when there is no solvent * Decoupled checkbox padding * test: add test for SampleSolvent rendering * test: add test for SolventDetails rendering --------- Co-authored-by: FabianMauz commit be0bcd83da967882e3e912f2d9ed58ccf302caf6 Author: Lan Le Date: Mon Oct 9 12:45:08 2023 +0200 feat: show nmrium button for reaction and research plan (#1471) Co-authored-by: Lan Le commit ee881fefd4ce37da533db58a325687c0320e71fa Author: Jan C. Brammer Date: Mon Oct 9 12:35:31 2023 +0200 feat: update reaction variations (#1567) * Rename variable for consistency * Use React Bootstrap buttons * Identify variations with sequential ID * Add vertical space around table tools * Remove uncessary guard commit 3c0b1e4de6af5a3870ce6c4f6eac2947749cbf40 Author: Fabian Mauz Date: Fri Oct 6 10:53:09 2023 +0200 fix: no attachments after research plan save * fix: add Attachment Fetcher as uploader for attachments * fix: upload attachments via AttachmentFetcher * fix: add needed parameter for next promise step * fix: upload attachments in genericElsFetcher only if no researchplan * fix: create multiple attachments on researchplan create * test: add tests for getting attachments of researchplan * fix: removed deprecated thumbnail job * fix: WIP map identifier to attachment * fix: removed index from loop * fix: add identifier to params * refactor: use loop with index instead of own index variable * todo: remove uploading of attachments via GenericElsFetcher and do the same for update usecase Refs: #1564 --------- Co-authored-by: nh9378 commit c1b45ae4105d2d19e74b88a5761cb41eb873e128 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Thu Oct 5 19:07:28 2023 +0200 feat: Import export sample as chemical * implementation for import chemicals to collection, added import_chemicals class and spec tests and refactored import_samples class to add import_type option to import samples * refactor construct_p_statements and construct_h_statements methods in chemical_service to solve duplication issue and refactored chemicals_service_spec tests accordingly * handle failing of import chemicals (sample will not be created, user will be notified which samples could not be imported, refactor import_chemicals and spec tests * allow import cas field on sample import for xlsx format * import chemicals: allow skipping import of chemical field if column header is null * refactor report_api code for exporting samples and chemicals * refactor code of export chemicals in report_helpers module into own class and improve export functionality of chemicals * write unit tests for ExportChemicals class * disable sdf format option for chemicals export and adjust exportModal height * refactor ExportImportButton component * refactor report_helpers module and import_samples_spec to fix failing spec in report_api_spec and import_samples_spec * allow import of decoupled samples * allow import & export of decoupled samples * allow SDS search for chemical when molecule does not exist (for decoupled samples) * allow import of merck safety sheets on import chemicals * allow import of float amount values for import chemicals * allow sample import with case insensitive values of decoupled column Refs: #1524 commit 77a091b7689af3731ae356791794cfc081eac3f4 Author: Lan Le Date: Thu Oct 5 19:02:52 2023 +0200 fix: add filter for NMR kind check if NMR CHMO entry is an ancestor Co-authored-by: Lan Le Ref: #1563 commit 10d35642ccb7e11b448fd6f06164171bf75b2d7f Author: TasnimMehzabin Date: Thu Oct 5 08:40:55 2023 +0200 feat: add a checkbox for dry solvents in the solvents section in the reactions table * add a checkbox for dry solvents in the solvents section in the reactions table add option to mark sample as dry solvent * refactor the code to fix rubocop warnings * allow import of dry_solvent attribute in import samples for xlsx and sdf formats * adjust dry-solvent element width in sampleForm component Refs: #1432 --------- Co-authored-by: nh9378 commit 1795b4b0729e0aea14909438099db628baf20c9c Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Thu Oct 5 08:36:14 2023 +0200 fix: load cas for molecules * fix loading cas when using pubchem service Refs: #1555 commit 1211fd6b0694e4485af951742b5aba037a296916 Author: TasnimMehzabin Date: Thu Oct 5 07:55:25 2023 +0200 fix: current_user.matrix getting null value Refs: #1554 commit c5c7b2faf4c7161d6e52dc4bd75ea976e1f54e4f Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Thu Oct 5 07:54:43 2023 +0200 fix: show example reaction label in settings page Refs: #1556 commit 06aa179b1fea53dc393cfc93877c69369e369509 Author: Jan C. Brammer Date: Thu Oct 5 07:52:47 2023 +0200 feat: update reaction variations * Use `name` as default material identifier * Allow zero ('0') values * Extend tooltip * Make inclusion of `variations` in report optional * Guard against missing material types * Allow any precision for numerical input Refs #1561 commit e73d76ed36e5ba214d9033c709b0d25fa210c7f6 Author: TasnimMehzabin Date: Wed Sep 27 15:56:52 2023 +0200 fix: comment fetch issue on new entities Refs: #1547 commit 9992a85878d4fb272b4fca9639b8d35f3c93f3f6 Author: Lan Le Date: Wed Sep 27 15:54:56 2023 +0200 chore: update chemspectra backend version to display label cv layout Co-authored-by: Lan Le Refs: #1546 commit ba6c1d9af0334da5a86c23d14e64714e0fe3a7f7 Author: Lan Le Date: Wed Sep 27 09:36:41 2023 +0200 feat: display combined image as preview if it exists fixed: fix UI issue on CV layout Co-authored-by: Lan Le Refs: #1526 commit 1a9fc9e730f24ffed42ef928b3938693a47e47ff Author: PiTrem Date: Fri Sep 22 13:23:25 2023 +0200 fix: wrong conflict resolution (#1542) commit fc4bf31e40400f43c88604a36396b886f6ef073e Author: PiTrem Date: Fri Sep 22 09:00:35 2023 +0200 test: update runner image (#1541) commit 154e18cea48ddd3b4c1c7b45623d16d501ca59a9 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Thu Sep 21 15:06:56 2023 +0200 test: "yarn test" errors & warnings * fix: removed useless prop "bsSize" * fix: added polyfills for unsupported browsers * fix: used instance of attachement * fix: disabled lifecycle update for testing * fix: added attachments as prop * Refactor: stub ChemicalFetcher.create & eslint fix * Fix: yarn test errors for ChemicalTab Refs: #1523 commit 4bd661989e4e1d98f8b0fee733794f0e20197299 Author: PiTrem Date: Wed Sep 20 07:19:47 2023 +0200 chore: prep v1.8.0-rc1 commit 98665123f0ab527ee71537eef2e0b3c43c9d1d69 Author: Pei Chi Huang Date: Wed Sep 20 07:12:07 2023 +0200 feat: LabiIMotion Integration * Generic Element * Generic Segment * Generic Dataset Refs: #1504 --------- Co-authored-by: Claire Lin commit 728e95ff4388fd9cb34e2bd6b582e1fd341e952b Author: PiTrem Date: Wed Sep 20 07:04:50 2023 +0200 fix: assets precompilation css issue ``` SassC::SyntaxError: Error: "var(--ag-internal-calculated-line-height)" is not a number for `min' on line 2381:11 of stdin, in function `min` from line 2381:11 of stdin >> height: min(var(--ag-internal-calculated-line-height), var(--ag-internal-p ``` Refs: #1538 commit 1b540e188ca8da89f71a4bcecd9b70cb0e65099f Author: Jan C. Brammer Date: Wed Sep 20 07:01:01 2023 +0200 feat: Reaction Variations * Add variations tab to reaction detail modal * Update `ag-grid` JS dependencies * Correctly deconstruct `material.amount` * Try using material labels as column headers * Extend example data structure for clarity * Show cell entries with three decimal places * Individualize reference material per row Make reference material immutable * Add auxiliary properties to variation materials * Compute yield * Update style * Update variations object on edits * Auto-size columns with min width * Add tooltip * Fix initialization of variations * Move variations-related logic into utils * Rename `utils` to `ReactionVariationsUtils` * Add tests for reaction variations * Remove toggeable material groups * Make unit selection more explicit * Don't cache reaction * Write reaction variations to .docx report * Consistently set product unit to `Amount` * Properly anonymize variations * Don't compute yield if `referenceMaterial` is missing * Remove redundant entry from entity definitions * Add `ReactionVariationEntity` * Use sub-entity for material aux * Make RuboCop happy * Make rows sortable by columns * Make rows draggable * Add row ID to tool-column * Make columns resizable * Prevent sub-columns from separating on dragging * Increase number of decimal places * Make material identifier configurable * Factor out `convertTemperature` * Make units configurable * Add column for solvents * Refactor: parametrize material types * Enforce specific units per material type * Add editable equivalents * Add molar mass to hover-over * Revert unused export Refs: #1409 commit 27b7dcde95de5d6af6e31e038243950bbbc78822 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Wed Sep 20 06:59:25 2023 +0200 Feat: filter options for admin user management * Eslint fixes * Feat: filter options for admin user management Refs: #1510 commit 0c39f7b4e7585c2058047f047ebc73a69487c441 Author: Johannes Haubold Date: Wed Sep 20 06:54:49 2023 +0200 feat: Move sample task inbox to header bar Refs: #1517 commit dc4283bbd6c15d12fd27fc42f157eb465e29d882 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Wed Sep 20 06:54:03 2023 +0200 feat: Enhance import samples for sdf * refactor export of sample solvent to export solvent name(not the solvent hash)- refactor export samples for sdf for melting and boiling points columns and write import sample job for sdf format and refactor and write import samples jobs spec tests for sdf format * fix typo & refactor boiling and melting points params in sample_api_spec test * add density column to import samples with sdf format and refactor sample_api_spec file * improve extract solvent and other functions for sdf export Ref: #1364 commit fc4d28661c4b4b4550fea71543dee1d9b1e73bf7 Author: PiTrem Date: Wed Sep 20 06:48:58 2023 +0200 chore: prep v1.7.3 (#1537) commit ff9b54715e5a7620609593996dff6a56eaf2ea02 Author: PiTrem Date: Tue Sep 19 12:57:43 2023 +0200 feat: update ext links in the Navbar menu dropdown - update links (added docu, removed kit/complat) - DRY - also added external-link icon to make clear the link is leaving the current domain. Refs: #1534 commit 605cea9750f031f2864d2e78ddd268d5b2f37181 Author: PiTrem Date: Tue Sep 19 09:12:22 2023 +0200 fix: reaction sort column default to created_at (#1533) After updating (from1.7.2 to efecaf8a9c), the user profile does not have yet filter.reaction. The default reaction sorting in the UI is 'list' (should be `created_at` descending) but the client will sent sort_column=updated_at instead of created_at thus confusing the user. thorough resolution should come with PR #1461 commit 38445b80af45de3df2f0b76c32c660a17f68ffe9 Author: Jan C. Brammer Date: Mon Sep 18 14:52:43 2023 +0200 chore: Add Cypress dependencies to Dockerfiles * Add Cypress dependencies to CI Dockerfile * Add Cypress dependencies to Dev Dockerfile Refs: #1491 commit f3e010eaf23e0074803f6379c04ea1055741af12 Author: Johannes Haubold Date: Mon Sep 18 14:48:33 2023 +0200 feat: Show sample name in SampleTask Api * Show sample name in SampleTask Api * Fix rubocop issue * Fix spec Refs: #1518 --------- Co-authored-by: Matthias Döring commit 40bc206fb0aeff7c6aad6bdec75259af53fad571 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Mon Sep 18 12:01:33 2023 +0200 fix: yield percentage error for reactions with decoupled products and disable error message for decoupled samples Refs: #1531 commit d97121d94538a4357e53f2308f56e51c18ed5e41 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Fri Sep 15 16:33:58 2023 +0200 feat: Add select all option for device inbox folder * Add (de)select all for datasets in device box * select all attachments in a dataset * separate unsortedbox and devicebox checkedIds * correct deleteCheckedDataset function * reset checkedDeviceIds when changing pages * move checkedDeviceIds to InboxStore * handle dataset attachments selection in inboxstore * update handlePrev / NextClick fix: pagination fix: deleteCheckedDataset to delete attachments Refs: #1437 commit 823c5116c0a3a1c566a775946a9f8acfae1eb1b7 Author: Lan Le Date: Fri Sep 15 16:30:04 2023 +0200 feat: update version of chem-spectra-app to handle FL datatype fix: cannot read processed data from Bruker Co-authored-by: Lan Le Refs: #1528 commit 74215ebd0dceac135e845933c5dfc53021e35e30 Author: Lan Le Date: Fri Sep 1 12:00:45 2023 +0200 fix: check the nmrium file before ignore generate commit d8ba70f0743cfe99bbaba2591ed9fbbb4e47947e Author: Lan Le Date: Mon Aug 28 14:37:19 2023 +0200 feat: update chemspectra frontend to fix bugs commit 03b28ce207b5de6e29582550efc0d5467ba9bf0a Author: Lan Le Date: Fri Aug 25 13:16:47 2023 +0200 fix: ignore predictions when it is null Co-authored-by: Lan Le Refs: #1507 commit 81ad5d081112111bd38f72ac0042bb0fa7ba80be Author: Fabian Mauz Date: Fri Aug 25 12:34:10 2023 +0200 fix: fixed wrong literatures mapping Refs: #1506 commit e29301f949fba54748b70116b0016b698cc0f60b Author: Fabian Mauz Date: Fri Aug 25 10:46:35 2023 +0200 fix: patch citationjs to process doi-sici * build(deps): bump citation-js to 0.6.8 * build: remove library patch in post_install script * feat: patch citation-js library to accept also SICIs Refs: #1486 commit 54871a323cbad54d0d5557f1f9beac5550365387 Author: PiTrem Date: Fri Aug 25 10:45:02 2023 +0200 chore: upg nodejs LTS to 18 * chore: update node LTS to 18 * ci image with upg node version * test(js): force mocha to exit * chore(js): upg whatwg fetch 2->3 * chore(js): upg test dependencies: jsdom, mocha, sinon * devcontainer --------- Co-authored-by: Jan C. Brammer Refs: #1489 commit 37acd1675050e0b76b4e0bd3e738e8dd224f1c5f Author: Fabian Mauz Date: Fri Aug 25 10:32:17 2023 +0200 fix: deletion of literature * fix: fixed reaction to response from the literatures api * style: apply eslint / rubocop rules * refactor: use UrlSearchParams instead of string concat * refactor: extract request params * feat: add ui response after removing literal * style: line length and correct isNaN usage * style: use deconstruction * test: add test for deleting literature * refactor: refactored and styled tests Refs: #1502 commit 24bf00c515fc85678b85555d4ea215bb01c80ee7 Author: TasnimMehzabin Date: Thu Aug 24 11:28:05 2023 +0200 feat: add more reagent to the reaction table reagent dropdown * add new values to reagents list * add new values to solvents list * add new purification method in Scheme tab of reaction * refactor the code to fix linting issues Refs: #1433 #562 commit 4aa09febbd9bd7d056b4b6f254c1c11b2d7429f2 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Aug 24 10:23:38 2023 +0200 chore: Bump puma from 5.6.5 to 5.6.7 Bumps [puma](https://github.com/puma/puma) from 5.6.5 to 5.6.7. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.6.5...v5.6.7) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Refs: #1488 commit 4e781496197cc604800b4470fcff3b872902e13e Author: TasnimMehzabin Date: Thu Aug 24 08:54:04 2023 +0200 fix: inbox UnsortedBox issues * fix: inbox UnsortedBox issues * fix the issue with attachment deletion in UnsortedBox.js * fix the UnsortedBox section closing on file upload * fix: add draggable cursor icon for the inbox header Refs: #1447 commit b812b48f098f2c0c64f945087a88d6645589c2d6 Author: Lan Le Date: Thu Aug 24 08:52:12 2023 +0200 fix(spectra): sorting multiplicity values Co-authored-by: Lan Le Refs: #1478 commit 9ea494f22a6602692cd3dfae6fd59a0634e6bdc4 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Thu Aug 24 08:51:11 2023 +0200 fix(spectra): prevent regenerating spectrum and duplicating jdx fix: fix duplicated png peak file Refs: #1479 commit 79cc12b50f8b7afc14046c6e6fe4a73b2426cfe0 Author: Johannes Haubold Date: Tue Aug 22 10:36:44 2023 +0200 fix: replace toSorted with manual sorting in SampleTaskInbox Refs: #1485 commit 5d2cf58096137571fe83c81ff247952f89b7f790 Author: Lan Le Date: Fri Aug 18 09:53:10 2023 +0200 feat: update version of chemspectra Co-authored-by: Lan Le Refs: #1480 commit 0735f723302fb5cd12cb43abafad581a91e631ae Author: Lan Le Date: Fri Aug 18 09:51:13 2023 +0200 chore: update information of chem-spectra-app Co-authored-by: Lan Le Refs: #1484 commit f75d55439f856b4f7e2dfffee3d2fbf2f2bcd4f6 Author: Lan Le Date: Thu Aug 17 15:57:35 2023 +0200 fix: hide NMRium button on non-NMR layouts Co-authored-by: Lan Le Refs: #1460 commit aa30d087c328af7d210074bd671fd3b0c9375892 Author: Lan Le Date: Wed Aug 16 14:52:48 2023 +0200 fix: 2D to work with new nmrium wrapper version feat: update function to save content for layout 15N, 29Si and 31P Co-authored-by: Lan Le Refs: #1436 commit 8347fe8e145aa56773725e78626693bd5a727329 Author: Mehmood Ghaffar Date: Wed Aug 16 13:19:20 2023 +0200 Fixed Cypress Tests (#1481) * create user test and researchplan extended test are added * collections api spec is now added * deleted unnecessary comments from collection_api_spec.cy.js * Remove redundant API test for now (conceptually replicated RSpec) * fixed failing test cases due to changes in chemotion ELN * fixed failing test and removed typos --------- Co-authored-by: Jan C. Brammer commit 412899294a67832fcbb6fba8179adb78fa770695 Author: PiTrem Date: Tue Aug 15 10:11:35 2023 +0200 chore: update README - acknowledge NFDI4Chem Refs: (#1472) commit 98a50308a03e80d6716dedebdf7c38d25f6b9092 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Tue Aug 15 09:47:54 2023 +0200 style: wellplates multiple readouts design tab * fix: popover out of bounds Refs: #1474 commit 953a2057a17aabad5e6f2d8ed6bd6e7cbc589c3a Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Tue Aug 15 09:46:55 2023 +0200 fix: white screen research plan ✅ style: better toggle ✅ style: better & consistent name-link in the reaction/sample image ✅ fix: showDetails issue when opening from list Ref: #1452 commit 6816ee1293117a162a4256941384dca9b13d5dcd Author: Lan Le Date: Thu Aug 10 16:08:32 2023 +0200 feat: add Emission, DLS ACF, DLS intensity layouts Co-authored-by: Lan Le Refs: #1374 commit 9ca632a4ebfe8e7a22358812880dbd94d4dbe3cc Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Thu Aug 10 13:40:11 2023 +0200 style: add-analysis button always visible Refs: #1465 commit ecf57bea92255ed39b190afb8f6a830aa93c3b4c Author: Fabian Mauz Date: Thu Aug 10 13:28:32 2023 +0200 fix(UI): image annotation tool image preview - force rerendering * fix: force rerendering after attachment preview change After changing/adding an attachment the preview variable of all attachments are reloaded, but the "refreshed" attachments are not set to the react state. I will force now a rerendering after the reload of the preview * style: eslint autofixes + method order fixes + react/destruct * test: add test for checking correct rendering Refs: #1467 commit a1c2d5b295a2438b046a129c8562fcf8e19116ce Author: PiTrem Date: Thu Aug 10 13:19:11 2023 +0200 fix: display not-accessible info for 401 status on sample fetched by id * fix navigation to existing sample without permission: when a query to fetch sample by id returns 401, the not accessible panel should be rendered * now also render the info panel for record (sample/reaction) not found (404) Ref: #1469 commit 10eb0d00be6d16ddb80ac95dbe5b5eae4f4e46a2 Author: PiTrem Date: Wed Aug 9 13:30:16 2023 +0200 fix: quill_to_html when type HashWithIndifferentAccess * also refactor: schmooze implementation Refs: #1458 commit cdab7e71fa0cc2e2369a2f04a3619f1d79546256 Author: Johannes Haubold Date: Wed Aug 9 13:29:33 2023 +0200 Always sort new sample tasks on top of list (#1456) commit ce8cab87f435c82b86468ff88c57d73d3b821a01 Author: Pei Chi Huang Date: Thu Aug 3 16:59:45 2023 +0200 chore: disable chem repository id fetch job as the resource is being updated commit 4d0c913c2dd6512acfaefa1ab34f3f8c4bfd2633 Author: Pei Chi Huang Date: Wed Aug 2 15:29:24 2023 +0200 chore: upgrade convert 1.0.0 Refs: #1450 commit 2467be7906c759a3ab136109edcd9bb44e3e4ff6 Author: Johannes Haubold Date: Tue Aug 1 16:02:27 2023 +0200 feat: Allow deletion of SampleTasks and fix SampleTask Inbox scroll issues * Allow deletion of SampleTasks and fix SampleTask Inbox scroll issues * Add confirmation dialog when deleting a sample task * Improve deletion dialogue * Add spec for DELETE sample task endpoint * Rubocop Refs: #1444 commit 1515f963644e65dd5b16985d09007b695df7c358 Author: PiTrem Date: Tue Aug 1 13:20:41 2023 +0200 fix(UI): missing generic element icon commit f6148e650c66c21b7d3d7acf86cc5391000106cf Author: PiTrem Date: Tue Aug 1 12:06:44 2023 +0200 chore: prepare v1.7.2 commit 2d8e2fabd621dcc0a00e3d0c5291a770c070006f Author: TasnimMehzabin Date: Mon Jul 31 15:56:23 2023 +0200 fix: Comment functionality * fix the CSS for header comment buttons to keep them together on smaller screens fix the display of the comment count in the reactions' list * refactor the code to fix linting warnings * refactor the code show the action of the products only toggle button * add comment section for the inventory tab in samples Ref: #1435 commit 4922fc39488a97afcc2b4466ae6cbf02a6776ba2 Author: TasnimMehzabin Date: Mon Jul 31 15:53:47 2023 +0200 fix: Sort reactions by creation time * fix the sort query based on the action stated in the sort icon for reactions Refs: #1439 commit e08d8cd1186763c090539b191f237cd5367b1016 Author: Lan Le Date: Mon Jul 31 15:48:11 2023 +0200 fix display wrong shifted peaks after zoom (#1443) Co-authored-by: Lan Le commit cd4048efb5341a5362b98d41538f4509db1092a1 Author: PiTrem Date: Wed Jul 26 11:57:18 2023 +0200 chores: VERSION commit 05acee669233a93dc63b897af2e350a02dcd2f90 Author: PiTrem Date: Fri Jul 21 15:26:58 2023 +0200 chores: clean stylesheet from plugin integration remnant commit cd9b7132d1d28b862a6da3dbba434f5c18a376a0 Author: Mehreen Mansur Date: Wed Jul 26 11:50:41 2023 +0200 fix(UI): available tab options * - include tab option from profile data - filter null values in hidden tab list Refs: #1427 --------- Co-authored-by: Mehreen Co-authored-by: PiTrem commit 3cae2fdd95153dfe91ed9d6ed40a82a9456191a1 Author: Mehreen Mansur Date: Wed Jul 26 11:20:49 2023 +0200 style: add edit icon in collection tabs Refs: #1425 Co-authored-by: Mehreen commit efecaf8a9c1d5b68ba23eca2e75eb0c8a177ef5c Author: TasnimMehzabin Date: Wed Jul 26 10:41:58 2023 +0200 fix: sort reaction list by created_at * modify the reaction list sorting to default to created_at as in < v1.7.0 disable the sort toggle for reaction list modify the sort icon popup text modify the test codes as reaction list is sorted by created_at by default Ref: #1429 commit 7d4dd22d8a246e1e0c0f680d0af832464305b368 Author: Lan Le Date: Wed Jul 26 10:26:29 2023 +0200 fix(chemspectra): cannot change ref area and display wrong shift Refs: #1431 Co-authored-by: Lan Le commit baa6a4fa9f15068115465fb9e2b4d203f294b865 Author: Mehreen Mansur Date: Mon Jul 24 08:58:46 2023 +0200 fix: total element count in list-tabs (#1426) Co-authored-by: Mehreen commit 7fe6e5834a484cbb8c70995fdbcf32d492864700 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Jul 21 13:21:59 2023 +0200 Bump word-wrap from 1.2.3 to 1.2.4 (#1421) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 313f34222c836a2eff72623653b14e49eae0d872 Author: TasnimMehzabin Date: Fri Jul 21 13:14:16 2023 +0200 featfix: Add the option to sort reactions based on short_label or updated_at * add the option to sort reactions based on short_label or updated_at * modify the test cases since default reaction sorting is based on short_label in place of updated_at * fix the sort query based on the action stated in the sort icon for reactions implement short_label descending sort for reactions modify sort icon texts modify the test cases as short_label desc sort is applied * modify the sort icon to display the current sort state modify the pop over text of the sort icon to describe the sorting action on click and also the current sort state Refs: #1418 commit 97fe249a05857131e7ff31b7010e1470f7a68b01 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Jul 21 12:56:24 2023 +0200 Bump semver from 5.7.1 to 5.7.2 (#1403) Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit ee68158d7973acfc5b8deedf2c41ce0715ff7fb7 Author: TasnimMehzabin Date: Thu Jul 20 14:01:44 2023 +0200 Refactor the private note api (#1414) * refactor the private_note_api.rb refactor the PrivateNoteFetcher.js refactor the private_note_api.spec.rb * fix rubocop warning commit ee2db103777d443dd490c51da9a7b8933a561eb5 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Tue Jul 18 12:54:57 2023 +0200 Fix inbox (de)select boxes (#1416) * fix checkbox for `unsorted` list * select/deselect/delete for current page only * update renderCheckAll commit 3645d848bf9eb3b3c8347d9b61b543cf94071d2a Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Mon Jul 17 09:42:35 2023 +0200 fetch attachment preview for new attachments only (#1410) commit b82e7222cf47460a92a08e547035d3e128bcb3f7 Author: Mehreen Mansur Date: Fri Jul 14 16:20:55 2023 +0200 Sync collection tab segments fix (#1411) * - tabs segment for sync collection entity - add css for tab cell sync collec entity to return empty tab_segment profile remove tab_segments from sync_collec_user entity * extract string utility function --------- Co-authored-by: Mehreen Co-authored-by: PiTrem commit 77f83bfd132101bd69efec380923f9ad47e7aa94 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Fri Jul 14 15:40:26 2023 +0200 fix bug undefined current_user for collectionEntity on reaction docx … (#1412) * fix bug undefined current_user for collectionEntity on reaction docx report export * add current_user argument to ReactionReportEntity and do refactor spec files commit 036748855410471e58cd62dddc3089a4ab9c3f4b Author: Mehmood Ghaffar Date: Fri Jul 14 13:31:05 2023 +0200 create user test and researchplan extended test are added (#1379) * create user test and researchplan extended test are added * added identifier to eliminate flakyness in researcPlan cypress-specs * collections api spec is now added * deleted unnecessary comments from collection_api_spec.cy.js * Remove redundant API test for now (conceptually replicated RSpec) --------- Co-authored-by: Jan C. Brammer commit 0190e0fcdc35d62d84107e465a7941030bdee259 Author: PiTrem Date: Tue Jul 11 11:42:31 2023 +0200 Pre v1.7.0 (#1405) * upd CHANGELOG for 1.7.0 * upd CHANGELOG for 1.6.2 commit cd2cb597472d4ad39485a983cd1811fb36c6acf9 Author: PiTrem Date: Tue Jul 11 10:44:03 2023 +0200 Calendar Notification: rescue SMTP error to avoid repeat notif (#1404) also set max_attempts to 1 when email sending fails (eg wrong configuration), avoid client notification to be repeated. commit a8218f86cf6cbbb0d7d54099de762f06b6445f1f Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Tue Jul 11 10:08:57 2023 +0200 1321 enhance import sample feature (#1341) * allow import of samples when melting or boiling points column are empty * refactor import_samples class * initial implementation of import sample delayed job * create channel and message for Import_sample_notification * notify user about outcome of delayed import samples job * add basic test for importing samples --------- Co-authored-by: FabianMauz Co-authored-by: PiTrem commit 7fa0ceee55684a6c14c6d0b9fca7643424157f57 Author: Martin Schneider Date: Mon Jul 10 15:58:04 2023 +0200 Feature/elements grouping (#1188) * fix generic elements collectable scopes and remove now unneeded sample specific scopes * move reset_pagination_page further down to not call it twice for molecule sort * add groupings to reactions, generic elements adjust samples grouping style store selected group in profile * add rspec sorting tests to elements and generic elements api, add time scope tests to samples api * remove methods moved out of ElementsTableEntries class before rebase --------- Co-authored-by: VadimKeller commit c060e5ffa62cd995152af7d8f6373c33780bdc23 Author: Mehreen Mansur Date: Mon Jul 10 15:18:58 2023 +0200 Element tab settings saved on collection level (#681) User can now save the tab settings of elements through the collection management --------- Co-authored-by: Mehreen commit ab558e19f44fc6439505be5c9b6cfea17a8379eb Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Mon Jul 10 15:01:39 2023 +0200 min height and lock resize only to vertical (#1390) commit 671e03b7a826f553ed45e4f74bba6aa4ef813b5f Author: Fabian Mauz Date: Mon Jul 10 13:14:19 2023 +0200 add max line length eslint rule (#1399) commit 00f26c14a4e3ea1e879ab514f0e7a0fa9dc4dbc4 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Mon Jul 10 10:26:59 2023 +0200 Unsaved sample changes retained when reselected from list (#1397) * track open sample tabs by index * track and not reload open element tabs commit fd6ec68d72a60bfbc51d1ad48398b8006fb43f35 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Mon Jul 10 10:02:39 2023 +0200 Groups ui revamp and making group admins set/unset admins (#1396) * add toggle admin btn and improving layout * adding users to group and make admin button * group api upd endpoint to allow add/rm of group admin for group admin * group admin api: allow group admin to demote itself when more than 1 admin present --------- Co-authored-by: PiTrem commit 54872ff63396e47332c0169a68d27e84be6103f0 Author: Adam Basha <55552142+adambasha0@users.noreply.github.com> Date: Fri Jul 7 10:08:53 2023 +0200 make height of sampleInfo row auto to fix styling bug for sample Tab bar (#1395) commit 702bb6bb80d09196b95653ec4357d59ef6ccd769 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Thu Jul 6 09:13:28 2023 +0200 Calendar allday events not shown consistently (#1394) * add 'all day' accessor * add alert for event title * exception for shorter than 1 day event commit 101d04c2fd2376e037a81dbd1e0f81b738399d42 Author: Chia-Lin Lin <42910431+cllde8@users.noreply.github.com> Date: Thu Jul 6 09:13:07 2023 +0200 structure editor with decoupled sample (#1393) commit d003f0a97d3eddb27a4c0f8895266367621652f1 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Tue Jul 4 20:53:29 2023 +0200 Analysis button bounds & badge styles (#1392) * Styling badges in header bar * removing redundant add-analysis button at bottom * formatting using Prettier and fixing some linting issues commit fb471bd83ceb34d905cecfabf5e5e17cc60004d8 Author: PiTrem Date: Tue Jul 4 20:52:08 2023 +0200 Unify datetime format (#1389) * use formatDate utility to format displayed timestamps (reduce the number of 'import moment.js' ) * fix Intl time format * Entities: use default timestamp formatter * timezoneHelper: default to iso format if parsed value invalid. Also use short format date llll, not LLLL. TODO use locale ? commit 565238df312559c558c194aa2260c1af61ec82e6 Author: f-idiris <112618970+f-idiris@users.noreply.github.com> Date: Tue Jul 4 20:47:25 2023 +0200 reformat dates (#1391) commit 1aa7153ab8c1416c3f3ab4f914744a7442c82581 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Mon Jul 3 14:15:47 2023 +0200 adding missing asterisks on signing up (#1375) commit c7f023c6903e055eb72ca430ed24407ea6bc10fd Author: Devang Patel <67055123+endevor-music@users.noreply.github.com> Date: Mon Jul 3 13:44:13 2023 +0200 Remove duplicate component in ResearchPlanDetails.js (#1387) commit 3e894cfb948547d8503ddb5564bbc4ab592f45b3 Author: PiTrem Date: Fri Jun 30 15:00:08 2023 +0200 User select in UI feature (#1385) * remove duplicated admin api endpoint also adapt equivalent non admin endpoint to use user simple entity * extract react select option formatting to utility function * remove deprecated fetcher commit 34ddbfe40de40ac43562eaa2e0d61d74b85fd2ab Author: Pei Chi Huang Date: Thu Jun 29 20:39:23 2023 +0200 login-and-signup-configurable (#1377) commit 241468b4949d400f3a1592e3e4e9b443f83d146a Author: Jan C. Brammer Date: Thu Jun 29 10:13:55 2023 +0200 Add delayed job worker to tasks.json (#1381) commit 5bf60a1a58bc941c1f27586a8f8a975142f66e01 Author: TasnimMehzabin Date: Tue Jun 27 14:40:52 2023 +0200 Comment functionality on shared and synchronized collections #699 (#1237) * Comment polymorphic model * associated to Sample Reaction RP screeen Wellplate * Notification to users commit f0c3b72ad29bbfc1ed2d6f4b02079920b12d2825 Author: Johannes Haubold Date: Tue Jun 27 14:05:42 2023 +0200 Expose target amount in sample task api (#1373) * Expose target amount in sample task api commit ee600b01a1031ec281b0053b4ac6525eca8be107 Author: Mostafa Mekky <59936007+mekkyz@users.noreply.github.com> Date: Tue Jun 27 14:02:45 2023 +0200 notification timestamps and formatting notification button (#1362) * notification timestamps, format notification button * styling inbox and notifications buttons * unit test for timezoneHelper * using Moment.js to correctly get the timezone * using timezone info from backend --- .dockerignore | 1 + .eslintrc | 7 + .github/workflows/ci.yml | 4 +- .gitignore | 10 + .nvmrc | 2 +- .service-dependencies | 5 +- .tool-versions | 2 +- .vscode/settings.json | 24 + .vscode/tasks.json | 7 +- CHANGELOG.md | 260 +- Dockerfile.chemotion-dev | 21 +- Dockerfile.github-ci | 5 +- Gemfile | 9 +- Gemfile.lock | 236 +- README-DEV.md | 22 + README.md | 14 +- VERSION | 4 +- app/api/api.rb | 74 +- app/api/chemotion/admin_api.rb | 31 +- app/api/chemotion/admin_generic_api.rb | 368 - app/api/chemotion/admin_user_api.rb | 57 +- app/api/chemotion/attachable_api.rb | 25 +- app/api/chemotion/attachment_api.rb | 45 + app/api/chemotion/cell_line_api.rb | 165 + app/api/chemotion/chem_spectra_api.rb | 108 + app/api/chemotion/chemical_api.rb | 2 +- app/api/chemotion/collection_api.rb | 126 +- app/api/chemotion/comment_api.rb | 150 + app/api/chemotion/editor_api.rb | 4 +- app/api/chemotion/element_api.rb | 15 +- app/api/chemotion/generic_dataset_api.rb | 27 - app/api/chemotion/generic_element_api.rb | 329 - app/api/chemotion/inbox_api.rb | 40 +- app/api/chemotion/inventory_api.rb | 44 + app/api/chemotion/literature_api.rb | 116 +- app/api/chemotion/message_api.rb | 3 +- app/api/chemotion/molecule_api.rb | 15 +- app/api/chemotion/ols_terms_api.rb | 6 +- app/api/chemotion/permission_api.rb | 86 +- app/api/chemotion/private_note_api.rb | 115 +- app/api/chemotion/profile_api.rb | 26 +- app/api/chemotion/public_api.rb | 20 +- app/api/chemotion/reaction_api.rb | 24 +- app/api/chemotion/report_api.rb | 34 +- app/api/chemotion/research_plan_api.rb | 25 +- app/api/chemotion/sample_api.rb | 377 +- app/api/chemotion/sample_task_api.rb | 10 +- app/api/chemotion/screen_api.rb | 34 +- app/api/chemotion/search_api.rb | 424 +- app/api/chemotion/segment_api.rb | 19 - app/api/chemotion/suggestion_api.rb | 164 +- app/api/chemotion/third_party_app_api.rb | 21 +- app/api/chemotion/ui_api.rb | 8 +- app/api/chemotion/user_api.rb | 97 +- app/api/entities/application_entity.rb | 1 + app/api/entities/attachment_entity.rb | 9 +- .../cell_line_material_name_entity.rb | 10 + app/api/entities/cell_line_sample_entity.rb | 17 + app/api/entities/channel_entity.rb | 11 +- app/api/entities/collection_entity.rb | 1 + app/api/entities/collection_root_entity.rb | 6 + app/api/entities/collection_sync_entity.rb | 3 + app/api/entities/comment_entity.rb | 16 + app/api/entities/container_entity.rb | 12 +- app/api/entities/dataset_entity.rb | 15 - app/api/entities/dataset_klass_entity.rb | 18 - app/api/entities/device_metadata_entity.rb | 8 + app/api/entities/element_entity.rb | 105 - app/api/entities/element_klass_entity.rb | 22 - app/api/entities/element_revision_entity.rb | 55 - app/api/entities/inbox_entity.rb | 28 +- app/api/entities/inventory_entity.rb | 10 + app/api/entities/matrice_entity.rb | 6 +- app/api/entities/message_entity.rb | 11 +- app/api/entities/private_note_entity.rb | 11 +- app/api/entities/reaction_entity.rb | 12 +- app/api/entities/reaction_report_entity.rb | 9 +- app/api/entities/reaction_variation_entity.rb | 74 + app/api/entities/research_plan_entity.rb | 7 +- app/api/entities/sample_entity.rb | 9 +- app/api/entities/sample_report_entity.rb | 10 +- app/api/entities/sample_task_entity.rb | 13 +- app/api/entities/screen_entity.rb | 7 +- app/api/entities/segment_entity.rb | 62 - app/api/entities/segment_klass_entity.rb | 18 - app/api/entities/segment_revision_entity.rb | 54 - app/api/entities/user_entity.rb | 44 +- app/api/entities/wellplate_entity.rb | 7 +- app/api/helpers/collection_helpers.rb | 58 +- app/api/helpers/comment_helpers.rb | 101 + app/api/helpers/container_helpers.rb | 76 +- app/api/helpers/generic_helpers.rb | 114 - app/api/helpers/reflection_helpers.rb | 11 + app/api/helpers/report_helpers.rb | 227 +- app/api/helpers/sample_association_helpers.rb | 100 - app/assets/javascripts/pages.js | 21 +- app/assets/stylesheets/_icons.scss | 1 + app/assets/stylesheets/application.scss.erb | 20 +- app/assets/stylesheets/attachment-list.scss | 132 + app/assets/stylesheets/cellLines.scss | 355 + app/assets/stylesheets/chemical-tab.scss | 10 +- .../stylesheets/collection_management.scss | 15 +- app/assets/stylesheets/common.scss | 42 +- .../CommentButton/CommentButton.scss | 31 + .../components/CommentModal/CommentModal.scss | 57 + .../stylesheets/components/inbox/inbox.scss | 7 + app/assets/stylesheets/components/select.scss | 17 + app/assets/stylesheets/elements_details.css | 12 + app/assets/stylesheets/elements_table.scss | 34 +- app/assets/stylesheets/format-container.scss | 64 +- app/assets/stylesheets/generic_element.scss | 89 + .../stylesheets/inventory-label-settings.scss | 37 + app/assets/stylesheets/molecules.scss | 6 + app/assets/stylesheets/omniauth.scss | 4 +- app/assets/stylesheets/research-plan.scss | 33 +- app/assets/stylesheets/sample.scss | 1 - app/assets/stylesheets/search.scss | 2 +- app/assets/stylesheets/search_modal.scss | 460 + app/assets/stylesheets/search_results.scss | 173 + app/assets/stylesheets/spectra.scss | 8 +- .../stylesheets/tab_layout_container.scss | 15 + app/controllers/pages_controller.rb | 4 +- app/jobs/gate_transfer_job.rb | 154 - app/jobs/import_samples_job.rb | 53 + app/jobs/refresh_element_tag_job.rb | 2 +- .../send_calendar_entry_notification_job.rb | 5 + app/jobs/transfer_repo_job.rb | 2 +- app/models/attachment.rb | 2 +- app/models/cellline_material.rb | 31 + app/models/cellline_sample.rb | 50 + app/models/channel.rb | 4 + app/models/collection.rb | 57 +- app/models/collections_cellline.rb | 73 + app/models/collections_element.rb | 52 - app/models/collections_vessel.rb | 26 + app/models/comment.rb | 85 + app/models/concerns/attachment_converter.rb | 27 - app/models/concerns/attachment_jcamp_aasm.rb | 30 +- app/models/concerns/collectable.rb | 23 +- app/models/concerns/collecting.rb | 6 +- app/models/concerns/datasetable.rb | 43 - app/models/concerns/element_codes.rb | 7 +- .../concerns/element_ui_state_scopes.rb | 31 +- .../concerns/generic_klass_revisions.rb | 20 - app/models/concerns/generic_revisions.rb | 40 - app/models/concerns/segmentable.rb | 35 - app/models/concerns/taggable.rb | 47 +- app/models/concerns/tagging.rb | 24 +- app/models/container.rb | 29 +- app/models/dataset.rb | 22 - app/models/dataset_klass.rb | 40 - app/models/dataset_klasses_revision.rb | 25 - app/models/datasets_revision.rb | 24 - app/models/element.rb | 105 - app/models/element_klass.rb | 42 - app/models/element_klasses_revision.rb | 24 - app/models/elements_revision.rb | 25 - app/models/elements_sample.rb | 26 - app/models/inventory.rb | 65 + app/models/matrice.rb | 5 + app/models/message.rb | 1 + app/models/molecule.rb | 39 +- app/models/reaction.rb | 30 +- app/models/report.rb | 55 +- app/models/research_plan.rb | 29 +- app/models/research_plans_wellplate.rb | 13 +- app/models/sample.rb | 39 +- app/models/screen.rb | 13 +- app/models/segment.rb | 25 - app/models/segment_klass.rb | 39 - app/models/segment_klasses_revision.rb | 25 - app/models/segments_revision.rb | 24 - app/models/sync_collections_user.rb | 32 +- app/models/third_party_app.rb | 11 +- app/models/user.rb | 163 +- app/models/vessel.rb | 34 + app/models/vessel_template.rb | 27 + app/models/wellplate.rb | 16 +- app/packs/entrypoints/application.js | 6 +- app/packs/src/apps/admin/AdminHome.js | 25 +- app/packs/src/apps/admin/AdminNavigation.js | 13 +- .../src/apps/admin/ChemSpectraLayouts.js | 311 + .../src/apps/admin/DatasetElementAdmin.js | 824 - .../src/apps/admin/GenericElementAdmin.js | 1013 -- app/packs/src/apps/admin/GroupsDevices.js | 17 +- app/packs/src/apps/admin/MatrixManagement.js | 13 +- .../src/apps/admin/SegmentElementAdmin.js | 995 -- app/packs/src/apps/admin/ThirdPartyApp.js | 81 +- app/packs/src/apps/admin/UserManagement.js | 548 +- .../src/apps/admin/generic/AttrCopyModal.js | 84 - .../src/apps/admin/generic/AttrEditModal.js | 86 - app/packs/src/apps/admin/generic/AttrForm.js | 21 - .../src/apps/admin/generic/AttrNewModal.js | 67 - .../apps/admin/generic/FieldCondEditModal.js | 226 - .../apps/admin/generic/GenericAdminModal.js | 75 + .../src/apps/admin/generic/KlassAttrForm.js | 65 - .../apps/admin/generic/LayerAttrEditModal.js | 49 - .../src/apps/admin/generic/LayerAttrForm.js | 78 - .../apps/admin/generic/LayerAttrNewModal.js | 47 - app/packs/src/apps/admin/generic/Preview.js | 209 - .../src/apps/admin/generic/SegmentAttrForm.js | 55 - .../apps/admin/generic/SelectAttrNewModal.js | 46 - .../apps/admin/generic/TemplateJsonModal.js | 90 - .../src/apps/admin/generic/UploadModal.js | 70 - app/packs/src/apps/admin/generic/Utils.js | 302 - app/packs/src/apps/admin/generic/collate.js | 66 - app/packs/src/apps/commandAndControl/CnC.js | 34 +- .../src/apps/commandAndControl/Navigation.js | 13 +- .../src/apps/converter/ConverterAdmin.js | 3 + app/packs/src/apps/generic/GenericAdminNav.js | 70 + .../src/apps/generic/GenericDatasetsAdmin.js | 254 + .../src/apps/generic/GenericElementsAdmin.js | 436 + .../src/apps/generic/GenericSegmentsAdmin.js | 403 + app/packs/src/apps/generic/SyncButton.js | 66 + app/packs/src/apps/generic/Utils.js | 156 + app/packs/src/apps/mydb/App.js | 41 +- .../mydb/collections/CollectionManagement.js | 2 + .../apps/mydb/collections/CollectionTabs.js | 237 + .../apps/mydb/collections/CollectionTree.js | 126 +- .../apps/mydb/collections/MyCollections.js | 21 +- .../mydb/collections/MySharedCollections.js | 13 +- .../collections/SharedWithMeCollections.js | 9 +- .../mydb/collections/SyncWithMeCollections.js | 9 +- .../SampleTaskNavigationElement.js | 41 - .../elements/details/ElementDetailSortTab.js | 81 +- .../mydb/elements/details/ElementDetails.js | 54 +- .../details/ElementFieldDragSource.js | 25 - .../details/ElementFieldDropTarget.js | 50 - .../apps/mydb/elements/details/GroupFields.js | 216 - .../mydb/elements/details/NumericInputUnit.js | 23 +- .../elements/details/PrivateNoteElement.js | 70 +- .../apps/mydb/elements/details/ViewSpectra.js | 59 +- .../details/cellLines/CellLineDetails.js | 256 + .../analysesTab/AnalysesContainer.js | 198 + .../cellLines/analysesTab/EditModeHeader.js | 166 + .../cellLines/analysesTab/EditModeRow.js | 53 + .../cellLines/analysesTab/OrderModeHeader.js | 115 + .../cellLines/analysesTab/OrderModeRow.js | 91 + .../details/cellLines/propertiesTab/Amount.js | 105 + .../cellLines/propertiesTab/CellLineName.js | 106 + .../propertiesTab/GeneralProperties.js | 270 + .../propertiesTab/InvalidPropertyWarning.js | 32 + .../details/literature/CitationPanel.js | 71 +- .../details/literature/CitationTools.js | 10 + .../details/literature/CitationType.js | 18 +- .../literature/DetailsTabLiteratures.js | 173 +- .../details/literature/LiteratureCommon.js | 155 +- .../details/reactions/ReactionDetails.js | 63 +- .../details/reactions/ReactionDetailsShare.js | 3 + .../analysesTab/ReactionDetailsContainers.js | 101 +- .../details/reactions/schemeTab/Material.js | 70 +- .../reactions/schemeTab/MaterialGroup.js | 8 +- .../schemeTab/ReactionDetailsScheme.js | 53 +- .../variationsTab/ReactionVariations.js | 379 + .../variationsTab/ReactionVariationsUtils.js | 196 + .../details/reports/SectionSpectrum.js | 2 +- .../ImageAnnotationEditButton.js | 96 +- .../researchPlans/ImageAnnotationModalSVG.js | 30 +- .../researchPlans/ResearchPlanDetails.js | 326 +- .../researchPlans/SaveEditedImageWarning.js | 28 + .../researchPlans/SaveResearchPlanWarning.js | 33 - .../ResearchPlanDetailsContainers.js | 104 +- .../ResearchPlanDetailsAttachments.js | 833 +- .../ResearchPlanDetailsField.js | 3 +- .../ResearchPlanDetailsFieldImage.js | 8 +- .../ResearchPlanDetailsFieldReaction.js | 110 +- .../ResearchPlanDetailsFieldSample.js | 127 +- .../ResearchPlanDetailsFieldTable.js | 2 +- .../ResearchPlanDetailsName.js | 14 +- .../wellplatesTab/EmbeddedWellplate.js | 76 +- .../elements/details/samples/SampleDetails.js | 1788 +- .../analysesTab/SampleDetailsContainers.js | 10 + .../analysesTab/SampleDetailsContainersAux.js | 134 +- .../analysesTab/SampleDetailsContainersCom.js | 213 +- .../propertiesTab/SampleDetailsSolvents.js | 49 +- .../propertiesTab/SampleDetailsSolventsDnd.js | 12 +- .../samples/propertiesTab/SampleForm.js | 484 +- .../propertiesTab/SampleSolventGroup.js | 141 +- .../propertiesTab/TextRangeWithAddon.js | 2 +- .../elements/details/screens/ScreenDetails.js | 22 +- .../EmbeddedResearchPlanDetails.js | 238 +- .../details/wellplates/WellplateDetails.js | 161 +- .../WellplateDetailsAttachments.js | 597 +- .../wellplates/designerTab/WellOverlay.js | 134 +- .../labels/ElementCollectionLabels.js | 95 +- .../labels/ElementResearchPlanLabels.js | 81 + .../apps/mydb/elements/list/AttachmentList.js | 335 + .../mydb/elements/list/ElementContainer.js | 2 + .../apps/mydb/elements/list/ElementsList.js | 130 +- .../apps/mydb/elements/list/ElementsTable.js | 720 +- .../elements/list/ElementsTableEntries.js | 434 +- .../list/ElementsTableGroupedEntries.js | 474 + .../list/ElementsTableSampleEntries.js | 171 +- .../list/cellLine/CellLineContainer.js | 32 + .../elements/list/cellLine/CellLineEntry.js | 175 + .../list/cellLine/CellLineItemEntry.js | 98 + .../list/cellLine/CellLineItemText.js | 36 + .../mydb/elements/tabLayout/TabLayoutCell.js | 39 +- .../elements/tabLayout/TabLayoutContainer.js | 30 +- .../apps/mydb/inbox/AttachmentContainer.js | 47 +- .../src/apps/mydb/inbox/DatasetContainer.js | 51 +- app/packs/src/apps/mydb/inbox/DeviceBox.js | 253 +- app/packs/src/apps/mydb/inbox/InboxModal.js | 338 +- app/packs/src/apps/mydb/inbox/UnsortedBox.js | 106 +- .../src/apps/mydb/inbox/UnsortedDataset.js | 3 +- app/packs/src/apps/mydb/index.js | 9 +- app/packs/src/apps/mydb/routes.js | 10 +- .../apps/settings/InventoryLabelSettings.js | 286 + app/packs/src/apps/userCounter/UserCounter.js | 110 +- app/packs/src/components/ChemicalTab.js | 39 +- app/packs/src/components/DragDropItemTypes.js | 4 +- app/packs/src/components/OlsComponent.js | 32 +- app/packs/src/components/UserLabels.js | 138 +- .../components/actions/CollectionActions.js | 0 app/packs/src/components/calendar/Calendar.js | 28 + .../calendar/CalendarEntryEditor.js | 7 +- .../src/components/comments/CommentButton.js | 53 + .../src/components/comments/CommentDetails.js | 122 + .../src/components/comments/CommentIcon.js | 38 + .../src/components/comments/CommentList.js | 54 + .../src/components/comments/CommentSection.js | 56 + .../comments/HeaderCommentSection.js | 99 + .../src/components/common/CommentModal.js | 334 + .../src/components/common/DeleteComment.js | 65 + .../components/common/HyperLinksSection.js | 113 +- app/packs/src/components/common/ImageModal.js | 144 +- app/packs/src/components/common/SampleName.js | 2 +- .../components/common/SpectraEditorButton.js | 129 + .../src/components/common/SvgWithPopover.js | 25 +- .../container/AttachmentDropzone.js | 72 +- .../container/ContainerComponent.js | 69 +- .../components/container/ContainerDataset.js | 497 - .../container/ContainerDatasetField.js | 156 +- .../container/ContainerDatasetModal.js | 201 +- .../container/ContainerDatasetModalContent.js | 734 + .../components/container/ContainerDatasets.js | 160 +- .../contextActions/ContextActions.js | 4 + .../components/contextActions/CreateButton.js | 45 +- .../contextActions/ExportImportButton.js | 123 +- .../components/contextActions/InboxButton.js | 55 +- .../components/contextActions/ModalExport.js | 150 +- .../ModalExportRadarCollection.js | 6 +- .../components/contextActions/ModalImport.js | 34 +- .../components/contextActions/NoticeButton.js | 394 +- app/packs/src/components/generic/AttrChk.js | 21 - .../src/components/generic/DefinedRenderer.js | 16 - .../components/generic/DropLinkRenderer.js | 31 - .../src/components/generic/DropRenderer.js | 31 - .../components/generic/DropTextRenderer.js | 25 - .../components/generic/EditorAnalysisBtn.js | 71 + .../src/components/generic/ElementField.js | 332 - .../src/components/generic/FieldSelect.js | 34 - .../components/generic/GenericAttachments.js | 177 +- .../components/generic/GenericContainer.js | 207 + .../generic/GenericContainerGroup.js | 68 + .../components/generic/GenericDSDetails.js | 155 +- .../src/components/generic/GenericElCommon.js | 401 - .../components/generic/GenericElCriteria.js | 194 +- .../generic/GenericElCriteriaModal.js | 47 - .../components/generic/GenericElDetails.js | 471 +- .../generic/GenericElDetailsContainers.js | 182 +- .../components/generic/GenericElDropTarget.js | 123 - .../generic/GenericElTableDropTarget.js | 130 - .../generic/GenericPropertiesFields.js | 390 - .../components/generic/GenericSGDetails.js | 98 + app/packs/src/components/generic/GridBtn.js | 40 - app/packs/src/components/generic/GridDnD.js | 42 - app/packs/src/components/generic/GridEntry.js | 74 - .../src/components/generic/GridSelect.js | 30 - .../src/components/generic/LayerSelect.js | 19 - .../src/components/generic/PreviewModal.js | 75 - .../components/generic/RevisionViewerBtn.js | 75 + app/packs/src/components/generic/SamOption.js | 53 - .../src/components/generic/SegmentDetails.js | 347 +- .../src/components/generic/SystemSelect.js | 19 - app/packs/src/components/generic/TableDef.js | 218 - .../src/components/generic/TableRecord.js | 229 - .../src/components/generic/TextFormula.js | 203 - .../src/components/generic/TypeSelect.js | 18 - .../components/generic/UConverterRenderer.js | 24 - .../managingActions/ManagingActions.js | 17 +- .../managingActions/ManagingModalSharing.js | 39 +- .../src/components/navigation/GroupElement.js | 436 +- .../src/components/navigation/NavHead.js | 53 +- .../components/navigation/NavNewSession.js | 56 +- .../src/components/navigation/Navigation.js | 42 +- .../src/components/navigation/UserAuth.js | 464 +- .../navigation/search/AutoCompleteInput.js | 9 +- .../components/navigation/search/Search.js | 205 +- .../nmriumWrapper/NMRiumDisplayer.js | 66 +- .../sampleTaskInbox/SampleTaskCard.js | 75 +- .../sampleTaskInbox/SampleTaskInbox.js | 27 +- .../SampleTaskNavigationElement.js | 52 + .../sampleTaskInbox/SampleTaskReloadButton.js | 19 + .../src/components/searchModal/SearchModal.js | 128 + .../searchModal/forms/AdvancedSearchRow.js | 249 + .../searchModal/forms/AnalysesFieldData.js | 50 + .../searchModal/forms/DetailSearch.js | 733 + .../searchModal/forms/KetcherRailsForm.js | 188 + .../searchModal/forms/MeasurementFieldData.js | 36 + .../searchModal/forms/NoFormSelected.js | 7 + .../searchModal/forms/PublicationFieldData.js | 41 + .../searchModal/forms/PublicationSearch.js | 111 + .../searchModal/forms/PublicationSearchRow.js | 124 + .../forms/SampleInventoryFieldData.js | 195 + .../searchModal/forms/SearchModalFunctions.js | 146 + .../searchModal/forms/SearchResult.js | 221 + .../forms/SearchResultTabContent.js | 183 + .../searchModal/forms/SelectFieldData.js | 649 + .../searchModal/forms/SelectMapperData.js | 31 + .../searchModal/forms/TextSearch.js | 214 + .../staticDropdownOptions/options.js | 805 +- .../staticDropdownOptions/reagents_kombi.js | 4168 ++--- .../structureEditor/StructureEditorModal.js | 24 +- app/packs/src/endpoints/ApiServices.js | 15 + app/packs/src/fetchers/AdminFetcher.js | 398 +- app/packs/src/fetchers/AttachmentFetcher.js | 562 +- app/packs/src/fetchers/BaseFetcher.js | 114 +- app/packs/src/fetchers/CellLinesFetcher.js | 154 + app/packs/src/fetchers/ChemSpectraFetcher.js | 45 + app/packs/src/fetchers/ChemicalFetcher.js | 6 +- app/packs/src/fetchers/CollectionsFetcher.js | 59 +- app/packs/src/fetchers/CommentFetcher.js | 72 + app/packs/src/fetchers/GenericBaseFetcher.js | 109 +- app/packs/src/fetchers/GenericDSsFetcher.js | 26 +- app/packs/src/fetchers/GenericElsFetcher.js | 202 +- app/packs/src/fetchers/GenericSgsFetcher.js | 56 + app/packs/src/fetchers/InboxFetcher.js | 33 +- app/packs/src/fetchers/InventoryFetcher.js | 32 + app/packs/src/fetchers/LiteraturesFetcher.js | 60 +- app/packs/src/fetchers/PrivateNoteFetcher.js | 2 +- app/packs/src/fetchers/ReactionsFetcher.js | 91 +- .../src/fetchers/ResearchPlansFetcher.js | 72 +- app/packs/src/fetchers/SampleTasksFetcher.js | 8 + app/packs/src/fetchers/SamplesFetcher.js | 114 +- app/packs/src/fetchers/SearchFetcher.js | 81 +- app/packs/src/fetchers/SegmentsFetcher.js | 15 - app/packs/src/fetchers/SuggestionsFetcher.js | 11 +- .../src/fetchers/ThirdPartyAppFetcher.js | 27 +- app/packs/src/fetchers/UsersFetcher.js | 11 +- app/packs/src/fetchers/WellplatesFetcher.js | 22 +- app/packs/src/models/Attachment.js | 2 +- app/packs/src/models/Comment.js | 93 + app/packs/src/models/Container.js | 31 +- app/packs/src/models/GenericDS.js | 5 +- app/packs/src/models/GenericEl.js | 112 +- app/packs/src/models/Reaction.js | 191 +- app/packs/src/models/ResearchPlan.js | 19 +- app/packs/src/models/Sample.js | 254 +- app/packs/src/models/Segment.js | 70 +- app/packs/src/models/Wellplate.js | 42 +- app/packs/src/models/cellLine/CellLine.js | 95 + .../src/models/cellLine/CellLineGroup.js | 49 + .../src/models/cellLine/CellLinePropTypes.js | 14 + .../src/models/collection/CollectionUtils.js | 13 + .../stores/alt/actions/CollectionActions.js | 22 + .../src/stores/alt/actions/CommentActions.js | 31 + .../src/stores/alt/actions/ElementActions.js | 85 +- .../src/stores/alt/actions/InboxActions.js | 44 +- .../src/stores/alt/actions/SpectraActions.js | 4 +- app/packs/src/stores/alt/actions/UIActions.js | 11 + .../src/stores/alt/actions/UserActions.js | 14 +- .../src/stores/alt/stores/CollectionStore.js | 6 - .../src/stores/alt/stores/CommentStore.js | 38 + .../src/stores/alt/stores/ElementStore.js | 116 +- app/packs/src/stores/alt/stores/InboxStore.js | 137 +- .../src/stores/alt/stores/LoadingStore.js | 1 + .../stores/alt/stores/NotificationStore.js | 71 +- .../src/stores/alt/stores/ReportStore.js | 114 +- .../src/stores/alt/stores/SpectraStore.js | 11 +- app/packs/src/stores/alt/stores/UIStore.js | 201 +- app/packs/src/stores/alt/stores/UserStore.js | 10 +- .../src/stores/mobx/CellLineDetailsStore.jsx | 226 + app/packs/src/stores/mobx/RootStore.jsx | 10 +- .../src/stores/mobx/SampleTasksStore.jsx | 14 +- app/packs/src/stores/mobx/SearchStore.jsx | 309 + app/packs/src/utilities/CellLineUtils.js | 32 + .../src/utilities/CollectionTabsHelper.js | 60 + app/packs/src/utilities/CommentHelper.js | 24 + app/packs/src/utilities/ElementUtils.js | 13 + app/packs/src/utilities/SpectraHelper.js | 78 +- app/packs/src/utilities/imageHelper.js | 11 +- app/packs/src/utilities/quillToolbarSymbol.js | 52 +- app/packs/src/utilities/routesUtils.js | 71 +- app/packs/src/utilities/selectHelper.js | 27 + app/packs/src/utilities/textHelper.js | 8 + app/packs/src/utilities/timezoneHelper.js | 42 + app/policies/element_policy.rb | 2 +- app/proxies/element_permission_proxy.rb | 17 +- .../element_detail_level_calculator.rb | 8 +- app/services/inbox_service.rb | 47 +- app/usecases/calendar_entries/index.rb | 6 +- app/usecases/cell_lines/create.rb | 90 + app/usecases/cell_lines/load.rb | 32 + app/usecases/cell_lines/update.rb | 110 + app/usecases/reactions/update_materials.rb | 2 + app/usecases/sample_tasks/create.rb | 2 +- app/usecases/search/advanced_search.rb | 137 + app/usecases/search/by_ids.rb | 147 + .../search/conditions_for_advanced_search.rb | 360 + app/usecases/search/shared_methods.rb | 98 + app/usecases/search/structure_search.rb | 77 + app/usecases/sharing/share_with_user.rb | 11 +- app/usecases/sharing/share_with_users.rb | 1 + app/views/devise/sessions/new.html.haml | 19 +- .../devise/shared/_affiliations.html.haml | 2 +- app/views/devise/shared/_dblogin.html.haml | 19 + app/views/devise/shared/_links.html.haml | 6 +- app/views/pages/affiliations.haml | 2 +- app/views/pages/gda.haml | 1 + app/views/pages/gea.haml | 1 + app/views/pages/gsa.haml | 1 + app/views/pages/settings.haml | 11 +- app/views/users/registrations/new.html.haml | 6 +- bin/run_mail_collector | 5 + config/application.rb | 3 + config/deploy.rb | 1 - config/environment.rb | 1 + config/initializers/delayed_job_config.rb | 3 - config/initializers/devise.rb | 11 +- config/initializers/eln_features.rb | 2 +- config/initializers/radar.rb | 1 - config/locales/en.yml | 3 +- config/profile_default.yml.example | 9 + config/routes.rb | 16 +- config/webpack/custom.js | 20 +- db/migrate/20210318133000_generic_datasets.rb | 2 +- ...014_generic_elements_revision_migration.rb | 8 +- ...000_generic_datasets_revision_migration.rb | 17 +- ...210727145003_generic_workflow_migration.rb | 39 + ...10820165003_generic_elements_to_element.rb | 16 + ...211105111420_add_dry_solvent_to_samples.rb | 5 + db/migrate/20220127000608_create_comments.rb | 19 + .../20220317004217_add_comment_channel.rb | 27 + ...06094235_add_tabs_segment_to_collection.rb | 5 + ...0712100010_add_segment_klass_identifier.rb | 23 + .../20220926103002_omniauth_providers.rb | 11 +- ...128073938_add_sort_indexes_to_reactions.rb | 6 + .../20230213102539_create_third_party_apps.rb | 6 +- ...30320132646_add_variations_to_reactions.rb | 5 + db/migrate/20230420121233_matrice_comment.rb | 12 + .../20230522075503_create_cellline_models.rb | 46 + ...230531142756_add_samples_import_channel.rb | 11 + .../20230613063121_update_comment_channel.rb | 23 + ...plain_text_field_for_description_fields.rb | 8 + ...0230630125148_cell_cline_data_migration.rb | 13 + ..._fill_new_plain_text_description_fields.rb | 27 + .../20230714100005_add_generic_updated_by.rb | 53 + db/migrate/20230804100000_att_com_state.rb | 9 + db/migrate/20230810100000_labimotion_class.rb | 6 + ...12_add_plain_text_content_to_containers.rb | 5 + ..._plain_text_content_field_at_containers.rb | 14 + .../20230927073354_create_vessel_templates.rb | 16 + db/migrate/20230927073410_create_vessels.rb | 15 + ...230927073435_create_collections_vessels.rb | 13 + .../20231106000000_cellline_element_init.rb | 10 + .../20231218191658_create_inventories.rb | 11 + ...18191929_add_inventories_to_collections.rb | 5 + ...19151632_add_weight_to_vessel_templates.rb | 6 + ...19152154_add_bar_and_qr_code_to_vessels.rb | 6 + ...ume_amount_to_float_in_vessel_templates.rb | 9 + .../20240129134421_rename_inbox_folders.rb | 26 + db/reagent_seeds.json | 2 + db/schema.rb | 190 +- db/seeds/development/vessels.seed.rb | 188 + db/seeds/shared/admin.seed.rb | 2 + lib/analyses/converter.rb | 295 - lib/chemotion/chemicals_service.rb | 42 +- lib/chemotion/jcamp.rb | 54 + lib/chemotion/meta_schmooze/meta_schmooze.rb | 83 + lib/chemotion/quill_to_html.rb | 76 +- lib/chemotion/quill_to_plain_text.rb | 22 + lib/datacollector/collector_helper.rb | 24 +- lib/datacollector/collector_helper_set.rb | 17 + lib/datacollector/datacollector_file.rb | 4 +- lib/datacollector/datacollector_folder.rb | 2 +- lib/datacollector/dc_logger.rb | 2 + lib/datacollector/fcollector.rb | 4 +- lib/datacollector/filecollector.rb | 13 +- lib/datacollector/foldercollector.rb | 36 +- lib/datacollector/mailcollector.rb | 185 +- lib/export/export_chemicals.rb | 176 + lib/export/export_collections.rb | 127 +- lib/export/export_excel.rb | 13 +- lib/export/export_json.rb | 264 - lib/export/export_research_plan.rb | 1 - lib/export/export_sdf.rb | 18 +- lib/export/export_table.rb | 26 +- lib/import/helper/cellline_importer.rb | 62 + lib/import/import_chemicals.rb | 170 + lib/import/import_collections.rb | 45 +- lib/import/import_json.rb | 6 +- lib/import/import_samples.rb | 581 +- lib/import/import_sdf.rb | 65 +- lib/pub_chem.rb | 92 +- lib/reporter/docx/detail_reaction.rb | 28 + lib/reporter/spectrum/detail.rb | 2 +- lib/reporter/worker.rb | 1 + lib/reporter/worker_spectrum.rb | 75 +- lib/template/Standard.docx | Bin 92215 -> 93876 bytes package.json | 36 +- package_postinstall.sh | 49 +- .../bao.default.edited.json | 13542 ++++++++++++++++ run-js-dev.sh | 13 +- run-ruby-dev.sh | 37 +- spec/api/chemotion/admin_api_spec.rb | 34 +- spec/api/chemotion/attachment_api_spec.rb | 42 +- spec/api/chemotion/cell_line_api_spec.rb | 253 + spec/api/chemotion/comment_api_spec.rb | 448 + .../api/chemotion/generic_dataset_api_spec.rb | 25 + .../api/chemotion/generic_element_api_spec.rb | 136 +- spec/api/chemotion/inventory_api_spec.rb | 54 + spec/api/chemotion/reaction_api_spec.rb | 98 +- spec/api/chemotion/sample_api_spec.rb | 284 +- spec/api/chemotion/sample_task_api_spec.rb | 20 +- spec/api/chemotion/search_api_spec.rb | 221 +- spec/api/chemotion/segment_api_spec.rb | 25 + spec/api/chemotion/suggestion_api_spec.rb | 122 +- .../api/chemotion/third_party_app_api_spec.rb | 54 +- spec/api/chemotion/ui_api_spec.rb | 2 +- spec/api/chemotion/user_api_spec.rb | 30 +- spec/api/collection_api_spec.rb | 399 +- spec/api/element_api_spec.rb | 93 +- spec/api/entities/inventory_entity_spec.rb | 22 + spec/api/entities/reaction_entity_spec.rb | 150 + spec/api/generic_api_spec.rb | 222 + spec/api/generic_segment_api_spec.rb | 212 + spec/api/helpers/generic_helpers_spec.rb | 3 +- spec/api/inbox_api_spec.rb | 45 + spec/api/literature_api_spec.rb | 270 +- spec/api/private_note_api.spec.rb | 264 +- spec/api/research_plan_api_spec.rb | 43 +- spec/cypress/end_to_end/admin.cy.js | 29 + spec/cypress/end_to_end/calendar_specs.cy.js | 65 + .../end_to_end/manage_collections.cy.js | 2 +- spec/cypress/end_to_end/manage_samples.cy.js | 11 +- spec/cypress/end_to_end/message.cy.js | 7 +- spec/cypress/end_to_end/research_plan.cy.js | 5 +- .../end_to_end/research_plan_with_user.cy.js | 40 +- .../cypress/end_to_end/samples_api_spec.cy.js | 34 + spec/cypress/end_to_end/search_modal.cy.js | 55 + .../cypress/end_to_end/share_collection.cy.js | 7 +- .../cypress/end_to_end/sync_collections.cy.js | 2 +- spec/cypress/support/commands.js | 11 + spec/factories/attachments.rb | 10 + spec/factories/calendar_entry.rb | 2 +- spec/factories/cell_line_material.rb | 19 + spec/factories/cell_line_sample.rb | 22 + spec/factories/collections_vessels.rb | 8 + spec/factories/comment.rb | 10 + spec/factories/containers.rb | 44 +- spec/factories/dataset_klasses.rb | 100 + spec/factories/datasets.rb | 6 + spec/factories/element_klasses.rb | 108 + spec/factories/elements.rb | 18 + spec/factories/inventory.rb | 9 + spec/factories/reactions.rb | 3 +- spec/factories/reports.rb | 3 +- spec/factories/research_plans.rb | 16 +- spec/factories/samples.rb | 18 + spec/factories/segments.rb | 8 + spec/factories/setment_klasses.rb | 8 + spec/factories/users.rb | 20 +- spec/factories/vessel_templates.rb | 13 + spec/factories/vessels.rb | 10 + .../body_OKKJLVBELUTLKV-UHFFFAOYSA-N.json | 412 + .../body_RJUFJBKOKNCXHH-UHFFFAOYSA-N.json | 477 + .../body_UHOVQNZJYSORNB-UHFFFAOYSA-N.json | 490 + .../body_XBDQKXXYIPTUBI-UHFFFAOYSA-N.json | 453 + .../import/20230629_two_cell_line_samples.zip | Bin 0 -> 167371 bytes spec/fixtures/import/collection_chemicals.zip | Bin 0 -> 6633 bytes .../import/sample_import_template.xlsx | Bin 0 -> 12653 bytes .../fixtures/spectrum_param_chloroform_d.json | 2 +- .../factories/AttachmentFactory.js | 18 +- spec/javascripts/factories/CellLineFactory.js | 62 + .../javascripts/factories/ContainerFactory.js | 16 +- spec/javascripts/factories/ReactionFactory.js | 10 +- .../factories/ResearchPlanFactory.js | 24 +- spec/javascripts/factories/SampleFactory.js | 2 +- spec/javascripts/fixture/chmos.js | 3 + spec/javascripts/fixture/reaction.js | 3 + spec/javascripts/fixture/report.js | 1 + spec/javascripts/helper/setup.js | 23 +- .../ReactionDetailsContainers.spec.js | 122 + .../ImageAnnotationEditButton.spec.js | 80 +- .../researchPlans/ResearchPlanDetails.spec.js | 129 +- .../ResearchPlanDetailsFieldImage.spec.js | 10 +- .../ResearchPlanDetailsContainers.spec.js | 123 + .../ResearchPlanDetailsAttachments.spec.js | 54 + .../SampleDetailsContainersAux.spec.js | 105 + .../propertiesTab/SampleSolventGroup.spec.js | 105 + .../src/components/ChemSpectraLayouts.spec.js | 91 + .../packs/src/components/ChemicalTab.spec.js | 42 +- .../packs/src/components/OlsComponent.spec.js | 88 + .../packs/src/fetchers/BaseFetcher.spec.js | 22 +- .../src/fetchers/ChemSpectraFetcher.spec.js | 126 + .../src/fetchers/ChemicalFetcher.spec.js | 28 +- .../src/fetchers/InventoryFetcher.spec.js | 107 + .../packs/src/models/Container.spec.js | 23 + .../packs/src/models/Reaction.spec.js | 174 +- .../packs/src/models/ResearchPlan.spec.js | 45 + .../packs/src/models/Sample.spec.js | 4 +- .../src/models/cellLine/CellLine.spec.js | 23 + .../src/models/cellLine/CellLineGroup.spec.js | 43 + .../models/collection/CollectionUtils.spec.js | 89 + .../packs/src/utilities/SpectraHelper.spec.js | 359 + spec/javascripts/stores/ReportStore.spec.js | 3 +- .../stores/mobx/CellLineDetailsStore.spec.js | 95 + .../utils/NumericInputUnit.spec.js | 26 +- spec/javascripts/utils/textHelper.spec.js | 33 + spec/javascripts/utils/timezoneHelper.spec.js | 45 + spec/jobs/import_samples_job_spec.rb | 126 + spec/lib/analyses/converter_spec.rb | 4 +- spec/lib/chemotion/chemicals_service_spec.rb | 24 - spec/lib/chemotion/jcamp_spec.rb | 80 + spec/lib/chemotion/quill_to_html_spec.rb | 22 +- .../lib/chemotion/quill_to_plain_text_spec.rb | 61 + spec/lib/export/export_chemicals_spec.rb | 197 + spec/lib/export/export_collections_spec.rb | 184 +- spec/lib/import/import_chemicals_spec.rb | 121 + spec/lib/import/import_collections_spec.rb | 58 +- spec/lib/import/import_export_json_spec.rb | 228 - spec/lib/import/import_samples_spec.rb | 54 +- .../lib/reporter/docx/detail_reaction_spec.rb | 1 + spec/lib/reporter/docx/document_spec.rb | 4 + spec/models/cellline_material_spec.rb | 7 + spec/models/cellline_sample_spec.rb | 13 + spec/models/code_log_spec.rb | 6 +- spec/models/collection_spec.rb | 28 +- spec/models/collections_cellline_spec.rb | 75 + spec/models/collections_vessel_spec.rb | 10 + spec/models/comment_spec.rb | 52 + spec/models/element_klass_spec.rb | 27 + spec/models/generic_dataset_spec.rb | 27 + spec/models/generic_element_spec.rb | 29 + spec/models/generic_segment_spec.rb | 29 + spec/models/inventory_spec.rb | 27 + spec/models/sample_spec.rb | 12 + spec/models/third_party_app_spec.rb | 4 +- spec/models/user_spec.rb | 7 +- spec/models/vessel_spec.rb | 29 + spec/models/vessel_template_spec.rb | 9 + .../element_detail_level_calculator_spec.rb | 3 +- spec/spec_helper.rb | 10 +- spec/support/report_helpers.rb | 3 +- spec/support/report_share.rb | 1 + .../acts_as_paranoid_soft_deletable_model.rb | 28 + spec/support/shoulda_matchers.rb | 8 + spec/usecases/cell_lines/create_spec.rb | 179 + spec/usecases/cell_lines/load_spec.rb | 54 + spec/usecases/cell_lines/update_spec.rb | 109 + yarn.lock | 4010 ++--- 752 files changed, 59986 insertions(+), 26355 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 README-DEV.md delete mode 100644 app/api/chemotion/admin_generic_api.rb create mode 100644 app/api/chemotion/cell_line_api.rb create mode 100644 app/api/chemotion/comment_api.rb delete mode 100644 app/api/chemotion/generic_dataset_api.rb delete mode 100644 app/api/chemotion/generic_element_api.rb create mode 100644 app/api/chemotion/inventory_api.rb delete mode 100644 app/api/chemotion/segment_api.rb create mode 100644 app/api/entities/cell_line_material_name_entity.rb create mode 100644 app/api/entities/cell_line_sample_entity.rb create mode 100644 app/api/entities/comment_entity.rb delete mode 100644 app/api/entities/dataset_entity.rb delete mode 100644 app/api/entities/dataset_klass_entity.rb delete mode 100644 app/api/entities/element_entity.rb delete mode 100644 app/api/entities/element_klass_entity.rb delete mode 100644 app/api/entities/element_revision_entity.rb create mode 100644 app/api/entities/inventory_entity.rb create mode 100644 app/api/entities/reaction_variation_entity.rb delete mode 100644 app/api/entities/segment_entity.rb delete mode 100644 app/api/entities/segment_klass_entity.rb delete mode 100644 app/api/entities/segment_revision_entity.rb create mode 100644 app/api/helpers/comment_helpers.rb delete mode 100644 app/api/helpers/generic_helpers.rb create mode 100644 app/api/helpers/reflection_helpers.rb delete mode 100644 app/api/helpers/sample_association_helpers.rb create mode 100644 app/assets/stylesheets/attachment-list.scss create mode 100644 app/assets/stylesheets/cellLines.scss create mode 100644 app/assets/stylesheets/components/CommentButton/CommentButton.scss create mode 100644 app/assets/stylesheets/components/CommentModal/CommentModal.scss create mode 100644 app/assets/stylesheets/components/inbox/inbox.scss create mode 100644 app/assets/stylesheets/components/select.scss create mode 100644 app/assets/stylesheets/inventory-label-settings.scss create mode 100644 app/assets/stylesheets/search_modal.scss create mode 100644 app/assets/stylesheets/search_results.scss delete mode 100644 app/jobs/gate_transfer_job.rb create mode 100644 app/jobs/import_samples_job.rb create mode 100644 app/models/cellline_material.rb create mode 100644 app/models/cellline_sample.rb create mode 100644 app/models/collections_cellline.rb delete mode 100644 app/models/collections_element.rb create mode 100644 app/models/collections_vessel.rb create mode 100644 app/models/comment.rb delete mode 100644 app/models/concerns/attachment_converter.rb delete mode 100644 app/models/concerns/datasetable.rb delete mode 100644 app/models/concerns/generic_klass_revisions.rb delete mode 100644 app/models/concerns/generic_revisions.rb delete mode 100644 app/models/concerns/segmentable.rb delete mode 100644 app/models/dataset.rb delete mode 100644 app/models/dataset_klass.rb delete mode 100644 app/models/dataset_klasses_revision.rb delete mode 100644 app/models/datasets_revision.rb delete mode 100644 app/models/element.rb delete mode 100644 app/models/element_klass.rb delete mode 100644 app/models/element_klasses_revision.rb delete mode 100644 app/models/elements_revision.rb delete mode 100644 app/models/elements_sample.rb create mode 100644 app/models/inventory.rb delete mode 100644 app/models/segment.rb delete mode 100644 app/models/segment_klass.rb delete mode 100644 app/models/segment_klasses_revision.rb delete mode 100644 app/models/segments_revision.rb create mode 100644 app/models/vessel.rb create mode 100644 app/models/vessel_template.rb create mode 100644 app/packs/src/apps/admin/ChemSpectraLayouts.js delete mode 100644 app/packs/src/apps/admin/DatasetElementAdmin.js delete mode 100644 app/packs/src/apps/admin/GenericElementAdmin.js delete mode 100644 app/packs/src/apps/admin/SegmentElementAdmin.js delete mode 100644 app/packs/src/apps/admin/generic/AttrCopyModal.js delete mode 100644 app/packs/src/apps/admin/generic/AttrEditModal.js delete mode 100644 app/packs/src/apps/admin/generic/AttrForm.js delete mode 100644 app/packs/src/apps/admin/generic/AttrNewModal.js delete mode 100644 app/packs/src/apps/admin/generic/FieldCondEditModal.js create mode 100644 app/packs/src/apps/admin/generic/GenericAdminModal.js delete mode 100644 app/packs/src/apps/admin/generic/KlassAttrForm.js delete mode 100644 app/packs/src/apps/admin/generic/LayerAttrEditModal.js delete mode 100644 app/packs/src/apps/admin/generic/LayerAttrForm.js delete mode 100644 app/packs/src/apps/admin/generic/LayerAttrNewModal.js delete mode 100644 app/packs/src/apps/admin/generic/Preview.js delete mode 100644 app/packs/src/apps/admin/generic/SegmentAttrForm.js delete mode 100644 app/packs/src/apps/admin/generic/SelectAttrNewModal.js delete mode 100644 app/packs/src/apps/admin/generic/TemplateJsonModal.js delete mode 100644 app/packs/src/apps/admin/generic/UploadModal.js delete mode 100644 app/packs/src/apps/admin/generic/Utils.js delete mode 100644 app/packs/src/apps/admin/generic/collate.js create mode 100644 app/packs/src/apps/generic/GenericAdminNav.js create mode 100644 app/packs/src/apps/generic/GenericDatasetsAdmin.js create mode 100644 app/packs/src/apps/generic/GenericElementsAdmin.js create mode 100644 app/packs/src/apps/generic/GenericSegmentsAdmin.js create mode 100644 app/packs/src/apps/generic/SyncButton.js create mode 100644 app/packs/src/apps/generic/Utils.js create mode 100644 app/packs/src/apps/mydb/collections/CollectionTabs.js delete mode 100644 app/packs/src/apps/mydb/collections/sampleTaskInbox/SampleTaskNavigationElement.js delete mode 100644 app/packs/src/apps/mydb/elements/details/ElementFieldDragSource.js delete mode 100644 app/packs/src/apps/mydb/elements/details/ElementFieldDropTarget.js delete mode 100644 app/packs/src/apps/mydb/elements/details/GroupFields.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/CellLineDetails.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/analysesTab/AnalysesContainer.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/analysesTab/EditModeHeader.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/analysesTab/EditModeRow.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/analysesTab/OrderModeHeader.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/analysesTab/OrderModeRow.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/propertiesTab/Amount.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/propertiesTab/CellLineName.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/propertiesTab/GeneralProperties.js create mode 100644 app/packs/src/apps/mydb/elements/details/cellLines/propertiesTab/InvalidPropertyWarning.js create mode 100644 app/packs/src/apps/mydb/elements/details/literature/CitationTools.js create mode 100644 app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariations.js create mode 100644 app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.js create mode 100644 app/packs/src/apps/mydb/elements/details/researchPlans/SaveEditedImageWarning.js delete mode 100644 app/packs/src/apps/mydb/elements/details/researchPlans/SaveResearchPlanWarning.js create mode 100644 app/packs/src/apps/mydb/elements/labels/ElementResearchPlanLabels.js create mode 100644 app/packs/src/apps/mydb/elements/list/AttachmentList.js create mode 100644 app/packs/src/apps/mydb/elements/list/ElementsTableGroupedEntries.js create mode 100644 app/packs/src/apps/mydb/elements/list/cellLine/CellLineContainer.js create mode 100644 app/packs/src/apps/mydb/elements/list/cellLine/CellLineEntry.js create mode 100644 app/packs/src/apps/mydb/elements/list/cellLine/CellLineItemEntry.js create mode 100644 app/packs/src/apps/mydb/elements/list/cellLine/CellLineItemText.js create mode 100644 app/packs/src/apps/settings/InventoryLabelSettings.js create mode 100644 app/packs/src/components/actions/CollectionActions.js create mode 100644 app/packs/src/components/comments/CommentButton.js create mode 100644 app/packs/src/components/comments/CommentDetails.js create mode 100644 app/packs/src/components/comments/CommentIcon.js create mode 100644 app/packs/src/components/comments/CommentList.js create mode 100644 app/packs/src/components/comments/CommentSection.js create mode 100644 app/packs/src/components/comments/HeaderCommentSection.js create mode 100644 app/packs/src/components/common/CommentModal.js create mode 100644 app/packs/src/components/common/DeleteComment.js create mode 100644 app/packs/src/components/common/SpectraEditorButton.js delete mode 100644 app/packs/src/components/container/ContainerDataset.js create mode 100644 app/packs/src/components/container/ContainerDatasetModalContent.js delete mode 100644 app/packs/src/components/generic/AttrChk.js delete mode 100644 app/packs/src/components/generic/DefinedRenderer.js delete mode 100644 app/packs/src/components/generic/DropLinkRenderer.js delete mode 100644 app/packs/src/components/generic/DropRenderer.js delete mode 100644 app/packs/src/components/generic/DropTextRenderer.js create mode 100644 app/packs/src/components/generic/EditorAnalysisBtn.js delete mode 100644 app/packs/src/components/generic/ElementField.js delete mode 100644 app/packs/src/components/generic/FieldSelect.js create mode 100644 app/packs/src/components/generic/GenericContainer.js create mode 100644 app/packs/src/components/generic/GenericContainerGroup.js delete mode 100644 app/packs/src/components/generic/GenericElCommon.js delete mode 100644 app/packs/src/components/generic/GenericElCriteriaModal.js delete mode 100644 app/packs/src/components/generic/GenericElDropTarget.js delete mode 100644 app/packs/src/components/generic/GenericElTableDropTarget.js delete mode 100644 app/packs/src/components/generic/GenericPropertiesFields.js create mode 100644 app/packs/src/components/generic/GenericSGDetails.js delete mode 100644 app/packs/src/components/generic/GridBtn.js delete mode 100644 app/packs/src/components/generic/GridDnD.js delete mode 100644 app/packs/src/components/generic/GridEntry.js delete mode 100644 app/packs/src/components/generic/GridSelect.js delete mode 100644 app/packs/src/components/generic/LayerSelect.js delete mode 100644 app/packs/src/components/generic/PreviewModal.js create mode 100644 app/packs/src/components/generic/RevisionViewerBtn.js delete mode 100644 app/packs/src/components/generic/SamOption.js delete mode 100644 app/packs/src/components/generic/SystemSelect.js delete mode 100644 app/packs/src/components/generic/TableDef.js delete mode 100644 app/packs/src/components/generic/TableRecord.js delete mode 100644 app/packs/src/components/generic/TextFormula.js delete mode 100644 app/packs/src/components/generic/TypeSelect.js delete mode 100644 app/packs/src/components/generic/UConverterRenderer.js rename app/packs/src/{apps/mydb/collections => components}/sampleTaskInbox/SampleTaskCard.js (58%) rename app/packs/src/{apps/mydb/collections => components}/sampleTaskInbox/SampleTaskInbox.js (83%) create mode 100644 app/packs/src/components/sampleTaskInbox/SampleTaskNavigationElement.js create mode 100644 app/packs/src/components/sampleTaskInbox/SampleTaskReloadButton.js create mode 100644 app/packs/src/components/searchModal/SearchModal.js create mode 100644 app/packs/src/components/searchModal/forms/AdvancedSearchRow.js create mode 100644 app/packs/src/components/searchModal/forms/AnalysesFieldData.js create mode 100644 app/packs/src/components/searchModal/forms/DetailSearch.js create mode 100644 app/packs/src/components/searchModal/forms/KetcherRailsForm.js create mode 100644 app/packs/src/components/searchModal/forms/MeasurementFieldData.js create mode 100644 app/packs/src/components/searchModal/forms/NoFormSelected.js create mode 100644 app/packs/src/components/searchModal/forms/PublicationFieldData.js create mode 100644 app/packs/src/components/searchModal/forms/PublicationSearch.js create mode 100644 app/packs/src/components/searchModal/forms/PublicationSearchRow.js create mode 100644 app/packs/src/components/searchModal/forms/SampleInventoryFieldData.js create mode 100644 app/packs/src/components/searchModal/forms/SearchModalFunctions.js create mode 100644 app/packs/src/components/searchModal/forms/SearchResult.js create mode 100644 app/packs/src/components/searchModal/forms/SearchResultTabContent.js create mode 100644 app/packs/src/components/searchModal/forms/SelectFieldData.js create mode 100644 app/packs/src/components/searchModal/forms/SelectMapperData.js create mode 100644 app/packs/src/components/searchModal/forms/TextSearch.js create mode 100644 app/packs/src/endpoints/ApiServices.js create mode 100644 app/packs/src/fetchers/CellLinesFetcher.js create mode 100644 app/packs/src/fetchers/ChemSpectraFetcher.js create mode 100644 app/packs/src/fetchers/CommentFetcher.js create mode 100644 app/packs/src/fetchers/GenericSgsFetcher.js create mode 100644 app/packs/src/fetchers/InventoryFetcher.js delete mode 100644 app/packs/src/fetchers/SegmentsFetcher.js create mode 100644 app/packs/src/models/Comment.js create mode 100644 app/packs/src/models/cellLine/CellLine.js create mode 100644 app/packs/src/models/cellLine/CellLineGroup.js create mode 100644 app/packs/src/models/cellLine/CellLinePropTypes.js create mode 100644 app/packs/src/models/collection/CollectionUtils.js create mode 100644 app/packs/src/stores/alt/actions/CommentActions.js create mode 100644 app/packs/src/stores/alt/stores/CommentStore.js create mode 100644 app/packs/src/stores/mobx/CellLineDetailsStore.jsx create mode 100644 app/packs/src/stores/mobx/SearchStore.jsx create mode 100644 app/packs/src/utilities/CellLineUtils.js create mode 100644 app/packs/src/utilities/CollectionTabsHelper.js create mode 100644 app/packs/src/utilities/CommentHelper.js create mode 100644 app/packs/src/utilities/selectHelper.js create mode 100644 app/packs/src/utilities/textHelper.js create mode 100644 app/packs/src/utilities/timezoneHelper.js create mode 100644 app/usecases/cell_lines/create.rb create mode 100644 app/usecases/cell_lines/load.rb create mode 100644 app/usecases/cell_lines/update.rb create mode 100644 app/usecases/search/advanced_search.rb create mode 100644 app/usecases/search/by_ids.rb create mode 100644 app/usecases/search/conditions_for_advanced_search.rb create mode 100644 app/usecases/search/shared_methods.rb create mode 100644 app/usecases/search/structure_search.rb create mode 100644 app/views/devise/shared/_dblogin.html.haml create mode 100644 app/views/pages/gda.haml create mode 100644 app/views/pages/gea.haml create mode 100644 app/views/pages/gsa.haml create mode 100644 bin/run_mail_collector create mode 100644 db/migrate/20210727145003_generic_workflow_migration.rb create mode 100644 db/migrate/20210820165003_generic_elements_to_element.rb create mode 100644 db/migrate/20211105111420_add_dry_solvent_to_samples.rb create mode 100644 db/migrate/20220127000608_create_comments.rb create mode 100644 db/migrate/20220317004217_add_comment_channel.rb create mode 100644 db/migrate/20220406094235_add_tabs_segment_to_collection.rb create mode 100644 db/migrate/20220712100010_add_segment_klass_identifier.rb create mode 100644 db/migrate/20221128073938_add_sort_indexes_to_reactions.rb create mode 100644 db/migrate/20230320132646_add_variations_to_reactions.rb create mode 100644 db/migrate/20230420121233_matrice_comment.rb create mode 100644 db/migrate/20230522075503_create_cellline_models.rb create mode 100644 db/migrate/20230531142756_add_samples_import_channel.rb create mode 100644 db/migrate/20230613063121_update_comment_channel.rb create mode 100644 db/migrate/20230630123412_add_additonal_plain_text_field_for_description_fields.rb create mode 100644 db/migrate/20230630125148_cell_cline_data_migration.rb create mode 100644 db/migrate/20230630140647_fill_new_plain_text_description_fields.rb create mode 100644 db/migrate/20230714100005_add_generic_updated_by.rb create mode 100644 db/migrate/20230804100000_att_com_state.rb create mode 100644 db/migrate/20230810100000_labimotion_class.rb create mode 100644 db/migrate/20230814110512_add_plain_text_content_to_containers.rb create mode 100644 db/migrate/20230814121455_fill_new_plain_text_content_field_at_containers.rb create mode 100644 db/migrate/20230927073354_create_vessel_templates.rb create mode 100644 db/migrate/20230927073410_create_vessels.rb create mode 100644 db/migrate/20230927073435_create_collections_vessels.rb create mode 100644 db/migrate/20231106000000_cellline_element_init.rb create mode 100644 db/migrate/20231218191658_create_inventories.rb create mode 100644 db/migrate/20231218191929_add_inventories_to_collections.rb create mode 100644 db/migrate/20231219151632_add_weight_to_vessel_templates.rb create mode 100644 db/migrate/20231219152154_add_bar_and_qr_code_to_vessels.rb create mode 100644 db/migrate/20231219162631_change_volume_amount_to_float_in_vessel_templates.rb create mode 100644 db/migrate/20240129134421_rename_inbox_folders.rb create mode 100644 db/seeds/development/vessels.seed.rb delete mode 100644 lib/analyses/converter.rb create mode 100644 lib/chemotion/meta_schmooze/meta_schmooze.rb create mode 100644 lib/chemotion/quill_to_plain_text.rb create mode 100644 lib/datacollector/collector_helper_set.rb create mode 100644 lib/export/export_chemicals.rb delete mode 100644 lib/export/export_json.rb create mode 100644 lib/import/helper/cellline_importer.rb create mode 100644 lib/import/import_chemicals.rb create mode 100644 public/ontologies_default/bao.default.edited.json create mode 100644 spec/api/chemotion/cell_line_api_spec.rb create mode 100644 spec/api/chemotion/comment_api_spec.rb create mode 100644 spec/api/chemotion/generic_dataset_api_spec.rb create mode 100644 spec/api/chemotion/inventory_api_spec.rb create mode 100644 spec/api/chemotion/segment_api_spec.rb create mode 100644 spec/api/entities/inventory_entity_spec.rb create mode 100644 spec/api/generic_api_spec.rb create mode 100644 spec/api/generic_segment_api_spec.rb create mode 100644 spec/cypress/end_to_end/admin.cy.js create mode 100644 spec/cypress/end_to_end/calendar_specs.cy.js create mode 100644 spec/cypress/end_to_end/samples_api_spec.cy.js create mode 100644 spec/cypress/end_to_end/search_modal.cy.js create mode 100644 spec/factories/cell_line_material.rb create mode 100644 spec/factories/cell_line_sample.rb create mode 100644 spec/factories/collections_vessels.rb create mode 100644 spec/factories/comment.rb create mode 100644 spec/factories/dataset_klasses.rb create mode 100644 spec/factories/datasets.rb create mode 100644 spec/factories/element_klasses.rb create mode 100644 spec/factories/elements.rb create mode 100644 spec/factories/inventory.rb create mode 100644 spec/factories/segments.rb create mode 100644 spec/factories/setment_klasses.rb create mode 100644 spec/factories/vessel_templates.rb create mode 100644 spec/factories/vessels.rb create mode 100644 spec/fixtures/body_OKKJLVBELUTLKV-UHFFFAOYSA-N.json create mode 100644 spec/fixtures/body_RJUFJBKOKNCXHH-UHFFFAOYSA-N.json create mode 100644 spec/fixtures/body_UHOVQNZJYSORNB-UHFFFAOYSA-N.json create mode 100644 spec/fixtures/body_XBDQKXXYIPTUBI-UHFFFAOYSA-N.json create mode 100644 spec/fixtures/import/20230629_two_cell_line_samples.zip create mode 100644 spec/fixtures/import/collection_chemicals.zip create mode 100644 spec/fixtures/import/sample_import_template.xlsx create mode 100644 spec/javascripts/factories/CellLineFactory.js create mode 100644 spec/javascripts/fixture/chmos.js create mode 100644 spec/javascripts/packs/src/apps/mydb/elements/details/reactions/analysesTab/ReactionDetailsContainers.spec.js create mode 100644 spec/javascripts/packs/src/apps/mydb/elements/details/researchPlans/analysesTab/ResearchPlanDetailsContainers.spec.js create mode 100644 spec/javascripts/packs/src/apps/mydb/elements/details/researchPlans/attachmentsTab/ResearchPlanDetailsAttachments.spec.js create mode 100644 spec/javascripts/packs/src/apps/mydb/elements/details/samples/analysesTab/SampleDetailsContainersAux.spec.js create mode 100644 spec/javascripts/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.spec.js create mode 100644 spec/javascripts/packs/src/components/ChemSpectraLayouts.spec.js create mode 100644 spec/javascripts/packs/src/components/OlsComponent.spec.js create mode 100644 spec/javascripts/packs/src/fetchers/ChemSpectraFetcher.spec.js create mode 100644 spec/javascripts/packs/src/fetchers/InventoryFetcher.spec.js create mode 100644 spec/javascripts/packs/src/models/Container.spec.js create mode 100644 spec/javascripts/packs/src/models/cellLine/CellLine.spec.js create mode 100644 spec/javascripts/packs/src/models/cellLine/CellLineGroup.spec.js create mode 100644 spec/javascripts/packs/src/models/collection/CollectionUtils.spec.js create mode 100644 spec/javascripts/packs/src/utilities/SpectraHelper.spec.js create mode 100644 spec/javascripts/stores/mobx/CellLineDetailsStore.spec.js create mode 100644 spec/javascripts/utils/textHelper.spec.js create mode 100644 spec/javascripts/utils/timezoneHelper.spec.js create mode 100644 spec/jobs/import_samples_job_spec.rb create mode 100644 spec/lib/chemotion/jcamp_spec.rb create mode 100644 spec/lib/chemotion/quill_to_plain_text_spec.rb create mode 100644 spec/lib/export/export_chemicals_spec.rb create mode 100644 spec/lib/import/import_chemicals_spec.rb delete mode 100644 spec/lib/import/import_export_json_spec.rb create mode 100644 spec/models/cellline_material_spec.rb create mode 100644 spec/models/cellline_sample_spec.rb create mode 100644 spec/models/collections_cellline_spec.rb create mode 100644 spec/models/collections_vessel_spec.rb create mode 100644 spec/models/comment_spec.rb create mode 100644 spec/models/element_klass_spec.rb create mode 100644 spec/models/generic_dataset_spec.rb create mode 100644 spec/models/generic_element_spec.rb create mode 100644 spec/models/generic_segment_spec.rb create mode 100644 spec/models/inventory_spec.rb create mode 100644 spec/models/vessel_spec.rb create mode 100644 spec/models/vessel_template_spec.rb create mode 100644 spec/support/shared_examples/acts_as_paranoid_soft_deletable_model.rb create mode 100644 spec/support/shoulda_matchers.rb create mode 100644 spec/usecases/cell_lines/create_spec.rb create mode 100644 spec/usecases/cell_lines/load_spec.rb create mode 100644 spec/usecases/cell_lines/update_spec.rb 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 ( +
+