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(