diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..16ac9e7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +1.1.0 - 2023-10-30 +=================== +* Initial version +* Generic Designer +* Workflow of Generic Element +* Repetitation of layers +* Drag Element to Element +* Dataset Metadata +* LabIMotion Template Hub Synchronization diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..492d198 --- /dev/null +++ b/Gemfile @@ -0,0 +1,25 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/labimotion/#{repo}.git" } + +ruby '2.7.7' ## '2.6.8' + +gem 'bootsnap', '>= 1.13.0', require: false + +group :development, :test do + gem 'byebug', platforms: %i[mri mingw x64_mingw] +end + +group :development do + gem 'web-console', '>= 4.2.0' + gem 'listen', '~> 3.3' + gem 'rack-mini-profiler', '~> 2.0' + gem 'spring' +end + +group :test do + gem 'capybara', '>= 3.26' + gem 'selenium-webdriver', '>= 4.0.0.rc1' + gem 'webdrivers' +end + +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..79d2266 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,131 @@ +GEM + remote: https://rubygems.org/ + specs: + actionpack (6.1.7.3) + actionview (= 6.1.7.3) + activesupport (= 6.1.7.3) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionview (6.1.7.3) + activesupport (= 6.1.7.3) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activemodel (6.1.7.3) + activesupport (= 6.1.7.3) + activesupport (6.1.7.3) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) + bindex (0.8.1) + bootsnap (1.16.0) + msgpack (~> 1.2) + builder (3.2.4) + byebug (11.1.3) + capybara (3.36.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (4.1.0) + concurrent-ruby (1.2.2) + crass (1.0.6) + erubi (1.12.0) + ffi (1.15.5) + i18n (1.13.0) + concurrent-ruby (~> 1.0) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.21.3) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + matrix (0.4.2) + method_source (1.0.0) + mini_mime (1.1.2) + mini_portile2 (2.8.4) + minitest (5.18.0) + msgpack (1.7.0) + nokogiri (1.13.10) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + nokogiri (1.13.10-x86_64-linux) + racc (~> 1.4) + public_suffix (5.0.1) + racc (1.6.2) + rack (2.2.7) + rack-mini-profiler (2.3.4) + rack (>= 1.2.0) + rack-test (2.1.0) + rack (>= 1.3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + 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) + method_source + rake (>= 12.2) + thor (~> 1.0) + rake (13.0.6) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + regexp_parser (2.8.0) + rexml (3.2.5) + rubyzip (2.3.2) + selenium-webdriver (4.1.0) + childprocess (>= 0.5, < 5.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2) + spring (3.1.1) + thor (1.2.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webdrivers (5.2.0) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.8) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + bootsnap (>= 1.13.0) + byebug + capybara (>= 3.26) + listen (~> 3.3) + rack-mini-profiler (~> 2.0) + selenium-webdriver (>= 4.0.0.rc1) + spring + tzinfo-data + web-console (>= 4.2.0) + webdrivers + +RUBY VERSION + ruby 2.7.7p221 + +BUNDLED WITH + 2.2.29 diff --git a/README.md b/README.md index b770fd2..779a573 100644 --- a/README.md +++ b/README.md @@ -1 +1,68 @@ -# labimotion \ No newline at end of file +# LabIMotion [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3755759.svg)](https://doi.org/10.5281/zenodo.8305411) + +LabIMotion is a software that provides the option to design new modules that can be adapted to the needs of the scientists. It includes Generic Elements, Segments, and Datasets. +They are structured using Components, which are introduced as layers and fields. Each generic element, segment, or dataset has the capacity to encompass multiple layers, and within each layer, there is the potential for multiple fields to be present. This hierarchical arrangement allows for a flexible and comprehensive organization of data and information. + + + +![Design Principles](https://www.chemotion.net/assets/images/generic_feature_outline-a58eee8e02ca7247e54f7ad17ee2c102.png) + + +## Version 1.0.18 of LabIMotion, featuring: + +* Generic Designer +* Workflow of Generic Element +* Repetitation of layers +* Drag Element to Element +* Dataset Metadata +* LabIMotion Template Hub Synchronization [**[LabIMotion Template Hub]**] + +--- + +This repository contains a backend service for the LabIMotion. It is written in **[Ruby]**. + +### Community + + * [GitHub discussions](https://github.com/LabIMotion/labimotion/discussions) + +### Code + + * [GitHub code](https://github.com/LabIMotion/labimotion) and [bug tracker](https://github.com/LabIMotion/labimotion/issues) + +--- + + +## Documentation + +Documentation for users **⸢ [Documentation] ⸥** + +Documentation for developers **⸢ [Technical Documentation] ⸥** + +--- + +## License + +Code released under the [AGPL-3.0 License]([https://www.gnu.org/licenses/agpl-3.0.txt](https://www.gnu.org/licenses/agpl-3.0.txt)). + +--- + +## 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. + + + + +[Documentation]: https://www.chemotion.net/docs/labimotion/ +[Technical Documentation]: https://www.rubydoc.info/gems/labimotion +[DFG]: https://www.dfg.de/en/ +[DFG Logo]: https://www.dfg.de/zentralablage/bilder/service/logos_corporate_design/logo_negativ_267.png +[Nicole Jung]: mailto:nicole.jung@kit.edu +[Karlsruhe Institute of Technology]: https://www.kit.edu/english/ +[Ruby]: https://www.ruby-lang.org/ +[LabIMotion Template Hub]: https://www.chemotion-repository.net/home/genericHub diff --git a/db/seeds/dataset_klasses.json b/db/seeds/dataset_klasses.json new file mode 100644 index 0000000..2fbf235 --- /dev/null +++ b/db/seeds/dataset_klasses.json @@ -0,0 +1,59 @@ +{ + "chmo": [ + { + "id": "CHMO:0000593", + "label": "1H nuclear magnetic resonance spectroscopy", + "position": 10, + "synonym": "1H NMR" + }, + { + "id": "CHMO:0000595", + "label": "13C nuclear magnetic resonance spectroscopy", + "position": 20, + "synonym": "13C NMR" + }, + { + "id": "CHMO:0000470", + "label": "mass spectrometry", + "position": 30, + "synonym": "MS" + }, + { + "id": "CHMO:0001075", + "label": "elemental analysis", + "position": 40, + "synonym": "EA" + }, + { + "id": "CHMO:0000497", + "label": "gas chromatography-mass spectrometry", + "position": 50, + "synonym": "GCMS" + }, + { + "id": "CHMO:0001009", + "label": "high-performance liquid chromatography", + "position": 60, + "synonym": "HPLC" + }, + { + "id": "CHMO:0000630", + "label": "infrared absorption spectroscopy", + "position": 70, + "synonym": "IR" + }, + { + "id": "CHMO:0001007", + "label": "thin-layer chromatography", + "position": 80, + "synonym": "TLC" + }, + { + "id": "CHMO:0000292", + "label": "ultraviolet-visible spectrophotometry", + "position": 90, + "synonym": "UV-VIS" + } + ] +} + diff --git a/labimotion.gemspec b/labimotion.gemspec new file mode 100644 index 0000000..d9f87cf --- /dev/null +++ b/labimotion.gemspec @@ -0,0 +1,14 @@ +require_relative "lib/labimotion/version" + +Gem::Specification.new do |spec| + spec.name = 'labimotion' + spec.version = Labimotion::VERSION + spec.summary = 'Chemotion LabIMotion' + spec.authors = ['Chia-Lin Lin', 'Pei-Chi Huang'] + spec.email = ['chia-lin.lin@kit.edu', 'pei-chi.huang@kit.edu'] + spec.homepage = 'https://github.com/LabIMotion/labimotion' + spec.license = 'AGPL-3.0' + spec.files = Dir['lib/**/*.rb', 'labimotion.rb'] + spec.require_paths = ['lib'] + spec.add_dependency "rails", "~> 6.1.7" +end diff --git a/lib/labimotion.rb b/lib/labimotion.rb new file mode 100644 index 0000000..77513b9 --- /dev/null +++ b/lib/labimotion.rb @@ -0,0 +1,99 @@ +# In your_gem_name.rb or main Ruby file +module Labimotion + + autoload :VERSION, 'labimotion/version' + + def self.logger + @@labimotion_logger ||= Logger.new(Rails.root.join('log/labimotion.log')) # rubocop:disable Style/ClassVars + end + + def self.log_exception(exception, current_user = nil) + Labimotion.logger.error("version: #{Labimotion::VERSION}; #{Labimotion::IS_RAILS5}, (#{current_user&.id}) \n Exception: #{exception.message}") + Labimotion.logger.error(exception.backtrace.join("\n")) + end + + autoload :Utils, 'labimotion/utils/utils' + + ######## APIs + autoload :GenericElementAPI, 'labimotion/apis/generic_element_api' + autoload :GenericDatasetAPI, 'labimotion/apis/generic_dataset_api' + autoload :SegmentAPI, 'labimotion/apis/segment_api' + autoload :LabimotionHubAPI, 'labimotion/apis/labimotion_hub_api' + autoload :ConverterAPI, 'labimotion/apis/converter_api' + + ######## Entities + autoload :ElementEntity, 'labimotion/entities/element_entity' + autoload :ElnElementEntity, 'labimotion/entities/eln_element_entity' + + autoload :SegmentEntity, 'labimotion/entities/segment_entity' + autoload :DatasetEntity, 'labimotion/entities/dataset_entity' + + autoload :GenericKlassEntity, 'labimotion/entities/generic_klass_entity' + autoload :ElementKlassEntity, 'labimotion/entities/element_klass_entity' + autoload :SegmentKlassEntity, 'labimotion/entities/segment_klass_entity' + autoload :DatasetKlassEntity, 'labimotion/entities/dataset_klass_entity' + + autoload :GenericEntity, 'labimotion/entities/generic_entity' + autoload :GenericPublicEntity, 'labimotion/entities/generic_public_entity' + autoload :KlassRevisionEntity, 'labimotion/entities/klass_revision_entity' + autoload :ElementRevisionEntity, 'labimotion/entities/element_revision_entity' + autoload :SegmentRevisionEntity, 'labimotion/entities/segment_revision_entity' + ## autoload :DatasetRevisionEntity, 'labimotion/entities/dataset_revision_entity' + + ######## Helpers + autoload :GenericHelpers, 'labimotion/helpers/generic_helpers' + autoload :ElementHelpers, 'labimotion/helpers/element_helpers' + autoload :SegmentHelpers, 'labimotion/helpers/segment_helpers' + autoload :DatasetHelpers, 'labimotion/helpers/dataset_helpers' + autoload :SearchHelpers, 'labimotion/helpers/search_helpers' + autoload :ConverterHelpers, 'labimotion/helpers/converter_helpers' + autoload :SampleAssociationHelpers, 'labimotion/helpers/sample_association_helpers' + autoload :RepositoryHelpers, 'labimotion/helpers/repository_helpers' + + ######## Libs + autoload :Converter, 'labimotion/libs/converter' + autoload :NmrMapper, 'labimotion/libs/nmr_mapper' + autoload :NmrMapperRepo, 'labimotion/libs/nmr_mapper_repo' ## for Chemotion Repository + autoload :TemplateHub, 'labimotion/libs/template_hub' + autoload :ExportDataset, 'labimotion/libs/export_dataset' + + ######## Utils + autoload :ConState, 'labimotion/utils/con_state' + autoload :Serializer, 'labimotion/utils/serializer' + autoload :Search, 'labimotion/utils/search' + + + ######## Collection + autoload :Export, 'labimotion/collection/export' + autoload :Import, 'labimotion/collection/import' + + ######## Models + autoload :Element, 'labimotion/models/element' + autoload :Segment, 'labimotion/models/segment' + autoload :Dataset, 'labimotion/models/dataset' + + autoload :ElementKlass, 'labimotion/models/element_klass' + autoload :SegmentKlass, 'labimotion/models/segment_klass' + autoload :DatasetKlass, 'labimotion/models/dataset_klass' + + autoload :ElementsRevision, 'labimotion/models/elements_revision' + autoload :SegmentsRevision, 'labimotion/models/segments_revision' + autoload :DatasetsRevision, 'labimotion/models/datasets_revision' + + autoload :ElementKlassesRevision, 'labimotion/models/element_klasses_revision' + autoload :SegmentKlassesRevision, 'labimotion/models/segment_klasses_revision' + autoload :DatasetKlassesRevision, 'labimotion/models/dataset_klasses_revision' + + autoload :ElementsSample, 'labimotion/models/elements_sample' + autoload :ElementsElement, 'labimotion/models/elements_element' + autoload :CollectionsElement, 'labimotion/models/collections_element' + + ######## Models/Concerns + autoload :GenericKlassRevisions, 'labimotion/models/concerns/generic_klass_revisions' + autoload :GenericRevisions, 'labimotion/models/concerns/generic_revisions' + autoload :Segmentable, 'labimotion/models/concerns/segmentable' + autoload :Datasetable, 'labimotion/models/concerns/datasetable' + autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter.rb' + + +end diff --git a/lib/labimotion/apis/converter_api.rb b/lib/labimotion/apis/converter_api.rb new file mode 100644 index 0000000..3d04b62 --- /dev/null +++ b/lib/labimotion/apis/converter_api.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Labimotion + class ConverterAPI < Grape::API + helpers do + end + resource :converter do + resource :profiles do + before do + @conf = Rails.configuration.try(:converter).try(:url) + @profile = Rails.configuration.try(:converter).try(:profile) + error!(406) unless @conf && @profile + end + desc 'fetch profiles' + get do + profiles = Labimotion::Converter.fetch_profiles + { profiles: profiles, client: @profile } + end + desc 'create profile' + post do + Labimotion::Converter.create_profile(params) + end + desc 'update profile' + route_param :id do + put do + Labimotion::Converter.update_profile(params) + end + end + desc 'delete profile' + route_param :id do + delete do + id = params[:id] + Labimotion::Converter.delete_profile(id) + end + end + end + + resource :options do + before do + error!(401) unless current_user.profile&.data['converter_admin'] == true + @conf = Rails.configuration.try(:converter).try(:url) + @profile = Rails.configuration.try(:converter).try(:profile) + error!(406) unless @conf && @profile + end + desc 'fetch options' + get do + options = Labimotion::Converter.fetch_options + { options: options, client: @profile } + end + end + + resource :tables do + before do + error!(401) unless current_user.profile&.data['converter_admin'] == true + @conf = Rails.configuration.try(:converter).try(:url) + @profile = Rails.configuration.try(:converter).try(:profile) + error!(406) unless @conf && @profile + end + desc 'create tables' + post do + res = Labimotion::Converter.create_tables(params[:file][0]['tempfile']) unless params[:file].empty? + res['metadata']['file_name'] = params[:file][0]['filename'] + res + end + end + end + end +end diff --git a/lib/labimotion/apis/generic_dataset_api.rb b/lib/labimotion/apis/generic_dataset_api.rb new file mode 100644 index 0000000..6680521 --- /dev/null +++ b/lib/labimotion/apis/generic_dataset_api.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Labimotion + ## Generic Dataset API + class GenericDatasetAPI < Grape::API + include Grape::Kaminari + helpers Labimotion::GenericHelpers + helpers Labimotion::DatasetHelpers + + resource :generic_dataset do + namespace :klasses do + desc 'get dataset klasses' + get do + list = klass_list(true) + present list.sort_by(&:place), with: Labimotion::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 = klass_list(params[:is_active]) + present list, with: Labimotion::DatasetKlassEntity, root: 'klass' + end + end + + namespace :fetch_repo do + desc 'fetch Generic Dataset Klass from Chemotion Repository' + get do + fetch_repo('DatasetKlass', current_user) + end + end + + namespace :create_repo_klass do + desc 'create Generic Dataset Klass' + params do + requires :identifier, type: String, desc: 'Identifier' + end + post do + msg = create_repo_klass(params, current_user, request.headers['Origin']) + klass = Labimotion::DatasetKlassEntity.represent(DatasetKlass.all) + { status: msg[:status], message: msg[:message], klass: klass } + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { error: e.message } + end + end + end + end +end diff --git a/lib/labimotion/apis/generic_element_api.rb b/lib/labimotion/apis/generic_element_api.rb new file mode 100644 index 0000000..a2a45de --- /dev/null +++ b/lib/labimotion/apis/generic_element_api.rb @@ -0,0 +1,408 @@ +# frozen_string_literal: true + +require 'labimotion/version' +module Labimotion + # Generic Element API + class GenericElementAPI < Grape::API + include Grape::Kaminari + helpers ContainerHelpers + helpers ParamsHelpers + helpers CollectionHelpers + helpers Labimotion::SampleAssociationHelpers + helpers Labimotion::GenericHelpers + helpers Labimotion::ElementHelpers + + 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 = Labimotion::ElementKlass.find_by(name: params[:name]) + present ek, with: Labimotion::ElementKlassEntity, root: 'klass' + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { 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 = klass_list(params[:generic_only]) + present list, with: Labimotion::ElementKlassEntity, root: 'klass' + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { klass: [] } + end + end + + namespace :create_element_klass do + desc 'create Generic Element Klass' + 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 + authenticate_admin!('elements') + create_element_klass(current_user, params) + 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 + authenticate_admin!('elements') + update_element_klass(current_user, params) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + 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 + list = list_klass_revisions(params) + present list, with: Labimotion::KlassRevisionEntity, root: 'revisions' + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + end + end + + namespace :element_revisions do + desc 'list Generic Element Revisions' + params do + requires :id, type: Integer, desc: 'Generic Element Id' + end + get do + list = element_revisions(params) + present list, with: Labimotion::ElementRevisionEntity, root: 'revisions' + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + end + end + + namespace :delete_klass_revision do + desc 'delete Klass Revision' + 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 + authenticate_admin!(params[:klass].gsub(/(Klass)/, 's').downcase) + delete_klass_revision(params) + status 201 + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + end + + namespace :delete_revision do + desc 'delete 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 + delete_revision(params) + status 201 + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + 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 = Labimotion::Segment.find(params[:id]) + list = klass.segments_revisions unless klass.nil? + present list&.sort_by(&:created_at).reverse, with: Labimotion::SegmentRevisionEntity, root: 'revisions' + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + 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 + if params[:att_type] == 'Sample' || params[:att_type] == 'Reaction' || params[:att_type] == 'ResearchPlan' + el = "#{params[:att_type]}".constantize.find_by(id: params[:att_id]) + else + el = "Labimotion::#{params[:att_type]}".constantize.find_by(id: params[:att_id]) + end + 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 + upload_generics_files(current_user, params) + true + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + end + + namespace :klasses_all do + desc 'get all klasses for admin function' + get do + list = Labimotion::ElementKlass.all.sort_by { |e| e.place } + present list, with: Labimotion::ElementKlassEntity, root: 'klass' + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + end + end + + namespace :fetch_repo do + desc 'fetch Generic Element Klass from Chemotion Repository' + get do + fetch_repo('ElementKlass', current_user) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + end + end + + namespace :create_repo_klass do + desc 'create Generic Element Klass' + params do + requires :identifier, type: String, desc: 'Identifier' + end + post do + msg = create_repo_klass(params, current_user, request.headers['Origin']) + klass = Labimotion::ElementKlassEntity.represent(ElementKlass.all) + { status: msg[:status], message: msg[:message], klass: klass } + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { error: e.message } + end + end + + namespace :de_activate_klass do + desc 'activate or deactivate Generic Klass' + params do + requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass] + requires :id, type: Integer, desc: 'Klass ID' + requires :is_active, type: Boolean, desc: 'Active or Inactive Klass' + end + after_validation do + authenticate_admin!(params[:klass].gsub(/(Klass)/, 's').downcase) + @klz = fetch_klass(params[:klass], params[:id]) + end + post do + deactivate_klass(params) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + end + + namespace :delete_klass do + desc 'delete Generic Klass' + params do + requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass] + requires :id, type: Integer, desc: 'Klass ID' + end + delete ':id' do + delete_klass(params) + status 201 + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + end + + namespace :update_template do + desc 'update Generic Properties Template' + params do + requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass] + requires :id, type: Integer, desc: 'Klass ID' + requires :properties_template, type: Hash + optional :release, type: String, default: 'draft', desc: 'release status', values: %w[draft major minor patch] + end + after_validation do + authenticate_admin!(params[:klass].gsub(/(Klass)/, 's').downcase) + @klz = fetch_klass(params[:klass], params[:id]) + end + post do + update_template(params, current_user) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + 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' + optional :sort_column, type: String, desc: 'sort by updated_at or selected layers property' + end + paginate per_page: 7, offset: 0, max_per_page: 100 + get do + scope = list_serialized_elements(params, current_user) + + reset_pagination_page(scope) + if Labimotion::IS_RAILS5 == true + generic_elements = paginate(scope).map { |s| ElementListPermissionProxy.new(current_user, s, user_ids).serialized } + else + generic_elements = paginate(scope).map do |element| + Labimotion::ElementEntity.represent( + element, + displayed_in_list: true, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, + ) + end + end + { generic_elements: generic_elements } + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { 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 = Labimotion::Element.find(params[:id]) + if Labimotion::IS_RAILS5 == true + { + element: ElementPermissionProxy.new(current_user, element, user_ids).serialized, + attachments: attach_thumbnail(element&.attachments) + } + else + { + element: Labimotion::ElementEntity.represent( + element, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, + ), + attachments: attach_thumbnail(element&.attachments) + } + end + rescue StandardError => e + Labimotion.log_exception(e, current_user) + end + end + + desc 'Create a element' + params do + requires :element_klass, type: Hash + requires :name, type: String + optional :properties, type: Hash + optional :properties_release, type: Hash + optional :collection_id, type: Integer + requires :container, type: Hash + optional :segments, type: Array, desc: 'Segments' + end + post do + begin + element = create_element(current_user, params) + if Labimotion::IS_RAILS5 == true + { + element: ElementPermissionProxy.new(current_user, element, user_ids).serialized, + attachments: attach_thumbnail(element&.attachments) + } + else + present( + element, + with: Labimotion::ElementEntity, + root: :element, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, + ) + end + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + end + + desc 'Update element by id' + params do + requires :id, type: Integer, desc: 'element id' + optional :name, type: String + requires :properties, type: Hash + optional :properties_release, 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, Labimotion::Element.find(params[:id])).update? + end + + put do + begin + element = update_element_by_id(current_user, params) + if Labimotion::IS_RAILS5 == true + { + element: ElementPermissionProxy.new(current_user, element, user_ids).serialized, + attachments: attach_thumbnail(element&.attachments) + } + else + { + element: Labimotion::ElementEntity.represent( + element, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels, + ), + attachments: attach_thumbnail(element&.attachments), + } + end + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + end + end + end + end +end diff --git a/lib/labimotion/apis/labimotion_hub_api.rb b/lib/labimotion/apis/labimotion_hub_api.rb new file mode 100644 index 0000000..aa96610 --- /dev/null +++ b/lib/labimotion/apis/labimotion_hub_api.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'open-uri' +require 'labimotion/models/hub_log' + +# Belong to Chemotion module +module Labimotion + # API for Public data + class LabimotionHubAPI < Grape::API + include Grape::Kaminari + + namespace :labimotion_hub do + namespace :list do + desc "get active generic templates" + params do + requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass] + optional :with_props, type: Boolean, desc: 'With Properties', default: false + end + get do + list = "Labimotion::#{params[:klass]}".constantize.where(is_active: true).where.not(released_at: nil) + list = list.where(is_generic: true) if params[:klass] == 'ElementKlass' + entities = Labimotion::GenericPublicEntity.represent(list, displayed: params[:with_props]) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + end + end + namespace :fetch do + desc "get active generic templates" + params do + requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass] + requires :origin, type: String, desc: 'origin' + requires :identifier, type: String, desc: 'Identifier' + end + post do + entity = "Labimotion::#{params[:klass]}".constantize.find_by(identifier: params[:identifier]) + Labimotion::HubLog.create(klass: entity, origin: params[:origin], uuid: entity.uuid, version: entity.version) + "Labimotion::#{params[:klass]}Entity".constantize.represent(entity) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + 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 = Labimotion::ElementKlass.where(is_active: true) if params[:generic_only].present? && params[:generic_only] == true + list = Labimotion::ElementKlass.where(is_active: true) unless params[:generic_only].present? && params[:generic_only] == true + list.pluck(:name) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + end + end + + end + end +end diff --git a/lib/labimotion/apis/segment_api.rb b/lib/labimotion/apis/segment_api.rb new file mode 100644 index 0000000..ae7ab75 --- /dev/null +++ b/lib/labimotion/apis/segment_api.rb @@ -0,0 +1,145 @@ +module Labimotion + class SegmentAPI < Grape::API + include Grape::Kaminari + helpers Labimotion::GenericHelpers + helpers Labimotion::SegmentHelpers + + 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 = klass_list(params[:element], true) + present list, with: Labimotion::SegmentKlassEntity, root: 'klass' + 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 = klass_list(nil, params[:is_active]) + present list, with: Labimotion::SegmentKlassEntity, root: 'klass' + 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 + authenticate_admin!('segments') + @klass = fetch_klass('ElementKlass', params[:element_klass]) + end + post do + create_segment_klass(current_user, params) + 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' + optional :identifier, type: String, desc: 'Segment Identifier' + end + after_validation do + authenticate_admin!('segments') + end + post do + update_segment_klass(current_user, params) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { error: e.message } + end + end + + namespace :fetch_repo_generic_template do + desc 'fetch segment templates from repository' + params do + requires :identifier, type: String, desc: 'identifier' + end + post do + sk_obj = fetch_repo_generic_template('Segment', params[:identifier]) + sk_obj = sk_obj.deep_symbolize_keys[:generic_template] + return { error: 'No template data found' } unless sk_obj.present? + + ek_obj = Labimotion::ElementKlass.find_by(name: sk_obj.dig(:element_klass, :klass_name)) + return { error: 'No related element data found' } unless ek_obj.present? + + segment_klass = Labimotion::SegmentKlass.find_or_create_by( + identifier: sk_obj.dig(:identifier), + ) + segment_klass.update(sk_obj.slice( + :label, + :desc, + :place, + :properties_release, + :uuid, + ).merge( + is_active: true, + properties_template: sk_obj.dig(:properties_release), # properties_release, + element_klass: ek_obj, + created_by: current_user.id, + released_at: DateTime.now, + sync_time: DateTime.now, + )) + + present segment_klass, with: Labimotion::SegmentKlassEntity, root: 'klass' + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { error: e.message } + end + end + + namespace :fetch_repo_generic_template_list do + desc 'fetch segment templates from repository' + get do + fetch_repo_generic_template_list('Segment') + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { error: e.message } + end + end + + namespace :fetch_repo do + desc 'fetch Generic Segment Klass from Chemotion Repository' + get do + fetch_repo('SegmentKlass', current_user) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { error: e.message } + end + end + + namespace :create_repo_klass do + desc 'create Generic Segment Klass' + params do + requires :identifier, type: String, desc: 'Identifier' + end + post do + msg = create_repo_klass(params, current_user, request.headers['Origin']) + klass = Labimotion::SegmentKlassEntity.represent(SegmentKlass.all) + { status: msg[:status], message: msg[:message], klass: klass } + rescue StandardError => e + Labimotion.log_exception(e, current_user) + ## { error: e.message } + raise e + end + end + end + end +end diff --git a/lib/labimotion/collection/export.rb b/lib/labimotion/collection/export.rb new file mode 100644 index 0000000..1d60d98 --- /dev/null +++ b/lib/labimotion/collection/export.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module Labimotion + ## Export + class Export + + def self.fetch_element_klasses(&fetch_many) + klasses = Labimotion::ElementKlass.where(is_active: true) + fetch_many.call(klasses, {'created_by' => 'User'}) + end + + def self.fetch_segment_klasses(&fetch_many) + klasses = Labimotion::SegmentKlass.where(is_active: true) + fetch_many.call(klasses, { + 'element_klass_id' => 'Labimotion::ElementKlass', + 'created_by' => 'User' + }) + end + + def self.fetch_dataset_klasses(&fetch_many) + klasses = Labimotion::DatasetKlass.where(is_active: true) + fetch_many.call(klasses, {'created_by' => 'User'}) + end + + def self.fetch_elements(collection, segments, attachments, fetch_many, fetch_one, fetch_containers) + # fetch_many.call(collection.elements, { + # 'element_klass_id' => 'Labimotion::ElementKlass', + # 'created_by' => 'User', + # }) + # fetch_many.call(collection.collections_elements, { + # 'collection_id' => 'Collection', + # 'element_id' => 'Labimotion::Element', + # }) + collection.elements.each do |element| + element, attachments = Labimotion::Export.fetch_properties(data, element, attachments, &fetch_one) + fetch_one.call(element, { + 'element_klass_id' => 'Labimotion::ElementKlass', + 'created_by' => 'User', + }) + fetch_containers.call(element) + segment, @attachments = Labimotion::Export.fetch_segments(element, attachments, &fetch_one) + segments += segment if segment.present? + end + + [segments, attachments] + + end + + def self.fetch_segments(element, attachments, &fetch_one) + element_type = element.class.name + segments = Labimotion::Segment.where("element_id = ? AND element_type = ?", element.id, element_type) + segments.each do |segment| + # segment = fetch_properties(segment) + segment, attachments = Labimotion::Export.fetch_properties(segment, attachments, &fetch_one) + # fetch_one.call(segment.segment_klass.element_klass) + # fetch_one.call(segment.segment_klass, { + # 'element_klass_id' => 'Labimotion::ElementKlass' + # }) + fetch_one.call(segment, { + 'element_id' => segment.element_type, + 'segment_klass_id' => 'Labimotion::SegmentKlass', + 'created_by' => 'User' + }) + end + [segments, attachments] + end + + def self.fetch_datasets(dataset, &fetch_one) + return if dataset.nil? + + fetch_one.call(dataset, { + 'element_id' => 'Container', + }) + fetch_one.call(dataset, { + 'element_id' => dataset.element_type, + 'dataset_klass_id' => 'Labimotion::DatasetKlass', + }) + [dataset] + end + + + def self.fetch_properties(instance, attachments, &fetch_one) + properties = instance.properties + properties['layers'].keys.each do |key| + layer = properties['layers'][key] + + # field_samples = layer['fields'].select { |ss| ss['type'] == 'drag_sample' } -- TODO for elements + # field_elements = layer['fields'].select { |ss| ss['type'] == 'drag_element' } -- TODO for elements + + field_molecules = layer['fields'].select { |ss| ss['type'] == 'drag_molecule' } + field_molecules.each do |field| + idx = properties['layers'][key]['fields'].index(field) + id = field["value"] && field["value"]["el_id"] unless idx.nil? + mol = Molecule.find(id) unless id.nil? + properties['layers'][key]['fields'][idx]['value']['el_id'] = fetch_one.call(mol) unless mol.nil? + end + + field_samples = layer['fields'].select { |ss| ss['type'] == 'drag_sample' } + field_samples.each do |field| + # idx = properties['layers'][key]['fields'].index(field) + # id = field["value"] && field["value"]["el_id"] unless idx.nil? + # ss = Sample.find(id) unless id.nil? + # properties['layers'][key]['fields'][idx]['value']['el_id'] = fetch_one.call(ss) unless ss.nil? + end + + field_uploads = layer['fields'].select { |ss| ss['type'] == 'upload' } + field_uploads.each do |upload| + idx = properties['layers'][key]['fields'].index(upload) + files = upload["value"] && upload["value"]["files"] + files&.each_with_index do |fi, fdx| + att = Attachment.find(fi['aid']) + attachments += [att] + properties['layers'][key]['fields'][idx]['value']['files'][fdx]['aid'] = fetch_one.call(att, {'attachable_id' => 'Labimotion::Segment'}) unless att.nil? + end + end + + field_tables = 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_samples = field['sub_fields'].select { |ss| ss['type'] == 'drag_sample' } -- not available yet + # field_table_uploads = field['sub_fields'].select { |ss| ss['type'] == 'upload' } -- not available yet + field_table_molecules = field['sub_fields'].select { |ss| ss['type'] == 'drag_molecule' } + if field_table_molecules.present? + col_ids = field_table_molecules.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'] + next unless svalue['el_id'].present? && svalue['el_inchikey'].present? + + tmol = Molecule.find_by(id: svalue['el_id']) + sub_value[col_id]['value']['el_id'] = fetch_one.call(tmol) unless tmol.nil? + end + end + end + end + end + instance.properties = properties + [instance, attachments] + end + end +end diff --git a/lib/labimotion/collection/import.rb b/lib/labimotion/collection/import.rb new file mode 100644 index 0000000..04de7d2 --- /dev/null +++ b/lib/labimotion/collection/import.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require 'labimotion/utils/import_utils' +module Labimotion + class Import + + def self.import_repo_segment_props(instances, attachments, attachable_uuid, fields) + primary_store = Rails.configuration.storage.primary_store + attachable = instances.fetch('Labimotion::Segment').fetch(attachable_uuid) + attachment = Attachment.where(id: attachments, filename: fields.fetch('identifier')).first + attachment.update!( + attachable_id: attachable.id, + attachable_type: 'SegmentProps', + con_state: Labimotion::ConState::NONE, + transferred: true, + aasm_state: fields.fetch('aasm_state'), + filename: fields.fetch('filename'), + content_type: fields.fetch('content_type'), + storage: primary_store + # checksum: fields.fetch('checksum'), + # created_at: fields.fetch('created_at'), + # updated_at: fields.fetch('updated_at') + ) + + properties = attachable.properties + properties['layers'].keys.each do |key| + layer = properties['layers'][key] + field_uploads = layer['fields'].select { |ss| ss['type'] == 'upload' } + field_uploads&.each do |upload| + idx = properties['layers'][key]['fields'].index(upload) + files = upload["value"] && upload["value"]["files"] + files&.each_with_index do |fi, fdx| + if properties['layers'][key]['fields'][idx]['value']['files'][fdx]['uid'] == fields.fetch('identifier') + properties['layers'][key]['fields'][idx]['value']['files'][fdx]['aid'] = attachment.id + properties['layers'][key]['fields'][idx]['value']['files'][fdx]['uid'] = attachment.identifier + end + end + end + end + attachable.update!(properties: properties) + attachment + end + + def self.import_datasets(data, instances, gt, current_user_id, &update_instances) + begin + data.fetch('Labimotion::Dataset', {}).each do |uuid, fields| + klass_id = fields['dataset_klass_id'] + dk_obj = data.fetch('Labimotion::DatasetKlass', {})[klass_id] + dk_id = dk_obj['identifier'] + element_uuid = fields.fetch('element_id') + element_type = fields.fetch('element_type') + element = instances.fetch(element_type).fetch(element_uuid) + + dataset_klass = Labimotion::DatasetKlass.find_by(identifier: dk_id) if dk_id.present? + next if gt == true && dataset_klass.nil? + + dkr = Labimotion::DatasetKlassesRevision.find_by(uuid: fields.fetch('klass_uuid')) + dataset_klass = dkr.dataset_klass if dataset_klass.nil? && dkr.present? + next if dataset_klass.nil? || dataset_klass.ols_term_id != dk_obj['ols_term_id'] + + dataset_klass = Labimotion::DatasetKlass.find_by(ols_term_id: dk_obj['ols_term_id']) if dataset_klass.nil? + next if dataset_klass.nil? + + dataset = Labimotion::Dataset.create!( + fields.slice( + 'properties', 'properties_release' + ).merge( + ## created_by: current_user_id, + element: element, + dataset_klass: dataset_klass, + uuid: SecureRandom.uuid, + klass_uuid: dkr&.uuid || dataset_klass.uuid + ) + ) + update_instances.call(uuid, dataset) + end + rescue StandardError => e + Rails.logger.error(e.backtrace) + raise + end + end + + def self.import_segments(data, instances, gt, current_user_id, &update_instances) + begin + data.fetch('Labimotion::Segment', {}).each do |uuid, fields| + klass_id = fields["segment_klass_id"] + sk_obj = data.fetch('Labimotion::SegmentKlass', {})[klass_id] + sk_id = sk_obj["identifier"] + ek_obj = data.fetch('Labimotion::ElementKlass').fetch(sk_obj["element_klass_id"]) + element_klass = Labimotion::ElementKlass.find_by(name: ek_obj['name']) if ek_obj.present? + next if element_klass.nil? || ek_obj.nil? || ek_obj['is_generic'] == true + + element_uuid = fields.fetch('element_id') + element_type = fields.fetch('element_type') + element = instances.fetch(element_type).fetch(element_uuid) + segment_klass = Labimotion::SegmentKlass.find_by(identifier: sk_id) if sk_id.present? + segment_klass = Labimotion::SegmentKlass.find_by(uuid: fields.fetch('klass_uuid')) if segment_klass.nil? + + if segment_klass.nil? + skr = Labimotion::SegmentKlassesRevision.find_by(uuid: fields.fetch('klass_uuid')) + segment_klass = skr.segment_klass if segment_klass.nil? && skr.present? + end + + next if segment_klass.nil? || element.nil? + + ## segment_klass = Labimotion::ImportUtils.create_segment_klass(sk_obj, segment_klass, element_klass, current_user_id) + + segment = Labimotion::Segment.create!( + fields.slice( + 'properties', 'properties_release' + ).merge( + created_by: current_user_id, + element: element, + segment_klass: segment_klass, + uuid: SecureRandom.uuid, + klass_uuid: skr&.uuid || segment_klass.uuid + ) + ) + + properties = Labimotion::ImportUtils.properties_handler(data, segment.properties) + segment.update!(properties: properties) + update_instances.call(uuid, segment) + end + rescue StandardError => e + Rails.logger.error(e.backtrace) + raise + end + end + + def self.import_elements(data, instances, gt, current_user_id, fetch_many, &update_instances) + data.fetch('Labimotion::Element', {}).each do |uuid, fields| + klass_id = fields["element_klass_id"] + ek_obj = data.fetch('Labimotion::ElementKlass', {})[klass_id] + ek_id = ek_obj["identifier"] + element_klass = Labimotion::ElementKlass.find_by(identifier: ek_id) if ek_id.present? + element_klass = Labimotion::ElementKlass.find_by(uuid: fields.fetch('klass_uuid')) if element_klass.nil? + + if element_klass.nil? + ekr = Labimotion::ElementKlassesRevision.find_by(uuid: fields.fetch('klass_uuid')) + element_klass = ekr.element_klass if element_klass.nil? && ekr.present? + end + next if element_klass.nil? + + element = Labimotion::Element.create!( + fields.slice( + 'name', 'properties', 'properties_release' + ).merge( + created_by: current_user_id, + element_klass: element_klass, + collections: fetch_many.call( + 'Collection', 'Labimotion::CollectionsElement', 'element_id', 'collection_id', uuid + ), + uuid: SecureRandom.uuid, + klass_uuid: ekr&.uuid || element_klass.uuid + ) + ) + + properties = Labimotion::ImportUtils.properties_handler(data, element.properties) + element.update!(properties: properties) + update_instances.call(uuid, element) + element.container = Container.create_root_container + end + rescue StandardError => e + Rails.logger.error(e.backtrace) + raise + end + end +end diff --git a/lib/labimotion/entities/application_entity.rb b/lib/labimotion/entities/application_entity.rb new file mode 100644 index 0000000..7a46207 --- /dev/null +++ b/lib/labimotion/entities/application_entity.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true +# +module Labimotion + ## ApplicationEntity + class ApplicationEntity < Grape::Entity + CUSTOM_ENTITY_OPTIONS = %i[anonymize_below anonymize_with].freeze + + format_with(:eln_timestamp) do |datetime| + datetime.present? ? datetime&.strftime('%Y-%m-%d %H:%M:%S %Z') : nil + end + + def self.expose!(*args) + fields = args.first + options = args.last.is_a?(Hash) ? args.pop : {} + options = merge_options(options) # merges additional params set in #with_options + expose_fields_with_anonymization!(fields, options) + end + + # rubocop:disable Metrics/MethodLength + def self.expose_fields_with_anonymization!(fields, options) + anonymize_below = options[:anonymize_below] || 0 + anonymize_with = options.key?(:anonymize_with) ? options[:anonymize_with] : '***' + + Array(fields).each do |field| + expose(field, options) do |represented_object, _options| + if detail_levels[represented_object.class] < anonymize_below + anonymize_with + elsif respond_to?(field, true) # Entity has a method with the same name + send(field) + elsif represented_object.respond_to?(field) + represented_object.public_send(field) + else + represented_object[field] # works both for AR and Hash objects + end + end + end + end + private_class_method :expose_fields_with_anonymization! + # rubocop:enable Metrics/MethodLength + + def self.expose_timestamps(timestamp_fields: %i[created_at updated_at], **additional_args) + timestamp_fields.each do |field| + expose field, format_with: :eln_timestamp, **additional_args + end + end + + # overridden method from Grape::Entity to support our custom anonymization options + # https://github.com/ruby-grape/grape-entity/blob/v0.7.1/lib/grape_entity/entity.rb#L565 + def self.valid_options(options) + options.each_key do |key| + next if OPTIONS.include?(key) || CUSTOM_ENTITY_OPTIONS.include?(key) + + raise ArgumentError, "#{key.inspect} is not a valid option." + end + + options[:using] = options.delete(:with) if options.key?(:with) + options + end + private_class_method :valid_options + + private + + def displayed_in_list? + options[:displayed_in_list] == true + end + + def current_user + unless options[:current_user] + raise MissingCurrentUserError, "#{self.class} requires a current user to work properly" + end + + options[:current_user] + end + + def detail_levels + maximal_default_levels = Hash.new(10) # every requested detail level will be returned as 10 + minimal_default_levels = Hash.new(0) # every requested detail level will be returned as 0 + return maximal_default_levels if !options.key?(:detail_levels) || options[:detail_levels].empty? + + # When explicitly configured detail levels are available, we want to return only those and all other + # requests (by using `detail_levels[SomeUnconfiguredModel]`) should return the minimum detail level + minimal_default_levels.merge(options[:detail_levels]) + end + + class MissingCurrentUserError < StandardError + end + end +end diff --git a/lib/labimotion/entities/dataset_entity.rb b/lib/labimotion/entities/dataset_entity.rb new file mode 100644 index 0000000..6add379 --- /dev/null +++ b/lib/labimotion/entities/dataset_entity.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +# +require 'labimotion/entities/application_entity' +module Labimotion + # Dataset entity + class DatasetEntity < ApplicationEntity + expose :id, :dataset_klass_id, :properties, :properties_release, :element_id, :element_type, :klass_ols, :klass_label, :klass_uuid + def klass_ols + object&.dataset_klass&.ols_term_id + end + + def klass_label + object&.dataset_klass&.label + end + end +end diff --git a/lib/labimotion/entities/dataset_klass_entity.rb b/lib/labimotion/entities/dataset_klass_entity.rb new file mode 100644 index 0000000..f58bdfb --- /dev/null +++ b/lib/labimotion/entities/dataset_klass_entity.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require 'labimotion/entities/generic_klass_entity' +module Labimotion + class DatasetKlassEntity < GenericKlassEntity + expose( + :ols_term_id, + ) + end +end diff --git a/lib/labimotion/entities/element_entity.rb b/lib/labimotion/entities/element_entity.rb new file mode 100644 index 0000000..0a6b5b7 --- /dev/null +++ b/lib/labimotion/entities/element_entity.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'labimotion/entities/application_entity' +## TODO: Refactor labimotion to use the same entities as chemotion +module Labimotion + 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! :properties_release + expose! :short_label + expose! :thumb_svg + expose! :type + expose! :uuid + end + + with_options(anonymize_below: 10) do + expose! :element_klass, anonymize_with: nil, using: 'Labimotion::ElementKlassEntity' + expose! :segments, anonymize_with: [], using: 'Labimotion::SegmentEntity' + expose! :tag, anonymize_with: nil, using: 'Entities::ElementTagEntity' + end + + expose_timestamps + + + private + + def is_restricted + detail_levels[Labimotion::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/lib/labimotion/entities/element_klass_entity.rb b/lib/labimotion/entities/element_klass_entity.rb new file mode 100644 index 0000000..83dd86c --- /dev/null +++ b/lib/labimotion/entities/element_klass_entity.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +# +require 'labimotion/entities/generic_klass_entity' + +module Labimotion + # ElementKlassEntity + class ElementKlassEntity < GenericKlassEntity + expose :name, :icon_name, :klass_prefix, :is_generic + end +end diff --git a/lib/labimotion/entities/element_revision_entity.rb b/lib/labimotion/entities/element_revision_entity.rb new file mode 100644 index 0000000..7c53eba --- /dev/null +++ b/lib/labimotion/entities/element_revision_entity.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'labimotion/entities/application_entity' +module Labimotion + # ElementRevisionEntity + class ElementRevisionEntity < ApplicationEntity + 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['select_options'] = object.properties_release['select_options'] + object.properties + end + end +end diff --git a/lib/labimotion/entities/eln_element_entity.rb b/lib/labimotion/entities/eln_element_entity.rb new file mode 100644 index 0000000..80bb050 --- /dev/null +++ b/lib/labimotion/entities/eln_element_entity.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true +# +require 'labimotion/entities/application_entity' +module Labimotion + ## ElementEntity + class ElnElementEntity < ApplicationEntity + with_options(anonymize_below: 0) do + expose! :created_by + expose! :id + expose! :is_restricted + expose! :klass_uuid + expose! :name + expose! :properties + expose! :properties_release + expose! :short_label + expose! :type + expose! :uuid + end + + expose( + :can_copy, unless: ->(_instance, _options) { displayed_in_list? } + ) + + private + + def properties + (object.properties['layers']&.keys || []).each do |key| + # layer = object.properties[key] + field_sample_molecules = object.properties['layers'][key]['fields'].select do |ss| + ss['type'] == 'drag_sample' || ss['type'] == 'drag_molecule' + end + 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? + + if field['type'] == 'drag_sample' + object.properties['layers'][key]['fields'][idx]['value']['el_label'] = + el.short_label + end + if field['type'] == 'drag_sample' + object.properties['layers'][key]['fields'][idx]['value']['el_tip'] = + el.short_label + end + 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' } + if field_table_molecules.present? + object.properties['layers'][key]['fields'][idx] = + set_table(field, field_table_molecules, 'Molecule') + end + + field_table_samples = field['sub_fields'].select { |ss| ss['type'] == 'drag_sample' } + if field_table_samples.present? + object.properties['layers'][key]['fields'][idx] = + set_table(field, field_table_samples, 'Sample') + end + end + end + object.properties + end + + def type + object.element_klass.name + 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| + unless sub_value[col_id].present? && sub_value[col_id]['value'].present? && sub_value[col_id]['value']['el_id'].present? + next + end + + 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 + end +end diff --git a/lib/labimotion/entities/generic_entity.rb b/lib/labimotion/entities/generic_entity.rb new file mode 100644 index 0000000..2c00aa0 --- /dev/null +++ b/lib/labimotion/entities/generic_entity.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'labimotion/entities/application_entity' + +# Entity module +module Labimotion + class GenericEntity < Labimotion::ApplicationEntity + expose :id, :is_active, :label, :desc, :place, :released_at + + expose :element_klass_id do |obj| + obj[:element_klass_id] || 0 + end + + expose :klass_name do |obj| + obj[:name] || '' + end + + expose :ols_term_id do |obj| + obj[:ols_term_id] || '' + end + + expose :icon_name do |obj| + obj[:icon_name] || '' + end + + expose :klass_prefix do |obj| + obj[:klass_prefix] || '' + end + + expose :is_generic do |obj| + obj[:is_generic] || true + end + + expose :uuid do |obj| + obj[:uuid] || '' + end + + expose :properties_release do |obj| + obj[:properties_release] || {} + end + + expose :element_klass do |obj| + if obj[:element_klass_id] + Labimotion::GenericEntity.represent(obj.element_klass) + else + {} + end + end + + expose :identifier do |obj| + obj[:identifier] || '' + end + end +end diff --git a/lib/labimotion/entities/generic_klass_entity.rb b/lib/labimotion/entities/generic_klass_entity.rb new file mode 100644 index 0000000..c988c87 --- /dev/null +++ b/lib/labimotion/entities/generic_klass_entity.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +# +require 'labimotion/entities/application_entity' +module Labimotion + # GenericKlassEntity + class GenericKlassEntity < ApplicationEntity + expose :id, :uuid, :label, :desc, :properties_template, :properties_release, :is_active, :version, + :place, :released_at, :identifier, :sync_time, :created_by, :updated_by, :created_at, :updated_at + expose_timestamps(timestamp_fields: [:released_at]) + expose_timestamps(timestamp_fields: [:created_at]) + expose_timestamps(timestamp_fields: [:updated_at]) + expose_timestamps(timestamp_fields: [:sync_time]) + end +end diff --git a/lib/labimotion/entities/generic_public_entity.rb b/lib/labimotion/entities/generic_public_entity.rb new file mode 100644 index 0000000..bb5a97d --- /dev/null +++ b/lib/labimotion/entities/generic_public_entity.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'labimotion/entities/application_entity' + +# Entity module +module Labimotion + class GenericPublicEntity < Labimotion::ApplicationEntity + expose! :uuid + expose! :name + expose! :desc + expose! :icon_name + expose! :klass_prefix + expose! :klass_name + expose! :label + expose! :identifier + expose! :version + expose! :released_at + expose! :properties_release, if: :displayed + expose :element_klass do |obj| + if obj[:element_klass_id] + { :label => obj.element_klass.label, :icon_name => obj.element_klass.icon_name } + else + {} + end + end + end +end diff --git a/lib/labimotion/entities/klass_revision_entity.rb b/lib/labimotion/entities/klass_revision_entity.rb new file mode 100644 index 0000000..56bf897 --- /dev/null +++ b/lib/labimotion/entities/klass_revision_entity.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Labimotion + # KlassRevisionEntity + class KlassRevisionEntity < ApplicationEntity + expose :id, :uuid, :properties_release, :version, :released_at + + expose :klass_id do |object| + klass_id = object.element_klass_id if object.respond_to? :element_klass_id + klass_id = object.segment_klass_id if object.respond_to? :segment_klass_id + klass_id = object.dataset_klass_id if object.respond_to? :dataset_klass_id + klass_id + end + + def released_at + object.released_at&.strftime('%d.%m.%Y, %H:%M') + end + end +end + diff --git a/lib/labimotion/entities/segment_entity.rb b/lib/labimotion/entities/segment_entity.rb new file mode 100644 index 0000000..eb24b97 --- /dev/null +++ b/lib/labimotion/entities/segment_entity.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'labimotion/entities/application_entity' +module Labimotion + ## Segment entity + class SegmentEntity < ApplicationEntity + expose :id, :segment_klass_id, :element_type, :element_id, :properties, :properties_release, :uuid, :klass_uuid, :klass_label + + def klass_label + object.segment_klass.label + end + + def properties # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength + return unless object.respond_to?(:properties) + + return if object&.properties.dig('layers').blank? + + object&.properties['layers'].each_key do |key| # rubocop:disable Metrics/BlockLength + next if object&.properties.dig('layers', key, 'fields').blank? + + 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 if sid.blank? + + el = Molecule.find_by(id: sid) + next if el.blank? + + next if object&.properties.dig('layers', key, 'fields', idx, 'value').blank? + + 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 if field_table_molecules.blank? + + col_ids = field_table_molecules.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_mol = Molecule.find_by(id: sub_value[col_id]['value']['el_id']) + next if find_mol.blank? + + 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/lib/labimotion/entities/segment_klass_entity.rb b/lib/labimotion/entities/segment_klass_entity.rb new file mode 100644 index 0000000..8e54616 --- /dev/null +++ b/lib/labimotion/entities/segment_klass_entity.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +require 'labimotion/entities/generic_klass_entity' +require 'labimotion/entities/element_klass_entity' +module Labimotion + class SegmentKlassEntity < Labimotion::GenericKlassEntity + expose :element_klass, using: Labimotion::ElementKlassEntity + end +end diff --git a/lib/labimotion/entities/segment_revision_entity.rb b/lib/labimotion/entities/segment_revision_entity.rb new file mode 100644 index 0000000..7354f0b --- /dev/null +++ b/lib/labimotion/entities/segment_revision_entity.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +# +require 'labimotion/entities/application_entity' +module Labimotion + class SegmentRevisionEntity < ApplicationEntity + 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/lib/labimotion/helpers/converter_helpers.rb b/lib/labimotion/helpers/converter_helpers.rb new file mode 100644 index 0000000..479df63 --- /dev/null +++ b/lib/labimotion/helpers/converter_helpers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +require 'grape' + +module Labimotion + ## ElementHelpers + module ConverterHelpers + extend Grape::API::Helpers + + def demo(params) + ### TODO: implement demo + end + end +end diff --git a/lib/labimotion/helpers/dataset_helpers.rb b/lib/labimotion/helpers/dataset_helpers.rb new file mode 100644 index 0000000..9763190 --- /dev/null +++ b/lib/labimotion/helpers/dataset_helpers.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +require 'grape' +module Labimotion + ## DatasetHelpers + module DatasetHelpers + extend Grape::API::Helpers + + def klass_list(is_active) + if is_active == true + Labimotion::DatasetKlass.where(is_active: true).order('place') || [] + else + Labimotion::DatasetKlass.all.order('place') || [] + end + end + + def create_repo_klass(params, current_user, origin) + response = Labimotion::TemplateHub.fetch_identifier('DatasetKlass', params[:identifier], origin) + attributes = response.slice('ols_term_id', 'label', 'desc', 'uuid', 'identifier', 'properties_release', 'version') # .except(:id, :is_active, :place, :created_by, :created_at, :updated_at) + attributes['properties_release']['identifier'] = attributes['identifier'] + attributes['properties_template'] = attributes['properties_release'] + attributes['place'] = ((Labimotion::DatasetKlass.all.length * 10) || 0) + 10 + attributes['is_active'] = false + attributes['updated_by'] = current_user.id + attributes['sync_by'] = current_user.id + attributes['sync_time'] = DateTime.now + dataset_klass = Labimotion::DatasetKlass.find_by(ols_term_id: attributes['ols_term_id']) + if dataset_klass.present? + if dataset_klass['uuid'] == attributes['uuid'] && dataset_klass['version'] == attributes['version'] + { status: 'success', message: "This dataset: #{attributes['label']} has the latest version!" } + else + ds = Labimotion::DatasetKlass.find_by(ols_term_id: attributes['ols_term_id']) + ds.update!(attributes) + ds.create_klasses_revision(current_user) + { status: 'success', message: "This dataset: [#{attributes['label']}] has been upgraded to the version: #{attributes['version']}!" } + end + else + attributes['created_by'] = current_user.id + ds = Labimotion::DatasetKlass.create!(attributes) + ds.create_klasses_revision(current_user) + { status: 'success', message: "The dataset: #{attributes['label']} has been created using version: #{attributes['version']}!" } + end + rescue StandardError => e + Labimotion.log_exception(e, current_user) + # { error: e.message } + raise e + end + end +end diff --git a/lib/labimotion/helpers/element_helpers.rb b/lib/labimotion/helpers/element_helpers.rb new file mode 100644 index 0000000..521a65a --- /dev/null +++ b/lib/labimotion/helpers/element_helpers.rb @@ -0,0 +1,339 @@ +# frozen_string_literal: true + +require 'grape' +require 'labimotion/utils/utils' +# require 'labimotion/models/element_klass' +module Labimotion + ## ElementHelpers + module ElementHelpers + extend Grape::API::Helpers + + def klass_list(is_generic_only) + if is_generic_only == true + Labimotion::ElementKlass.where(is_active: true, is_generic: true).order('place') || [] + else + Labimotion::ElementKlass.where(is_active: true).order('place') || [] + end + end + + def create_element_klass(current_user, params) + uuid = SecureRandom.uuid + template = { uuid: uuid, layers: {}, select_options: {} } + attributes = declared(params, include_missing: false) + attributes[:properties_template] = template if attributes[:properties_template].nil? + attributes[:properties_template]['uuid'] = uuid + attributes[:properties_template]['pkg'] = Labimotion::Utils.pkg(attributes[:properties_template]['pkg']) + attributes[:properties_template]['klass'] = 'ElementKlass' + 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 = Labimotion::ElementKlass.create!(attributes) + new_klass.reload + new_klass.create_klasses_revision(current_user) + klass_names_file = Rails.root.join('app/packs/klasses.json') + klasses = Labimotion::ElementKlass.where(is_active: true)&.pluck(:name) || [] + File.write(klass_names_file, klasses) + klasses + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def update_element_klass(current_user, params) + place = params[:place] || 100 + begin + place = place.to_i if place.present? && place.to_i == place.to_f + rescue StandardError + place = 100 + end + klass = Labimotion::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! + klass + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def create_element(current_user, params) + klass = params[:element_klass] || {} + uuid = SecureRandom.uuid + params[:properties]['uuid'] = uuid + params[:properties]['klass_uuid'] = klass[:uuid] + params[:properties]['pkg'] = Labimotion::Utils.pkg(params[:properties]['pkg']) + params[:properties]['klass'] = 'Element' + params[:properties]['identifier'] = klass[:identifier] + properties = params[:properties] + properties.delete('flow') unless properties['flow'].nil? + properties.delete('flowObject') unless properties['flowObject'].nil? + properties.delete('select_options') unless properties['select_options'].nil? + attributes = { + name: params[:name], + element_klass_id: klass[:id], + uuid: uuid, + klass_uuid: klass[:uuid], + properties: properties, + properties_release: params[:properties_release], + created_by: current_user.id, + } + element = Labimotion::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], current_user) + element.save! + element.save_segments(segments: params[:segments], current_user_id: current_user.id) + element + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def update_element_by_id(current_user, params) + element = Labimotion::Element.find(params[:id]) + update_datamodel(params[:container], current_user) + properties = update_sample_association(element, params[:properties], current_user) + params.delete(:container) + params.delete(:properties) + attributes = declared(params.except(:segments), include_missing: false) + properties['pkg'] = Labimotion::Utils.pkg(properties['pkg']) + if element.klass_uuid != properties['klass_uuid'] || element.properties != properties || element.name != params[:name] + properties['klass'] = 'Element' + uuid = SecureRandom.uuid + properties['uuid'] = uuid + + properties.delete('flow') unless properties['flow'].nil? + properties.delete('flowObject') unless properties['flowObject'].nil? + properties.delete('select_options') unless properties['select_options'].nil? + + 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 + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def upload_generics_files(current_user, params) + 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], + "Labimotion::#{params[:att_type]}", + params[:att_id], + params[:attfilesIdentifier], + current_user.id + ) + end + (attach_ary << att_ary).flatten! unless att_ary&.empty? + + if Labimotion::IS_RAILS5 == true + TransferThumbnailToPublicJob.set(queue: "transfer_thumbnail_to_public_#{current_user.id}").perform_now(attach_ary) unless attach_ary.empty? + TransferFileFromTmpJob.set(queue: "transfer_file_from_tmp_#{current_user.id}").perform_now(attach_ary) unless attach_ary.empty? + end + true + rescue StandardError => e + Labimotion.log_exception(e, current_user) + false + end + + def element_revisions(params) + klass = Labimotion::Element.find(params[:id]) + list = klass.elements_revisions unless klass.nil? + list&.sort_by(&:created_at)&.reverse&.first(10) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def list_user_elements(scope, params) + from = params[:from_date] + to = params[:to_date] + by_created_at = params[:filter_created_at] || false + + if params[:sort_column]&.include?('.') + layer, field = params[:sort_column].split('.') + + element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type]) + allowed_fields = element_klass.properties_release.dig('layers', layer, 'fields')&.pluck('field') || [] + + if field.in?(allowed_fields) + query = ActiveRecord::Base.sanitize_sql( + [ + "LEFT JOIN LATERAL( + SELECT field->'value' AS value + FROM jsonb_array_elements(properties->'layers'->:layer->'fields') a(field) + WHERE field->>'field' = :field + ) a ON true", + { layer: layer, field: field }, + ], + ) + scope = scope.joins(query).order('value ASC NULLS FIRST') + else + scope = scope.order(updated_at: :desc) + end + else + scope = scope.order(updated_at: :desc) + end + + 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 + scope = scope.updated_time_to(Time.at(to) + 1.day) if to && !by_created_at + scope + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def list_serialized_elements(params, current_user) + 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 + Labimotion::Element + .joins(:element_klass, :collections_elements) + .where( + element_klasses: { name: params[:el_type] }, + collections_elements: { collection_id: collection_id }, + ).includes(:tag, collections: :sync_collections_users) + else + Labimotion::Element.none + end + + ## TO DO: refactor labimotion + from = params[:from_date] + to = params[:to_date] + by_created_at = params[:filter_created_at] || false + if params[:sort_column]&.include?('.') + layer, field = params[:sort_column].split('.') + + element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type]) + allowed_fields = element_klass.properties_release.dig('layers', layer, 'fields')&.pluck('field') || [] + + if field.in?(allowed_fields) + query = ActiveRecord::Base.sanitize_sql( + [ + "LEFT JOIN LATERAL( + SELECT field->'value' AS value + FROM jsonb_array_elements(properties->'layers'->:layer->'fields') a(field) + WHERE field->>'field' = :field + ) a ON true", + { layer: layer, field: field }, + ], + ) + scope = scope.joins(query).order('value ASC NULLS FIRST') + else + scope = scope.order(updated_at: :desc) + end + else + scope = scope.order(updated_at: :desc) + end + + 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 + scope + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def create_repo_klass(params, current_user, origin) + response = Labimotion::TemplateHub.fetch_identifier('ElementKlass', params[:identifier], origin) + attributes = response.slice('name', 'label', 'desc', 'icon_name', 'uuid', 'klass_prefix', 'is_generic', 'identifier', 'properties_release', 'version') + attributes['properties_release']['identifier'] = attributes['identifier'] + attributes['properties_template'] = attributes['properties_release'] + attributes['place'] = ((Labimotion::DatasetKlass.all.length * 10) || 0) + 10 + attributes['is_active'] = false + attributes['updated_by'] = current_user.id + attributes['sync_by'] = current_user.id + attributes['sync_time'] = DateTime.now + + element_klass = Labimotion::ElementKlass.find_by(identifier: attributes['identifier']) + if element_klass.present? + if element_klass['uuid'] == attributes['uuid'] && element_klass['version'] == attributes['version'] + { status: 'success', message: "This element: #{attributes['name']} has the latest version!" } + else + element_klass.update!(attributes) + element_klass.create_klasses_revision(current_user) + { status: 'success', message: "This element: [#{attributes['name']}] has been upgraded to the version: #{attributes['version']}!" } + end + else + exist_klass = Labimotion::ElementKlass.find_by(name: attributes['name']) + if exist_klass.present? + { status: 'error', message: "The name [#{attributes['name']}] is already in use." } + else + attributes['created_by'] = current_user.id + element_klass = Labimotion::ElementKlass.create!(attributes) + element_klass.create_klasses_revision(current_user) + { status: 'success', message: "The element: #{attributes['name']} has been created using version: #{attributes['version']}!" } + end + end + rescue StandardError => e + Labimotion.log_exception(e, current_user) + # { error: e.message } + raise e + end + + def attach_thumbnail(_attachments) + attachments = _attachments&.map do |attachment| + _att = Entities::AttachmentEntity.represent(attachment, serializable: true) + _att[:thumbnail] = attachment.thumb ? Base64.encode64(attachment.read_thumbnail) : nil + _att + end + attachments + end + + end +end diff --git a/lib/labimotion/helpers/generic_helpers.rb b/lib/labimotion/helpers/generic_helpers.rb new file mode 100644 index 0000000..a33b220 --- /dev/null +++ b/lib/labimotion/helpers/generic_helpers.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true +require 'grape' +require 'labimotion/utils/utils' +# Helper for associated sample +module Labimotion + ## Generic Helpers + module GenericHelpers + extend Grape::API::Helpers + + def authenticate_admin!(type) + error!('401 Unauthorized', 401) unless current_user.generic_admin[type] + end + + def fetch_klass(name, id) + klz = "Labimotion::#{name}".constantize.find_by(id: id) + error!("#{name.gsub(/(Klass)/, '')} is invalid. Please re-select.", 500) if klz.nil? + klz + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def deactivate_klass(params) + klz = fetch_klass(params[:klass], params[:id]) + klz&.update!(is_active: params[:is_active]) + generate_klass_file unless klz.class.name != 'ElementKlass' + klz + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def delete_klass(params) + authenticate_admin!(params[:klass].gsub(/(Klass)/, 's').downcase) + klz = fetch_klass(params[:klass], params[:id]) + klz&.destroy! + generate_klass_file unless klz.class.name != 'ElementKlass' + status 201 + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def update_template(params, current_user) + klz = fetch_klass(params[:klass], params[:id]) + uuid = SecureRandom.uuid + properties = params[:properties_template] + properties['uuid'] = uuid + klz.version = Labimotion::Utils.next_version(params[:release], klz.version) + properties['version'] = klz.version + properties['pkg'] = Labimotion::Utils.pkg(params['pkg'] || (klz.properties_template && klz.properties_template['pkg'])) + properties['klass'] = klz.class.name.split('::').last + properties['identifier'] = klz.identifier + properties.delete('eln') if properties['eln'].present? + klz.updated_by = current_user.id + klz.properties_template = properties + klz.save! + klz.reload + klz.create_klasses_revision(current_user) if params[:release] != 'draft' + klz + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def delete_klass_revision(params) + revision = "Labimotion::#{params[:klass]}esRevision".constantize.find(params[:id]) + klass = "Labimotion::#{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! + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def delete_revision(params) + revision = "Labimotion::#{params[:klass]}sRevision".constantize.find(params[:id]) + element = "Labimotion::#{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! + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def list_klass_revisions(params) + klass = "Labimotion::#{params[:klass]}".constantize.find_by(id: params[:id]) + list = klass.send("#{params[:klass].underscore}es_revisions") unless klass.nil? + list&.order(released_at: :desc)&.limit(10) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + + ############### + def generate_klass_file + klass_names_file = Rails.root.join('app/packs/klasses.json') + klasses = Labimotion::ElementKlass.where(is_active: true)&.pluck(:name) || [] + File.write(klass_names_file, klasses) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def fetch_properties_uploads(properties) + uploads = [] + 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| + uploads.push({ layer: key, field: field['field'], uid: file['uid'], filename: file['filename'] }) + end + end + end + uploads + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def update_properties_upload(element, properties, att, pa) + 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) + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def create_uploads(type, id, files, param_info, user_id) + 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| + next if map_info[key]['files'].empty? + + if type == 'Segment' + element = Labimotion::Segment.find_by(element_id: id, segment_klass_id: key) + elsif type == 'Element' + element = Labimotion::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]) + + a = Attachment.new( + bucket: file[:container_id], + filename: fobj['filename'], + con_state: Labimotion::ConState::NONE, + file_path: file[:tempfile], + created_by: user_id, + created_for: user_id, + content_type: file[:type], + attachable_type: map_info[key]['type'], + attachable_id: element.id, + ) + begin + a.save! + + update_properties_upload(element, element.properties, a, pa) + attach_ary.push(a.id) + ensure + tempfile.close + tempfile.unlink + end + end + element.send("#{type.downcase}s_revisions")&.last&.destroy! + element.save! + end + attach_ary + rescue StandardError => e + Labimotion.log_exception(e, current_user) + error!('Error while uploading files.', 500) + raise e + end + + def create_attachments(files, del_files, type, id, identifier, user_id) + attach_ary = [] + (files || []).each_with_index do |file, index| + next unless (tempfile = file[:tempfile]) + + att = Attachment.new( + bucket: file[:container_id], + filename: file[:filename], + con_state: Labimotion::ConState::NONE, + file_path: file[:tempfile], + created_by: user_id, + created_for: user_id, + identifier: identifier[index], + content_type: file[:type], + attachable_type: type, + attachable_id: id, + ) + begin + 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) + end + attach_ary + rescue StandardError => e + Labimotion.log_exception(e) + raise e + end + + def fetch_repo_generic_template(klass, identifier) + Chemotion::Generic::Fetch::Template.exec(API::TARGET, klass, identifier) + end + + def fetch_repo_generic_template_list(name = false) + Chemotion::Generic::Fetch::Template.list(API::TARGET, name) + end + + def fetch_repo(name, current_user) + # current_klasses = "Labimotion::#{name}".constantize.where.not(identifier: nil)&.pluck(:identifier) || [] + response = Labimotion::TemplateHub.list(name) + # if response && response['list'].present? && response['list'].length.positive? + # filter_list = response['list']&.reject do |ds| + # current_klasses.include?(ds['identifier']) + # end || [] + # end + # filter_list || [] + (response && response['list']) || [] + rescue StandardError => e + Labimotion.log_exception(e, current_user) + { error: 'Cannot connect to Chemotion Repository' } + end + end +end diff --git a/lib/labimotion/helpers/repository_helpers.rb b/lib/labimotion/helpers/repository_helpers.rb new file mode 100644 index 0000000..6d0ed0d --- /dev/null +++ b/lib/labimotion/helpers/repository_helpers.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require 'grape' + +module Labimotion + ## RepositoryHelpers + module RepositoryHelpers + extend Grape::API::Helpers + + def copy_datsets(**args) + return if args[:dataset].nil? + + end + end +end diff --git a/lib/labimotion/helpers/sample_association_helpers.rb b/lib/labimotion/helpers/sample_association_helpers.rb new file mode 100644 index 0000000..ae43bb4 --- /dev/null +++ b/lib/labimotion/helpers/sample_association_helpers.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +module Labimotion + # 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) + 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 + rescue StandardError => e + Labimotion.log_exception(e, current_user) + nil + 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 + Labimotion::ElementsSample.find_or_create_by(element_id: element.id, sample_id: subsample.id) + end + end + end + sds + rescue StandardError => e + Labimotion.log_exception(e, current_user) + [] + end + + def update_sample_association(element, properties, current_user) + sds = [] + els = [] + 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 + Labimotion::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) + + field_elements = layer['fields'].select { |ss| ss['type'] == 'drag_element' } + field_elements.each do |field| + idx = properties['layers'][key]['fields'].index(field) + sid = field.dig('value', 'el_id') + next if sid.blank? || sid == element.id + + el = Labimotion::Element.find_by(id: sid) + next if el.nil? + + Labimotion::ElementsElement.find_or_create_by(parent_id: element.id, element_id: el.id) + els << el.id + end + + end + es_list = Labimotion::ElementsSample.where(element_id: element.id).where.not(sample_id: sds) + ee_list = Labimotion::ElementsElement.where(parent_id: element.id).where.not(element_id: els&.flatten) + # Labimotion::ElementsSample.where(element_id: element.id).where.not(sample_id: sds)&.destroy_all + # Labimotion::ElementsElement.where(parent_id: element.id).where.not(element_id: els&.flatten)&.destroy_all + + es_list.destroy_all if es_list.present? + ee_list.destroy_all if ee_list.present? + + properties + rescue StandardError => e + Labimotion.log_exception(e, current_user) + end + end +end diff --git a/lib/labimotion/helpers/search_helpers.rb b/lib/labimotion/helpers/search_helpers.rb new file mode 100644 index 0000000..5ad7b9e --- /dev/null +++ b/lib/labimotion/helpers/search_helpers.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +require 'grape' +# require 'labimotion/models/segment_klass' +module Labimotion + ## ElementHelpers + module SearchHelpers + extend Grape::API::Helpers + + def serialization_elements(result, page, page_size, element_ids, params) + # 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) + # 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| + # Labimotion::ElementEntity.represent(element, displayed_in_list: true).serializable_hash + # end + + # result["#{klass.name}s"] = { + # elements: serialized_elements, + # totalElements: element_ids_for_klass.size, + # page: page, + # pages: pages(element_ids_for_klass.size), + # perPage: page_size, + # ids: element_ids_for_klass, + # } + # end + # result + end + + + def gl_elements_search(col, params) + # element_scope = Element.joins(:collections_elements).where( + # 'collections_elements.collection_id = ? and collections_elements.element_type = (?)', collection.id, params[:selection][:genericElName] + # ) + # if params[:selection][:searchName].present? + # element_scope = element_scope.where('name like (?)', + # "%#{params[:selection][:searchName]}%") + # end + # if params[:selection][:searchShowLabel].present? + # element_scope = element_scope.where('short_label like (?)', "%#{params[:selection][:searchShowLabel]}%") + # end + # 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 + end +end diff --git a/lib/labimotion/helpers/segment_helpers.rb b/lib/labimotion/helpers/segment_helpers.rb new file mode 100644 index 0000000..1330669 --- /dev/null +++ b/lib/labimotion/helpers/segment_helpers.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true +require 'grape' +require 'labimotion/models/segment_klass' +require 'labimotion/utils/utils' + +module Labimotion + ## ElementHelpers + module SegmentHelpers + extend Grape::API::Helpers + + def klass_list(el_klass, is_active=false) + scope = SegmentKlass.all + scope = scope.where(is_active: is_active) if is_active.present? && is_active == true + scope = scope.joins(:element_klass).where(klass_element: params[:element], is_active: true) if el_klass.present? + scope.order('place') || [] + end + + def create_segment_klass(current_user, params) + 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].presence || template) + template['pkg'] = Labimotion::Utils.pkg(template['pkg']) + template['klass'] = 'SegmentKlass' + attributes.merge!(properties_template: template, element_klass: @klass, created_by: current_user.id, + place: place) + attributes[:is_active] = false + attributes[:uuid] = uuid + attributes[:released_at] = DateTime.now + attributes[:properties_release] = attributes[:properties_template] + klass = Labimotion::SegmentKlass.create!(attributes) + klass.reload + klass.create_klasses_revision(current_user) + klass + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def update_segment_klass(current_user, params) + segment = fetch_klass('SegmentKlass', params[:id]) + 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) + segment + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + + def create_repo_klass(params, current_user, origin) + response = Labimotion::TemplateHub.fetch_identifier('SegmentKlass', params[:identifier], origin) + attributes = response.slice('label', 'desc', 'uuid', 'identifier', 'released_at', 'properties_release', 'version') + attributes['properties_release']['identifier'] = attributes['identifier'] + attributes['properties_template'] = attributes['properties_release'] + attributes['place'] = ((Labimotion::SegmentKlass.all.length * 10) || 0) + 10 + attributes['is_active'] = false + attributes['updated_by'] = current_user.id + attributes['sync_by'] = current_user.id + attributes['sync_time'] = DateTime.now + + element_klass = Labimotion::ElementKlass.find_by(identifier: response['element_klass']['identifier']) + element_klass = Labimotion::ElementKlass.find_by(name: response['element_klass']['name'], is_generic: false) if element_klass.nil? + return { status: 'error', message: "The element [#{response['element_klass']['name']}] does not exist in this instance" } if element_klass.nil? + + # el_attributes = response['element_klass'].slice('name', 'label', 'desc', 'uuid', 'identifier', 'icon_name', 'klass_prefix', 'is_generic', 'released_at') + # el_attributes['properties_template'] = response['element_klass']['properties_release'] + # Labimotion::ElementKlass.create!(el_attributes) + + attributes['element_klass_id'] = element_klass.id + segment_klass = Labimotion::SegmentKlass.find_by(identifier: attributes['identifier']) + if segment_klass.present? + if segment_klass['uuid'] == attributes['uuid'] && segment_klass['version'] == attributes['version'] + { status: 'success', message: "This segment: #{attributes['label']} has the latest version!" } + else + segment_klass.update!(attributes) + segment_klass.create_klasses_revision(current_user) + { status: 'success', message: "This segment: [#{attributes['label']}] has been upgraded to the version: #{attributes['version']}!" } + end + else + exist_klass = Labimotion::SegmentKlass.find_by(label: attributes['label'], element_klass_id: element_klass.id) + if exist_klass.present? + { status: 'error', message: "The segment [#{attributes['label']}] is already in use." } + else + attributes['created_by'] = current_user.id + segment_klass = Labimotion::SegmentKlass.create!(attributes) + segment_klass.create_klasses_revision(current_user) + { status: 'success', message: "The segment: #{attributes['label']} has been created using version: #{attributes['version']}!" } + end + end + rescue StandardError => e + Labimotion.log_exception(e, current_user) + raise e + end + end +end diff --git a/lib/labimotion/libs/converter.rb b/lib/labimotion/libs/converter.rb new file mode 100644 index 0000000..0c08888 --- /dev/null +++ b/lib/labimotion/libs/converter.rb @@ -0,0 +1,357 @@ +# frozen_string_literal: true + +require 'net/http' +require 'uri' +require 'json' +require 'date' +require 'labimotion/version' +require 'labimotion/utils/utils' + +# rubocop: disable Metrics/AbcSize +# rubocop: disable Metrics/MethodLength +# rubocop: disable Metrics/ClassLength +# rubocop: disable Metrics/CyclomaticComplexity + +module Labimotion + class Converter + def self.logger + @@converter_logger ||= Logger.new(Rails.root.join('log/converter.log')) # rubocop:disable Style/ClassVars + end + + def self.process_ds(id, current_user = {}) + att = Attachment.find_by(id: id, con_state: Labimotion::ConState::CONVERTED) + return if att.nil? || att.attachable_id.nil? || att.attachable_type != 'Container' + + dsr = [] + ols = nil + if Labimotion::IS_RAILS5 == true + Zip::File.open(att.store.path) do |zip_file| + res = Labimotion::Converter.collect_metadata(zip_file) if att.filename.split('.')&.last == 'zip' + ols = res[:o] unless res&.dig(:o).nil? + dsr.push(res[:d]) unless res&.dig(:d).nil? + end + else + Zip::File.open(att.attachment_attacher.file.url) do |zip_file| + res = Labimotion::Converter.collect_metadata(zip_file) if att.filename.split('.')&.last == 'zip' + ols = res[:o] unless res&.dig(:o).nil? + dsr.push(res[:d]) unless res&.dig(:d).nil? + end + end + dsr.flatten! + dataset = build_ds(att.attachable_id, ols) + update_ds(dataset, dsr, current_user) if dataset.present? + att.update_column(:con_state, Labimotion::ConState::COMPLETED) + rescue StandardError => e + Labimotion::Converter.logger.error ["Att ID: #{att&.id}, OLS: #{ols}", "DSR: #{dsr}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR) + raise e + end + + def self.uri(api_name) + url = Rails.configuration.converter.url + "#{url}#{api_name}" + end + + def self.timeout + Rails.configuration.try(:converter).try(:timeout) || 30 + end + + def self.extname + '.jdx' + end + + def self.client_id + Rails.configuration.converter.profile || '' + end + + def self.secret_key + Rails.configuration.converter.secret_key || '' + end + + def self.auth + { username: client_id, password: secret_key } + end + + def self.date_time + DateTime.now.strftime('%Q') + end + + def self.signature(jbody) + md5 = Digest::MD5.new + md5.update jbody + mdhex = md5.hexdigest + mdall = mdhex << secret_key + @signature = Digest::SHA1.hexdigest mdall + end + + def self.header(opt = {}) + opt || {} + end + + def self.vor_conv(id) + conf = Rails.configuration.try(:converter).try(:url) + oa = Attachment.find(id) + folder = Rails.root.join('tmp/uploads/converter') + FileUtils.mkdir_p(folder) + return nil if conf.nil? || oa.nil? + + { a: oa, f: folder } + end + + def self.collect_metadata(zip_file) # rubocop: disable Metrics/PerceivedComplexity + dsr = [] + ols = nil + zip_file.each do |entry| + next unless entry.name == 'metadata/converter.json' + + metadata = entry.get_input_stream.read.force_encoding('UTF-8') + jdata = JSON.parse(metadata) + + ols = jdata['ols'] + matches = jdata['matches'] + matches&.each do |match| + idf = match['identifier'] + idr = match['result'] + if idf&.class == Hash && idr&.class == Hash && !idf['outputLayer'].nil? && !idf['outputKey'].nil? && !idr['value'].nil? # rubocop:disable Layout/LineLength + dsr.push(layer: idf['outputLayer'], field: idf['outputKey'], value: idr['value']) + end + end + end + { d: dsr, o: ols } + end + + def self.handle_response(oat, response) # rubocop: disable Metrics/PerceivedComplexity + dsr = [] + ols = nil + + begin + tmp_file = Tempfile.new(encoding: 'ascii-8bit') + tmp_file.write(response.parsed_response) + tmp_file.rewind + + name = response&.headers && response&.headers['content-disposition']&.split('=')&.last + filename = oat.filename + name = "#{File.basename(filename, '.*')}.zip" if name.nil? + + att = Attachment.new( + filename: name, + file_path: tmp_file.path, + ## content_type: file[:type], + attachable_id: oat.attachable_id, + attachable_type: 'Container', + con_state: Labimotion::ConState::CONVERTED, + created_by: oat.created_by, + created_for: oat.created_for, + ) + # att.attachment_attacher.attach(tmp_file) + if att.valid? && Labimotion::IS_RAILS5 == false + ## att.attachment_attacher.create_derivatives + att.save! + end + if att.valid? && Labimotion::IS_RAILS5 == true + att.save! + primary_store = Rails.configuration.storage.primary_store + att.update!(storage: primary_store) + end + process_ds(att.id) + rescue StandardError => e + raise e + ensure + tmp_file&.close + end + end + + def self.process(data) + return data[:a].con_state if data[:a]&.attachable_type != 'Container' + response = nil + begin + ofile = Rails.root.join(data[:f], data[:a].filename) + if Labimotion::IS_RAILS5 == true + FileUtils.cp(data[:a].store.path, ofile) + else + FileUtils.cp(data[:a].attachment_url, ofile) + end + + File.open(ofile, 'r') do |f| + body = { file: f } + response = HTTParty.post( + uri('conversions'), + basic_auth: auth, + body: body, + timeout: timeout, + ) + end + if response.ok? + Labimotion::Converter.handle_response(data[:a], response) + Labimotion::ConState::PROCESSED + else + Labimotion::Converter.logger.error ["Converter Response Error: id: [#{data[:a]&.id}], filename: [#{data[:a]&.filename}], response: #{response}"].join($INPUT_RECORD_SEPARATOR) + Labimotion::ConState::ERROR + end + rescue StandardError => e + Labimotion::Converter.logger.error ["process fail: #{id}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR) + Labimotion::ConState::ERROR + ensure + FileUtils.rm_f(ofile) + end + end + + def self.jcamp_converter(id) + data = Labimotion::Converter.vor_conv(id) + return if data.nil? + + Labimotion::Converter.process(data) + rescue StandardError => e + Labimotion::Converter.logger.error ["jcamp_converter fail: #{id}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR) + Labimotion::ConState::ERROR + end + + def self.generate_ds(att_id, current_user = {}) + dsr_info = Labimotion::Converter.fetch_dsr(att_id) + begin + return unless dsr_info && dsr_info[:info]&.length.positive? + + dataset = Labimotion::Converter.build_ds(att_id, dsr_info[:ols]) + Labimotion::Converter.update_ds(dataset, dsr_info[:info], current_user) if dataset.present? + rescue StandardError => e + Labimotion::Converter.logger.error ["Att ID: #{att_id}, OLS: #{dsr_info[:ols]}", "DSR: #{dsr_info[:info]}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR) + ensure + Labimotion::Converter.clean_dsr(att_id) + end + end + + def self.build_ds(att_id, ols) + cds = Container.find_by(id: att_id) + dataset = Labimotion::Dataset.find_by(element_type: 'Container', element_id: cds.id) + return dataset unless dataset.nil? + + klass = Labimotion::DatasetKlass.find_by(ols_term_id: ols) + return if klass.nil? + + uuid = SecureRandom.uuid + props = klass.properties_release + props['uuid'] = uuid + props['pkg'] = Labimotion::Utils.pkg(props['pkg']) + props['klass'] = 'Dataset' + Labimotion::Dataset.create!( + uuid: uuid, + dataset_klass_id: klass.id, + element_type: 'Container', + element_id: cds.id, + properties: props, + properties_release: klass.properties_release, + klass_uuid: klass.uuid + ) + end + + def self.update_ds(dataset, dsr, current_user = nil) # rubocop: disable Metrics/PerceivedComplexity + layers = dataset.properties['layers'] || {} + new_prop = dataset.properties + dsr.each do |ds| + layer = layers[ds[:layer]] + next if layer.nil? || layer['fields'].nil? + + fields = layer['fields'].select{ |f| f['field'] == ds[:field] } + fi = fields&.first + idx = layer['fields'].find_index(fi) + fi['value'] = ds[:value] + fi['device'] = ds[:device] || ds[:value] + new_prop['layers'][ds[:layer]]['fields'][idx] = fi + end + new_prop.dig('layers', 'general', 'fields')&.each_with_index do |fi, idx| + if fi['field'] == 'creator' && current_user.present? + fi['label'] = fi['label'] + fi['value'] = current_user.name + fi['system'] = current_user.name + new_prop['layers']['general']['fields'][idx] = fi + end + end + element = Container.find(dataset.element_id)&.root_element + element.present? && element&.class&.name == 'Sample' && new_prop.dig('layers', 'sample_details', 'fields')&.each_with_index do |fi, idx| + if fi['field'] == 'id' + fi['value'] = element.id + fi['system'] = element.id + new_prop['layers']['general']['fields'][idx] = fi + end + + if fi['field'] == 'label' + fi['value'] = element.short_label + fi['system'] = element.short_label + new_prop['layers']['general']['fields'][idx] = fi + end + end + dataset.properties = new_prop + dataset.save! + end + + def self.ts(method, identifier, params = nil) + Rails.cache.send(method, "#{Labimotion::Converter.new.class.name}#{identifier}", params) + end + + def self.fetch_dsr(att_id) + Labimotion::Converter.ts('read', att_id) + end + + def self.clean_dsr(att_id) + Labimotion::Converter.ts('delete', att_id) + end + + def self.fetch_options + options = { basic_auth: auth, timeout: timeout } + response = HTTParty.get(uri('options'), options) + response.parsed_response if response.code == 200 + end + + def self.delete_profile(id) + options = { basic_auth: auth, timeout: timeout } + response = HTTParty.delete("#{uri('profiles')}/#{id}", options) + response.parsed_response if response.code == 200 + end + + def self.create_profile(data) + options = { basic_auth: auth, timeout: timeout, body: data.to_json, headers: { 'Content-Type' => 'application/json' } } + response = HTTParty.post(uri('profiles'), options) + response.parsed_response if response.code == 201 + end + + def self.update_profile(data) + options = { basic_auth: auth, timeout: timeout, body: data.to_json, headers: { 'Content-Type' => 'application/json' } } + response = HTTParty.put("#{uri('profiles')}/#{data[:id]}", options) + response.parsed_response if response.code == 200 + end + + def self.fetch_profiles + options = { basic_auth: auth, timeout: timeout } + response = HTTParty.get(uri('profiles'), options) + response.parsed_response if response.code == 200 + end + + def self.create_tables(tmpfile) + res = {} + File.open(tmpfile.path, 'r') do |file| + body = { file: file } + response = HTTParty.post( + uri('tables'), + basic_auth: auth, + body: body, + timeout: timeout, + ) + res = response.parsed_response + end + res + end + + def self.metadata(id) + att = Attachment.find(id) + return if att.nil? || att.attachable_id.nil? || att.attachable_type != 'Container' + + ds = Labimotion::Dataset.find_by(element_type: 'Container', element_id: att.attachable_id) + att.update_column(:con_state, Labimotion::ConState::COMPLETED) if ds.present? + process_ds(att.id) if ds.nil? + end + end +end + +# rubocop: enable Metrics/AbcSize +# rubocop: enable Metrics/MethodLength +# rubocop: enable Metrics/ClassLength +# rubocop: enable Metrics/CyclomaticComplexity diff --git a/lib/labimotion/libs/export_dataset.rb b/lib/labimotion/libs/export_dataset.rb new file mode 100644 index 0000000..50d481e --- /dev/null +++ b/lib/labimotion/libs/export_dataset.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true +require 'export_table' +require 'labimotion/version' + +module Labimotion + ## ExportDataset + class ExportDataset + DEFAULT_ROW_WIDTH = 100 + DEFAULT_ROW_HEIGHT = 20 + + def initialize(**args) + @xfile = Axlsx::Package.new + @file_extension = 'xlsx' + @xfile.workbook.styles.fonts.first.name = 'Calibri' + end + + def res_name(id) + element_name = Container.find(id)&.root_element&.short_label + ols = ols_name(id) + "#{element_name}_#{ols.gsub(' ', '_')}.xlsx" + end + + def ols_name(id) + ds = Labimotion::Dataset.find_by(element_id: id, element_type: 'Container') + return nil if ds.nil? + + name = ds.dataset_klass.label + + match = name.match(/\((.*?)\)/) + name = match && match.length > 1 ? match[1] : name + + name = '1H NMR' if ds.dataset_klass.ols_term_id == 'CHMO:0000593' + name = '13C NMR' if ds.dataset_klass.ols_term_id == 'CHMO:0000595' + name.slice(0, 26) + end + + def description(ds, id) + wb = @xfile.workbook + sheet = @xfile.workbook.add_worksheet(name: 'Description') + header_style = sheet.styles.add_style(sz: 12, fg_color: 'FFFFFF', bg_color: '00008B', border: { style: :thick, color: 'FF777777', edges: [:bottom] }) + sheet.add_row(['File name', res_name(id)]) + sheet.add_row(['Time', Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")] ) + sheet.add_row(['']) + sheet.add_row(['Fields description of sheet:' + ds.dataset_klass.label]) + sheet.add_row(['Fields', 'Field description'], style: header_style) + sheet.add_row(['Layer Label', 'The label of the layer']) + sheet.add_row(['Field Label', 'The label of the field']) + sheet.add_row(['Value', 'The current value of the field']) + sheet.add_row(['Unit', 'The unit of the field']) + sheet.add_row(['Name', 'The key of the field, can be used to identify the field']) + sheet.add_row(['Type', 'The type of the field']) + sheet.add_row(['From device?', '[Yes] if the field is from the device, [No] if the field is manually entered by the user, [SYS] if the field is automatically generated by the system']) + sheet.add_row(['Device source', 'The source tag of the device file, available only if the ontology term is 1H NMR or 13C NMR']) + sheet.add_row(['Device data', 'The original device data, can not be changed after data uploaded']) + sheet.add_row(['']) + sheet.add_row(['']) + sheet.add_row(['']) + sheet.add_row(['', '(This file is automatically generated by the system.)']) + end + + def export(id) + ds = Labimotion::Dataset.find_by(element_id: id, element_type: 'Container') + return if ds.nil? + + description(ds, id) + + wb = @xfile.workbook + name = ols_name(id) + return if name.nil? + + sheet = @xfile.workbook.add_worksheet(name: name) + sheet.add_row([ds.dataset_klass.label]) + header_style = sheet.styles.add_style(sz: 12, fg_color: 'FFFFFF', bg_color: '00008B', border: { style: :thick, color: 'FF777777', edges: [:bottom] }) + layer_style = sheet.styles.add_style(b: true, bg_color: 'CEECF5') + sheet.add_row(header, style: header_style) + + layers = ds.properties['layers'] || {} + layer_keys = layers.keys.sort_by { |key| layers[key]['position'] } + layer_keys.each do |key| + layer = layers[key] + sheet.add_row([layer['label'], ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], style: layer_style) + sorted_fields = layer['fields'].sort_by { |obj| obj['position'] } + sorted_fields.each do |field| + next if field['type'] == 'dummy' + + type = field['type'] + from_device = field['device'].present? ? 'Yes' : 'No' + from_device = field['system'].present? ? 'SYS' : from_device + type = "#{field['type']}-#{field['option_layers']}" if field['type'] == 'select' || field['type'] == 'system-defined' + + show_value = field['value'] =~ /\A\d+,\d+\z/ ? field['value']&.gsub(',', '.') : field['value'] + sheet.add_row([' ', field['label'], show_value, field['value_system'], field['field'], type, from_device, field['dkey'], field['system'] || field['device']].freeze) + end + # sheet.column_widths nil, nil, nil, nil, 0, 0, 0, 0, 0 + end + end + + def spectra(id) + wb = @xfile.workbook + gds = Labimotion::Dataset.find_by(element_id: id, element_type: 'Container') + cds = Container.find(id) + cds_csv = cds.attachments.where(aasm_state: 'csv') + csv_length = cds_csv.length + return if csv_length.zero? + cds_csv.each_with_index do |att, idx| + name = File.basename(att.filename, '.csv') + name = name.slice(0, (25 - csv_length.to_s.length - 1)) + sheet_name = "#{name}_#{idx}" + sheet = @xfile.workbook.add_worksheet(name: sheet_name) + + if Labimotion::IS_RAILS5 == true + File.open(att.store.path) do |fi| + fi.each_line do |line| + sheet.add_row(line.split(',')) + end + end + else + File.open(att.attachment_url) do |fi| + fi.each_line do |line| + sheet.add_row(line.split(',')) + end + end + end + end + end + + def header + ['Layer Label', 'Field Label', 'Value', 'Unit', 'Name', 'Type', 'from device?', 'Device source', 'Device data'].freeze + end + + def read + @xfile.to_stream.read + end + + end +end diff --git a/lib/labimotion/libs/nmr_mapper.rb b/lib/labimotion/libs/nmr_mapper.rb new file mode 100644 index 0000000..7398d69 --- /dev/null +++ b/lib/labimotion/libs/nmr_mapper.rb @@ -0,0 +1,302 @@ +# frozen_string_literal: true + +require 'labimotion/version' +require 'labimotion/utils/utils' + +module Labimotion + ## NmrMapper + class NmrMapper + def self.process_ds(id, current_user = {}) + att = Attachment.find_by(id: id, con_state: Labimotion::ConState::NMR) + return if att.nil? + + content = is_brucker_binary(id) + if content.nil? + Labimotion::ConState::NONE + else + data = process(att, id, content) + generate_ds(id, att.attachable_id, data, current_user) + Labimotion::ConState::COMPLETED + end + end + + def self.is_brucker_binary(id) + att = Attachment.find_by(id: id, con_state: Labimotion::ConState::NMR) + return if att.nil? + + if Labimotion::IS_RAILS5 == true + Zip::File.open(att.store.path) do |zip_file| + zip_file.each do |entry| + if entry.name.include?('/pdata/') && entry.name.include?('parm.txt') + metadata = entry.get_input_stream.read.force_encoding('UTF-8') + return metadata + end + end + end + else + if att&.attachment_attacher&.file&.url + Zip::File.open(att.attachment_attacher.file.url) do |zip_file| + zip_file.each do |entry| + if entry.name.include?('/pdata/') && entry.name.include?('parm.txt') + metadata = entry.get_input_stream.read.force_encoding('UTF-8') + return metadata + end + end + end + end + end + nil + end + + def self.process(att, id, content) + return if att.nil? || content.nil? + + lines = content.split("\n").reject(&:empty?) + metadata = {} + lines.map do |ln| + arr = ln.split(/\s+/) + metadata[arr[0]] = arr[1..-1].join(' ') if arr.length > 1 + end + ols = 'CHMO:0000593' if metadata['NUC1'] == '1H' + ols = 'CHMO:0000595' if metadata['NUC1'] == '13C' + + { content: { metadata: metadata, ols: ols } } + # if content.present? && att.present? + # Labimotion::NmrMapper.ts('write', att.attachable_id, + # content: { metadata: metadata, ols: ols }) + # end + end + + def self.fetch_content(id) + atts = Attachment.where(attachable_id: id) + return if atts.nil? + + atts.each do |att| + content = Labimotion::NmrMapper.ts('read', att.id) + return content if content.present? + end + end + + + def self.generate_ds(id, cid, data, current_user = {}) + return if data.nil? || cid.nil? + + obj = Labimotion::NmrMapper.build_ds(cid, data[:content]) + return if obj.nil? || obj[:ols].nil? + + Labimotion::NmrMapper.update_ds_1h(cid, obj, current_user) if obj[:ols] == 'CHMO:0000593' + Labimotion::NmrMapper.update_ds_1h(cid, obj, current_user) if obj[:ols] == 'CHMO:0000595' + end + + def self.update_ds_13c(id, obj) + # dataset = obj[:dataset] + # metadata = obj[:metadata] + # new_prop = dataset.properties + + # dataset.properties = new_prop + # dataset.save! + end + + def self.set_data(prop, field, idx, layer_name, field_name, value) + return if field['field'] != field_name || value&.empty? + + field['value'] = value + prop['layers'][layer_name]['fields'][idx] = field + prop + end + + def self.update_ds_1h(id, obj, current_user) + dataset = obj[:dataset] + metadata = obj[:metadata] + new_prop = dataset.properties + new_prop.dig('layers', 'general', 'fields')&.each_with_index do |fi, idx| + # new_prop = set_data(new_prop, fi, idx, 'general', 'title', metadata['NAME']) + if fi['field'] == 'title' && metadata['NAME'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['NAME'] + fi['device'] = metadata['NAME'] + fi['dkey'] = 'NAME' + new_prop['layers']['general']['fields'][idx] = fi + end + + if fi['field'] == 'date' && metadata['Date_'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['Date_'] + fi['device'] = metadata['Date_'] + fi['dkey'] = 'Date_' + new_prop['layers']['general']['fields'][idx] = fi + end + + if fi['field'] == 'time' && metadata['Time'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['Time'] + fi['device'] = metadata['Time'] + fi['dkey'] = 'Time' + new_prop['layers']['general']['fields'][idx] = fi + end + + if fi['field'] == 'creator' && current_user.present? + ## fi['label'] = fi['label'] + fi['value'] = current_user.name + new_prop['layers']['general']['fields'][idx] = fi + end + end + element = Container.find(id)&.root_element + element.present? && element&.class&.name == 'Sample' && new_prop.dig('layers', 'sample_details', + 'fields')&.each_with_index do |fi, idx| + if fi['field'] == 'label' + fi['value'] = element.short_label + new_prop['layers']['sample_details']['fields'][idx] = fi + end + if fi['field'] == 'id' + fi['value'] = element.id + new_prop['layers']['sample_details']['fields'][idx] = fi + end + end + + new_prop.dig('layers', 'instrument', 'fields')&.each_with_index do |fi, idx| + if fi['field'] == 'instrument' && metadata['INSTRUM'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['INSTRUM'] + fi['device'] = metadata['INSTRUM'] + fi['dkey'] = 'INSTRUM' + new_prop['layers']['instrument']['fields'][idx] = fi + end + end + + new_prop.dig('layers', 'equipment', 'fields')&.each_with_index do |fi, idx| + if fi['field'] == 'probehead' && metadata['PROBHD'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['PROBHD'] + fi['device'] = metadata['PROBHD'] + fi['dkey'] = 'PROBHD' + new_prop['layers']['equipment']['fields'][idx] = fi + end + end + + new_prop.dig('layers', 'sample_preparation', 'fields')&.each_with_index do |fi, idx| + if fi['field'] == 'solvent' && metadata['SOLVENT'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['SOLVENT'] + fi['device'] = metadata['SOLVENT'] + fi['dkey'] = 'SOLVENT' + fi['value'] = 'chloroform-D1 (CDCl3)' if metadata['SOLVENT'] == 'CDCl3' + new_prop['layers']['sample_preparation']['fields'][idx] = fi + end + end + + new_prop.dig('layers', 'set', 'fields')&.each_with_index do |fi, idx| + if fi['field'] == 'temperature' && metadata['TE'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['TE'].split(/\s+/).first + fi['device'] = metadata['TE'] + fi['dkey'] = 'TE' + fi['value_system'] = metadata['TE'].split(/\s+/).last + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'ns' && metadata['NS'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['NS'] + fi['device'] = metadata['NS'] + fi['dkey'] = 'NS' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'PULPROG' && metadata['PULPROG'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['PULPROG'] + fi['device'] = metadata['PULPROG'] + fi['dkey'] = 'PULPROG' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'td' && metadata['TD'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['TD'] + fi['device'] = metadata['TD'] + fi['dkey'] = 'TD' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'done' && metadata['D1'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['D1'] + fi['device'] = metadata['D1'] + fi['dkey'] = 'D1' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'sf' && metadata['SF'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['SF'] + fi['device'] = metadata['SF'] + fi['dkey'] = 'SF' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'sfoone' && metadata['SFO1'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['SFO1'] + fi['device'] = metadata['SFO1'] + fi['dkey'] = 'SFO1' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'sfotwo' && metadata['SFO2'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['SFO2'] + fi['device'] = metadata['SFO2'] + fi['dkey'] = 'SFO2' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'nucone' && metadata['NUC1'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['NUC1'] + fi['device'] = metadata['NUC1'] + fi['dkey'] = 'NUC1' + new_prop['layers']['set']['fields'][idx] = fi + end + if fi['field'] == 'nuctwo' && metadata['NUC2'].present? + ## fi['label'] = fi['label'] + fi['value'] = metadata['NUC2'] + fi['device'] = metadata['NUC2'] + fi['dkey'] = 'NUC2' + new_prop['layers']['set']['fields'][idx] = fi + end + end + dataset.properties = new_prop + dataset.save! + end + + def self.ts(method, identifier, params = nil) + Rails.cache.send(method, "#{Labimotion::NmrMapper.new.class.name}#{identifier}", params) + end + + def self.clean(id) + Labimotion::NmrMapper.ts('delete', id) + end + + def self.build_ds(id, content) + ds = Container.find_by(id: id) + return if ds.nil? || content.nil? + + ols = content[:ols] + metadata = content[:metadata] + + return if ols.nil? || metadata.nil? + + klass = Labimotion::DatasetKlass.find_by(ols_term_id: ols) + return if klass.nil? + + uuid = SecureRandom.uuid + props = klass.properties_release + props['uuid'] = uuid + props['pkg'] = Labimotion::Utils.pkg(props['pkg']) + props['klass'] = 'Dataset' + dataset = Labimotion::Dataset.create!( + uuid: uuid, + dataset_klass_id: klass.id, + element_type: 'Container', + element_id: ds.id, + properties: props, + properties_release: klass.properties_release, + klass_uuid: klass.uuid, + ) + { dataset: dataset, metadata: metadata, ols: ols } + end + end +end diff --git a/lib/labimotion/libs/template_hub.rb b/lib/labimotion/libs/template_hub.rb new file mode 100644 index 0000000..d968e9b --- /dev/null +++ b/lib/labimotion/libs/template_hub.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'net/http' +require 'uri' +require 'json' +require 'date' + +# rubocop: disable Metrics/AbcSize +# rubocop: disable Metrics/MethodLength + +module Labimotion + ## TemplateHub + class TemplateHub + TARGET = Rails.env.production? ? 'https://www.chemotion-repository.net/' : 'http://localhost:3000/' + + def self.uri(api_name) + url = TARGET + "#{url}api/v1/labimotion_hub/#{api_name}" + end + + + def self.header(opt = {}) + opt || { timeout: 10, headers: { 'Content-Type' => 'text/json' } } + end + + def self.handle_response(oat, response) # rubocop: disable Metrics/PerceivedComplexity + begin + response&.success? ? 'OK' : 'ERROR' + rescue StandardError => e + raise e + ensure + ## oat.update(status: response&.success? ? 'done' : 'failure') + end + end + + def self.list(klass) + body = { klass: klass } + response = HTTParty.get("#{uri('list')}?klass=#{klass}", timeout: 10) + # response.parsed_response if response.code == 200 + JSON.parse(response.body) if response.code == 200 + rescue StandardError => e + Labimotion.log_exception(e) + error!('Cannot connect to Chemotion Repository', 401) + end + + def self.fetch_identifier(klass, identifier, origin) + body = { klass: klass, identifier: identifier, origin: origin } + response = HTTParty.post( + uri('fetch'), + body: body, + timeout: 10 + ) + # response.parsed_response if response.code == 200 + JSON.parse(response.body) if response.code == 201 + rescue StandardError => e + Labimotion.log_exception(e) + error!('Cannot connect to Chemotion Repository', 401) + end + end +end + +# rubocop: enable Metrics/AbcSize +# rubocop: enable Metrics/MethodLength +# rubocop: enable Metrics/ClassLength +# rubocop: enable Metrics/CyclomaticComplexity diff --git a/lib/labimotion/models/collections_element.rb b/lib/labimotion/models/collections_element.rb new file mode 100644 index 0000000..3906759 --- /dev/null +++ b/lib/labimotion/models/collections_element.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Labimotion + class CollectionsElement < ApplicationRecord + acts_as_paranoid + self.table_name = :collections_elements + belongs_to :collection + belongs_to :element, class_name: 'Labimotion::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(eids, from_col_ids) + element_ids = Labimotion::Element.get_associated_elements(eids) + sample_ids = Labimotion::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(eids, from_col_ids, to_col_ids, element_type='') + element_ids = Labimotion::Element.get_associated_elements(eids) + sample_ids = Labimotion::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) + update_tag_by_element_ids(element_ids) + end + + def self.create_in_collection(eids, to_col_ids, element_type='') + element_ids = Labimotion::Element.get_associated_elements(eids) + sample_ids = Labimotion::Element.get_associated_samples(element_ids) + static_create_in_collection(element_ids, to_col_ids) + CollectionsSample.create_in_collection(sample_ids, to_col_ids) + update_tag_by_element_ids(element_ids) + end + end +end diff --git a/lib/labimotion/models/concerns/attachment_converter.rb b/lib/labimotion/models/concerns/attachment_converter.rb new file mode 100644 index 0000000..fd551f7 --- /dev/null +++ b/lib/labimotion/models/concerns/attachment_converter.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Labimotion + 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 + return if self.has_attribute?(:con_state) == false || con_state.present? + + if Rails.configuration.try(:converter).try(:url) && ACCEPTED_FORMATS.include?(File.extname(filename&.downcase)) + self.con_state = Labimotion::ConState::WAIT + end + + if File.extname(filename&.downcase) == '.zip' && attachable&.dataset.nil? + self.con_state = Labimotion::ConState::NMR + end + + self.con_state = Labimotion::ConState::NONE if con_state.nil? + end + + def exec_converter + return if self.has_attribute?(:con_state) == false || self.con_state.nil? || self.con_state == Labimotion::ConState::NONE + + return if attachable_id.nil? && self.con_state != Labimotion::ConState::WAIT + + case con_state + when Labimotion::ConState::NMR + self.con_state = Labimotion::NmrMapper.process_ds(id) + update_column(:con_state, con_state) + when Labimotion::ConState::WAIT + self.con_state = Labimotion::Converter.jcamp_converter(id) + update_column(:con_state, con_state) + when Labimotion::ConState::CONVERTED + Labimotion::Converter.metadata(id) + end + end + end + end +end diff --git a/lib/labimotion/models/concerns/datasetable.rb b/lib/labimotion/models/concerns/datasetable.rb new file mode 100644 index 0000000..595e54c --- /dev/null +++ b/lib/labimotion/models/concerns/datasetable.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Datasetable concern +require 'labimotion/utils/utils' + +module Labimotion + ## Datasetable concern + module Datasetable + extend ActiveSupport::Concern + + included do + has_one :dataset, as: :element, class_name: 'Labimotion::Dataset' + end + + def not_dataset? + self.class.name == 'Container' && container_type != 'dataset' + end + + def save_dataset(**args) + return if not_dataset? + + klass = Labimotion::DatasetKlass.find_by(id: args[:dataset_klass_id]) + uuid = SecureRandom.uuid + props = args[:properties] + props['pkg'] = Labimotion::Utils.pkg(props['pkg']) + props['identifier'] = klass.identifier if klass.identifier.present? + props['uuid'] = uuid + props['klass'] = 'Dataset' + + ds = Labimotion::Dataset.find_by(element_type: self.class.name, element_id: id) + if ds.present? && (ds.klass_uuid != props['klass_uuid'] || ds.properties != props) + ds.update!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: args[:dataset_klass_id], properties: props, klass_uuid: props['klass_uuid']) + end + return if ds.present? + + props['klass_uuid'] = klass.uuid + Labimotion::Dataset.create!(properties_release: klass.properties_release, 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? + + Labimotion::Dataset.where(element_type: self.class.name, element_id: id).destroy_all + end + end +end diff --git a/lib/labimotion/models/concerns/generic_klass_revisions.rb b/lib/labimotion/models/concerns/generic_klass_revisions.rb new file mode 100644 index 0000000..09644a3 --- /dev/null +++ b/lib/labimotion/models/concerns/generic_klass_revisions.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Labimotion + ## Generic Klass Revisions Helpers + module GenericKlassRevisions + extend ActiveSupport::Concern + included do + # has_many :element_klasses_revisions, dependent: :destroy + end + + def create_klasses_revision(current_user) + properties_release = properties_template + migrate_workflow if properties_release['flow'].present? + + if properties_release['flowObject'].present? + elements = (properties_release['flowObject']['nodes'] || []).map do |el| + if el['data'].present? && el['data']['lKey'].present? + layer = properties_release['layers'][el['data']['lKey']] + el['data']['layer'] = layer if layer.present? + end + el + end + properties_release['flowObject']['nodes'] = elements + end + klass_attributes = { + uuid: properties_template['uuid'], + properties_template: properties_release, + properties_release: properties_release, + released_at: DateTime.now, + updated_by: current_user&.id, + released_by: current_user&.id, + } + + self.update!(klass_attributes) + reload + attributes = { + released_by: released_by, + uuid: uuid, + version: version, + properties_release: properties_release, + released_at: released_at + } + attributes["#{self.class.name.underscore.split('/').last}_id"] = id + "#{self.class.name}esRevision".constantize.create(attributes) + end + end +end diff --git a/lib/labimotion/models/concerns/generic_revisions.rb b/lib/labimotion/models/concerns/generic_revisions.rb new file mode 100644 index 0000000..e12f5c7 --- /dev/null +++ b/lib/labimotion/models/concerns/generic_revisions.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# GenericRevisions concern +module Labimotion + 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 == 'Labimotion::Element' + end + + def save_to_vault + attributes = { + uuid: uuid, + klass_uuid: klass_uuid, + properties: properties, + properties_release: properties_release + } + attributes["#{Labimotion::Utils.element_name_dc(self.class.name)}_id"] = id + attributes['name'] = name if self.class.name == 'Labimotion::Element' + "#{self.class.name}sRevision".constantize.create(attributes) + end + + def delete_attachments + att_ids = [] + properties && 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 +end diff --git a/lib/labimotion/models/concerns/segmentable.rb b/lib/labimotion/models/concerns/segmentable.rb new file mode 100644 index 0000000..cfca683 --- /dev/null +++ b/lib/labimotion/models/concerns/segmentable.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'labimotion/utils/utils' + +module Labimotion + # Segmentable concern + module Segmentable + extend ActiveSupport::Concern + included do + has_many :segments, as: :element, dependent: :destroy, class_name: 'Labimotion::Segment' + end + + def copy_segments(**args) + return if args[:segments].nil? + + segments = save_segments(segments: args[:segments], current_user_id: args[:current_user_id]) + segments.each do |segment| + properties = segment.properties + properties['layers'].keys.each do |key| + layer = properties['layers'][key] + field_uploads = layer['fields'].select { |ss| ss['type'] == 'upload' } + field_uploads&.each do |upload| + idx = properties['layers'][key]['fields'].index(upload) + files = upload["value"] && upload["value"]["files"] + files&.each_with_index do |fi, fdx| + aid = properties['layers'][key]['fields'][idx]['value']['files'][fdx]['aid'] + unless aid.nil? + copied_att = Attachment.find(aid)&.copy(attachable_type: 'SegmentProps', attachable_id: segment.id, transferred: true) + unless copied_att.nil? + copied_att.save! + properties['layers'][key]['fields'][idx]['value']['files'][fdx]['aid'] = copied_att.id + properties['layers'][key]['fields'][idx]['value']['files'][fdx]['uid'] = copied_att.identifier + end + end + end + end + end + segment.update!(properties: properties) + end + end + + def save_segments(**args) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + return if args[:segments].nil? + segments = [] + args[:segments]&.each do |seg| + klass = Labimotion::SegmentKlass.find_by(id: seg['segment_klass_id']) + uuid = SecureRandom.uuid + props = seg['properties'] + props['pkg'] = Labimotion::Utils.pkg(props['pkg']) + props['identifier'] = klass.identifier if klass.identifier.present? + props['uuid'] = uuid + props['klass'] = 'Segment' + segment = Labimotion::Segment.find_by(element_type: Labimotion::Utils.element_name(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) + segment.update!(properties_release: klass.properties_release, properties: props, uuid: uuid, klass_uuid: props['klass_uuid']) + segments.push(segment) + end + next if segment.present? + + props['klass_uuid'] = klass.uuid + segment = Labimotion::Segment.create!(properties_release: klass.properties_release, 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) + segments.push(segment) + end + segments + end + end +end diff --git a/lib/labimotion/models/concerns/workflow.rb b/lib/labimotion/models/concerns/workflow.rb new file mode 100644 index 0000000..42ab989 --- /dev/null +++ b/lib/labimotion/models/concerns/workflow.rb @@ -0,0 +1,36 @@ +module Labimotion + # Segmentable concern + module Workflow + extend ActiveSupport::Concern + + def split_workflow(properties) + return if properties['flow'].nil? + + if properties['flow'].present? + properties['flowObject'] = {} + elements = properties['flow']['elements'] || {} + properties['flowObject']['nodes'] = elements.select { |obj| obj['source'].nil? } + properties['flowObject']['edges'] = elements.select { |obj| obj['source'] && obj['source'] != obj['target'] }.map do |obj| + obj['markerEnd'] = { 'type': 'arrowclosed' } + obj + end + properties['flowObject']['viewport'] = { + "x": properties['flow']['position'][0] || 0, + "y": properties['flow']['position'][1] || 0, + "zoom": properties['flow']['zoom'] || 1 + } + properties.delete('flow') + end + properties + end + + def migrate_workflow + return if properties_template.nil? || properties_release.nil? + + return if properties_template['flow'].nil? && properties_release['flow'].nil? + + update_column(:properties_template, split_workflow(properties_template)) if properties_template['flow'] + update_column(:properties_release, split_workflow(properties_release)) if properties_release['flow'] + end + end +end diff --git a/lib/labimotion/models/dataset.rb b/lib/labimotion/models/dataset.rb new file mode 100644 index 0000000..196fd75 --- /dev/null +++ b/lib/labimotion/models/dataset.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require 'labimotion/models/concerns/generic_revisions' + +# ## This is the first version of the dataset class +module Labimotion + # ## This is the first version of the dataset class + class Dataset < ApplicationRecord + self.table_name = :datasets + acts_as_paranoid + include GenericRevisions + belongs_to :dataset_klass, class_name: 'Labimotion::DatasetKlass' + belongs_to :element, polymorphic: true, class_name: 'Labimotion::Element' + end +end diff --git a/lib/labimotion/models/dataset_klass.rb b/lib/labimotion/models/dataset_klass.rb new file mode 100644 index 0000000..05bc033 --- /dev/null +++ b/lib/labimotion/models/dataset_klass.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require 'labimotion/models/concerns/generic_klass_revisions' + +module Labimotion + class DatasetKlass < ApplicationRecord + acts_as_paranoid + self.table_name = :dataset_klasses + include GenericKlassRevisions + has_many :datasets, dependent: :destroy, class_name: 'Labimotion::Dataset' + has_many :dataset_klasses_revisions, dependent: :destroy, class_name: 'Labimotion::DatasetKlassesRevision' + + 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 Labimotion::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 } + Labimotion::DatasetKlass.create!(attributes) + end + true + end + end +end diff --git a/lib/labimotion/models/dataset_klasses_revision.rb b/lib/labimotion/models/dataset_klasses_revision.rb new file mode 100644 index 0000000..8edcbd1 --- /dev/null +++ b/lib/labimotion/models/dataset_klasses_revision.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Labimotion + class DatasetKlassesRevision < ApplicationRecord + self.table_name = :dataset_klasses_revisions + acts_as_paranoid + has_one :dataset_klass, class_name: 'Labimotion::DatasetKlass' + end +end diff --git a/lib/labimotion/models/datasets_revision.rb b/lib/labimotion/models/datasets_revision.rb new file mode 100644 index 0000000..d5c3b3f --- /dev/null +++ b/lib/labimotion/models/datasets_revision.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Labimotion + class DatasetsRevision < ApplicationRecord + acts_as_paranoid + self.table_name = :datasets_revisions + has_one :dataset, class_name: 'Labimotion::Dataset' + end +end diff --git a/lib/labimotion/models/element.rb b/lib/labimotion/models/element.rb new file mode 100644 index 0000000..2f03671 --- /dev/null +++ b/lib/labimotion/models/element.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true +require 'labimotion/models/concerns/generic_revisions' +require 'labimotion/models/concerns/segmentable' +require 'labimotion/models/concerns/workflow' + +module Labimotion + class Element < ApplicationRecord + acts_as_paranoid + self.table_name = :elements + include PgSearch if Labimotion::IS_RAILS5 == true + include PgSearch::Model if Labimotion::IS_RAILS5 == false + include ElementUIStateScopes + include Collectable + include Taggable + include Workflow + 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) } + + belongs_to :element_klass, class_name: 'Labimotion::ElementKlass' + + has_many :collections_elements, inverse_of: :element, dependent: :destroy, class_name: 'Labimotion::CollectionsElement' + has_many :collections, through: :collections_elements + has_many :attachments, as: :attachable + has_many :elements_samples, dependent: :destroy, class_name: 'Labimotion::ElementsSample' + has_many :samples, through: :elements_samples, source: :sample + has_one :container, :as => :containable + has_many :elements_revisions, dependent: :destroy, class_name: 'Labimotion::ElementsRevision' + + accepts_nested_attributes_for :collections_elements + + 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 :creator, foreign_key: :created_by, class_name: 'User' + validates :creator, presence: true + + has_many :elements_elements, foreign_key: :parent_id, class_name: 'Labimotion::ElementsElement' + has_many :elements, through: :elements_elements, source: :element, class_name: 'Labimotion::Element' + + before_create :auto_set_short_label + after_create :update_counter + before_destroy :delete_attachment + + + def attachments + Attachment.where(attachable_id: self.id, attachable_type: self.class.name) + end + + def self.get_associated_samples(element_ids) + Labimotion::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 + + def self.get_associated_elements(element_ids) + pids = Labimotion::Element.where(id: element_ids).pluck :id + get_ids = proc do |eids| + eids.each do |p| + cs = Labimotion::Element.find_by(id: p)&.elements.where.not(id: pids).pluck :id + next if cs.empty? + + pids = (pids << cs).flatten.uniq + get_ids.call(cs) + end + end + get_ids.call(pids) + pids + end + + def thumb_svg + if Labimotion::IS_RAILS5 == true + image_atts = attachments.select do |a_img| + a_img&.content_type&.match(Regexp.union(%w[jpg jpeg png tiff tif])) + end + + attachment = image_atts[0] || attachments[0] + preview = attachment.read_thumbnail if attachment + preview && Base64.encode64(preview) || 'not available' + else + image_atts = attachments.select(&:type_image?) + attachment = image_atts[0] || attachments[0] + preview = attachment&.read_thumbnail + (preview && Base64.encode64(preview)) || 'not available' + end + end + + + def migrate_workflow + return if properties.nil? || properties_release.nil? + + return if properties['flow'].nil? && properties_release['flow'].nil? + + update_column(:properties, split_workflow(properties)) if properties['flow'] + update_column(:properties_release, split_workflow(properties_release)) if properties_release['flow'] + 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 +end diff --git a/lib/labimotion/models/element_klass.rb b/lib/labimotion/models/element_klass.rb new file mode 100644 index 0000000..6a708fe --- /dev/null +++ b/lib/labimotion/models/element_klass.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require 'labimotion/models/concerns/generic_klass_revisions' +require 'labimotion/models/concerns/workflow' + +module Labimotion + class ElementKlass < ApplicationRecord + self.table_name = :element_klasses + acts_as_paranoid + include GenericKlassRevisions + include Workflow + has_many :elements, dependent: :destroy, class_name: 'Labimotion::Element' + has_many :segment_klasses, dependent: :destroy, class_name: 'Labimotion::SegmentKlass' + has_many :element_klasses_revisions, dependent: :destroy, class_name: 'Labimotion::ElementKlassesRevision' + + 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('app/packs/klasses.json'), + klasses&.to_json || [] + ) + end + + end +end diff --git a/lib/labimotion/models/element_klasses_revision.rb b/lib/labimotion/models/element_klasses_revision.rb new file mode 100644 index 0000000..d0d0237 --- /dev/null +++ b/lib/labimotion/models/element_klasses_revision.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'labimotion/models/concerns/workflow' + +module Labimotion + class ElementKlassesRevision < ApplicationRecord + acts_as_paranoid + self.table_name = :element_klasses_revisions + include Workflow + has_one :element_klass, class_name: 'Labimotion::ElementKlass' + + + def migrate_workflow + return if properties_release.nil? || properties_release['flow'].nil? + + update_column(:properties_release, split_workflow(properties_release)) if properties_release['flow'] + end + end +end diff --git a/lib/labimotion/models/elements_element.rb b/lib/labimotion/models/elements_element.rb new file mode 100644 index 0000000..ddf4ae4 --- /dev/null +++ b/lib/labimotion/models/elements_element.rb @@ -0,0 +1,11 @@ + +module Labimotion + class ElementsElement < ApplicationRecord + self.table_name = :elements_elements + acts_as_paranoid + belongs_to :element, class_name: 'Labimotion::Element' + belongs_to :parent, foreign_key: :parent_id, class_name: 'Labimotion::Element' + + # include Tagging + end +end diff --git a/lib/labimotion/models/elements_revision.rb b/lib/labimotion/models/elements_revision.rb new file mode 100644 index 0000000..87083e6 --- /dev/null +++ b/lib/labimotion/models/elements_revision.rb @@ -0,0 +1,8 @@ + +module Labimotion + class ElementsRevision < ApplicationRecord + self.table_name = :elements_revisions + acts_as_paranoid + has_one :element, class_name: 'Labimotion::Element' + end +end diff --git a/lib/labimotion/models/elements_sample.rb b/lib/labimotion/models/elements_sample.rb new file mode 100644 index 0000000..28397ea --- /dev/null +++ b/lib/labimotion/models/elements_sample.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Labimotion + class ElementsSample < ApplicationRecord + acts_as_paranoid + self.table_name = :elements_samples + has_one :element, class_name: 'Labimotion::Element' + belongs_to :sample + include Tagging + end +end diff --git a/lib/labimotion/models/hub_log.rb b/lib/labimotion/models/hub_log.rb new file mode 100644 index 0000000..6ffba0a --- /dev/null +++ b/lib/labimotion/models/hub_log.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Labimotion + class HubLog < ApplicationRecord + self.table_name = :hub_logs + belongs_to :klass, polymorphic: true, optional: true + end +end diff --git a/lib/labimotion/models/segment.rb b/lib/labimotion/models/segment.rb new file mode 100644 index 0000000..b89b739 --- /dev/null +++ b/lib/labimotion/models/segment.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require 'labimotion/models/concerns/generic_revisions' + +module Labimotion + class Segment < ApplicationRecord + acts_as_paranoid + self.table_name = :segments + include GenericRevisions + + belongs_to :segment_klass, class_name: 'Labimotion::SegmentKlass' + belongs_to :element, polymorphic: true, class_name: 'Labimotion::Element' + has_many :segments_revisions, dependent: :destroy, class_name: 'Labimotion::SegmentsRevision' + end +end diff --git a/lib/labimotion/models/segment_klass.rb b/lib/labimotion/models/segment_klass.rb new file mode 100644 index 0000000..a4356f7 --- /dev/null +++ b/lib/labimotion/models/segment_klass.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require 'labimotion/models/concerns/generic_klass_revisions' +require 'labimotion/models/concerns/workflow' + +module Labimotion + class SegmentKlass < ApplicationRecord + self.table_name = :segment_klasses + acts_as_paranoid + include GenericKlassRevisions + include Workflow + belongs_to :element_klass, class_name: 'Labimotion::ElementKlass' + has_many :segments, dependent: :destroy, class_name: 'Labimotion::Segment' + has_many :segment_klasses_revisions, dependent: :destroy, class_name: 'Labimotion::SegmentKlassesRevision' + + 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 +end diff --git a/lib/labimotion/models/segment_klasses_revision.rb b/lib/labimotion/models/segment_klasses_revision.rb new file mode 100644 index 0000000..2dab5bd --- /dev/null +++ b/lib/labimotion/models/segment_klasses_revision.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Labimotion + class SegmentKlassesRevision < ApplicationRecord + acts_as_paranoid + self.table_name = :segment_klasses_revisions + has_one :segment_klass, class_name: 'Labimotion::SegmentKlass' + end +end diff --git a/lib/labimotion/models/segments_revision.rb b/lib/labimotion/models/segments_revision.rb new file mode 100644 index 0000000..c5668b5 --- /dev/null +++ b/lib/labimotion/models/segments_revision.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Labimotion + class SegmentsRevision < ApplicationRecord + acts_as_paranoid + self.table_name = :segments_revisions + has_one :segment, class_name: 'Labimotion::Segment' + end +end diff --git a/lib/labimotion/utils/con_state.rb b/lib/labimotion/utils/con_state.rb new file mode 100644 index 0000000..eafbbaf --- /dev/null +++ b/lib/labimotion/utils/con_state.rb @@ -0,0 +1,13 @@ +module Labimotion + ## Converter State + class ConState + NONE = 0 ## Labimotion::ConState::NONE + WAIT = 1 + NMR = 2 + CONVERTED = 3 + READ = 4 + PROCESSED = 5 + COMPLETED = 6 + ERROR = 9 + end +end diff --git a/lib/labimotion/utils/import_utils.rb b/lib/labimotion/utils/import_utils.rb new file mode 100644 index 0000000..63f8de4 --- /dev/null +++ b/lib/labimotion/utils/import_utils.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Labimotion + ## Import Utils + class ImportUtils + def self.proc_sample(layer, key, data, properties) + field_samples = layer['fields'].select { |ss| ss['type'] == 'drag_sample' } + field_samples.each do |field| + idx = properties['layers'][key]['fields'].index(field) + id = field["value"] && field["value"]["el_id"] unless idx.nil? + + # mol = Molecule.find_or_create_by_molfile(data.fetch('Molecule')[id]['molfile']) unless id.nil? + # unless mol.nil? + # properties['layers'][key]['fields'][idx]['value']['el_id'] = mol.id + # properties['layers'][key]['fields'][idx]['value']['el_tip'] = "#{mol.inchikey}@@#{mol.cano_smiles}" + # properties['layers'][key]['fields'][idx]['value']['el_label'] = mol.iupac_name + # end + end + properties + rescue StandardError => e + Rails.logger.error(e.backtrace) + raise + end + + def self.proc_molecule(layer, key, data, properties) + field_molecules = layer['fields'].select { |ss| ss['type'] == 'drag_molecule' } + field_molecules.each do |field| + idx = properties['layers'][key]['fields'].index(field) + id = field["value"] && field["value"]["el_id"] unless idx.nil? + mol = Molecule.find_or_create_by_molfile(data.fetch('Molecule')[id]['molfile']) unless id.nil? + unless mol.nil? + properties['layers'][key]['fields'][idx]['value']['el_id'] = mol.id + properties['layers'][key]['fields'][idx]['value']['el_tip'] = "#{mol.inchikey}@@#{mol.cano_smiles}" + properties['layers'][key]['fields'][idx]['value']['el_label'] = mol.iupac_name + end + end + properties + rescue StandardError => e + Rails.logger.error(e.backtrace) + raise + end + + def self.proc_table(layer, key, data, properties) + field_tables = layer['fields'].select { |ss| ss['type'] == 'table' } + field_tables&.each do |field| + tidx = layer['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' } + if field_table_molecules.present? + col_ids = field_table_molecules.map { |x| x.values[0] } + col_ids.each do |col_id| + field['sub_values'].each_with_index do |sub_value, vdx| + 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'] + next unless svalue['el_id'].present? && svalue['el_inchikey'].present? + + tmol = Molecule.find_or_create_by_molfile(data.fetch('Molecule')[svalue['el_id']]['molfile']) unless svalue['el_id'].nil? + unless tmol.nil? + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_id'] = tmol.id + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_tip'] = "#{tmol.inchikey}@@#{tmol.cano_smiles}" + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_label'] = tmol.cano_smiles + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_smiles'] = tmol.cano_smiles + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_iupac'] = tmol.iupac_name + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_inchikey'] = tmol.inchikey + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_svg'] = File.join('/images', 'molecules', tmol.molecule_svg_file) + properties['layers'][key]['fields'][tidx]['sub_values'][vdx][col_id]['value']['el_molecular_weight'] = tmol.molecular_weight + end + end + end + end + end + properties + rescue StandardError => e + Rails.logger.error(e.backtrace) + raise + end + + def self.properties_handler(data, properties) + properties && properties['layers'] && properties['layers'].keys&.each do |key| + layer = properties['layers'][key] + properties = proc_molecule(layer, key, data, properties) + properties = proc_table(layer, key, data, properties) + # properties = proc_sample(layer, key, data, properties) + end + properties + rescue StandardError => e + Rails.logger.error(e.backtrace) + raise + end + + def self.create_segment_klass(sk_obj, segment_klass, element_klass, current_user_id) + return if segment_klass.present? || element_klass.nil? || sk_obj.nil? + + segment_klass = Labimotion::SegmentKlass.create!(sk_obj.slice( + 'label', + 'desc', + 'properties_template', + 'is_active', + 'place', + 'properties_release', + 'uuid', + 'identifier', + 'sync_time' + ).merge( + element_klass: element_klass, + created_by: current_user_id, + released_at: DateTime.now + ) + ) + + segment_klass + end + end +end diff --git a/lib/labimotion/utils/search.rb b/lib/labimotion/utils/search.rb new file mode 100644 index 0000000..6209311 --- /dev/null +++ b/lib/labimotion/utils/search.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module Labimotion + class Search + + def self.unique_element_layers(id) + Labimotion::ElementKlass.where(id: id).select("jsonb_object_keys((properties_release->>'layers')::jsonb) as keys").map(&:keys).uniq + end + + def self.unique_segment_layers(id) + Labimotion::SegmentKlass.where(id: id).select("jsonb_object_keys((properties_release->>'layers')::jsonb) as keys").map(&:keys).uniq + end + + def self.elements_search(params, current_user, c_id, dl) + collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id) + element_scope = Labimotion::Element.joins(:collections_elements).where('collections_elements.collection_id = ?', collection.id).joins(:element_klass).where('element_klasses.id = elements.element_klass_id AND element_klasses.name = ?', params[:selection][:genericElName]) + element_scope = element_scope.where('elements.name like (?)', "%#{params[:selection][:searchName]}%") if params[:selection][:searchName].present? + element_scope = element_scope.where('elements.short_label like (?)', "%#{params[:selection][:searchShowLabel]}%") if params[:selection][:searchShowLabel].present? + if params[:selection][:searchProperties].present? + unique_layers = Labimotion::Search.unique_element_layers(params[:selection] && params[:selection][:genericKlassId]) + params[:selection][:searchProperties] && params[:selection][:searchProperties][:layers] && params[:selection][:searchProperties][:layers].keys.each do |lk| + layer = params[:selection][:searchProperties][:layers][lk] + reg_layer = /^#{lk}$|(^#{lk}\.)/ + uni_keys = unique_layers.select { |uni_key| uni_key.match(reg_layer) } + qs = layer[:fields].select { |f| f[:value].present? || f[:type] == 'input-group' } + qs.each do |f| + query_field = {} + if f[:type] == 'input-group' + sfs = f[:sub_fields].map { |e| { "id": e[:id], "value": e[:value] } } + query_field = { "fields": [{ "field": f[:field].to_s, "sub_fields": sfs }] } unless sfs.empty? + elsif %w[checkbox integer system-defined].include? f[:type] + query_field = { "fields": [{ "field": f[:field].to_s, "value": f[:value] }] } + elsif %w[drag_element drag_molecule drag_sample].include? f[:type] + vfs = { "el_label": f[:value] } + query_field = { "fields": [{ "field": f[:field].to_s, "value": vfs }] } unless f[:value].empty? + else + query_field = { "fields": [{ "field": f[:field].to_s, "value": f[:value].to_s }] } + end + next unless query_field.present? + + sqls = [] + uni_keys.select { |uni_key| uni_key.match(reg_layer) }.each do |e| + sql = ActiveRecord::Base.send(:sanitize_sql_array, ["(properties->'layers' @> ?)", { "#{e}": query_field }.to_json]) + sqls = sqls.push(sql) + end + element_scope = element_scope.where(sqls.join(' OR ')) + end + end + end + element_scope + end + + def self.segments_search(ids, type) + eids = ids + seg_scope = Labimotion::Segment.where(element_type: type, element_id: ids) + if params[:selection][:segSearchProperties].present? && params[:selection][:segSearchProperties].length > 0 + params[:selection][:segSearchProperties].each do |segmentSearch| + has_params = false + next unless segmentSearch[:id].present? && segmentSearch[:searchProperties].present? && segmentSearch[:searchProperties][:layers].present? + unique_layers = Labimotion::Search.unique_segment_layers(segmentSearch[:id]) + segmentSearch[:searchProperties] && segmentSearch[:searchProperties][:layers] && segmentSearch[:searchProperties][:layers].keys.each do |lk| + layer = segmentSearch[:searchProperties][:layers][lk] + reg_layer = /^#{lk}$|(^#{lk}\.)/ + uni_keys = unique_layers.select { |uni_key| uni_key.match(reg_layer) } + qs = layer[:fields].select { |f| f[:value].present? || f[:type] == 'input-group' } + qs.each do |f| + query_field = {} + if f[:type] == 'input-group' + sfs = f[:sub_fields].map { |e| { "id": e[:id], "value": e[:value] } } + query_field = { "fields": [{ "field": f[:field].to_s, "sub_fields": sfs }] } unless sfs.empty? + elsif %w[checkbox integer system-defined].include? f[:type] + query_field = { "fields": [{ "field": f[:field].to_s, "value": f[:value] }] } + elsif %w[drag_element drag_molecule drag_sample].include? f[:type] + vfs = { "el_label": f[:value] } + query_field = { "fields": [{ "field": f[:field].to_s, "value": vfs }] } unless f[:value].empty? + else + query_field = { "fields": [{ "field": f[:field].to_s, "value": f[:value].to_s }] } + end + next unless query_field.present? + + has_params = true + + sqls = [] + uni_keys.select { |uni_key| uni_key.match(reg_layer) }.each do |e| + sql = ActiveRecord::Base.send(:sanitize_sql_array, ["(properties->'layers' @> ?)", { "#{e}": query_field }.to_json]) + sqls = sqls.push(sql) + end + seg_scope = seg_scope.where(sqls.join(' OR ')) + end + end + eids = (seg_scope.pluck(:element_id)) & eids if has_params == true + end + end + type.classify.constantize.where(id: eids) + end + + def self.samples_search(c_id = @c_id) + sqls = [] + sps = params[:selection][:searchProperties] + collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id) + element_scope = Sample.joins(:collections_samples).where('collections_samples.collection_id = ?', collection.id) + return element_scope if sps.empty? + + sps[:propCk].keys.each { |k| sqls.push(ActiveRecord::Base.send(:sanitize_sql_array, ["#{k} = (?)", sps[:propCk][k]])) if Sample.column_names.include?(k) } if sps[:propCk].present? + sps[:stereo].keys.each { |k| sqls.push(ActiveRecord::Base.send(:sanitize_sql_array, ['(stereo->> ? = ?)', k, sps[:stereo][k]])) } if sps[:stereo].present? + sps[:propTx].keys.each { |k| sqls = sqls.push(ActiveRecord::Base.send(:sanitize_sql, "#{k} like ('%#{sps[:propTx][k]}%')")) if Sample.column_names.include?(k) } if sps[:propTx].present? + element_scope = element_scope.where(sqls.join(' AND ')) + element_scope + end + + end +end diff --git a/lib/labimotion/utils/serializer.rb b/lib/labimotion/utils/serializer.rb new file mode 100644 index 0000000..0ecba6c --- /dev/null +++ b/lib/labimotion/utils/serializer.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Labimotion + class Serializer + def self.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 + sub_value[col_id]['value']['el_decoupled'] = find_obj.decoupled + end + end + end + field + end + + def self.element_properties(object) + object.properties['layers']&.keys.each do |key| + # layer = object.properties[key] + field_sample_molecules = object.properties['layers'][key]['fields'].select { |ss| %w[drag_sample drag_element drag_element].include?(ss['type']) } + field_sample_molecules.each do |field| + idx = object.properties['layers'][key]['fields'].index(field) + sid = field.dig('value') != '' && field.dig('value', 'el_id') + next unless sid.present? + + case field['type'] + when 'drag_sample' + el = Sample.find_by(id: sid) + when 'drag_molecule' + el = Molecule.find_by(id: sid) + when 'drag_element' + el = Labimotion::Element.find_by(id: sid) + end + 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 %w[drag_sample drag_element].include?(field['type']) + object.properties['layers'][key]['fields'][idx]['value']['el_tip'] = el.short_label if %w[drag_sample].include?(field['type']) + object.properties['layers'][key]['fields'][idx]['value']['el_tip'] = "#{el.element_klass&.label}@@#{el.name}" if %w[drag_element].include?(field['type']) + object.properties['layers'][key]['fields'][idx]['value']['icon_name'] = el.element_klass&.icon_name || '' if %w[drag_element].include?(field['type']) + 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) if %w[drag_sample drag_molecule].include?(field['type']) + object.properties['layers'][key]['fields'][idx]['value']['el_decoupled'] = el.decoupled if %w[drag_sample].include?(field['type']) + 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 + end +end diff --git a/lib/labimotion/utils/utils.rb b/lib/labimotion/utils/utils.rb new file mode 100644 index 0000000..77324b3 --- /dev/null +++ b/lib/labimotion/utils/utils.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'labimotion/version' +module Labimotion + ## Generic Utils + class Utils + def self.klass_by_collection(name) + names = name.split('::') + if names.size == 1 + name[11..] + else + "#{names[0]}::#{names.last[11..]}" + end + rescue StandardError => e + Labimotion.log_exception(e, current_user) + name + end + + def self.elname_by_collection(name) + names = name.split('::') + if names.size == 1 + name[11..].underscore + else + names.last[11..].underscore + end + rescue StandardError => e + Labimotion.log_exception(e) + name.constantize + end + + def self.col_by_element(name) + names = name.split('::') + if names.size == 1 + "collections_#{name.underscore.pluralize}" + else + "collections_#{names.last.underscore.pluralize}" + end + rescue StandardError => e + Labimotion.log_exception(e) + name..underscore.pluralize + end + + def self.element_name(name) + names = name.split('::') + if names.size == 1 + name + else + names.last + end + rescue StandardError => e + Labimotion.log_exception(e) + name + end + + def self.element_name_dc(name) + Labimotion::Utils.element_name(name)&.downcase + end + + def self.next_version(release, current_version) + case release + when 'draft' + current_version + when 'major' + if current_version.nil? || current_version.split('.').length < 2 + '1.0' + else + "#{current_version&.split('.').first.to_i + 1}.0" + end + when 'minor' + if current_version.nil? || current_version&.split('.').length < 2 + '0.1' + else + "#{current_version&.split('.').first.to_i.to_s}.#{current_version&.split('.').last.to_i + 1}" + end + else + current_version + end + rescue StandardError => e + Labimotion.log_exception(e) + current_version + end + + def self.pkg(pkg) + pkg = {} if pkg.nil? + pkg['eln'] = Chemotion::Application.config.version + pkg['labimotion'] = Labimotion::VERSION + pkg + end + end +end diff --git a/lib/labimotion/version.rb b/lib/labimotion/version.rb new file mode 100644 index 0000000..b0ef304 --- /dev/null +++ b/lib/labimotion/version.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +## Labimotion Version +module Labimotion + IS_RAILS5 = false + VERSION_ELN = '1.0.18' + VERSION_REPO = '0.3.1' + + VERSION = Labimotion::VERSION_REPO if Labimotion::IS_RAILS5 == true + VERSION = Labimotion::VERSION_ELN if Labimotion::IS_RAILS5 == false +end