diff --git a/.env.development b/.env.dev.example similarity index 51% rename from .env.development rename to .env.dev.example index 864faef3f..373a8a7dd 100644 --- a/.env.development +++ b/.env.dev.example @@ -1,16 +1,7 @@ -SFTP_HOST=sftp -SFTP_PORT=22 -SFTP_UPLOAD_FOLDER=uploads -SFTP_USER=sftp_test -SFTP_PASSWORD=sftp_test - MESSAGE_ENABLE=false MESSAGE_AUTO_INTERNAL=6000 MESSAGE_IDLE_TIME=12 -# JWT key for novnc target encryption -NOVNC_SECRET='secret' - # Allow unconfirmed email: leave blank for always, or set a number of days (integer); # also set 0 to have email being confirmed before first sign in. DEVISE_ALLOW_UNCONFIRMED='' @@ -22,3 +13,31 @@ DEVISE_DISABLED_SIGN_UP='' # Any new account to be inactive by default => only admin can (de)activate DEVISE_NEW_ACCOUNT_INACTIVE=false +# email of the Repository user +SYS_EMAIL='example@mail.net' + +# id of public collection +PUBLIC_COLL_ID=0 +SCHEME_ONLY_REACTIONS_COLL_ID=0 + +DOI_SYMBOL='DOI_SYMBOL' +DOI_PWD='DOI_PWD' +DOI_PREFIX='10.XXXX' +DOI_DOMAIN='DOI.DOMA.IN' + +PUBCHEM_LOGIN='PUBCHEM_LOGIN' +PUBCHEM_PASSWORD='PUBCHEM_PW' + + +PUBLISH_MODE='staging' + +ARTICLE_PATH='public/newsroom/' + +# user ids of howto editors +# HOWTO_EDITOR='1,2,3' + +# user ids of news editors +# NEWSROOM_EDITOR='1,2,3' + +# user ids of reviewers +# REVIEWERS='1,2,3' diff --git a/.eslintrc b/.eslintrc index bd8b60e9b..66d6863a7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,6 +11,7 @@ "rules": { "no-console": ["off"], "comma-dangle": [1,"only-multiline"], - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }] + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "react/no-multi-comp": [0, { "ignoreStateless": true }] } } diff --git a/.gitignore b/.gitignore index fe9521dce..306dba27f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ .coveralls.yml .env +.env.development +.env.test +.env.production /config/mailcollector.yml /config/datamailcollector.yml @@ -32,12 +35,19 @@ !/config/data_collector_keys/.keep /config/database.yml +/config/repository_database.yml /config/storage.yml /config/spectra.yml /config/editors.yml /node_modules +/public/newsroom/* +!/public/newsroom/.keep +/public/howto/* +!/public/howto/.keep + +!/public/images/molecules/.keep /public/images/molecules/* !/public/images/molecules/.keep @@ -68,6 +78,7 @@ /public/images/* !/public/images/wild_card/ !/public/images/ghs/ +!/public/images/creative_common/ /public/ontologies/* !/public/ontologies/.keep @@ -77,6 +88,10 @@ !/public/ontologies/rxno.default.json !/public/ontologies/rxno.default.edited.json +/public/directives/* +!/public/images/directives/.keep +!/public/images/directives/directives.html + /uploads/* !/public/attachments/.keep diff --git a/Gemfile b/Gemfile index 806d03831..fa8bcd37d 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,8 @@ gem 'bibtex-ruby' # state machine gem 'aasm' +gem 'bootsnap', require: false + group :development do gem 'sdoc', '~> 0.4.0', group: :doc @@ -91,6 +93,7 @@ gem 'kaminari-grape' gem "rdkit_chem", git: "https://github.com/CamAnNguyen/rdkit_chem" gem 'api-pagination' +gem 'rack-cors' gem 'pundit' @@ -150,8 +153,10 @@ gem 'coveralls', require: false # to compile from github/openbabel/openbabel master # gem 'openbabel', '2.4.1.2', git: 'https://github.com/ComPlat/openbabel-gem' # to compile from github/openbabel/openbabel branch openbabel-2-4-x +# gem 'openbabel', '2.4.90.1', git: 'https://github.com/ComPlat/openbabel-gem' gem 'openbabel', '2.4.90.3', git: 'https://github.com/ComPlat/openbabel-gem.git', branch: 'hot-fix-svg' + gem 'barby' gem 'prawn' gem 'prawn-svg' @@ -163,13 +168,14 @@ gem 'swot', git: 'https://github.com/leereilly/swot.git', branch: 'master', ref: 'bfe392b4cd52f62fbc1d83156020275719783dd1' # gem 'gman', '~> 7.0.3' gem 'activejob-status' +gem 'moneta' group :development, :test do gem 'binding_of_caller' gem 'annotate' - gem 'mailcatcher', '0.7.1' + # gem 'mailcatcher', '0.7.1' # Call 'byebug' anywhere in the code to stop execution # and get a debugger console @@ -230,4 +236,6 @@ if File.exists?(eln_plugin) eval_gemfile eln_plugin end +#gem 'reposit', git: 'git@git.scc.kit.edu:complat/reposit.git' + #### diff --git a/Gemfile.lock b/Gemfile.lock index 93434ae4c..d0d2fc360 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM api-pagination (4.8.2) arel (6.0.4) ast (2.4.0) - autoprefixer-rails (9.6.4) + autoprefixer-rails (9.7.6) execjs awesome_print (1.8.0) axiom-types (0.1.1) @@ -164,10 +164,12 @@ GEM coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) - bibtex-ruby (5.1.2) + bibtex-ruby (5.1.3) latex-decode (~> 0.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) + bootsnap (1.3.2) + msgpack (~> 1.0) bootstrap-kaminari-views (0.0.5) kaminari (>= 0.13) rails (>= 3.1) @@ -239,7 +241,7 @@ GEM css_parser (1.7.0) addressable daemons (1.3.1) - database_cleaner (1.7.0) + database_cleaner (1.8.4) debug_inspector (0.0.3) delayed_cron_job (0.7.2) delayed_job (>= 4.1) @@ -369,23 +371,14 @@ GEM latex-decode (0.3.1) launchy (2.4.3) addressable (~> 2.3) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - loofah (2.4.0) + listen (3.2.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.5.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) - mailcatcher (0.7.1) - eventmachine (= 1.0.9.1) - mail (~> 2.3) - rack (~> 1.5) - sinatra (~> 1.2) - skinny (~> 0.2.3) - sqlite3 (~> 1.3) - thin (~> 1.5.0) memoist (0.16.0) memory_profiler (0.9.13) meta_request (0.7.0) @@ -399,6 +392,8 @@ GEM mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.12.2) + moneta (1.0.0) + msgpack (1.2.6) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.1.0) @@ -427,7 +422,7 @@ GEM parallel (1.17.0) paranoia (2.4.2) activerecord (>= 4.0, < 6.1) - parser (2.6.4.1) + parser (2.7.1.0) ast (~> 2.4.0) pdf-core (0.7.0) pg (0.20.0) @@ -448,8 +443,7 @@ GEM rack (>= 0.4) rack-contrib (1.8.0) rack (~> 1.4) - rack-protection (1.5.5) - rack + rack-cors (1.0.2) rack-test (0.6.3) rack (>= 1.0) rails (4.2.11.1) @@ -546,7 +540,7 @@ GEM safe_yaml (1.0.5) sassc (2.2.1) ffi (~> 1.9) - sassc-rails (2.1.1) + sassc-rails (2.1.2) railties (>= 4.0.0) sassc (>= 2.0) sprockets (> 3.0) @@ -568,14 +562,7 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sinatra (1.4.8) - rack (~> 1.5) - rack-protection (~> 1.4) - tilt (>= 1.3, < 3) sixarm_ruby_unaccent (1.2.0) - skinny (0.2.4) - eventmachine (~> 1.0.0) - thin (>= 1.5, < 1.7) slackistrano (3.8.4) capistrano (>= 3.8.1) spring (2.0.2) @@ -588,7 +575,6 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sqlite3 (1.4.1) sshkit (1.18.2) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) @@ -610,7 +596,7 @@ GEM parallel (~> 1.0) railties (>= 4) sprockets (~> 3.0) - tzinfo (1.2.5) + tzinfo (1.2.7) thread_safe (~> 0.1) uglifier (4.1.20) execjs (>= 0.3.0, < 3) @@ -663,6 +649,7 @@ DEPENDENCIES better_errors bibtex-ruby binding_of_caller + bootsnap bootstrap-sass (~> 3.4.1) browserify-rails (~> 4.2.0) bullet @@ -710,9 +697,9 @@ DEPENDENCIES kaminari-grape ketcherails (~> 0.1.6)! launchy (~> 2.4.3) - mailcatcher (= 0.7.1) memory_profiler meta_request + moneta net-sftp net-ssh nokogiri @@ -724,6 +711,7 @@ DEPENDENCIES prawn prawn-svg pundit + rack-cors rack-mini-profiler! rails (= 4.2.11.1) rdkit_chem! diff --git a/README.md b/README.md index 601d4f852..c030b80c6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Chemotion Electronic Lab Notebook +# Chemotion REPOSITORY -An ELN for chemists! +A repository for chemists based on chemotion ELN ! ## Funding @@ -9,7 +9,7 @@ This project has been funded by the ![DFG](http://www.dfg.de/includes/images/df ## License -Chemotion_ELN: an Electronic Lab Notebook for Chemists. +Chemotion_REPOSITORY for Chemists. Copyright (C) 2015-current Nicole Jung (nicole.jung(at)kit.edu) of the Karlsruhe Institute of Technology. @@ -33,15 +33,4 @@ Copyright (C) 2015-current Nicole Jung (nicole.jung(at)kit.edu) of the Karlsruh see [INSTALL.md][INSTALL] -## Code Status - -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1054134.svg)](https://doi.org/10.5281/zenodo.1054134) - -[![Build Status](https://travis-ci.org/ComPlat/chemotion_ELN.svg?branch=master)](https://travis-ci.org/ComPlat/chemotion_ELN) - -[![Coverage Status](https://coveralls.io/repos/github/ComPlat/chemotion_ELN/badge.svg)](https://coveralls.io/github/ComPlat/chemotion_ELN) - - - - [INSTALL]: INSTALL.md diff --git a/app/api/api.rb b/app/api/api.rb index 03b6fbad3..db6b63059 100644 --- a/app/api/api.rb +++ b/app/api/api.rb @@ -27,7 +27,9 @@ def is_public_request? '/api/v1/chemspectra/', '/api/v1/ketcher/layout', '/api/v1/gate/receiving', - '/api/v1/gate/ping' + '/api/v1/gate/ping', + '/api/v1/search/', + '/api/v1/suggestion' ) end @@ -125,4 +127,6 @@ def to_json_camel_case(val) mount Chemotion::EditorAPI mount Chemotion::UiAPI mount Chemotion::OlsTermsAPI + mount Chemotion::RepositoryAPI + mount Chemotion::ArticleAPI end diff --git a/app/api/chemotion/article_api.rb b/app/api/chemotion/article_api.rb new file mode 100644 index 000000000..73f78864d --- /dev/null +++ b/app/api/chemotion/article_api.rb @@ -0,0 +1,187 @@ +require 'moneta' + +module Chemotion + class ArticleAPI < Grape::API + resource :articles do + + helpers do + def resize_image(file, tmp_path, no_resize = false) + image = Magick::Image.read(file[:tempfile].path).first + image = image.resize_to_fit(400, 268) unless no_resize + image.format = 'png' + + FileUtils.mkdir_p(tmp_path) + timg = Tempfile.new('image_', tmp_path) + timg.binmode + timg.write(image.to_blob) + timg.flush + { cover_image: File.basename(timg.path) || '' } + end + + def store_image(params) + sourcepath = params[:public_path] + params[:pfad] + targetpath = params[:public_path] + params[:key] + '_' + params[:pfad] + FileUtils.cp(sourcepath, targetpath) if File.exist?(sourcepath) + end + + def create_or_update_file(params) + key = params[:key] + public_path = params[:public_path] + FileUtils.mkdir_p(public_path) + store = Moneta.build do + use :Transformer, key: [:json], value: [:json] + adapter :File, dir: public_path + end + store_idx = store.key?('index.json') ? store['index.json'] : [] + updated_file = (store_idx&.length > 0 && store_idx&.select{ |a| a['key'] == key }) || [] + raise '401 Unauthorized' if updated_file&.length > 0 && updated_file[0]['creator_id'] != current_user.id + created_at = updated_file&.length > 0 ? updated_file[0]['created_at'] : Time.now + published_at = params[:published_at].blank? ? created_at : DateTime.parse(params[:published_at]).to_time + updated_at = params[:updated_at].blank? ? published_at : DateTime.parse(params[:updated_at]).to_time + key = updated_file&.length > 0 ? updated_file[0]['key'] : SecureRandom.uuid + store_idx.delete_if { |a| a['key'] == key } + filename = key + '_cover.png' + filepath = public_path + filename + cover_image = params[:cover_image] + if cover_image.present? && cover_image != filename + sourcepath = public_path + cover_image + FileUtils.cp(sourcepath, filepath) if File.exist? sourcepath + end + params[:article].each do |stelle| + next unless stelle['pfad'].present? && (!stelle['pfad'].include?(key)) + store_image({public_path: public_path, key: key, pfad: stelle['pfad']}) + stelle['pfad'] = key + '_' + stelle['pfad'] + end + header = { + key: key, + title: params[:title], + cover_image: cover_image.present? ? filename : '', + creator_name: current_user.name, + creator_id: current_user.id, + created_at: created_at, + firstParagraph: params[:firstParagraph], + published_at: published_at, + updated_at: updated_at + } + store_idx.unshift(header) + store['index.json'] = store_idx + filestore = { + content: params[:content], + title: params[:title], + cover_image: cover_image.present? ? filename : '', + creator_name: current_user.name, + creator_id: current_user.id, + created_at: created_at, + firstParagraph: params[:firstParagraph], + published_at: published_at, + updated_at: updated_at, + article: params[:article], + } + store[key] = filestore + filestore + rescue StandardError => e + puts e + error!('401 Unauthorized. Please contact the author or administrator.', 401) + ensure + store&.close + end + + def delete_file(key, public_path) + FileUtils.mkdir_p(public_path) + store = Moneta.build do + use :Transformer, key: [:json], value: [:json] + adapter :File, dir: public_path + end + store_idx = store.key?('index.json') ? store['index.json'] : [] + error!('404 Not Found', 404) unless store.key?('index.json') + updated_file = (store_idx&.length > 0 && store_idx&.select{ |a| a['key'] == key }) || [] + raise '401 Unauthorized' if updated_file&.length > 0 && updated_file[0]['creator_id'] != current_user.id + + store_idx.delete_if { |a| a['key'] == key } + store['index.json'] = store_idx + FileUtils.rm_r(Dir.glob(public_path + key + '*'), force: true) + key + rescue StandardError => e + puts e + error!('401 Unauthorized. Please contact the author or administrator.', 401) + ensure + store&.close + end + end + + desc 'Create or Update a news' + params do + optional :key, type: String, desc: 'key' + requires :title, type: String, desc: 'title' + optional :cover_image, type: String, desc: 'cover_image URL' + optional :content, type: Hash do + optional :ops, type: Array[Hash] + end + optional :firstParagraph, type: String, desc: 'first paragraph of content' + optional :published_at, type: String, desc: 'published date' + optional :updated_at, type: String, desc: 'updated date' + optional :article, type: Array, desc: 'full of content' + end + + post 'create_or_update' do + error!('401 Unauthorized', 401) unless current_user&.is_article_editor + public_path = File.join((ENV['ARTICLE_PATH'] || 'public/newsroom/')) + create_or_update_file(params.deep_merge(public_path: public_path)) + end + + desc 'Create or Update a howto' + params do + optional :key, type: String, desc: 'howto key' + requires :title, type: String, desc: 'title' + optional :cover_image, type: String, desc: 'cover_image URL' + optional :content, type: Hash do + optional :ops, type: Array[Hash] + end + optional :firstParagraph, type: String, desc: 'first paragraph of content' + optional :published_at, type: String, desc: 'published date' + optional :updated_at, type: String, desc: 'updated date' + optional :article, type: Array, desc: 'full of content' + end + post :create_or_update_howto do + error!('401 Unauthorized', 401) unless current_user&.is_howto_editor + public_path = File.join((ENV['HOWTO_PATH'] || 'public/howto/')) + create_or_update_file(params.deep_merge(public_path: public_path)) + end + + desc 'Delete a howto' + params do + requires :key, type: String, desc: 'howto key' + end + before do + error!('401 Unauthorized', 401) unless current_user&.is_howto_editor + end + post 'delete_howto' do + delete_file(params[:key], File.join((ENV['HOWTO_PATH'] || 'public/howto/'))) + end + + desc 'Delete a news' + before do + error!('401 Unauthorized', 401) unless current_user&.is_article_editor + end + delete ':key' do + delete_file(params[:key], File.join((ENV['ARTICLE_PATH'] || 'public/newsroom/'))) + end + + desc 'Image section of Editor' + params do + requires :file, type: Array, desc: 'image file' + requires :editor_type, type: String, desc: 'howto editor or newsroom editor' + end + post 'editor_image' do + p_path = 'public/' + params[:editor_type] + '/' + e_path = ENV[params[:editor_type].upcase + '_PATH'] + if params[:file] + img = resize_image(params[:file][0], File.join((e_path || p_path)), true) + { pfad_image: img[:cover_image], cover_image: img[:cover_image] } + else + { pfad_image: '', cover_image: '' } + end + end + end + end +end diff --git a/app/api/chemotion/collection_api.rb b/app/api/chemotion/collection_api.rb index db4aec432..471ae7f4e 100644 --- a/app/api/chemotion/collection_api.rb +++ b/app/api/chemotion/collection_api.rb @@ -39,8 +39,12 @@ class CollectionAPI < Grape::API desc "Return all locked and unshared serialized collection roots of current user" get :locked do - current_user.collections.includes(:shared_users) + if (current_user.type == 'Anonymous') + [] + else + current_user.collections.includes(:shared_users) .locked.unshared.roots.order('label ASC') + end end get_child = Proc.new do |children, collects| diff --git a/app/api/chemotion/element_api.rb b/app/api/chemotion/element_api.rb index 9a43bb30a..7687329b1 100644 --- a/app/api/chemotion/element_api.rb +++ b/app/api/chemotion/element_api.rb @@ -85,18 +85,32 @@ class ElementAPI < Grape::API .where(collections: { id: @collection.id }, reactions_samples: { reaction_id: deleted['reaction'] }) .destroy_all.map(&:id) + sql_pub = "(element_id in (?) and element_type = 'Sample') or (element_id in (?) and element_type = 'Reaction')" + Publication.where(sql_pub, deleted['sample'], deleted['reaction']) + .map(&:root).uniq.each do |e| + e.update_state(Publication::STATE_DECLINED) + e.proces_element(Publication::STATE_DECLINED) + e.inform_users(Publication::STATE_DECLINED, current_user.id) + end { selecteds: params[:selecteds].select { |sel| !deleted.fetch(sel['type'], []).include?(sel['id']) } } end desc "return selected elements from the list. (only samples an reactions)" post do + selected = { 'samples' => [], 'reactions' => [] } - %w[sample reaction].each do |element| - next unless params[element][:checkedAll] || params[element][:checkedIds].present? - selected[element + 's'] = @collection.send(element + 's').by_ui_state(params[element]).map do |e| - ElementPermissionProxy.new(current_user, e, user_ids).serialized - end + + @collection_ids = [@collection.id] + Collection.joins(:sync_collections_users) + .where('sync_collections_users.collection_id = collections.id and sync_collections_users.user_id = ?', current_user).references(:collections)&.pluck(:id) + + selected['samples'] = Sample.joins(:collections_samples).where('collections_samples.collection_id in (?)',@collection_ids).by_ui_state(params['sample']).distinct.map do |e| + ElementPermissionProxy.new(current_user, e, user_ids).serialized + end + + selected['reactions'] = Reaction.joins(:collections_reactions).where('collections_reactions.collection_id in (?)',@collection_ids).by_ui_state(params['reaction']).distinct.map do |e| + ElementPermissionProxy.new(current_user, e, user_ids).serialized end + # TODO: fallback if sample are not in owned collection and currentCollection is missing # (case when cloning report) selected diff --git a/app/api/chemotion/gate_api.rb b/app/api/chemotion/gate_api.rb index 99c651242..0499e8f36 100644 --- a/app/api/chemotion/gate_api.rb +++ b/app/api/chemotion/gate_api.rb @@ -5,6 +5,7 @@ class UriHTTPType def self.parse(value) URI.parse value end + def self.parsed?(value) value.is_a? URI::HTTP end @@ -252,6 +253,41 @@ def self.parsed?(value) { jwt: token } end end + + namespace :register_eln do + params do + requires :origin, type: UriHTTPType, desc: 'remote eln adress' + end + + after_validation do + error!('401 Unauthorized - no ELN Gate collection', 401) unless (@collec = Collection.find_by( + user_id: current_user.id, is_locked: true, label: 'ELN Gate' + )) + end + + post do + origin = URI.join(params[:origin], '/').to_s + payload = { + collection: @collec.id, + # label: @collec.label[0..20], + iss: current_user.email, + exp: (Time.now + 28.days).to_i, + origin: origin + } + secret = Rails.application.secrets.secret_key_base + token = JWT.encode payload, secret + AuthenticationKey.create!( + user_id: current_user.id, + fqdn: origin, + role: 'gate in', + token: token + ) + # TODO: add a boolean on collection to allow AuthenticationKey + # or use sync_collections_users ?? + redirect(URI.join(origin, "/api/v1/gate/register_repo?token=#{token}").to_s) + end + end + end end end diff --git a/app/api/chemotion/literature_api.rb b/app/api/chemotion/literature_api.rb index fddc1cafc..e79716a33 100644 --- a/app/api/chemotion/literature_api.rb +++ b/app/api/chemotion/literature_api.rb @@ -13,6 +13,8 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = resource :literatures do after_validation do + @is_owned = nil + @is_public = nil unless request.url =~ /doi\/metadata|ui_state|collection/ @element_klass = params[:element_type].classify @element = @element_klass.constantize.find_by(id: params[:element_id]) @@ -22,18 +24,40 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = else @element_policy.update? end - error!('401 Unauthorized', 401) unless allowed + + @is_public = "Collections#{params[:element_type].classify}".constantize.where( + "#{params[:element_type]}_id = ? and collection_id in (?)", + params[:element_id], + [Collection.public_collection_id, Collection.scheme_only_reactions_collection.id] + ).presence + error!('401 Unauthorized', 401) unless allowed || @is_public + @cat = @is_public ? 'public' : 'detail' end end + + + desc "Return the literature list for the given element" params do requires :element_id, type: Integer requires :element_type, type: String, values: %w[sample reaction research_plan] + optional :is_all, type: Boolean, default: false end get do - { literatures: citation_for_elements } + if (params[:is_all] && params[:is_all] == true && params[:element_type] == 'reaction') + literatures = citation_for_elements(params[:element_id], @element_klass, @cat) || [] + reaction = Reaction.find(params[:element_id]) + reaction.products.each do |p| + literatures = literatures + citation_for_elements(p.id, 'Sample', @cat) + end + { literatures: literatures } + else + { literatures: citation_for_elements(params[:element_id], @element_klass, @cat) } + end + # literatures = Literature.by_element_attributes_and_cat(params[:element_id], @element_klass, %w[detail public]) + # { literatures: literatures } end desc 'create a literature entry' @@ -70,14 +94,14 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = user_id: current_user.id, element_type: @element_klass, element_id: params[:element_id], - category: 'detail' + category: @cat } unless Literal.find_by(attributes) Literal.create(attributes) @element.touch end - { literatures: citation_for_elements } + { literatures: citation_for_elements(params[:element_id], @element_klass, @cat) } end params do @@ -92,7 +116,7 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = # user_id: current_user.id, element_type: @element_klass, element_id: params[:element_id], - category: 'detail' + category: @cat )&.destroy! end @@ -105,9 +129,21 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = after_validation do set_var(params[:id], params[:is_sync_to_me]) error!(404) unless @c + if !@is_owned + obj = fetch_collection_w_current_user(params[:id], params[:is_sync_to_me]) + @is_public = obj['shared_by'] && obj['shared_by']['initials'] == 'CI' + end end get do + if @is_public + return { + collectionRefs: Literature.none, + sampleRefs: Literature.by_element_attributes_and_cat(sample_ids, 'Sample', 'public').group('literatures.id'), + reactionRefs: Literature.by_element_attributes_and_cat(reaction_ids, 'Reaction', 'public').group('literatures.id'), + researchPlanRefs: Literature.none, + } + end sample_ids = @dl_s > 1 ? @c.sample_ids : [] reaction_ids = @dl_r > 1 ? @c.reaction_ids : [] research_plan_ids = @dl_rp > 1 ? @c.research_plan_ids : [] @@ -148,10 +184,15 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = @sids = @dl_s > 1 ? @c.samples.by_ui_state(declared(params)[:sample]).pluck(:id) : [] @rids = @dl_r > 1 ? @c.reactions.by_ui_state(declared(params)[:reaction]).pluck(:id) : [] @cat = "detail" + if !@is_owned + obj = fetch_collection_w_current_user(params[:id], params[:is_sync_to_me]) + @is_public = obj['shared_by_id'] && obj['shared_by_id'] == User.chemotion_user.id + end end post do - if params[:ref] && @pl >= 1 + @cat = @is_public ? 'public' : 'detail' + if params[:ref] && (@pl >= 1 || @is_public) lit = if params[:ref][:is_new] Literature.find_or_create_by( doi: params[:ref][:doi], @@ -171,7 +212,7 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = user_id: current_user.id, element_type: type, element_id: id, - category: 'detail' + category: @cat ) end end diff --git a/app/api/chemotion/public_api.rb b/app/api/chemotion/public_api.rb index cb7c49475..620ff197f 100644 --- a/app/api/chemotion/public_api.rb +++ b/app/api/chemotion/public_api.rb @@ -1,5 +1,8 @@ +require 'open-uri' + module Chemotion class PublicAPI < Grape::API + include Grape::Kaminari helpers do def send_notification(attachment, user, status, has_error = false) data_args = { 'filename': attachment.filename, 'comment': 'the file has been updated' } @@ -26,6 +29,74 @@ def send_notification(attachment, user, status, has_error = false) status 204 end + namespace :search do + params do + requires :inchikey, type: String, desc: 'inchikey' + end + post do + molecule = Molecule.joins("inner join samples on molecules.id = samples.molecule_id and samples.deleted_at is null") + .joins("inner join publications on samples.id = publications.element_id and publications.state like '%completed%' and publications.element_type = 'Sample'") + .find_by(inchikey: params[:inchikey]) + { molecule_id: molecule&.id } + end + end + + namespace :article_init do + get do + { is_article_editor: current_user&.is_article_editor || false } + end + end + + namespace :howto_init do + get do + { is_howto_editor: current_user&.is_howto_editor || false } + end + end + + namespace :find_adv_valuess do + helpers do + def query_authors(name) + result = User.where(type: %w(Person Group Collaborator)).where( + <<~SQL + users.id in ( + select distinct(pa.author_id)::integer from publication_authors pa + ) + SQL + ) + .by_name(params[:name]).limit(3) + .select( + <<~SQL + id as key, first_name, last_name, first_name || chr(32) || last_name as name, first_name || chr(32) || last_name || chr(32) || '(' || name_abbreviation || ')' as label + SQL + ) + end + def query_ontologies(name) + result = PublicationOntologies.where('LOWER(ontologies) ILIKE ? ',"%#{params[:name]}%").limit(3) + .select( + <<~SQL + term_id as key, label, label as name + SQL + ).uniq + end + end + desc 'Find top 3 matched advanced values' + params do + requires :name, type: String, allow_blank: false, regexp: /^[a-zA-Z]+(([',. -][a-zA-Z ])?[a-zA-Z]*)*$/ + requires :adv_type, type: String, allow_blank: false, desc: 'Type', values: %w[Authors Ontologies] + end + get do + result = case params[:adv_type] + when 'Authors' + query_authors(params[:name]) + when 'Ontologies' + query_ontologies(params[:name]) + else + [] + end + { result: result } + end + end + namespace :download do desc 'download file for editoring' before do @@ -60,7 +131,7 @@ def send_notification(attachment, user, status, has_error = false) before do error!('401 Unauthorized', 401) if params[:key].nil? payload = JWT.decode(params[:key], Rails.application.secrets.secret_key_base) unless params[:key].nil? - error!('401 Unauthorized', 401) if payload&.length == 0 + error!('401 Unauthorized', 401) unless payload.present? @status = params[:status].is_a?(Integer) ? params[:status] : 0 if @status > 1 @@ -85,7 +156,6 @@ def send_notification(attachment, user, status, has_error = false) end post do - # begin case @status when 1 @@ -150,18 +220,18 @@ def send_notification(attachment, user, status, has_error = false) desc "Return all current organizations" get "organizations" do - Affiliation.pluck("DISTINCT organization") + Affiliation.where.not(organization: ENV['BLIST_ORGANIZATIONS']).pluck("DISTINCT organization") end desc "Return all current departments" get "departments" do - Affiliation.pluck("DISTINCT department") + Affiliation.where.not(department: ENV['BLIST_DEPARTMENTS']).pluck("DISTINCT trim(department)") end - desc "Return all current groups" - get "groups" do - Affiliation.pluck("DISTINCT affiliations.group") - end + # desc "Return all current groups" + # get "groups" do + # Affiliation.pluck("DISTINCT affiliations.group") + # end desc "return organization's name from email domain" get "swot" do @@ -170,6 +240,399 @@ def send_notification(attachment, user, status, has_error = false) Affiliation.where(domain: params[:domain]).where.not(organization: nil).first&.organization end end + + get 'collection' do + pub_coll = Collection.public_collection + if current_user + coll = SyncCollectionsUser.find_by(user_id: current_user.id, collection_id: pub_coll.id) + { id: coll.id, is_sync_to_me: true } + else + { id: nil } + end + end + + resource :pid do + params do + requires :id, type: Integer + end + desc "Query samples, reaction and datasets from publication id" + post do + pub = Publication.find(params[:id]) + return "/home" unless pub + + case pub.element_type + when 'Sample' + return "/molecules/#{pub.element.molecule_id}" if pub.state&.match(Regexp.union(%w[completed])) + return "/review/review_sample/#{pub.element_id}" if %w[pending reviewed accepted].include?(pub.state) && pub.ancestry.nil? + if %w[pending reviewed accepted].include?(pub.state) && !pub.ancestry.nil? + root = pub.root + return "/review/review_reaction/#{root.element_id}" if root && %w[pending reviewed accepted].include?(root.state) + end + when 'Reaction' + return "/reactions/#{pub.element_id}" if pub.state&.match(Regexp.union(%w[completed])) + return "/review/review_reaction/#{pub.element_id}" if %w[pending reviewed accepted].include?(pub.state) + when 'Container' + return "/datasets/#{pub.element_id}" if pub.state&.match(Regexp.union(%w[completed])) + if %w[pending reviewed accepted].include?(pub.state) && !pub.ancestry.nil? + root = pub.root + return "/review/review_#{root.element_type=='Reaction'? 'reaction' : 'sample'}/#{root.element_id}" if root && %w[pending reviewed accepted].include?(root.state) + end + else + return "/home" + end + end + end + + resource :inchikey do + params do + requires :inchikey, type: String + optional :type, type: String # value: [] + optional :version, type: String + end + desc "Query samples and datasets from inchikey and type" + post do + inchikey = params[:inchikey] + molecule = Molecule.find_by(inchikey: inchikey) + return "/home" unless molecule + + type = params[:type] + return "/molecules/#{molecule.id.to_s}" if type.empty? + + version = params[:version] ? params[:version] : "" + analyses = Collection.public_collection.samples + .where("samples.molecule_id = ?", molecule.id.to_s) + .map(&:analyses).flatten + + analyses_filtered = analyses.select { |a| + em = a.extended_metadata + check = em['kind'].to_s.gsub(/\s/, '') == type + check = check && (em['analysis_version'] || '1') == version unless version.empty? + check + } + analysis = analyses_filtered.first + return "/datasets/#{analysis.id.to_s}" + end + end + + resource :molecules do + desc "Return PUBLIC serialized molecules" + params do + optional :page, type: Integer, desc: "page" + optional :pages, type: Integer, desc: "pages" + optional :per_page, type: Integer, desc: "per page" + optional :adv_flag, type: Boolean, desc: 'advanced search?' + optional :adv_type, type: String, desc: 'advanced search type', values: %w[Authors Ontologies] + optional :adv_val, type: Array[String], desc: 'advanced search value', regexp: /^(\d+|([[:alpha:]]+:\d+))$/ + end + paginate per_page: 10, offset: 0, max_per_page: 100 + get '/', each_serializer: MoleculeGuestListSerializer do + public_collection_id = Collection.public_collection_id + params[:adv_val] + adv_search = ' ' + if params[:adv_flag] == true && params[:adv_type].present? && params[:adv_val].present? + case params[:adv_type] + when 'Authors' + adv_search = <<~SQL + INNER JOIN publication_authors pub on pub.element_id = samples.id and pub.element_type = 'Sample' and pub.state = 'completed' + and author_id in ('#{params[:adv_val].join("','")}') + SQL + when 'Ontologies' + adv_search = <<~SQL + INNER JOIN publication_ontologies pub on pub.element_id = samples.id and pub.element_type = 'Sample' + and term_id in ('#{params[:adv_val].join("','")}') + SQL + end + end + sample_join = <<~SQL + INNER JOIN ( + SELECT molecule_id, published_at max_published_at, sample_svg_file + FROM ( + SELECT samples.*, pub.published_at, rank() OVER (PARTITION BY molecule_id order by pub.published_at desc) as rownum + FROM samples, publications pub + WHERE pub.element_type='Sample' and pub.element_id=samples.id and pub.deleted_at ISNULL + and samples.id IN ( + SELECT samples.id FROM samples + INNER JOIN collections_samples cs on cs.collection_id = #{public_collection_id} and cs.sample_id = samples.id and cs.deleted_at ISNULL + #{adv_search} + )) s where rownum = 1 + ) s on s.molecule_id = molecules.id + SQL + + paginate(Molecule.joins(sample_join).order("s.max_published_at desc").select( + <<~SQL + molecules.*, sample_svg_file + SQL + )) + end + end + + resource :reactions do + desc 'Return PUBLIC serialized reactions' + params do + optional :page, type: Integer, desc: 'page' + optional :pages, type: Integer, desc: 'pages' + optional :per_page, type: Integer, desc: 'per page' + optional :adv_flag, type: Boolean, desc: 'is it advanced search?' + optional :adv_type, type: String, desc: 'advanced search type', values: %w[Authors Ontologies] + optional :adv_val, type: Array[String], desc: 'advanced search value', regexp: /^(\d+|([[:alpha:]]+:\d+))$/ + optional :scheme_only, type: Boolean, desc: 'is it a scheme-only reaction?', default: false + end + paginate per_page: 10, offset: 0, max_per_page: 100 + get '/', each_serializer: ReactionGuestListSerializer do + + if params[:adv_flag] === true && params[:adv_type].present? && params[:adv_val].present? + case params[:adv_type] + when 'Authors' + adv_search = <<~SQL + INNER JOIN publication_authors pub on pub.element_id = reactions.id and pub.element_type = 'Reaction' and pub.state = 'completed' + and author_id in ('#{params[:adv_val].join("','")}') + SQL + when 'Ontologies' + str_term_id = params[:adv_val].split(',').map { |val| val}.to_s + adv_search = <<~SQL + INNER JOIN publication_ontologies pub on pub.element_id = reactions.id and pub.element_type = 'Reaction' + and term_id in ('#{params[:adv_val].join("','")}') + SQL + else + adv_search = ' ' + end + else + adv_search = ' ' + end + + if params[:scheme_only] + paginate(Collection.scheme_only_reactions_collection.reactions.joins(adv_search).joins(:publication).includes(:publication).references(:publication).order('publications.published_at desc').uniq) + else + paginate(Collection.public_collection.reactions.joins(adv_search).joins(:publication).includes(:publication).references(:publication).order('publications.published_at desc').uniq) + end + end + end + + resource :publicElement do + desc "Return PUBLIC serialized elements (Reaction, sample)" + paginate per_page: 10, offset: 0, max_per_page: 100 + get '/', each_serializer: MoleculeGuestListSerializer do + public_collection_id = Collection.public_collection_id + sample_join = <<~SQL + INNER JOIN ( + SELECT molecule_id, max(pub.published_at) max_updated_at + FROM samples + INNER JOIN collections_samples cs on cs.collection_id = #{public_collection_id} and cs.sample_id = samples.id and cs.deleted_at ISNULL + INNER JOIN publications pub on pub.element_type='Sample' and pub.element_id=samples.id and pub.deleted_at ISNULL + GROUP BY samples.molecule_id + ) s on s.molecule_id = molecules.id + SQL + paginate(Molecule.joins(sample_join).order("s.max_updated_at desc")) + end + end + + resource :last_published do + desc "Return Last PUBLIC serialized entities" + get do + s_pub = Publication.where(element_type: 'Sample', state: 'completed').order(:published_at).last + sample = s_pub.element + + r_pub = Publication.where(element_type: 'Reaction', state: 'completed').order(:published_at).last + reaction = r_pub.element + + { last_published: { sample: { id: sample.id, sample_svg_file: sample.sample_svg_file, molecule: sample.molecule, tag: s_pub.taggable_data, contributor: User.find(s_pub.published_by).name }, + reaction: { id: reaction.id, reaction_svg_file: reaction.reaction_svg_file, tag: r_pub.taggable_data, contributor: User.find(r_pub.published_by).name } } } + end + end + + resource :last_published_sample do + desc "Return PUBLIC serialized molecules" + get do + sample = Collection.public_collection.samples.includes(:molecule, :residues). + where("samples.id not in (select reactions_samples.sample_id from reactions_samples where type != 'ReactionsProductSample')").order(:created_at).last + #TODO have and use a dedicated serializer for public sample + sample + end + end + + resource :dataset do + desc "Return PUBLISHED serialized dataset" + params do + requires :id, type: Integer, desc: "Dataset id" + end + get do + dataset = Container.find(params[:id]) + sample = dataset.root.containable + cids = sample.collections.pluck :id + if cids.include?(Collection.public_collection_id) + molecule = sample.molecule + + ds_json = ContainerSerializer.new(dataset).serializable_hash.deep_symbolize_keys + ds_json[:dataset_doi] = dataset.full_doi + ds_json[:pub_id] = dataset.publication&.id + + res = { + dataset: ds_json, + sample_svg_file: sample.sample_svg_file, + molecule: { + sum_formular: molecule.sum_formular, + molecular_weight: molecule.molecular_weight, + cano_smiles: molecule.cano_smiles, + inchistring: molecule.inchistring, + inchikey: molecule.inchikey, + molecule_svg_file: molecule.molecule_svg_file, + pubchem_cid: molecule.tag.taggable_data["pubchem_cid"] + }, + license: dataset.tag.taggable_data["publication"]["license"] || 'CC BY-SA', + publication: { + author_ids: sample&.publication&.taggable_data['author_ids'] || [], + creators: sample&.publication&.taggable_data['creators'] || [], + affiliation_ids: sample&.publication&.taggable_data['affiliation_ids'] || [], + affiliations: sample&.publication&.taggable_data['affiliations'] || {}, + published_at: sample&.publication&.taggable_data['published_at'], + } + } + else + res = nil + end + + return res + end + end + + resource :reaction do + helpers RepositoryHelpers + desc "Return PUBLISHED serialized reaction" + params do + requires :id, type: Integer, desc: "Reaction id" + end + get do + r = CollectionsReaction.where(reaction_id: params[:id], collection_id: [Collection.public_collection_id, Collection.scheme_only_reactions_collection.id]) + return nil unless r.present? + + reaction = Reaction.where('id = ?', params[:id]) + .select( + <<~SQL + reactions.id, reactions.name, reactions.description, reactions.reaction_svg_file, reactions.short_label, + reactions.status, reactions.tlc_description, reactions.tlc_solvents, reactions.rf_value, + reactions.temperature, reactions.timestamp_start,reactions.timestamp_stop,reactions.observation, + reactions.rinchi_string, reactions.rinchi_long_key, reactions.rinchi_short_key,reactions.rinchi_web_key, + (select json_extract_path(taggable_data::json, 'publication') from publications where element_type = 'Reaction' and element_id = reactions.id) as publication, + reactions.duration + SQL + ) + .includes( + container: :attachments + ).last + literatures = get_literature(params[:id],'Reaction') || [] + reaction.products.each do |p| + literatures += get_literature(p.id,'Sample') + end + schemeList = get_reaction_table(params[:id]) + entities = Entities::ReactionEntity.represent(reaction, serializable: true) + entities[:literatures] = literatures unless entities.nil? || literatures.nil? || literatures.length == 0 + entities[:schemes] = schemeList unless entities.nil? || schemeList.nil? || schemeList.length == 0 + entities[:isLogin] = current_user.present? + entities + end + end + + resource :molecule do + desc "Return serialized molecule with list of PUBLISHED dataset" + params do + requires :id, type: Integer, desc: "Molecule id" + optional :adv_flag, type: Boolean, desc: "advanced search flag" + optional :adv_type, type: String, desc: "advanced search type", allow_blank: true, values: %w[Authors Ontologies] + optional :adv_val, type: Array[String], desc: 'advanced search value', regexp: /^(\d+|([[:alpha:]]+:\d+))$/ + end + get do + molecule = Molecule.find(params[:id]) + pub_id = Collection.public_collection_id + if params[:adv_flag].present? && params[:adv_flag] == true && params[:adv_type].present? && params[:adv_type] == 'Authors' && params[:adv_val].present? + adv = <<~SQL + INNER JOIN publication_authors rs on rs.element_id = samples.id and rs.element_type = 'Sample' and rs.state = 'completed' + and rs.author_id in ('#{params[:adv_val].join("','")}') + SQL + else + adv = '' + end + + pub_samples = Collection.public_collection.samples + .includes(:molecule,:tag).where("samples.molecule_id = ?", molecule.id) + .where( + <<~SQL + samples.id in ( + SELECT samples.id FROM samples + INNER JOIN collections_samples cs on cs.collection_id = #{pub_id} and cs.sample_id = samples.id and cs.deleted_at ISNULL + INNER JOIN publications pub on pub.element_type='Sample' and pub.element_id=samples.id and pub.deleted_at ISNULL + #{adv} + ) + SQL + ) + .select( + <<~SQL + samples.*, (select published_at from publications where element_type='Sample' and element_id=samples.id and deleted_at is null) as published_at + SQL + ) + .order('published_at desc') + published_samples = pub_samples.map do |s| + containers = Entities::ContainerEntity.represent(s.container) + tag = s.tag.taggable_data['publication'] + #u = User.find(s.tag.taggable_data['publication']['published_by'].to_i) + #time = DateTime.parse(s.tag.taggable_data['publication']['published_at']) + #published_time = time.strftime("%A, %B #{time.day.ordinalize} %Y %H:%M") + #aff = u.affiliations.first + next unless tag + literatures = Literature.by_element_attributes_and_cat(s.id, 'Sample', 'public') + .joins("inner join users on literals.user_id = users.id") + .select( + <<~SQL + literatures.*, + json_object_agg(users.name_abbreviation, users.first_name || chr(32) || users.last_name) as ref_added_by + SQL + ).group('literatures.id').as_json + reaction_ids = ReactionsProductSample.where(sample_id: s.id).pluck(:reaction_id) + pub = Publication.find_by(element_type: 'Sample', element_id: s.id) + sid = pub.taggable_data["sid"] unless pub.nil? || pub.taggable_data.nil? + + tag.merge(analyses: containers, literatures: literatures, sample_svg_file: s.sample_svg_file, + sample_id: s.id, reaction_ids: reaction_ids, sid: sid, showed_name: s.showed_name, pub_id: pub.id) + end + published_samples = published_samples.flatten.compact + + { + molecule: MoleculeGuestSerializer.new(molecule).serializable_hash.deep_symbolize_keys, + published_samples: published_samples, + isLogin: current_user.nil? ? false : true + } + end + end + + resource :metadata do + desc "metadata of publication" + params do + requires :id, type: Integer, desc: "Id" + requires :type, type: String, desc: "Type", values: %w[sample reaction container] + end + after_validation do + @publication = Publication.find_by( + element_type: params['type'].classify, + element_id: params['id'] + ) + error!('404 Publication not found', 404) unless @publication && @publication.state.include?("completed") + end + desc "Download metadata_xml" + get :download do + el_type = params['type'] == "container" ? "analysis" : params['type'] + filename = URI.escape("metadata_#{el_type}_#{@publication.element_id}-#{Time.new.strftime("%Y%m%d%H%M%S")}.xml") + content_type('application/octet-stream') + header['Content-Disposition'] = "attachment; filename=" + filename + env['api.format'] = :binary + @publication.metadata_xml + end + end + + resource :published_statics do + desc 'Return PUBLIC statics' + get do + ActiveRecord::Base.connection.exec_query('select * from publication_statics as ps') + end + end end namespace :upload do @@ -192,7 +655,7 @@ def send_notification(attachment, user, status, has_error = false) key = AuthenticationKey.find_by(token: token) - helper = CollectorHelper.new(key.user.email , recipient_email) + helper = CollectorHelper.new(key.user.email, recipient_email) if helper.sender_recipient_known? dataset = helper.prepare_new_dataset(subject) diff --git a/app/api/chemotion/reaction_api.rb b/app/api/chemotion/reaction_api.rb index ea127f093..76321c07d 100644 --- a/app/api/chemotion/reaction_api.rb +++ b/app/api/chemotion/reaction_api.rb @@ -282,7 +282,7 @@ class ReactionAPI < Grape::API get do reaction = Reaction.find(params[:id]) - { reaction: ElementPermissionProxy.new(current_user, reaction, user_ids).serialized, literatures: citation_for_elements(params[:id], 'Reaction') } + { reaction: ElementPermissionProxy.new(current_user, reaction, user_ids).serialized, literatures: citation_for_elements(params[:id], 'Reaction'), publication: Publication.find_by(element: reaction) || {} } end end diff --git a/app/api/chemotion/report_api.rb b/app/api/chemotion/report_api.rb index d53dd86db..a0102cafe 100644 --- a/app/api/chemotion/report_api.rb +++ b/app/api/chemotion/report_api.rb @@ -19,6 +19,22 @@ def time_now end resource :reports do + desc "get DOI list" + params do + requires :elements + end + post :dois do + elements = params[:elements] + pub_list = [] + elements.each do |element| + publication = Publication.find_by(element_id: element[:id], element_type: element[:type].capitalize) + pub_list.push(publication) unless publication.nil? + # publications = [publication] + publication.descendants + end + entities = Entities::PublicationEntity.represent(pub_list, serializable: true) + {dois: entities || []} + end + desc "Build a reaction report using the contents of a JSON file" params do requires :id @@ -203,6 +219,7 @@ def time_now optional :fileDescription end post :reports, each_serializer: ReportSerializer do +# byebug spl_settings = hashize(params[:splSettings]) rxn_settings = hashize(params[:rxnSettings]) si_rxn_settings = hashize(params[:siRxnSettings]) diff --git a/app/api/chemotion/repository_api.rb b/app/api/chemotion/repository_api.rb new file mode 100644 index 000000000..ada3fd486 --- /dev/null +++ b/app/api/chemotion/repository_api.rb @@ -0,0 +1,1025 @@ +require 'securerandom' +module Chemotion + class RepositoryAPI < Grape::API + include Grape::Kaminari + helpers ContainerHelpers + helpers ParamsHelpers + helpers CollectionHelpers + helpers SampleHelpers + helpers SubmissionHelpers + + namespace :repository do + helpers do + def fetch_embargo_collection(cid) + if (cid == 0) + chemotion_user = User.chemotion_user + new_col_label = current_user.initials + '_' + Time.now.strftime("%Y-%m-%d") + col_check = Collection.where([" label like ? ", new_col_label+'%']) + new_col_label = new_col_label << '_' << (col_check&.length+1)&.to_s if col_check&.length > 0 + new_embargo_col = Collection.create!(user: chemotion_user, + label: new_col_label, ancestry: current_user.publication_embargo_collection.id) + SyncCollectionsUser.find_or_create_by(user: current_user, shared_by_id: chemotion_user.id, collection_id: new_embargo_col.id, + permission_level: 0, sample_detail_level: 10, reaction_detail_level: 10, + fake_ancestry: current_user.publication_embargo_collection.sync_collections_users.first.id.to_s) + new_embargo_col + else + Collection.find(cid) + end + end + + def duplicate_analyses(new_element, analyses_arr, ik = nil) + unless new_element.container + Container.create_root_container(containable: new_element) + new_element.reload + end + analyses = Container.analyses_container(new_element.container.id).first + parent_publication = new_element.publication + analyses_arr && analyses_arr.each do |ana| + new_ana = analyses.children.create( + name: ana.name, + container_type: ana.container_type, + description: ana.description + ) + new_ana.extended_metadata = ana.extended_metadata + new_ana.save! + + # move reserved doi + if (d = ana.doi) + d.update(doiable: new_ana) + else + d = Doi.create_for_analysis!(new_ana, ik) + end + Publication.create!( + state: Publication::STATE_PENDING, + element: new_ana, + original_element: ana, + published_by: current_user.id, + doi: d, + parent: new_element.publication, + taggable_data: @publication_tag.merge( + author_ids: @author_ids + ) + ) + # duplicate datasets and copy attachments + ana.children.where(container_type: 'dataset').each do |ds| + new_dataset = new_ana.children.create(container_type: "dataset") + ds.attachments.each do |att| + copied_att = att.copy(attachable_type: 'Container', attachable_id: new_dataset.id, transferred: true) + copied_att.save! + new_dataset.attachments << copied_att + + # copy publication image file to public/images/publications/{attachment.id}/{attachment.filename} + if MimeMagic.by_path(copied_att.filename)&.type&.start_with?("image") + file_path = File.join('public/images/publications/', copied_att.id.to_s, '/', copied_att.filename) + public_path = File.join('public/images/publications/', copied_att.id.to_s) + FileUtils.mkdir_p(public_path) + File.write(file_path, copied_att.store.read_file.force_encoding("utf-8")) if copied_att.store.file_exist? + end + + end + + new_dataset.name = ds.name + new_dataset.extended_metadata = ds.extended_metadata + new_dataset.save! + end + end + end + + def reviewer_collections + c = current_user.pending_collection + User.reviewer_ids.each do |rev_id| + SyncCollectionsUser.find_or_create_by( + collection_id: c.id, + user_id: rev_id, + shared_by_id: c.user_id, + permission_level: 3, + sample_detail_level: 10, + reaction_detail_level: 10, + label: "REVIEWING", + ) + end + end + + # Create(clone) publication sample/analyses with dois + def duplicate_sample(sample = @sample, analyses = @analyses, parent_publication_id = nil) + new_sample = sample.dup + new_sample.collections << current_user.pending_collection + new_sample.collections << Collection.element_to_review_collection + new_sample.collections << @embargo_collection unless @embargo_collection.nil? + new_sample.save! + unless @literals.nil? + lits = @literals&.select { |lit| lit['element_type'] == 'Sample' && lit['element_id'] == sample.id} + duplicate_literals(new_sample, lits) + end + duplicate_analyses(new_sample, analyses, new_sample.molecule.inchikey) + has_analysis = new_sample.analyses.present? + if (has_analysis = new_sample.analyses.present?) + if (d = sample.doi) + d.update!(doiable: new_sample) + else + d = Doi.create_for_element!(new_sample) + end + pub = Publication.create!( + state: Publication::STATE_PENDING, + element: new_sample, + original_element: sample, + published_by: current_user.id, + doi: d, + parent_id: parent_publication_id, + taggable_data: @publication_tag.merge( + author_ids: @author_ids, + original_analysis_ids: analyses.pluck(:id), + analysis_ids: new_sample.analyses.pluck(:id) + ) + ) + end + new_sample.analyses.each do |ana| + Publication.find_by(element: ana).update(parent: pub) + end + new_sample + end + + def concat_author_ids(coauthors = params[:coauthors]) + coauthor_ids = coauthors.map do |coa| + val = coa.strip + next val.to_i if val =~ /^\d+$/ + User.where(type: %w(Person Collaborator)).where.not(confirmed_at: nil).find_by(email: val)&.id if val =~ /^\S+@\S+$/ + end.compact + [current_user.id] + coauthor_ids + end + + def duplicate_reaction(reaction, analysis_set) + new_reaction = reaction.dup + if analysis_set && analysis_set.length > 0 + analysis_set_ids = analysis_set.map(&:id) + reaction_analysis_set = reaction.analyses.where(id: analysis_set_ids) + end + princhi_string, princhi_long_key, princhi_short_key, princhi_web_key = reaction.products_rinchis + + new_reaction.collections << current_user.pending_collection + new_reaction.collections << Collection.element_to_review_collection + new_reaction.collections << @embargo_collection unless @embargo_collection.nil? + + # composer = SVG::ReactionComposer.new(paths, temperature: temperature_display_with_unit, + # solvents: solvents_in_svg, + # show_yield: true) + # new_reaction.reaction_svg_file = composer.compose_reaction_svg_and_save(prefix: Time.now) + dir = File.join(Rails.root, 'public','images','reactions') + rsf = reaction.reaction_svg_file + path = File.join(dir, rsf) + new_rsf = "#{Time.now.to_i}-#{rsf}" + dest = File.join(dir, new_rsf) + + new_reaction.save! + unless @literals.nil? + lits = @literals&.select { |lit| lit['element_type'] == 'Reaction' && lit['element_id'] == reaction.id} + duplicate_literals(new_reaction, lits) + end + if File.exists? path + FileUtils.cp(path, dest) + new_reaction.update_columns(reaction_svg_file: new_rsf) + end + # new_reaction.save! + et = new_reaction.tag + data = et.taggable_data || {} + # data[:products_rinchi] = { + # rinchi_string: princhi_string, + # rinchi_long_key: princhi_long_key, + # rinchi_short_key: princhi_short_key, + # rinchi_web_key: princhi_web_key + # } + et.update!(taggable_data: data) + + if (d = reaction.doi) + d.update!(doiable: new_reaction) + else + # NB: the reaction has still no sample, so it cannot get a proper rinchi needed for the doi + # => use the one from original reaction + d = Doi.create_for_element!(new_reaction, "reaction/" + reaction.products_short_rinchikey_trimmed) + end + + pub = Publication.create!( + state: Publication::STATE_PENDING, + element: new_reaction, + original_element: reaction, + published_by: current_user.id, + doi: d, + taggable_data: @publication_tag.merge( + author_ids: @author_ids, + original_analysis_ids: analysis_set_ids, + products_rinchi: { + rinchi_string: princhi_string, + rinchi_long_key: princhi_long_key, + rinchi_short_key: princhi_short_key, + rinchi_web_key: princhi_web_key + }) + ) + + duplicate_analyses(new_reaction, reaction_analysis_set, "reaction/" + reaction.products_short_rinchikey_trimmed) + reaction.reactions_samples.each do |rs| + new_rs = rs.dup + sample = current_user.samples.find_by(id: rs.sample_id) + if @scheme_only == true + sample.target_amount_value = 0.0 + sample.real_amount_value = nil + end + sample_analysis_set = sample.analyses.where(id: analysis_set_ids) + new_sample = duplicate_sample(sample, sample_analysis_set, pub.id) + sample.tag_as_published(new_sample, sample_analysis_set) + new_rs.sample_id = new_sample + new_rs.reaction_id = new_reaction.id + new_rs.sample_id = new_sample.id + new_rs.reaction_id = new_reaction.id + new_rs.save! + end + + new_reaction.update_svg_file! + new_reaction.reload + new_reaction.save! + new_reaction.reload + end + + def create_publication_tag(contributor, author_ids, license) + authors = User.where(type: %w(Person Collaborator), id: author_ids) + .includes(:affiliations) + .order("position(users.id::text in '#{author_ids}')") + affiliations = authors.map(&:current_affiliations) + affiliations_output = {} + affiliations.flatten.each do |aff| + affiliations_output[aff.id] = aff.output_full + end + { + published_by: author_ids[0], + author_ids: author_ids, + creators: authors.map { |author| + { + 'givenName' => author.first_name, + 'familyName' => author.last_name, + 'name' => author.name, + 'ORCID' => author.orcid, + 'affiliationIds' => author.current_affiliations.map(&:id), + 'id' => author.id + } + }, + contributors: { + 'givenName' => contributor.first_name, + 'familyName' => contributor.last_name, + 'name' => contributor.name, + 'ORCID' => contributor.orcid, + 'affiliations' => contributor.current_affiliations.map{ |aff| aff.output_full }, + 'id' => contributor.id + }, + affiliations: affiliations_output, + affiliation_ids: affiliations.map { |as| as.map(&:id) }, + queued_at: DateTime.now, + license: license, + scheme_only: @scheme_only + } + end + + def prepare_reaction_data + reviewer_collections + new_reaction = duplicate_reaction(@reaction, @analysis_set) + reaction_analysis_set = @reaction.analyses.where(id: @analysis_set_ids) + @reaction.tag_as_published(new_reaction, reaction_analysis_set) + new_reaction.create_publication_tag(current_user, @author_ids, @license) + new_reaction.samples.each do |new_sample| + new_sample.create_publication_tag(current_user, @author_ids, @license) + end + Publication.where(element: new_reaction).first + end + + def duplicate_literals(element,literals) + literals&.each do |lit| + attributes = { + literature_id: lit.literature_id, + element_id: element.id, + element_type: lit.element_type, + category: 'detail', + user_id: lit.user_id + } + Literal.create(attributes) + end + end + + def prepare_sample_data + reviewer_collections + new_sample = duplicate_sample(@sample, @analyses) + @sample.tag_as_published(new_sample, @analyses) + new_sample.create_publication_tag(current_user, @author_ids, @license) + @sample.untag_reserved_suffix + Publication.where(element: new_sample).first + end + + + def find_embargo_collection(root_publication) + has_embargo_col = root_publication.element&.collections&.select { |c| c['ancestry'].to_i == User.find(root_publication.published_by).publication_embargo_collection.id } + has_embargo_col && has_embargo_col.length > 0 ? has_embargo_col.first.label : '' + end + end + + desc 'Get review list' + params do + optional :type, type: String, desc: "Type" + optional :state, type: String, desc: "State" + optional :page, type: Integer, desc: "page" + optional :pages, type: Integer, desc: "pages" + optional :per_page, type: Integer, desc: "per page" + + end + paginate per_page: 10, offset: 0, max_per_page: 100 + get 'list' do + type = (params[:type].empty? || params[:type] == 'All') ? ['Sample','Reaction'] : params[:type].chop! + state = (params[:state].empty? || params[:state] == 'All') ? [Publication::STATE_PENDING, Publication::STATE_REVIEWED, Publication::STATE_ACCEPTED] : params[:state] + list = if User.reviewer_ids.include?(current_user.id) + Publication.where(state: state, ancestry: nil, element_type: type) + else + Publication.where(state: state, ancestry: nil, element_type: type, published_by: current_user.id) + end.order(updated_at: :desc) + elements = [] + paginate(list).each do |e| + element_type = e.element&.class&.name + next if element_type.nil? + + u = User.find(e.published_by) unless e.published_by.nil? + svg_file = e.element.reaction_svg_file if element_type == 'Reaction' + title = e.element.short_label if element_type == 'Reaction' + + svg_file = e.element.sample_svg_file if element_type == 'Sample' + title = e.element.short_label if element_type == 'Sample' + + scheme_only = element_type == 'Reaction' && e.taggable_data && e.taggable_data['scheme_only'] + elements.push( + id: e.element_id, svg: svg_file, type: element_type, title: title, + published_by: u&.name, submit_at: e.updated_at, state: e.state, embargo: find_embargo_collection(e), scheme_only: scheme_only + ) + end + { elements: elements } + end + + desc 'Get embargo list' + get 'embargo_list' do + if (current_user.type == 'Anonymous') + cols = Collection.where(id: current_user.sync_in_collections_users.pluck(:collection_id)).where.not(label: 'chemotion').order('label ASC') + else + cols = Collection.where(ancestry: current_user.publication_embargo_collection.id).order('label ASC') + end + { repository: cols, current_user: {id: current_user.id, type: current_user.type} } + end + + resource :reaction do + helpers RepositoryHelpers + desc "Return PUBLISHED serialized reaction" + params do + requires :id, type: Integer, desc: "Reaction id" + optional :is_public, type: Boolean, default: true + end + get do + reaction = Reaction.where(id: params[:id]) + .select( + <<~SQL + reactions.id, reactions.name, reactions.description, reactions.reaction_svg_file, reactions.short_label, + reactions.status, reactions.tlc_description, reactions.tlc_solvents, reactions.rf_value, + reactions.temperature, reactions.timestamp_start,reactions.timestamp_stop,reactions.observation, + reactions.rinchi_string, reactions.rinchi_long_key, reactions.rinchi_short_key,reactions.rinchi_web_key, + (select json_extract_path(taggable_data::json, 'publication') from publications where element_type = 'Reaction' and element_id = reactions.id) as publication, + reactions.duration + SQL + ).includes(container: :attachments).last + literatures = get_literature(params[:id], 'Reaction', params[:is_public] ? 'public' : 'detail') || [] + reaction.products.each do |p| + literatures += get_literature(p.id,'Sample', params[:is_public]? 'public' : 'detail') + end + schemeList = get_reaction_table(params[:id]) + publication = Publication.find_by(element_id: params[:id], element_type: 'Reaction') + published_user = User.find(publication.published_by) unless publication.nil? + entities = Entities::ReactionEntity.represent(reaction, serializable: true) + entities[:literatures] = literatures unless entities.nil? || literatures.blank? + entities[:schemes] = schemeList unless entities.nil? || schemeList.blank? + { reaction: entities, reviewLevel: repo_review_level(params[:id], 'Reaction'), pub_name: published_user&.name || '' } + end + end + + resource :sample do + helpers RepositoryHelpers + desc "Return Review serialized Sample" + params do + requires :id, type: Integer, desc: "Sample id" + optional :is_public, type: Boolean, default: true + end + get do + sample = Sample.where(id: params[:id]) + .includes(:molecule,:tag).last + molecule = Molecule.find(sample.molecule_id) unless sample.nil? + containers = Entities::ContainerEntity.represent(sample.container) + publication = Publication.find_by(element_id: params[:id], element_type: 'Sample') + published_user = User.find(publication.published_by) unless publication.nil? + literatures = get_literature(params[:id], 'Sample', params[:is_public]? 'public' : 'detail') + { + molecule: MoleculeGuestSerializer.new(molecule).serializable_hash.deep_symbolize_keys, + sample: sample, + publication: publication, + literatures: literatures, + analyses: containers, + doi: Entities::DoiEntity.represent(sample.doi, serializable: true), + pub_name: published_user&.name, + reviewLevel: repo_review_level(params[:id], 'Sample') + } + end + end + + resource :metadata do + desc "metadata of publication" + params do + requires :id, type: Integer, desc: "Id" + requires :type, type: String, desc: "Type", values: %w[sample reaction] + end + after_validation do + @root_publication = Publication.find_by( + element_type: params['type'].classify, + element_id: params['id'] + ).root + error!('404 Publication not found', 404) unless @root_publication + error!('401 Unauthorized', 401) unless (User.reviewer_ids.include?(current_user.id) || @root_publication.published_by == current_user.id) + end + post :preview do + mt = [] + root_publication = @root_publication + publications = [root_publication] + root_publication.descendants + publications.each do |pub| + mt.push({element_type: pub.element_type, metadata_xml: pub.datacite_metadata_xml}) + end + { metadata: mt } + end + post :preview_zip do + env['api.format'] = :binary + content_type('application/zip, application/octet-stream') + root_publication = @root_publication + publications = [root_publication] + root_publication.descendants + filename = URI.escape("metadata_#{root_publication.element_type}_#{root_publication.element_id}-#{Time.new.strftime("%Y%m%d%H%M%S")}.zip") + header('Content-Disposition', "attachment; filename=\"#{filename}\"") + zip = Zip::OutputStream.write_buffer do |zip| + publications.each do |pub| + el_type = pub.element_type == "Container" ? "analysis" : pub.element_type.downcase + zip.put_next_entry URI.escape("metadata_#{el_type}_#{pub.element_id}.xml") + zip.write pub.datacite_metadata_xml + end + end + zip.rewind + zip.read + end + end + + namespace :reviewing do + helpers do + # TODO: mv to model + def save_comments(root, comments, summary, feedback) + review = root.review || {} + review['summary'] = summary + review['feedback'] = feedback + root.update!(review: review) + end + + # TODO: mv to model + def save_comment(root, comment) + review = root.review || {} + if review.empty? + root.update_column(:review, { comments: comment }) + else + comments = review['comments'] || {} + review['comments'] = comments.deep_merge(comment || {}) + root.update!(review: review) + root.review + end + end + + def accept_new_sample(root, sample) + ap = Publication.create!( + state: Publication::STATE_PENDING, + element: sample, + doi: sample.doi, + published_by: root.published_by, + parent: root, + taggable_data: root.taggable_data + ) + sample.analyses.each do |a| + accept_new_analysis(ap, a) + end + end + + def accept_new_analysis(root, analysis) + ap = Publication.create!( + state: Publication::STATE_PENDING, + element: analysis, + doi: analysis.doi, + published_by: root.published_by, + parent: root, + taggable_data: root.taggable_data + ) + atag = ap.taggable_data + aids = atag&.delete('analysis_ids') + aoids = atag&.delete('original_analysis_ids') + ap.save! if aids || aoids + + analysis.children.where(container_type: "dataset").each do |ds| + ds.attachments.each do |att| + if MimeMagic.by_path(att.filename)&.type&.start_with?("image") && att.store.file_exist? + file_path = File.join('public/images/publications/', att.id.to_s, '/', att.filename) + public_path = File.join('public/images/publications/', att.id.to_s) + FileUtils.mkdir_p(public_path) + File.write(file_path, att.store.read_file.force_encoding("utf-8")) if att.store.file_exist? + end + end + end + end + + def element_submit(root) + root.descendants.each { |np| np.destroy! if np.element.nil? } + root.element.reserve_suffix + root.element.reserve_suffix_analyses(root.element.analyses) if root.element.analyses&.length > 0 + root.element.analyses&.each do |a| + accept_new_analysis(root, a) if Publication.find_by(element: a).nil? + end + + case root.element_type + when 'Sample' + analyses_ids = root.element.analyses.pluck(:id) + root.update!(taggable_data: root.taggable_data.merge(analysis_ids: analyses_ids)) + root.element.analyses.each do |sa| + accept_new_analysis(root,ana) if Publication.find_by(element: sa).nil? + end + + when 'Reaction' + root.element.products.each do |p| + Publication.find_by(element_type:'Sample', element_id: p.id)&.destroy! if p.analyses&.length == 0 + next if p.analyses&.length == 0 + p.reserve_suffix + p.reserve_suffix_analyses(p.analyses) + prod_pub = Publication.find_by(element: p); + if prod_pub.nil? + accept_new_sample(root, p) + else + p.analyses.each do |rpa| + accept_new_analysis(prod_pub,rpa) if Publication.find_by(element: rpa).nil? + end + end + end + end + root.reload + root.update_columns(doi_id: root.element.doi.id) unless root.doi_id == root.element.doi.id + root.descendants.each { |pub_a| + next if pub_a.element.nil? + pub_a.update_columns(doi_id: pub_a.element.doi.id) unless pub_a.doi_id == pub_a.element&.doi&.id + } + end + + def public_literature(root_publication) + publications = [root_publication] + root_publication.descendants + publications.each do |pub| + next unless pub.element_type=='Reaction' || pub.element_type =='Sample' + literals = Literal.where(element_type: pub.element_type, element_id: pub.element_id) + literals&.each { |l| l.update_columns(category: 'public') } unless literals.nil? + end + end + + end + + desc "process reviewed publication" + params do + requires :id, type: Integer, desc: "Id" + requires :type, type: String, desc: "Type", values: %w[sample reaction] + optional :comments, type: Hash + optional :summary, type: String + optional :feedback, type: String + end + + after_validation do + @root_publication = Publication.find_by( + element_type: params['type'].classify, + element_id: params['id'] + ).root + error!('401 Unauthorized', 401) unless ((User.reviewer_ids.include?(current_user.id) && @root_publication.state == Publication::STATE_PENDING) || (@root_publication.published_by == current_user.id && @root_publication.state == Publication::STATE_REVIEWED )) + end + + post :comments do + save_comments( + @root_publication, + params[:comments], + params[:summary], + params[:feedback] + ) unless params[:comments].nil? && params[:summary].nil? && params[:feedback].nil? + @root_publication.element + end + + post :comment do + save_comment(@root_publication, params[:comments]) unless params[:comments].nil? + @root_publication.review + end + + post :reviewed do + save_comments(@root_publication, params[:comments], params[:summary], params[:feedback]) unless params[:comments].nil? && params[:summary].nil? && params[:feedback].nil? + element_submit(@root_publication) + @root_publication.update_state(Publication::STATE_REVIEWED) + @root_publication.process_element(Publication::STATE_REVIEWED) + @root_publication.inform_users(Publication::STATE_REVIEWED) + @root_publication.element + end + + post :submit do + save_comments(@root_publication, params[:comments], params[:summary], params[:feedback]) unless params[:comments].nil? && params[:summary].nil? && params[:feedback].nil? + element_submit(@root_publication) + @root_publication.update_state(Publication::STATE_PENDING) + @root_publication.process_element(Publication::STATE_PENDING) + @root_publication.inform_users(Publication::STATE_PENDING) + @root_publication.element + end + + post :accepted do + save_comments(@root_publication, params[:comments], params[:summary], params[:feedback]) unless params[:comments].nil? && params[:summary].nil? && params[:feedback].nil? + element_submit(@root_publication) + public_literature(@root_publication) + # element_accepted(@root_publication) + + @root_publication.update_state(Publication::STATE_ACCEPTED) + @root_publication.process_element(Publication::STATE_ACCEPTED) + @root_publication.inform_users(Publication::STATE_ACCEPTED) + @root_publication.element + if params['type'] == 'sample' + { + sample: SampleSerializer.new(@root_publication.element).serializable_hash.deep_symbolize_keys, + message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' + } + elsif params['type'] == 'reaction' + { + reaction: ReactionSerializer.new(@root_publication.element).serializable_hash.deep_symbolize_keys, + message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' + } + end + end + post :declined do + save_comments(@root_publication, params[:comments], params[:summary], params[:feedback]) unless params[:comments].nil? && params[:summary].nil? && params[:feedback].nil? + @root_publication.update_state('declined') + @root_publication.process_element('declined') + @root_publication.inform_users(Publication::STATE_DECLINED, current_user.id) + @root_publication.element + end + end + + namespace :publishSample do + desc "Publish Samples with chosen Dataset" + params do + requires :sampleId, type: Integer, desc: "Sample Id" + requires :analysesIds, type: Array[Integer], desc: "Selected analyses ids" + optional :coauthors, type: Array[String], default: [], desc: "Co-author (User)" + optional :refs, type: Array[Integer], desc: "Selected references" + optional :embargo, type: Integer, desc: "Embargo collection" + requires :license, type: String, desc: "Creative Common License" + requires :addMe, type: Boolean, desc: "add me as author" + end + + after_validation do + @sample = current_user.samples.find_by(id: params[:sampleId]) + @analyses = @sample && @sample.analyses.where(id: params[:analysesIds]) + @literals = Literal.where(id: params[:refs]) unless params[:refs].nil? || params[:refs].empty? + ols_validation(@analyses) + if params[:addMe] + @author_ids = [current_user.id] + coauthor_validation(params[:coauthors]) + else + @author_ids = coauthor_validation(params[:coauthors]) + end + error!('401 Unauthorized', 401) unless @sample + error!('404 analyses not found', 404) if @analyses.empty? + end + + post do + @license = params[:license] + @publication_tag = create_publication_tag(current_user, @author_ids, @license) + @embargo_collection = fetch_embargo_collection(params[:embargo]) if params[:embargo].present? && params[:embargo] >= 0 + pub = prepare_sample_data + pub.process_element + pub.inform_users + + @sample.reload + { + sample: SampleSerializer.new(@sample).serializable_hash.deep_symbolize_keys, + message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' + } + end + + put :dois do + @sample.reserve_suffix + @sample.reserve_suffix_analyses(@analyses) + @sample.reload + @sample.tag_reserved_suffix(@analyses) + { sample: SampleSerializer.new(@sample).serializable_hash.deep_symbolize_keys } + end + end + + # desc: submit reaction data for publication + namespace :publishReaction do + desc "Publish Reaction with chosen Dataset" + params do + requires :reactionId, type: Integer, desc: "Reaction Id" + requires :analysesIds, type: Array[Integer], desc: "Selected analyses ids" + optional :coauthors, type: Array[String], default: [], desc: "Co-author (User)" + optional :refs, type: Array[Integer], desc: "Selected references" + optional :embargo, type: Integer, desc: "Embargo collection" + requires :license, type: String, desc: "Creative Common License" + requires :addMe, type: Boolean, desc: "add me as author" + end + + after_validation do + @scheme_only = false + @reaction = current_user.reactions.find_by(id: params[:reactionId]) + error!('404 found no reaction to publish', 401) unless @reaction + @analysis_set = @reaction.analyses.where(id: params[:analysesIds]) | Container.where(id: (@reaction.samples.map(&:analyses).flatten.map(&:id) & params[:analysesIds])) + ols_validation(@analysis_set) + if params[:addMe] + @author_ids = [current_user.id] + coauthor_validation(params[:coauthors]) + else + @author_ids = coauthor_validation(params[:coauthors]) + end + error!('404 found no analysis to publish', 404) unless @analysis_set.present? + + #error!('Reaction Publication not authorized', 401) + @analysis_set_ids = @analysis_set.map(&:id) + @literals = Literal.where(id: params[:refs]) unless params[:refs].nil? || params[:refs].empty? + end + + post do + @license = params[:license] + @publication_tag = create_publication_tag(current_user, @author_ids, @license) + @embargo_collection = fetch_embargo_collection(params[:embargo]) if params[:embargo].present? && params[:embargo] >= 0 + pub = prepare_reaction_data + pub.process_element + pub.inform_users + + @reaction.reload + { + reaction: ReactionSerializer.new(@reaction).serializable_hash.deep_symbolize_keys, + message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' + } + end + + put :dois do + reaction_products = @reaction.products.select { |s| s.analyses.select { |a| a.id.in?@analysis_set_ids }.count > 0 } + @reaction.reserve_suffix + reaction_products.each do |p| + d = p.reserve_suffix + et = p.tag + et.update!( + taggable_data: (et.taggable_data || {}).merge(reserved_doi: d.full_doi) + ) + end + @reaction.reserve_suffix_analyses(@analysis_set) + @reaction.reload + @reaction.tag_reserved_suffix(@analysis_set) + @reaction.reload + { + reaction: ReactionSerializer.new(@reaction).serializable_hash.deep_symbolize_keys, + message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' + } + end + end + + # desc: submit reaction data (scheme only) for publication + namespace :publishReactionScheme do + desc 'Publish Reaction Scheme only' + params do + requires :reactionId, type: Integer, desc: 'Reaction Id' + requires :temperature, type: Hash, desc: 'Temperature' + requires :duration, type: Hash, desc: 'Duration' + requires :products, type: Array, desc: 'Products' + optional :coauthors, type: Array[String], default: [], desc: 'Co-author (User)' + optional :embargo, type: Integer, desc: 'Embargo collection' + requires :license, type: String, desc: 'Creative Common License' + requires :addMe, type: Boolean, desc: "add me as author" + requires :schemeDesc, type: Boolean, desc: "publish scheme" + end + + after_validation do + @reaction = current_user.reactions.find_by(id: params[:reactionId]) + @scheme_only = true + error!('404 found no reaction to publish', 401) unless @reaction + schemeYield = params[:products] && params[:products].map{|v| v.slice(:id, :_equivalent)} + @reaction.reactions_samples.select{|rs| rs.type == 'ReactionsProductSample'}.map do |p| + py = schemeYield.select{ |o| o['id'] == p.sample_id } + p.equivalent = py[0]['_equivalent'] if py && py.length > 0 + p.scheme_yield = py[0]['_equivalent'] if py && py.length > 0 + end + + @reaction.reactions_samples.select{|rs| rs.type != 'ReactionsProductSample'}.map do |p| + p.equivalent = 0 + end + @reaction.name = '' + @reaction.purification = '{}' + @reaction.dangerous_products = '{}' + unless params[:schemeDesc] + @reaction.description = {"ops"=>[{"insert"=>""}]} + end + @reaction.observation = {"ops"=>[{"insert"=>""}]} + @reaction.tlc_solvents = '' + @reaction.tlc_description = '' + @reaction.rf_value = 0 + @reaction.rxno = nil + @reaction.role = '' + @reaction.temperature = params[:temperature] + @reaction.duration = "#{params[:duration][:dispValue]} #{params[:duration][:dispUnit]}" unless params[:duration].nil? + if params[:addMe] + @author_ids = [current_user.id] + coauthor_validation(params[:coauthors]) + else + @author_ids = coauthor_validation(params[:coauthors]) + end + end + + post do + @license = params[:license] + @publication_tag = create_publication_tag(current_user, @author_ids, @license) + @embargo_collection = fetch_embargo_collection(params[:embargo]) if params[:embargo].present? && params[:embargo] >= 0 + pub = prepare_reaction_data + pub.process_element + pub.inform_users + + @reaction.reload + { + reaction: ReactionSerializer.new(@reaction).serializable_hash.deep_symbolize_keys, + message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' + } + end + end + + namespace :embargo do + helpers do + def handle_embargo_collections(col) + col.update_columns(ancestry: current_user.published_collection.id) + sync_emb_col = col.sync_collections_users.where(user_id: current_user.id)&.first + sync_published_col = SyncCollectionsUser.joins("INNER JOIN collections ON collections.id = sync_collections_users.collection_id ") + .where("collections.label='Published Elements'") + .where("sync_collections_users.user_id = #{current_user.id}").first + sync_emb_col.update_columns(fake_ancestry: sync_published_col.id) + end + + def remove_anonymous(col) + anonymous_ids = col.sync_collections_users.joins("INNER JOIN users on sync_collections_users.user_id = users.id") + .where("users.type='Anonymous'").pluck(:user_id) + anonymous_ids.each do |anonymous_id| + anonymous = Anonymous.find(anonymous_id) + anonymous.sync_in_collections_users.destroy_all + anonymous.collections.each { |c| c.really_destroy! } + anonymous.really_destroy! + end + end + + def remove_embargo_collection(col) + col.sync_collections_users.destroy_all + col.really_destroy! + end + end + desc "Generate account with chosen Embargo" + params do + requires :collection_id, type: Integer, desc: "Embargo Collection Id" + end + + after_validation do + @embargo_collection = Collection.find(params[:collection_id]) + @sync_emb_col = @embargo_collection.sync_collections_users.where(user_id: current_user.id)&.first + error!('404 found no collection', 401) unless @sync_emb_col + end + + get :list do + sample_list = Publication.where(ancestry: nil, element: @embargo_collection.samples).order(updated_at: :desc) + reaction_list = Publication.where(ancestry: nil, element: @embargo_collection.reactions).order(updated_at: :desc) + list = sample_list + reaction_list + elements = [] + list.each do |e| + element_type = e.element&.class&.name + u = User.find(e.published_by) unless e.published_by.nil? + svg_file = e.element.sample_svg_file if element_type == 'Sample' + title = e.element.short_label if element_type == 'Sample' + + svg_file = e.element.reaction_svg_file if element_type == 'Reaction' + title = e.element.short_label if element_type == 'Reaction' + + scheme_only = element_type == 'Reaction' && e.taggable_data && e.taggable_data['scheme_only'] + elements.push( + id: e.element_id, svg: svg_file, type: element_type, title: title, + published_by: u&.name, submit_at: e.updated_at, state: e.state, scheme_only: scheme_only + ) + end + { elements: elements, embargo_id: params[:collection_id], current_user: { id: current_user.id, type: current_user.type } } + end + + post :account do + begin + # create Anonymous user + name_abbreviation = "e#{SecureRandom.random_number(9999)}" + email = "#{@embargo_collection.id}.#{name_abbreviation}@chemotion.net" + pwd = Devise.friendly_token.first(8) + first_name = "External" + last_name = "Chemotion" + type = 'Anonymous' + + params = { email: email, password: pwd, first_name: first_name, last_name: last_name, type: type, name_abbreviation: name_abbreviation, confirmed_at: Time.now } + new_obj = User.create!(params) + new_obj.profile.update!({data: {}}) + # sync collection with Anonymous user + chemotion_user = User.chemotion_user + root_label = "with %s" %chemotion_user.name_abbreviation + rc = Collection.find_or_create_by(user: new_obj, shared_by_id: chemotion_user.id, is_locked: true, is_shared: true, label: root_label) + + # Chemotion Collection + SyncCollectionsUser.find_or_create_by(user: new_obj, shared_by_id: chemotion_user.id, collection_id: Collection.public_collection_id, + permission_level: 0, sample_detail_level: 10, reaction_detail_level: 10, fake_ancestry: rc.id.to_s) + + + SyncCollectionsUser.find_or_create_by(user: new_obj, shared_by_id: chemotion_user.id, collection_id: @embargo_collection.id, + permission_level: 0, sample_detail_level: 10, reaction_detail_level: 10, fake_ancestry: rc.id.to_s) + + # send mail + if ENV['PUBLISH_MODE'] == 'production' + PublicationMailer.mail_external_review(current_user, @embargo_collection.label, email, pwd).deliver_now + end + + { message: 'A temporary account has been created' } + rescue StandardError => e + { error: e.message } + end + end + + post :release do + begin + pub_samples = Publication.where(ancestry: nil, element: @embargo_collection.samples).order(updated_at: :desc) + pub_reactions = Publication.where(ancestry: nil, element: @embargo_collection.reactions).order(updated_at: :desc) + pub_list = pub_samples + pub_reactions + check_state = pub_list.select { |pub| pub.state != Publication::STATE_ACCEPTED } + if check_state.present? + { error: "Embargo #{@embargo_collection.label} release failed, because not all elements have been 'accepted'."} + else + remove_anonymous(@embargo_collection) + handle_embargo_collections(@embargo_collection) + case ENV['PUBLISH_MODE'] + when 'production' + if Rails.env.production? + ChemotionEmbargoPubchemJob.set(queue: "publishing_embargo_#{@embargo_collection.id}").perform_later(@embargo_collection.id) + end + when 'staging' + ChemotionEmbargoPubchemJob.set(queue: "publishing_embargo_#{@embargo_collection.id}").perform_later(@embargo_collection.id) + #ChemotionEmbargoPubchemJob.perform_now(@embargo_collection.id) + else 'development' + end + + + { message: "Embargo #{@embargo_collection.label} has been released" } + end + + rescue StandardError => e + { error: e.message } + end + end + + post :delete do + begin + element_cnt = @embargo_collection.samples.count + @embargo_collection.reactions.count + if element_cnt.positive? + { error: "Delete Embargo #{@embargo_collection.label} deletion failed: the collection is not empty. Please refresh your page."} + else + remove_anonymous(@embargo_collection) + remove_embargo_collection(@embargo_collection) + { message: "Embargo #{@embargo_collection.label} has been deleted" } + end + rescue StandardError => e + { error: e.message } + end + end + + post :move do + begin + #@new_embargo = params[:new_embargo] + @element = params[:element] + @new_embargo_collection = fetch_embargo_collection(params[:new_embargo]&.to_i) if params[:new_embargo].present? && params[:new_embargo]&.to_i >= 0 + case @element['type'] + when 'Sample' + CollectionsSample + when 'Reaction' + CollectionsReaction + end.remove_in_collection(@element['id'], [@embargo_collection.id]) + + case @element['type'] + when 'Sample' + CollectionsSample + when 'Reaction' + CollectionsReaction + end.create_in_collection(@element['id'], [@new_embargo_collection.id]) + + { col_id: @embargo_collection.id, + new_embargo: @new_embargo_collection, + is_new_embargo: params[:new_embargo]&.to_i == 0, + message: "#{@element['type']} [#{@element['title']}] has been moved from Embargo Bundle [#{@embargo_collection.label}] to Embargo Bundle [#{@new_embargo_collection.label}]" } + rescue StandardError => e + { error: e.message } + end + end + end + end + end +end diff --git a/app/api/chemotion/sample_api.rb b/app/api/chemotion/sample_api.rb index 65c1f63a6..614855a97 100644 --- a/app/api/chemotion/sample_api.rb +++ b/app/api/chemotion/sample_api.rb @@ -432,7 +432,7 @@ class SampleAPI < Grape::API all_coll = Collection.get_all_collection_for_user(current_user.id) sample.collections << all_coll - + sample.container = update_datamodel(params[:container]) sample.save! diff --git a/app/api/chemotion/search_api.rb b/app/api/chemotion/search_api.rb index 97b2b93e2..ffea01931 100644 --- a/app/api/chemotion/search_api.rb +++ b/app/api/chemotion/search_api.rb @@ -204,6 +204,34 @@ def serialization_by_elements_and_page(elements, page = 1, molecule_sort = false reactions = elements.fetch(:reactions, []) wellplates = elements.fetch(:wellplates, []) screens = elements.fetch(:screens, []) + + if params[:is_public] + molecules = Molecule.joins(:samples).where("samples.id in (?)", samples).includes(:tag).select( + <<~SQL + molecules.*, max(samples.sample_svg_file) sample_svg_file + SQL + ).group('molecules.id').uniq + serialized_molecules = paginate(molecules).map { |m| MoleculeGuestListSerializer.new(m).serializable_hash } + filter_reactions = Reaction.where("id in (?)", reactions) + serialized_reactions = paginate(filter_reactions).map { |r| ReactionGuestListSerializer.new(r).serializable_hash } + return { + publicMolecules: { + molecules: serialized_molecules, + totalElements: molecules.size, + page: page, + perPage: page_size, + ids: molecules.pluck(:id) + }, + publicReactions: { + reactions: serialized_reactions, + totalElements: reactions.size, + page: page, + perPage: page_size, + ids: filter_reactions.pluck(:id) + } + } + end + samples_data = serialize_samples(samples, page, search_by_method, molecule_sort) serialized_samples = samples_data[:data] samples_size = samples_data[:size] @@ -349,42 +377,42 @@ def elements_by_scope(scope, collection_id = @c_id) .includes(molecule: :tag) user_reactions = Reaction.by_collection_id(collection_id).includes( :literatures, :tag, - reactions_starting_material_samples: :sample, - reactions_solvent_samples: :sample, - reactions_reactant_samples: :sample, + # reactions_starting_material_samples: :sample, + # reactions_solvent_samples: :sample, + # reactions_reactant_samples: :sample, reactions_product_samples: :sample, ) - user_wellplates = Wellplate.by_collection_id(collection_id).includes( - wells: :sample - ) - user_screens = Screen.by_collection_id(collection_id) + # user_wellplates = Wellplate.by_collection_id(collection_id).includes( + # wells: :sample + # ) + # user_screens = Screen.by_collection_id(collection_id) case scope&.first when Sample elements[:samples] = scope&.pluck(:id) elements[:reactions] = ( user_reactions.by_sample_ids(scope&.map(&:id)).pluck(:id) ).uniq - elements[:wellplates] = user_wellplates.by_sample_ids(scope&.map(&:id)).uniq.pluck(:id) - elements[:screens] = user_screens.by_wellplate_ids(elements[:wellplates]).pluck(:id) + # elements[:wellplates] = user_wellplates.by_sample_ids(scope&.map(&:id)).uniq.pluck(:id) + # elements[:screens] = user_screens.by_wellplate_ids(elements[:wellplates]).pluck(:id) when Reaction elements[:reactions] = scope&.pluck(:id) elements[:samples] = user_samples.by_reaction_ids(scope&.map(&:id)).pluck(:id).uniq - elements[:wellplates] = user_wellplates.by_sample_ids(elements[:samples]).uniq.pluck(:id) - elements[:screens] = user_screens.by_wellplate_ids(elements[:wellplates]).pluck(:id) - when Wellplate - elements[:wellplates] = scope&.pluck(:id) - elements[:screens] = user_screens.by_wellplate_ids(elements[:wellplates]).uniq.pluck(:id) - elements[:samples] = user_samples.by_wellplate_ids(elements[:wellplates]).uniq.pluck(:id) - elements[:reactions] = ( - user_reactions.by_sample_ids(elements[:samples]).pluck(:id) - ).uniq - when Screen - elements[:screens] = scope&.pluck(:id) - elements[:wellplates] = user_wellplates.by_screen_ids(scope).uniq.pluck(:id) - elements[:samples] = user_samples.by_wellplate_ids(elements[:wellplates]).uniq.pluck(:id) - elements[:reactions] = ( - user_reactions.by_sample_ids(elements[:samples]).pluck(:id) - ).uniq.pluck(:id) + # elements[:wellplates] = user_wellplates.by_sample_ids(elements[:samples]).uniq.pluck(:id) + # elements[:screens] = user_screens.by_wellplate_ids(elements[:wellplates]).pluck(:id) + # when Wellplate + # elements[:wellplates] = scope&.pluck(:id) + # elements[:screens] = user_screens.by_wellplate_ids(elements[:wellplates]).uniq.pluck(:id) + # elements[:samples] = user_samples.by_wellplate_ids(elements[:wellplates]).uniq.pluck(:id) + # elements[:reactions] = ( + # user_reactions.by_sample_ids(elements[:samples]).pluck(:id) + # ).uniq + # when Screen + # elements[:screens] = scope&.pluck(:id) + # elements[:wellplates] = user_wellplates.by_screen_ids(scope).uniq.pluck(:id) + # elements[:samples] = user_samples.by_wellplate_ids(elements[:wellplates]).uniq.pluck(:id) + # elements[:reactions] = ( + # user_reactions.by_sample_ids(elements[:samples]).pluck(:id) + # ).uniq.pluck(:id) when AllElementSearch::Results # TODO check this samples_ids + molecules_ids ???? elements[:samples] = (scope&.samples_ids + scope&.molecules_ids) @@ -394,15 +422,15 @@ def elements_by_scope(scope, collection_id = @c_id) user_reactions.by_sample_ids(elements[:samples]).pluck(:id) ).uniq - elements[:wellplates] = ( - scope&.wellplates_ids + - user_wellplates.by_sample_ids(elements[:samples]).pluck(:id) - ).uniq + # elements[:wellplates] = ( + # scope&.wellplates_ids + + # user_wellplates.by_sample_ids(elements[:samples]).pluck(:id) + # ).uniq - elements[:screens] = ( - scope&.screens_ids + - user_screens.by_wellplate_ids(elements[:wellplates]).pluck(:id) - ).uniq + # elements[:screens] = ( + # scope&.screens_ids + + # user_screens.by_wellplate_ids(elements[:wellplates]).pluck(:id) + # ).uniq end elements @@ -410,6 +438,11 @@ def elements_by_scope(scope, collection_id = @c_id) end resource :search do + after_validation do + check_params_collection_id + set_var_for_unsigned_user unless current_user + end + namespace :all do desc "Return all matched elements and associations for substring query" params do diff --git a/app/api/chemotion/suggestion_api.rb b/app/api/chemotion/suggestion_api.rb index 5d0d8bea1..85993e616 100644 --- a/app/api/chemotion/suggestion_api.rb +++ b/app/api/chemotion/suggestion_api.rb @@ -154,6 +154,8 @@ def search_possibilities_by_type_user_and_collection(type) resource :suggestions do after_validation do + check_params_collection_id + set_var_for_unsigned_user unless current_user set_var end diff --git a/app/api/chemotion/sync_collection_api.rb b/app/api/chemotion/sync_collection_api.rb index 98098930a..02dea91f5 100644 --- a/app/api/chemotion/sync_collection_api.rb +++ b/app/api/chemotion/sync_collection_api.rb @@ -38,10 +38,29 @@ class SyncCollectionAPI < Grape::API get_child = proc do |children, collections| children.each do |obj| child = collections.select { |dt| dt['ancestry'] == obj['id'].to_s } + get_child.call(child, collections) if child.count.positive? obj[:children] = child if child.count.positive? end end + handle_review = proc do |collections| + cols = [] + collections.each do |col| + unless col['label'] == 'Reviewing' || col['label'] == 'Pending Publications' || col['label'] == 'Element To Review' || col['label'] == 'Reviewed' + cols.push(col) + next + end + oc = SyncCollectionsUser.find(col['id'])&.collection + sc = (oc&.samples&.joins(:publication)&.where('publications.ancestry is null') || []).length + rc = (oc&.reactions&.joins(:publication)&.where('publications.ancestry is null') || []).length + next if (sc + rc).zero? + + col['label'] = col['label'] + ",S#{sc},R#{rc}" if col['label'] == 'Reviewing' || col['label'] == 'Element To Review' || col['label'] == 'Reviewed' + cols.push(col) + end + cols + end + desc 'Return all remote serialized collections' get :sync_remote_roots do collections = Collection.joins(:sync_collections_users) @@ -57,6 +76,7 @@ class SyncCollectionAPI < Grape::API SQL ).as_json root_ancestries = [] + collections = handle_review.call(collections) collections.each do |obj| root_ancestries.push(obj['ancestry']) end diff --git a/app/api/chemotion/user_api.rb b/app/api/chemotion/user_api.rb index 673972e01..360018dd0 100644 --- a/app/api/chemotion/user_api.rb +++ b/app/api/chemotion/user_api.rb @@ -1,17 +1,23 @@ # frozen_string_literal: true + module Chemotion class UserAPI < Grape::API - resource :users do - desc 'Find top 3 matched user names' params do requires :name, type: String end get 'name' do - unless params[:name].nil? || params[:name].empty? - { users: User.where(type: %w(Person Group)).by_name(params[:name]).limit(3) - .select('first_name','last_name','name','id','name_abbreviation', 'name_abbreviation as abb', 'type as user_type')} + if params[:name].present? && params[:name].gsub(/\s/, '').size > 3 + { + users: User.where(type: %w[Person Group]).where.not(confirmed_at: nil) + .by_name(params[:name]).limit(3).joins(:affiliations) + .select( + 'first_name', 'last_name', 'name', 'id', 'name_abbreviation', + 'name_abbreviation as abb', + 'jsonb_object_agg(affiliations.id, affiliations.department || chr(44)|| chr(32) || affiliations.organization || chr(44)|| chr(32) || affiliations.country) as aff' + ).group('first_name', 'last_name', 'name', 'id', 'name_abbreviation') + } else { users: [] } end @@ -28,6 +34,120 @@ class UserAPI < Grape::API end end + resource :collaborators do + namespace :list do + desc 'fetch collaborators of current user' + get do + ids = UsersCollaborator.where(user_id: current_user.id).pluck(:collaborator_id) + data = User.where(id: ids) + present data, with: Entities::CollaboratorEntity, root: 'authors' + end + end + namespace :user do + desc 'fetch collaborators of current user' + params do + optional :name, type: String + optional :first, type: String + end + get do + sql_str = [] + if params[:name].present? && params[:first].present? + sql_str = [" LOWER(last_name) like ? and LOWER(first_name) like ? ",'%'+params[:name].downcase+'%','%'+params[:first].downcase+'%'] + end + if !params[:name].present? && params[:first].present? + sql_str = [" LOWER(first_name) LIKE ? ",'%'+params[:first].downcase+'%'] + end + if params[:name].present? && !params[:first].present? + sql_str = [" LOWER(last_name) LIKE ? ",'%'+params[:name].downcase+'%'] + end + + data = Person.where.not(confirmed_at: nil).where(sql_str) + present data, with: Entities::CollaboratorEntity, root: 'users' + end + end + namespace :add do + desc 'add user to my collabration' + params do + requires :id, type: Integer + end + post do + new_author = UsersCollaborator.create({ user_id: current_user.id, collaborator_id: params[:id] }) + user = User.find(params[:id]) + present user, with: Entities::CollaboratorEntity, root: 'user' + end + end + namespace :add_aff do + desc 'add user to my collabration' + params do + requires :id, type: Integer + requires :department, type: String + requires :organization, type: String + requires :country, type: String + end + post do + collaborator = User.find(params[:id]) + aff = [Affiliation.find_or_create_by(country: params[:country], + organization: params[:organization], department: params[:department])] + collaborator.affiliations << aff unless aff.nil? + present collaborator, with: Entities::CollaboratorEntity, root: 'user' + end + end + namespace :delete do + desc 'remove user from my collabration' + params do + requires :id, type: Integer + end + post do + uc = UsersCollaborator.find_by(user_id: current_user.id, collaborator_id: params[:id]) + uc.delete + #present user, with: Entities::CollaboratorEntity, root: 'user' + end + end + namespace :delete_aff do + desc 'remove affilication from my collabration' + params do + requires :user_id, type: Integer + requires :aff_id, type: Integer + end + post do + + ua = UserAffiliation.find_by(user_id: params[:user_id], affiliation_id: params[:aff_id]) + ua.destroy! + + user = User.find(params[:user_id]) + present user, with: Entities::CollaboratorEntity, root: 'user' + end + end + namespace :create do + desc 'create and add user to my collabration' + params do + requires :lastName, type: String + requires :firstName, type: String + optional :email, type: String + requires :department, type: String + requires :organization, type: String + requires :country, type: String + end + post do + attributes = {} #declared(params, include_missing: false) + attributes[:first_name] = params[:firstName] + attributes[:last_name] = params[:lastName] + attributes[:type] = 'Collaborator' + attributes[:confirmed_at] = DateTime.now + attributes[:name_abbreviation] = "c#{SecureRandom.random_number(9999)}" + attributes[:password] = Devise.friendly_token.first(8) + attributes[:email] = "#{current_user.name_abbreviation}.#{attributes[:name_abbreviation]}@chemotion.net" + new_user = User.create!(attributes) + new_user.profile.update!({data: {}}) + new_user.affiliations = [Affiliation.find_or_create_by(country: params[:country], + organization: params[:organization], department: params[:department])] + + new_author = UsersCollaborator.create({ user_id: current_user.id, collaborator_id: new_user.id }) + present new_user, with: Entities::CollaboratorEntity, root: 'user' + end + end + end + resource :groups do rescue_from ActiveRecord::RecordInvalid do |error| message = error.record.errors.messages.map { |attr, msg| diff --git a/app/api/entities/collaborator_entity.rb b/app/api/entities/collaborator_entity.rb new file mode 100644 index 000000000..0e6c469d3 --- /dev/null +++ b/app/api/entities/collaborator_entity.rb @@ -0,0 +1,6 @@ +module Entities + class CollaboratorEntity < Grape::Entity + expose :id, :name, :initials, :email, :type + expose :affiliations + end + end diff --git a/app/api/entities/collection_sync_entity.rb b/app/api/entities/collection_sync_entity.rb index 83074f082..8897387e4 100644 --- a/app/api/entities/collection_sync_entity.rb +++ b/app/api/entities/collection_sync_entity.rb @@ -42,6 +42,9 @@ class CollectionSyncEntity < Grape::Entity expose :sharer do |obj| obj['temp_sharer'] end + expose :is_public do |obj| + obj['shared_by'] && obj['shared_by']['initials'] == 'CI' + end expose :children, as: 'children', using: Entities::CollectionSyncEntity end end diff --git a/app/api/entities/container_entity.rb b/app/api/entities/container_entity.rb index 7e72061e5..b84a4d1be 100644 --- a/app/api/entities/container_entity.rb +++ b/app/api/entities/container_entity.rb @@ -1,6 +1,16 @@ module Entities class ContainerEntity < Grape::Entity expose :big_tree, merge: true + expose :dataset_doi + # expose :doi, if: -> (obj, opts) { obj.respond_to? :doi} + + def dataset_doi + object.full_doi + end + + def pub_id + object.publication&.id + end def big_tree(container = object) dataset_ids = {} @@ -10,11 +20,15 @@ def big_tree(container = object) ## mapping analysis element as['children'] = c2s.map do |c2, c3s| a = c2.attributes.slice('id', 'container_type', 'name', 'description') + a['dataset_doi'] = c2.full_doi if c2.respond_to? :full_doi + a['pub_id'] = c2.publication&.id if c2.respond_to? :publication a['extended_metadata'] = get_extended_metadata(c2) dids = [] ## mapping datasets a['children'] = c3s.map do |c3, _| ds = c3.attributes.slice('id', 'container_type', 'name', 'description') + ds['dataset_doi'] = c3.full_doi if c3.respond_to? :full_doi + ds['pub_id'] = c3.publication&.id if c3.respond_to? :publication dids << ds['id'] ds['extended_metadata'] = get_extended_metadata(c3) ds @@ -30,6 +44,8 @@ def big_tree(container = object) code_logs = CodeLog.where(source_id: dataset_ids.keys, source: 'container').to_a bt.dig('children', 0, 'children')&.each do |analysis| + analysis['dataset_doi'] = analysis.full_doi if analysis.respond_to? :full_doi + analysis['pub_id'] = analysis.publication&.id if analysis.respond_to? :publication analysis['preview_img'] = preview_img(dataset_ids[analysis['id']], attachments) analysis['code_log'] = code_logs.find { |cl| cl.source_id == analysis['id'] }.attributes analysis['children'].each do |dataset| diff --git a/app/api/entities/doi_entity.rb b/app/api/entities/doi_entity.rb new file mode 100644 index 000000000..d636fa127 --- /dev/null +++ b/app/api/entities/doi_entity.rb @@ -0,0 +1,5 @@ +module Entities + class DoiEntity < Grape::Entity + expose :id, :inchikey, :suffix, :full_doi + end + end diff --git a/app/api/entities/publication_entity.rb b/app/api/entities/publication_entity.rb new file mode 100644 index 000000000..b6a049aeb --- /dev/null +++ b/app/api/entities/publication_entity.rb @@ -0,0 +1,24 @@ +module Entities + class PublicationEntity < Grape::Entity + expose :element_id, :element_type, :taggable_data + expose :svg + expose :analysis_type + expose :children, as: 'children', using: Entities::PublicationEntity + expose :dois + + def dois + # od = object.descendants&.sort { |x, y| y.id <=> x.id } + # dois = od&.map{ |o| o.taggable_data['doi'] } + + ([object] + object.descendants)&.map{ |o| o.doi.full_doi } + end + def svg + s = object.element&.reaction_svg_file if object.element_type == 'Reaction' + s = object.element&.sample_svg_file if object.element_type == 'Sample' + s + end + def analysis_type + analysis_type = object.element&.extended_metadata['kind'] if object.element_type == 'Container' + end + end +end diff --git a/app/api/entities/reaction_entity.rb b/app/api/entities/reaction_entity.rb new file mode 100644 index 000000000..edb572d72 --- /dev/null +++ b/app/api/entities/reaction_entity.rb @@ -0,0 +1,21 @@ +module Entities + class ReactionEntity < Grape::Entity + expose :id, documentation: { type: "Integer", desc: "Reaction's unique id"} + expose :name, :short_label, :description, :publication, :reaction_svg_file, + :rinchi_long_key, :rinchi_short_key, :rinchi_string + expose :rinchi_web_key, if: -> (obj, opts) { obj.respond_to? :rinchi_web_key} + # expose :analysis_samples_reactions, if: -> (obj, opts) { obj.respond_to? :analysis_samples_reactions} + expose :container, using: Entities::ContainerEntity + expose :products, using: Entities::SampleEntity + expose :doi, using: Entities::DoiEntity + expose :status, if: -> (obj, opts) { obj.respond_to? :status} + expose :tlc_description, if: -> (obj, opts) { obj.respond_to? :tlc_description} + expose :tlc_solvents, if: -> (obj, opts) { obj.respond_to? :tlc_solvents} + expose :temperature, if: -> (obj, opts) { obj.respond_to? :temperature} + expose :timestamp_start, if: -> (obj, opts) { obj.respond_to? :timestamp_start} + expose :timestamp_stop, if: -> (obj, opts) { obj.respond_to? :timestamp_stop} + expose :observation, if: -> (obj, opts) { obj.respond_to? :observation} + expose :rf_value, if: -> (obj, opts) { obj.respond_to? :rf_value} + expose :duration + end +end diff --git a/app/api/entities/sample_entity.rb b/app/api/entities/sample_entity.rb index 5b44b7cd9..cc9f8b7fc 100644 --- a/app/api/entities/sample_entity.rb +++ b/app/api/entities/sample_entity.rb @@ -3,6 +3,8 @@ class SampleEntity < Entities::SampleAttrEntity expose :molecule expose :container, using: Entities::ContainerEntity expose :tag + expose :publication + expose :doi, using: Entities::DoiEntity expose :residues expose :elemental_compositions, using: Entities::ElementalCompositionEntity diff --git a/app/api/entities/user_entity.rb b/app/api/entities/user_entity.rb index 87f755866..b4d417ee7 100644 --- a/app/api/entities/user_entity.rb +++ b/app/api/entities/user_entity.rb @@ -17,6 +17,17 @@ class UserEntity < Grape::Entity expose :is_templates_moderator, documentation: { type: "Boolean", desc: "ketcherails template administrator" } expose :molecule_editor, documentation: { type: 'Boolean', desc: 'molecule administrator' } expose :account_active, documentation: { type: 'Boolean', desc: 'User Account Active or Inactive' } + expose :affiliations + expose :is_article_editor, :is_howto_editor + + def affiliations + a = {} + object.affiliations.select( + 'id', + 'affiliations.department || chr(44)|| chr(32) || affiliations.organization || chr(44)|| chr(32) || affiliations.country as aff' + ).reduce(a){|acc, affiliation| a[affiliation.id] = affiliation.aff} + a + end def samples_count object.counters['samples'].to_i diff --git a/app/api/helpers/collection_helpers.rb b/app/api/helpers/collection_helpers.rb index 12d9697f4..fb57ef1f8 100644 --- a/app/api/helpers/collection_helpers.rb +++ b/app/api/helpers/collection_helpers.rb @@ -102,12 +102,17 @@ def fetch_source_collection_for_assign end def set_var(c_id = params[:collection_id], is_sync = params[:is_sync]) + public_col = !is_sync && [Collection.public_collection_id, Collection.scheme_only_reactions_collection_id].include?(params[:collection_id]) + if public_col + @c_id = params[:collection_id] if public_col + else @c_id = fetch_collection_id_w_current_user(c_id, is_sync) + end @c = Collection.find_by(id: @c_id) cu_id = current_user&.id @is_owned = cu_id && ((@c.user_id == cu_id && !@c.is_shared) || @c.shared_by_id == cu_id) - @dl = { + @dl ||= { permission_level: 10, sample_detail_level: 10, reaction_detail_level: 10, @@ -116,7 +121,7 @@ def set_var(c_id = params[:collection_id], is_sync = params[:is_sync]) researchplan_detail_level: 10, } - @dl = detail_level_for_collection(c_id, is_sync) unless @is_owned + @dl = detail_level_for_collection(c_id, is_sync) unless @is_owned || [Collection.public_collection_id, Collection.scheme_only_reactions_collection_id].include?(@c_id) @pl = @dl[:permission_level] @dl_s = @dl[:sample_detail_level] @dl_r = @dl[:reaction_detail_level] @@ -124,4 +129,27 @@ def set_var(c_id = params[:collection_id], is_sync = params[:is_sync]) @dl_sc = @dl[:screen_detail_level] @dl_rp = @dl[:researchplan_detail_level] end + + def check_params_collection_id + params[:collection_id] = case params[:collection_id] + when 'public' + Collection.public_collection_id + when 'schemeOnly' + Collection.scheme_only_reactions_collection_id + else + params[:collection_id] + end + end + + def set_var_for_unsigned_user + params[:is_sync] = false + @dl = { + permission_level: 0, + sample_detail_level: 10, + reaction_detail_level: 10, + wellplate_detail_level: 0, + screen_detail_level: 0, + researchplan_detail_level: 0 + } + end end diff --git a/app/api/helpers/report_helpers.rb b/app/api/helpers/report_helpers.rb index 7e30ae956..5692d154d 100644 --- a/app/api/helpers/report_helpers.rb +++ b/app/api/helpers/report_helpers.rb @@ -209,6 +209,7 @@ def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) , res.residue_type, s.molfile_version , s.stereo->>'abs' as "stereo_abs", s.stereo->>'rel' as "stereo_rel" , #{columns} + , ets.taggable_data#>>'{publication,doi}' as "doi" from ( select s.id as s_id @@ -231,6 +232,7 @@ def build_sql_sample_sample(columns, c_id, ids, checkedAll = false) left join molecules m on s.molecule_id = m.id left join molecule_names mn on s.molecule_name_id = mn.id left join residues res on res.sample_id = s.id + left join element_tags ets on ets.taggable_type = 'Sample' and ets.taggable_id = s.id order by #{order}; SQL end @@ -263,6 +265,7 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) , anac.extended_metadata->'content' as "content" , anac.extended_metadata->'status' as "status" , clg.id as uuid + , ets.taggable_data#>>'{publication,analysis_doi}' as "doi" , (select array_to_json(array_agg(row_to_json(dataset))) from ( select datc."name" as "dataset name" @@ -284,6 +287,7 @@ def build_sql_sample_analyses(columns, c_id, ids, checkedAll = false) inner join container_hierarchies ch on cont.id = ch.ancestor_id and ch.generations = 2 inner join containers anac on anac.id = ch.descendant_id left join code_logs clg on clg."source" = 'container' and clg.source_id = anac.id + left join element_tags ets on ets.taggable_type = 'Container' and ets.taggable_id = anac.id where cont.containable_type = '#{cont_type}' and cont.containable_id = #{t}.id ) analysis ) as analyses diff --git a/app/api/helpers/repository_helpers.rb b/app/api/helpers/repository_helpers.rb new file mode 100644 index 000000000..e5be04f9e --- /dev/null +++ b/app/api/helpers/repository_helpers.rb @@ -0,0 +1,123 @@ +module RepositoryHelpers + extend Grape::API::Helpers + + def check_repo_review_permission(element) + return true if User.reviewer_ids&.include? current_user.id + pub = Publication.find_by(element_id: element.id, element_type: element.class.name) + return false if pub.nil? + return true if pub && pub.published_by == current_user.id && ( pub.state == Publication::STATE_REVIEWED || pub.state == Publication::STATE_PENDING) + return false + end + + def repo_review_level(id, type) + return 3 if User.reviewer_ids&.include? current_user.id + pub = Publication.find_by(element_id: id, element_type: type.classify) + return 0 if pub.nil? + return 2 if pub.published_by === current_user.id + sync_cols = pub.element.sync_collections_users.where(user_id: current_user.id) + return 1 if (sync_cols&.length > 0) + return 0 + end + + def get_literature(id, type, cat='public') + literatures = Literature.by_element_attributes_and_cat(id, type.classify, cat) + .joins("inner join users on literals.user_id = users.id") + .select( + <<~SQL + literatures.* , literals.element_type, + json_object_agg(users.name_abbreviation, users.first_name || chr(32) || users.last_name) as ref_added_by + SQL + ).group('literatures.id, literals.element_type').as_json + literatures + end + + def get_reaction_table(id) + schemeAll = ReactionsSample.where('reaction_id = ? and type != ?', id, 'ReactionsPurificationSolventSample') + .joins(:sample) + .joins("inner join molecules on samples.molecule_id = molecules.id") + .select( + <<~SQL + reactions_samples.id, + molecules.iupac_name, molecules.sum_formular, + molecules.molecular_weight, + samples.real_amount_value, samples.real_amount_unit, + samples.target_amount_value, samples.target_amount_unit, + samples.purity, samples.density, samples.external_label, + samples.molarity_value, samples.molarity_unit, + reactions_samples.equivalent,reactions_samples.scheme_yield, + reactions_samples."position" as rs_position, + case when reactions_samples."type" = 'ReactionsStartingMaterialSample' then 'starting_materials' + when reactions_samples."type" = 'ReactionsReactantSample' then 'reactants' + when reactions_samples."type" = 'ReactionsProductSample' then 'products' + when reactions_samples."type" = 'ReactionsSolventSample' then 'solvents' + when reactions_samples."type" = 'ReactionsPurificationSolventSample' then 'purification_solvents' + else reactions_samples."type" + end mat_group, + case when reactions_samples."type" = 'ReactionsStartingMaterialSample' then 1 + when reactions_samples."type" = 'ReactionsReactantSample' then 2 + when reactions_samples."type" = 'ReactionsProductSample' then 3 + when reactions_samples."type" = 'ReactionsSolventSample' then 4 + when reactions_samples."type" = 'ReactionsPurificationSolventSample' then 5 + else 6 + end type_seq + SQL + ).order('reactions_samples.position ASC').as_json + + schemeSorted = schemeAll.sort_by {|o| o['type_seq']} + + solvents_sum = schemeAll.select{ |d| d['mat_group'] === 'solvents'}.sum { |r| + value = r['real_amount_value'].nil? ? r['target_amount_value'].to_f : r['real_amount_value'].to_f + unit = r['real_amount_value'].nil? ? r['target_amount_unit'] : r['real_amount_unit'] + + has_molarity = !r['molarity_value'].nil? && r['molarity_value'] > 0.0 && (r['density'] === 0.0) || false + has_density = !r['density'].nil? && r['density'] > 0.0 && (r['molarity_value'] === 0.0) || false + + molarity = r['molarity_value'] && r['molarity_value'].to_f || 1.0 + density = r['density'] && r['density'].to_f || 1.0 + purity = r['purity'] && r['purity'].to_f || 1.0 + molecular_weight = r['molecular_weight'] && r['molecular_weight'].to_f || 1.0 + + r['amount_g'] = unit === 'g'? value : unit === 'mg'? value.to_f / 1000.0 : unit === 'mol' ? (value / purity) * molecular_weight : unit === 'l' && !has_molarity && !has_density ? 0 : has_molarity ? value * molarity * molecular_weight : value * density * 1000 + r['amount_l'] = unit === 'l'? value : !has_molarity && !has_density ? 0 : has_molarity ? (r['amount_g'].to_f * purity) / (molarity * molecular_weight) : has_density ? r['amount_g'].to_f / (density * 1000) : 0 + r['amount_l'].nil? ? 0 : r['amount_l'].to_f + } + + schemeList = [] + schemeList = schemeSorted.map do |r| + scheme = {} + value = r['real_amount_value'].nil? ? r['target_amount_value'].to_f : r['real_amount_value'].to_f + unit = r['real_amount_value'].nil? ? r['target_amount_unit'] : r['real_amount_unit'] + + has_molarity = !r['molarity_value'].nil? && r['molarity_value'] > 0.0 && (r['density'] === 0.0) || false + has_density = !r['density'].nil? && r['density'] > 0.0 && (r['molarity_value'] === 0.0) || false + + molarity = r['molarity_value'] && r['molarity_value'].to_f || 1.0 + density = r['density'] && r['density'].to_f || 1.0 + purity = r['purity'] && r['purity'].to_f || 1.0 + molecular_weight = r['molecular_weight'] && r['molecular_weight'].to_f || 1.0 + r['amount_g'] = unit === 'g'? value : unit === 'mg'? value.to_f / 1000.0 : unit === 'mol' ? (value / purity) * molecular_weight : unit === 'l' && !has_molarity && !has_density ? 0 : has_molarity ? value * molarity * molecular_weight : value * density * 1000 + r['amount_l'] = unit === 'l'? value : !has_molarity && !has_density ? 0 : has_molarity ? (r['amount_g'].to_f * purity) / (molarity * molecular_weight) : has_density ? r['amount_g'].to_f / (density * 1000) : 0 + + if r['mat_group'] === 'solvents' + r['equivalent'] = r['amount_l'] / solvents_sum + else + r['amount_mol'] = unit === 'mol'? value : has_molarity ? r['amount_l'] * molarity : r['amount_g'].to_f * purity / molecular_weight + r['dmv'] = !has_molarity && !has_density ? '- / -' : has_density ? + density.to_s + ' / - ' : ' - / ' + molarity.to_s + r['molarity_unit'] + end + + r.delete('real_amount_value'); + r.delete('real_amount_unit'); + r.delete('target_amount_value'); + r.delete('target_amount_unit'); + r.delete('molarity_value'); + r.delete('molarity_unit'); + r.delete('purity'); + r.delete('molecular_weight'); + r.delete('rs_position'); + r.delete('density'); + r + end + schemeList + end + +end diff --git a/app/api/helpers/submission_helpers.rb b/app/api/helpers/submission_helpers.rb new file mode 100644 index 000000000..6062acfe6 --- /dev/null +++ b/app/api/helpers/submission_helpers.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# A helper for submission +module SubmissionHelpers + extend Grape::API::Helpers + + def ols_validation(analyses) + analyses.each do |ana| + error!('analyses check fail', 404) if (ana.extended_metadata['kind'].match /^\w{3,4}\:\d{6,7}\s\|\s\w+/).nil? + end + end + + def coauthor_validation(coauthors) + coauthor_ids = [] + coauthors.each do |coa| + val = coa.strip + p = User.where(type: %w[Person Collaborator]).where.not(confirmed_at: nil).where('id = ? or email = ?', val.to_i, val.to_s).first + error!('invalid co-author: ' + val.to_s, 404) if p.nil? + coauthor_ids << p.id + end + coauthor_ids + end +end diff --git a/app/assets/javascripts/admin/AdminHome.js b/app/assets/javascripts/admin/AdminHome.js index 283fdbc15..13213326b 100644 --- a/app/assets/javascripts/admin/AdminHome.js +++ b/app/assets/javascripts/admin/AdminHome.js @@ -1,6 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Grid, Row, Col, Nav, NavItem } from 'react-bootstrap'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; import AdminNavigation from './AdminNavigation'; import Notifications from '../components/Notifications'; import AdminDashboard from './AdminDashboard'; @@ -170,7 +172,8 @@ class AdminHome extends React.Component { ); } } +const AdminHomeWithDnD = DragDropContext(HTML5Backend)(AdminHome); document.addEventListener('DOMContentLoaded', () => { const domElement = document.getElementById('AdminHome'); - if (domElement) { ReactDOM.render(, domElement); } + if (domElement) { ReactDOM.render(, domElement); } }); diff --git a/app/assets/javascripts/affiliations.js b/app/assets/javascripts/affiliations.js index 3f23eaf47..f7c861d14 100644 --- a/app/assets/javascripts/affiliations.js +++ b/app/assets/javascripts/affiliations.js @@ -32,7 +32,6 @@ $(function() { attachAutoComplete("countries", "country-select") attachAutoComplete("organizations", "organization-select") attachAutoComplete("departments", "department-select") - attachAutoComplete("groups", "group-select") }); $("input#user_email").focusout( diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js index f9da1f3fc..5624f192c 100644 --- a/app/assets/javascripts/components.js +++ b/app/assets/javascripts/components.js @@ -8,6 +8,10 @@ var ChemScanner = require('./components/chemscanner/ChemScanner'); var ChemSpectra = require('./components/chemspectra/ChemSpectra'); var ChemSpectraEditor = require('./components/chemspectra/ChemSpectraEditor'); var MoleculeModerator = require('./components/MoleculeModerator'); +var RepoNewsEditor = require('./libHome/RepoNewsEditor'); +var RepoNewsReader = require('./libHome/RepoNewsReader'); +var RepoHowToEditor = require('./libHome/RepoHowToEditor'); +var RepoHowToReader = require('./libHome/RepoHowToReader'); var App = require('./components/App'); //= require_self diff --git a/app/assets/javascripts/components/CollectionSubtree.js b/app/assets/javascripts/components/CollectionSubtree.js index 0ef9d3705..09461a582 100644 --- a/app/assets/javascripts/components/CollectionSubtree.js +++ b/app/assets/javascripts/components/CollectionSubtree.js @@ -11,7 +11,27 @@ import UserInfos from './UserInfos'; import GatePushBtn from './common/GatePushBtn' import { collectionShow, scollectionShow } from './routesUtils'; - +const labeling = (label) => { + if (typeof (label) === 'string' && (label.startsWith('Reviewing') || label.startsWith('Element To Review') || label.startsWith('Reviewed'))) { + const ls = label.split(','); + if (ls.length >= 3) { + const sicon = ls[1].substr(1) === '0' ? '' : {ls[1].substr(1)}   ; + const ricon = ls[2].substr(1) === '0' ? '' : {ls[2].substr(1)}   ; + return label.startsWith('Reviewing') ? + ( +
+ {ls[0]}   {sicon} {ricon} +
+ ) : + ( +
+ {ls[0]}   {sicon} {ricon} +
+ ); + } + } + return label; +} export default class CollectionSubtree extends React.Component { constructor(props) { @@ -113,7 +133,6 @@ export default class CollectionSubtree extends React.Component { } } - expandButton() { let icon = this.state.visible ? 'minus' : 'plus'; @@ -146,12 +165,6 @@ export default class CollectionSubtree extends React.Component { } handleClick(e) { - const {fakeRoot} = this.props - if (fakeRoot) { - e.stopPropagation() - return - } - const { root } = this.state let {visible} = this.state const uiState = UIStore.getState() @@ -223,9 +236,10 @@ export default class CollectionSubtree extends React.Component { render() { - const {fakeRoot} = this.props - const {label, root} = this.state - let {visible} = this.state + const { root, visible } = this.state + let { label } = this.state + + label = labeling(label); let style if (!visible) { diff --git a/app/assets/javascripts/components/CollectionTree.js b/app/assets/javascripts/components/CollectionTree.js index 3af8effcf..caa823c5f 100644 --- a/app/assets/javascripts/components/CollectionTree.js +++ b/app/assets/javascripts/components/CollectionTree.js @@ -16,7 +16,7 @@ import UserInfos from './UserInfos'; import DeviceBox from './inbox/DeviceBox'; import UnsortedBox from './inbox/UnsortedBox'; -const colVisibleTooltip = Toggle own collections; +const colVisibleTooltip = Toggle My Collections; export default class CollectionTree extends React.Component { constructor(props) { @@ -37,8 +37,9 @@ export default class CollectionTree extends React.Component { syncCollectionVisible: false, inbox: inboxState.inbox, numberOfAttachments: inboxState.numberOfAttachments, - inboxVisible: false - }; + inboxVisible: false, + syncChemotionVisible: true, + } this.onChange = this.onChange.bind(this); this.onClickInbox = this.onClickInbox.bind(this); @@ -48,7 +49,7 @@ export default class CollectionTree extends React.Component { CollectionStore.listen(this.onChange); InboxStore.listen(this.onChange); CollectionActions.fetchLockedCollectionRoots(); - CollectionActions.fetchUnsharedCollectionRoots(); + // CollectionActions.fetchUnsharedCollectionRoots(); CollectionActions.fetchSharedCollectionRoots(); CollectionActions.fetchRemoteCollectionRoots(); CollectionActions.fetchSyncInCollectionRoots(); @@ -93,11 +94,25 @@ export default class CollectionTree extends React.Component { return newRoots; } + publicRoots(roots, preservePublic) { + let newRoots =[] + roots.forEach((root) => { + if(preservePublic) { + if (root.is_public) newRoots.push(root) + } else { + if (!root.is_public) newRoots.push(root) + } + }) + + return newRoots + } + unsharedSubtrees() { let roots = this.state.unsharedRoots; - roots = roots.filter(function(item) { return !item.isNew}) + // roots = roots.filter(function(item) { return !item.isNew}) - return this.subtrees(roots, null, false); + // return this.subtrees(roots, null, false); + return null } sharedSubtrees() { @@ -182,6 +197,7 @@ export default class CollectionTree extends React.Component { remoteSyncInSubtrees() { let {syncInRoots, syncCollectionVisible} = this.state syncInRoots = this.removeOrphanRoots(syncInRoots) + syncInRoots = this.publicRoots(syncInRoots, false) let labelledRoots = syncInRoots.map(e => { return update(e, {label: {$set: @@ -207,6 +223,43 @@ export default class CollectionTree extends React.Component { false, syncCollectionVisible) } + publicSubtrees() { + let {syncInRoots, syncChemotionVisible} = this.state + syncInRoots = this.removeOrphanRoots(syncInRoots) + syncInRoots = this.publicRoots(syncInRoots, true) + + let orderedRoots = [] + if (syncInRoots && syncInRoots[0] && syncInRoots[0].children) { + syncInRoots[0].children.map((e,idx) => { + if (e.label.match(/hemotion/) ) { + orderedRoots[0] = e; + orderedRoots[0].label = 'Chemotion'; + } else if (typeof e.label === 'string' && e.label === 'Scheme-only reactions') { + orderedRoots[1] = e; + } else if (e.label.match(/Published Elements/)) { + orderedRoots[2] = e + orderedRoots[2].label = 'My Published Elements' + } else if (e.label === 'Pending Publications') {orderedRoots[3] = e } + else if (typeof e.label === 'string' && e.label.startsWith('Reviewing')) {orderedRoots[4] = e } + else if (typeof e.label === 'string' && e.label.startsWith('Element To Review')) {orderedRoots[5] = e } + else if (typeof e.label === 'string' && e.label.startsWith('Reviewed')) { orderedRoots[6] = e } + else if (e.label === 'Embargoed Publications') {orderedRoots[7] = e } + else {orderedRoots[idx+10] = e } + }) + } + + let subTreeLabels = ( +
+
this.setState({syncChemotionVisible: !syncChemotionVisible})} + > +
+
+ ) + + return this.subtrees(orderedRoots, subTreeLabels, + true, syncChemotionVisible) + } labelRoot(sharedToOrBy, rootCollection) { let shared = rootCollection[sharedToOrBy] @@ -228,7 +281,12 @@ export default class CollectionTree extends React.Component { subtrees(roots, label, isRemote, visible = true) { let subtrees = roots.map((root, index) => { - return + return }) let subtreesVisible = visible ? "" : "none" @@ -244,7 +302,7 @@ export default class CollectionTree extends React.Component { collectionManagementButton() { return ( -
+
@@ -575,6 +577,7 @@ class Material extends Component { @@ -588,6 +591,7 @@ class Material extends Component { active style={style} onClick={() => this.toggleTarget(isTarget)} + disabled={this.props.reaction.is_published == true} bsStyle={isTarget ? 'success' : 'primary'} bsSize="small" >{isTarget ? 't' : 'r'} diff --git a/app/assets/javascripts/components/MaterialGroup.js b/app/assets/javascripts/components/MaterialGroup.js index 8e89a3d2e..7bbc9b3ac 100644 --- a/app/assets/javascripts/components/MaterialGroup.js +++ b/app/assets/javascripts/components/MaterialGroup.js @@ -11,6 +11,7 @@ import Reaction from './models/Reaction'; import { defaultMultiSolventsSmilesOptions } from './staticDropdownOptions/options'; import { ionic_liquids } from './staticDropdownOptions/ionic_liquids'; import { reagents_kombi } from './staticDropdownOptions/reagents_kombi'; +import { RequiredLabel } from '../libHome/RepoCommon'; const MaterialGroup = ({ materials, materialGroup, deleteMaterial, onChange, @@ -189,7 +190,7 @@ const GeneralMaterialGroup = ({ { isReactants && {reagentDd} } { !isReactants && {refTHead} } { !isReactants && {headers.tr} } - { !isReactants && {headers.amount} } + {!isReactants && {headers.amount}  } { !isReactants && } { !isReactants && } { showLoadingColumn && !isReactants && {headers.loading} } diff --git a/app/assets/javascripts/components/Navigation.js b/app/assets/javascripts/components/Navigation.js index 985a8f72d..56be00039 100644 --- a/app/assets/javascripts/components/Navigation.js +++ b/app/assets/javascripts/components/Navigation.js @@ -113,7 +113,8 @@ export default class Navigation extends React.Component { render() { const { modalProps, showAdvancedSearch } = this.state; const { profile } = UserStore.getState(); - const { customClass } = (profile && profile.data) || {}; + // const { customClass } = (profile && profile.data) || {}; + const customClass = 'btn-unified' return (this.state.currentUser ? @@ -132,7 +133,7 @@ export default class Navigation extends React.Component { : {this.navHeader()}
diff --git a/app/assets/javascripts/components/PubchemLabels.js b/app/assets/javascripts/components/PubchemLabels.js index 374364a7e..cba940964 100644 --- a/app/assets/javascripts/components/PubchemLabels.js +++ b/app/assets/javascripts/components/PubchemLabels.js @@ -8,9 +8,9 @@ const PubchemLabels = ({element}) =>{ marginLeft: "5px", marginRight: "5px" }; - if (!cid) {labelStyle.WebkitFilter = "grayscale(100%)"} + if (!cid || isNaN(cid)) {labelStyle.WebkitFilter = "grayscale(100%)"} const handleOnClick = (e) => { - if (!!cid){ + if (cid && !isNaN(cid)){ window.open("https://pubchem.ncbi.nlm.nih.gov/compound/" + cid, '_blank') } e.stopPropagation() diff --git a/app/assets/javascripts/components/PublishCommon.js b/app/assets/javascripts/components/PublishCommon.js new file mode 100644 index 000000000..1604f1ae5 --- /dev/null +++ b/app/assets/javascripts/components/PublishCommon.js @@ -0,0 +1,316 @@ +import React from 'react'; +import { + Button, + Tooltip, + OverlayTrigger, +} from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import Aviator from 'aviator'; +import Sample from './models/Sample'; +import { sampleShowOrNew, reactionShow } from './routesUtils'; +import Reaction from './models/Reaction'; +import { isNmrPass, isDatasetPass } from './utils/ElementUtils'; + +const labelStyle = { + display: 'inline-block', + marginLeft: '5px', + marginRight: '5px' +}; + +const handleClick = (e, id, clickType) => { + e.preventDefault(); + e.stopPropagation(); + const uri = Aviator.getCurrentURI(); + const uriArray = uri.split(/\//); + switch (clickType) { + case 'Reaction': + Aviator.navigate(`/${uriArray[1]}/${uriArray[2]}/reaction/${id}`, { silent: true }); + reactionShow({ params: { reactionID: id } }); + break; + + default: + Aviator.navigate(`/${uriArray[1]}/${uriArray[2]}/sample/${id}`, { silent: true }); + sampleShowOrNew({ params: { sampleID: id } }); + break; + } +}; + +const validateMolecule = (element) => { + const validates = []; + const sample = element; + const analyses = sample.analysisArray(); + analyses.forEach((al) => { + const status = al.extended_metadata.status || ''; + const kind = al.extended_metadata.kind || ''; + if (status !== 'Confirmed') { + validates.push({ name: `analysis [${al.name}]`, value: false, message: `[${sample.name || sample.short_label}] Analysis [${al.name}]: Status must be Confirmed.` }); + } + if (kind === '' || (kind.split('|').length < 2)) { + validates.push({ name: `analysis [${al.name}]`, value: false, message: `[${sample.name || sample.short_label}] Analysis [${al.name}]: Type is invalid.` }); + } + if (!isNmrPass(al, sample)) { + validates.push({ name: `analysis [${al.name}]`, value: false, message: `[${sample.name || sample.short_label}] Analysis [${al.name}]: Content is invalid, NMR check fails.` }); + } + if (!isDatasetPass(al)) { + validates.push({ name: `analysis [${al.name}]`, value: false, message: `[${sample.name || sample.short_label}] Analysis [${al.name}]: Dataset is incomplete. Please check that: 1. for NMR, Mass, or IR analyses, at least one dataset has been attached with an image and a jcamp files. 2. the instrument field is not empty.` }); + } + }); + return validates; +}; + +const PublishBtnReaction = ({ reaction, showModal }) => { + const tagData = (reaction.tag && reaction.tag.taggable_data) || {}; + // NB set publishedId to true to hide it + const publishedId = tagData.public_reaction || (tagData.publication && tagData.publication.queued_at); + const notPublishable = reaction.notPublishable; // false or [samples] + const isDisabled = reaction.changed || reaction.isNew || !!notPublishable; + const btnTip = (reaction.changed || reaction.isNew) ? 'Publication panel cannot be open on unsaved reaction.' : 'Open the reaction publication panel'; + const btnTipNotPub = notPublishable && `Product(s) ${notPublishable.map(s => s.short_label).join()} not publishable`; + return ( + (!publishedId && !tagData.publication) ? ( + {btnTipNotPub || btnTip}} + > + + + ) : + ); +}; + +PublishBtnReaction.propTypes = { + showModal: PropTypes.func.isRequired, + reaction: PropTypes.instanceOf(Reaction).isRequired, +}; + +const PublishBtn = ({ sample, showModal }) => { + const tagData = (sample.tag && sample.tag.taggable_data) || {}; + + const publishedId = tagData.public_sample; + const isPoly = sample._contains_residues; + + return (sample.can_publish && !sample.isEdited && !publishedId && !tagData.publication) ? ( + {isPoly ? 'Cannot publish polymer structure!' : 'Open the sample publication panel'}} + > + + + ) : ; +}; + +PublishBtn.propTypes = { + showModal: PropTypes.func.isRequired, + sample: PropTypes.instanceOf(Sample).isRequired, +}; + +const ReviewPublishBtn = ({ element, showComment, validation }) => { + const tagData = (element.tag && element.tag.taggable_data) || {}; + const publishedId = tagData.public_sample || tagData.public_reaction; + const isDecline = (tagData && tagData.decline === true) || false; + const canPublish = element.can_publish || (element.type === 'reaction' && !element.notPublishable && element.is_published === false) + + const isEdit = element.type === 'reaction' ? element.changed : element.isEdited; + const reviewBtn = (canPublish && !isEdit && !publishedId && tagData.publication) ? ( + Submit for Publication} + > + + + ) : (); + const commentBtn = ((canPublish && !publishedId && tagData.publication) || isDecline) ? ( + Reviewer's comment} + > + + + ) : () + return ( + + {reviewBtn} + {commentBtn} + + ) +}; + + +const PublishedTag = ({ element }) => { + const tag = (element && element.tag) || {}; + const tagData = (tag && tag.taggable_data) || {}; + const tagType = tag.taggable_type; + const isPending = (tagData && tagData.publish_pending && tagData.publish_pending === true) || false; + let tip = ''; + let publishedId; + switch (tagType) { + case 'Reaction': + publishedId = tagData.public_reaction; + if (isPending) { + tip = 'Reaction is being reviewed'; + } else { + tip = 'Reaction has been published'; + } + break; + + default: + publishedId = tagData.public_sample; + if (isPending) { + tip = 'Sample is being reviewed'; + } else { + tip = 'Sample has been published'; + } + break; + } + return ( + publishedId ? ( + {tip}} + > +