diff --git a/.env.production.example b/.env.production.example index d1dc30f6a..eb4db7265 100644 --- a/.env.production.example +++ b/.env.production.example @@ -68,6 +68,3 @@ SENTRY_BACKEND_SAMPLE_RATE=0.5 SENTRY_FRONTEND_DSN=https://sentryserver/OTHER-ID SENTRY_FRONTEND_SAMPLE_RATE=1.0 - -## For REPO -MATOMO_URL=https://matomo.tld diff --git a/Gemfile b/Gemfile index 33db436a3..8ea2233f7 100644 --- a/Gemfile +++ b/Gemfile @@ -60,7 +60,7 @@ gem 'kaminari' gem 'kaminari-grape' gem 'ketcherails', git: 'https://github.com/complat/ketcher-rails.git', branch: 'upgrade-to-rails-6' -gem 'labimotion', '1.3.2' +gem 'labimotion', '1.4.0.1' gem 'mimemagic', '0.3.10' gem 'mime-types' diff --git a/Gemfile.lock b/Gemfile.lock index 387d94be4..8f92aaa64 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -447,7 +447,7 @@ GEM rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - labimotion (1.3.2) + labimotion (1.4.0.1) rails (~> 6.1.7) latex-decode (0.4.0) launchy (2.5.0) @@ -918,7 +918,7 @@ DEPENDENCIES kaminari kaminari-grape ketcherails! - labimotion (= 1.3.2) + labimotion (= 1.4.0.1) launchy listen memory_profiler diff --git a/app/api/chemotion/element_api.rb b/app/api/chemotion/element_api.rb index aee921953..7b8d247ca 100644 --- a/app/api/chemotion/element_api.rb +++ b/app/api/chemotion/element_api.rb @@ -83,12 +83,12 @@ class ElementAPI < Grape::API elements = @collection.send(element + 's').by_ui_state(params[element]) elements.each do |el| - pub = el.publication + pub = el.publication if el.respond_to?(:publication) next if pub.nil? pub.update_state(Publication::STATE_DECLINED) pub.process_element(Publication::STATE_DECLINED) - pub.inform_users(Publication::STATE_DECLINED, current_user.id) + pub.process_new_state_job(Publication::STATE_DECLINED, current_user.id) end deleted[element] = elements.destroy_all.map(&:id) diff --git a/app/api/chemotion/public_api.rb b/app/api/chemotion/public_api.rb index a6ae313df..f5e304308 100644 --- a/app/api/chemotion/public_api.rb +++ b/app/api/chemotion/public_api.rb @@ -31,13 +31,11 @@ class PublicAPI < Grape::API end end - desc 'Public initialization' - params do - end get 'initialize' do { - molecule_viewer: Matrice.molecule_viewer + molecule_viewer: Matrice.molecule_viewer, + u: Rails.configuration.u || {}, } end @@ -377,8 +375,9 @@ def query_embargo(name) 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 Embargo] + optional :adv_type, type: String, desc: 'advanced search type', values: %w[Authors Ontologies Embargo Label] optional :adv_val, type: Array[String], desc: 'advanced search value', regexp: /^(\d+|([[:alpha:]]+:\d+))$/ + optional :label_val, type: Integer, desc: 'label_val' optional :req_xvial, type: Boolean, default: false, desc: 'xvial is required or not' end paginate per_page: 10, offset: 0, max_per_page: 100 @@ -407,13 +406,18 @@ def query_embargo(name) SQL end end + if params[:adv_type] == 'Label' && params[:label_val].present? + label_search = <<~SQL + and pub.taggable_data->'user_labels' @> '#{params[:label_val]}' + SQL + end sample_join = <<~SQL INNER JOIN ( SELECT molecule_id, published_at max_published_at, sample_svg_file, id as sid 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 + WHERE pub.element_type='Sample' and pub.element_id=samples.id and pub.deleted_at ISNULL #{label_search} 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 @@ -465,8 +469,9 @@ def query_embargo(name) 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 Embargo] + optional :adv_type, type: String, desc: 'advanced search type', values: %w[Authors Ontologies Embargo Label] optional :adv_val, type: Array[String], desc: 'advanced search value', regexp: /^(\d+|([[:alpha:]]+:\d+))$/ + optional :label_val, type: Integer, desc: 'label_val' 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 @@ -518,6 +523,9 @@ def query_embargo(name) else col_scope = Collection.public_collection.reactions.joins(adv_search).joins(:publication).select(embargo_sql).order('publications.published_at desc') end + if params[:adv_type] == 'Label' && params[:label_val].present? + col_scope = col_scope.where("publications.taggable_data->'user_labels' @> '?'", params[:label_val]) + end reset_pagination_page(col_scope) list = paginate(col_scope) entities = Entities::ReactionPublicationListEntity.represent(list, serializable: true) @@ -574,8 +582,8 @@ def query_embargo(name) 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 } } } + { last_published: { sample: { id: sample.id, sample_svg_file: sample.sample_svg_file, molecule: sample.molecule, tag: s_pub.taggable_data, contributor: User.with_deleted.find(s_pub.published_by).name }, + reaction: { id: reaction.id, reaction_svg_file: reaction.reaction_svg_file, tag: r_pub.taggable_data, contributor: User.with_deleted.find(r_pub.published_by).name } } } end end @@ -638,7 +646,6 @@ def query_embargo(name) end resource :embargo do - helpers RepositoryHelpers desc "Return PUBLISHED serialized collection" params do requires :id, type: Integer, desc: "collection id" @@ -652,15 +659,18 @@ def query_embargo(name) resource :col_list do - helpers RepositoryHelpers after_validation do @embargo_collection = Collection.find(params[:collection_id]) @pub = @embargo_collection.publication error!('401 Unauthorized', 401) if @pub.nil? if @pub.state != 'completed' - error!('401 Unauthorized', 401) unless current_user.present? && (User.reviewer_ids.include?(current_user.id) || @pub.published_by == current_user.id || current_user.type == 'Anonymous') + is_reviewer = User.reviewer_ids.include?(current_user&.id) + is_submitter = (@pub.published_by == current_user&.id || @pub.review&.dig('submitters')&.include?(current_user&.id)) && SyncCollectionsUser.find_by(user_id: current_user.id, collection_id: @embargo_collection.id).present? + is_anonymous = current_user&.type == 'Anonymous' && SyncCollectionsUser.find_by(user_id: current_user.id, collection_id: @embargo_collection.id).present? + error!('401 Unauthorized', 401) unless current_user.present? && (is_reviewer || is_submitter || is_anonymous) end + @is_reviewer = User.reviewer_ids.include?(current_user&.id) end get do anasql = <<~SQL @@ -672,7 +682,7 @@ def query_embargo(name) elements = [] list.each do |e| element_type = e.element&.class&.name - u = User.find(e.published_by) unless e.published_by.nil? + u = User.with_deleted.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' @@ -685,13 +695,12 @@ def query_embargo(name) published_by: u&.name, submit_at: e.created_at, state: e.state, scheme_only: scheme_only, ana_cnt: e.ana_cnt ) end - is_reviewer = User.reviewer_ids.include?(current_user&.id) - { elements: elements, embargo: @pub, embargo_id: params[:collection_id], current_user: { id: current_user&.id, type: current_user&.type, is_reviewer: is_reviewer } } + + { elements: elements, embargo: @pub, embargo_id: params[:collection_id], current_user: { id: current_user&.id, type: current_user&.type, is_reviewer: @is_reviewer } } end end resource :col_element do - helpers RepositoryHelpers params do requires :collection_id, type: Integer, desc: "collection id" requires :el_id, type: Integer, desc: "element id" @@ -717,7 +726,6 @@ def query_embargo(name) end resource :reaction do - helpers RepositoryHelpers desc "Return PUBLISHED serialized reaction" params do requires :id, type: Integer, desc: "Reaction id" @@ -738,16 +746,16 @@ def query_embargo(name) end resource :molecule do - helpers RepositoryHelpers 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 Embargo] + optional :adv_type, type: String, desc: 'advanced search type', allow_blank: true, values: %w[Authors Ontologies Embargo Label] optional :adv_val, type: Array[String], desc: 'advanced search value', regexp: /^(\d+|([[:alpha:]]+:\d+))$/ + optional :label_val, type: Integer, desc: 'label_val' end get do - get_pub_molecule(params[:id], params[:adv_flag], params[:adv_type], params[:adv_val]) + get_pub_molecule(params[:id], params[:adv_flag], params[:adv_type], params[:adv_val], params[:label_val]) end end @@ -895,11 +903,33 @@ def query_embargo(name) error!('404 Is not published yet', 404) unless @publication&.state&.include?('completed') end get do - Base64.encode64(@attachment.read_thumbnail) if @attachment.thumb + if @attachment.thumb + thumbnail = @attachment.read_thumbnail + thumbnail ? Base64.encode64(thumbnail) : nil + else + nil + end end end end + resource :export_metadata do + desc 'Get dataset metadata of publication' + params do + requires :id, type: Integer, desc: "Dataset Id" + end + before do + @dataset_id = params[:id] + @container = Container.find_by(id: @dataset_id) + element = @container.root.containable + @publication = Publication.find_by(element: element, state: 'completed') if element.present? + error!('404 Publication not found', 404) unless @publication.present? + end + get do + prepare_and_export_dataset(@container.id) + end + end + resource :metadata do desc "batch download metadata" params do @@ -916,11 +946,11 @@ def query_embargo(name) result = declared(params, include_missing: false) list = [] limit = params[:limit] - params[:offset] > 1000 ? params[:offset] + 1000 : params[:limit] - scope = Publication.where(element_type: params[:type], state: 'completed') + scope = Publication.includes(:doi).where(element_type: params[:type], state: 'completed') scope = scope.where('published_at >= ?', params[:date_from]) if params[:date_from].present? scope = scope.where('published_at <= ?', params[:date_to]) if params[:date_to].present? publications = scope.order(:published_at).offset(params[:offset]).limit(limit) - publications.map do |publication| + publications.each do |publication| inchikey = publication&.doi&.suffix list.push("#{service_url}#{api_url}#{inchikey}") if inchikey.present? end @@ -929,23 +959,6 @@ def query_embargo(name) result end - resource :export do - desc 'Get dataset metadata of publication' - params do - requires :id, type: Integer, desc: "Dataset Id" - end - before do - @dataset_id = params[:id] - @container = Container.find_by(id: @dataset_id) - element = @container.root.containable - @publication = Publication.find_by(element: element, state: 'completed') if element.present? - error!('404 Publication not found', 404) unless @publication.present? - end - get do - prepare_and_export_dataset(@container.id) - end - end - desc "metadata of publication" params do optional :id, type: Integer, desc: "Id" @@ -1001,15 +1014,15 @@ def query_embargo(name) end end - resource :service do - desc 'convert molfile to 3d' + resource :represent do + desc 'represent molfile structure' params do - requires :molfile, type: String, desc: 'Molecule molfile' + requires :mol, type: String, desc: 'Molecule molfile' end - post :convert do - convert_to_3d(params[:molfile]) + post :structure do + represent_structure(params[:mol]) rescue StandardError => e - return { msg: { level: 'error', message: e } } + return { molfile: params[:mol], msg: { level: 'error', message: e } } end end end diff --git a/app/api/chemotion/reaction_api.rb b/app/api/chemotion/reaction_api.rb index b20f88ff2..4bdc7b848 100644 --- a/app/api/chemotion/reaction_api.rb +++ b/app/api/chemotion/reaction_api.rb @@ -9,6 +9,7 @@ class ReactionAPI < Grape::API helpers ParamsHelpers helpers LiteratureHelpers helpers ProfileHelpers + helpers UserLabelHelpers resource :reactions do desc 'Return serialized reactions' @@ -17,6 +18,7 @@ class ReactionAPI < Grape::API optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id' optional :from_date, type: Integer, desc: 'created_date from in ms' optional :to_date, type: Integer, desc: 'created_date to in ms' + optional :user_label, type: Integer, desc: 'user label' optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at' optional :sort_column, type: String, desc: 'sort by created_at, updated_at, rinchi_short_key, or rxno', values: %w[created_at updated_at rinchi_short_key rxno], @@ -52,17 +54,19 @@ class ReactionAPI < Grape::API from = params[:from_date] to = params[:to_date] + user_label = params[:user_label] by_created_at = params[:filter_created_at] || false sort_column = params[:sort_column].presence || 'created_at' sort_direction = params[:sort_direction].presence || (%w[created_at updated_at].include?(sort_column) ? 'DESC' : 'ASC') - scope = scope.includes_for_list_display.order("#{sort_column} #{sort_direction}") + scope = scope.includes_for_list_display.order("reactions.#{sort_column} #{sort_direction}") scope = scope.created_time_from(Time.at(from)) if from && by_created_at scope = scope.created_time_to(Time.at(to) + 1.day) if to && by_created_at scope = scope.updated_time_from(Time.at(from)) if from && !by_created_at scope = scope.updated_time_to(Time.at(to) + 1.day) if to && !by_created_at + scope = scope.by_user_label(user_label) if user_label reset_pagination_page(scope) @@ -158,11 +162,11 @@ class ReactionAPI < Grape::API requires :materials, type: Hash optional :literatures, type: Hash - requires :container, type: Hash optional :duration, type: String optional :rxno, type: String optional :segments, type: Array + optional :user_labels, type: Array optional :variations, type: [Hash] end route_param :id do @@ -175,6 +179,8 @@ class ReactionAPI < Grape::API put do reaction = @reaction attributes = declared(params, include_missing: false) + update_element_labels(reaction, attributes[:user_labels], current_user.id) + attributes.delete(:user_labels) materials = attributes.delete(:materials) attributes.delete(:literatures) attributes.delete(:id) @@ -224,6 +230,7 @@ class ReactionAPI < Grape::API optional :origin, type: Hash optional :reaction_svg_file, type: String optional :segments, type: Array + optional :user_labels, type: Array requires :materials, type: Hash optional :literatures, type: Hash requires :container, type: Hash @@ -241,6 +248,7 @@ class ReactionAPI < Grape::API container_info = params[:container] attributes.delete(:container) attributes.delete(:segments) + attributes.delete(:user_labels) collection = current_user.collections.where(id: collection_id).take attributes[:created_by] = current_user.id @@ -275,6 +283,7 @@ class ReactionAPI < Grape::API end reaction.container = update_datamodel(container_info) reaction.save! + update_element_labels(reaction, params[:user_labels], current_user.id) reaction.save_segments(segments: params[:segments], current_user_id: current_user.id) CollectionsReaction.create(reaction: reaction, collection: collection) if collection.present? diff --git a/app/api/chemotion/repository_api.rb b/app/api/chemotion/repository_api.rb index 5b460f35b..1d3c24933 100644 --- a/app/api/chemotion/repository_api.rb +++ b/app/api/chemotion/repository_api.rb @@ -5,479 +5,77 @@ module Chemotion # Repository API class RepositoryAPI < Grape::API include Grape::Kaminari - helpers ContainerHelpers - helpers ParamsHelpers - helpers CollectionHelpers - helpers SampleHelpers - helpers SubmissionHelpers - helpers EmbargoHelpers + helpers RepoParamsHelpers + helpers RepositoryHelpers namespace :repository do - helpers do - 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&.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') - - new_dataset.name = ds.name - new_dataset.extended_metadata = ds.extended_metadata - new_dataset.save! - clone_attachs = ds.attachments - Usecases::Attachments::Copy.execute!(clone_attachs, new_dataset, current_user.id) if clone_attachs.present? - 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.reprocess_svg if new_sample.sample_svg_file.blank? - 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! - new_sample.copy_segments(segments: sample.segments, current_user_id: current_user.id) if sample.segments - duplicate_residues(new_sample, sample) if sample.residues - duplicate_elemental_compositions(new_sample, sample) if sample.elemental_compositions - duplicate_user_labels(new_sample, sample) ## if sample.tag.taggable_data['user_labels'] - 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! - new_reaction.copy_segments(segments: reaction.segments, current_user_id: current_user.id) - 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(Arel.sql("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 do |author| - { - 'givenName' => author.first_name, - 'familyName' => author.last_name, - 'name' => author.name, - 'ORCID' => author.orcid, - 'affiliationIds' => author.current_affiliations.map(&:id), - 'id' => author.id - } - end, - contributors: { - 'givenName' => contributor.first_name, - 'familyName' => contributor.last_name, - 'name' => contributor.name, - 'ORCID' => contributor.orcid, - 'affiliations' => contributor.current_affiliations.map(&: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 - pub = Publication.where(element: new_reaction).first - add_submission_history(pub) - pub - end - - def duplicate_user_labels(newSample, originalSample) - user_labels = originalSample.tag&.taggable_data.dig('user_labels') - return if user_labels.nil? - - tag = newSample.tag - taggable_data = tag.taggable_data || {} - taggable_data['user_labels'] = user_labels - tag.update!(taggable_data: taggable_data) - end - - def duplicate_elemental_compositions(newSample, originalSample) - originalSample&.elemental_compositions&.each do |ec| - newComposition = ElementalComposition.find_or_create_by(sample_id: newSample.id, composition_type: ec.composition_type) - newComposition.update_columns(data: ec.data, loading: ec.loading) - end - end - - def duplicate_residues(newSample, originalSample) - originalSample&.residues&.each do |res| - newRes = Residue.find_or_create_by(sample_id: newSample.id, residue_type: res.residue_type) - newRes.update_columns(custom_info: res.custom_info) - end - 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, - litype: lit.litype - } - 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 - pub = Publication.where(element: new_sample).first - add_submission_history(pub) - pub - end - - def add_submission_history(root) - init_node = { - state: 'submission', - action: 'submission', - timestamp: Time.now.strftime('%d-%m-%Y %H:%M:%S'), - username: current_user.name, - user_id: current_user.id, - type: 'submit' - } - review = root.review || {} - history = review['history'] || [] - history << init_node - - current_node = { - action: 'reviewing', - type: 'reviewed', - state: 'pending' - } - history << current_node - review['history'] = history - review['reviewers'] = @group_reviewers if @group_reviewers.present? - root.update!(review: review) - end + before do + error!('404 user not found', 404) unless current_user end - desc 'Get review list' - params do - optional :type, type: String, desc: 'Type' - optional :state, type: String, desc: 'State' - optional :search_type, type: String, desc: 'search type', values: %w[All Name Embargo Submitter] - optional :search_value, type: String, desc: 'search value' - optional :page, type: Integer, desc: 'page' - optional :pages, type: Integer, desc: 'pages' - optional :per_page, type: Integer, desc: 'per page' + after_validation do + @is_reviewer = User.reviewer_ids.include?(current_user.id) + @is_embargo_viewer = User.embargo_viewer_ids.include?(current_user.id) end - paginate per_page: 10, offset: 0, max_per_page: 100 - get 'list' do - type = params[:type].blank? || params[:type] == 'All' ? %w[Sample Reaction] : params[:type].chop! - state = params[:state].empty? || params[:state] == 'All' ? [Publication::STATE_PENDING, Publication::STATE_REVIEWED, Publication::STATE_ACCEPTED] : params[:state] - pub_scope = 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).where("published_by = ? OR (review -> 'reviewers')::jsonb @> '?'", current_user.id, current_user.id) - end - unless params[:search_value].blank? || params[:search_value] == 'All' - case params[:search_type] - when 'Submitter' - pub_scope = pub_scope.where(published_by: params[:search_value]) - when 'Embargo' - embargo_search = <<~SQL - (element_type = 'Reaction' and element_id in (select reaction_id from collections_reactions cr where cr.deleted_at is null and cr.collection_id = ?)) - or - (element_type = 'Sample' and element_id in (select sample_id from collections_samples cs where cs.deleted_at is null and cs.collection_id = ?)) - SQL - embargo_search = ActiveRecord::Base.send(:sanitize_sql_array, [embargo_search, params[:search_value], params[:search_value]]) - pub_scope = pub_scope.where(embargo_search) - when 'Name' - r_name_sql = " r.short_label like '%#{ActiveRecord::Base.send(:sanitize_sql_like, params[:search_value])}%' " - s_name_sql = " s.short_label like '%#{ActiveRecord::Base.send(:sanitize_sql_like, params[:search_value])}%' " - name_search = <<~SQL - (element_type = 'Reaction' and element_id in (select id from reactions r where #{r_name_sql})) - or - (element_type = 'Sample' and element_id in (select id from samples s where #{s_name_sql})) - SQL - pub_scope = pub_scope.where(name_search) - end - end - - list = pub_scope.order('publications.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' - review_info = repo_review_info(e, current_user&.id, true) - checklist = e.review && e.review['checklist'] if User.reviewer_ids.include?(current_user&.id) || review_info[:groupleader] == true - 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, checklist: checklist || {}, review_info: review_info, isReviewer: User.reviewer_ids.include?(current_user&.id) || false, - published_by: u&.name, submitter_id: u&.id, submit_at: e.created_at, state: e.state, embargo: find_embargo_collection(e).label, scheme_only: scheme_only - ) + namespace :review_list do + helpers ReviewHelpers + desc 'Get review list' + params do + use :get_review_list_params + end + before do + end + paginate per_page: 10, offset: 0, max_per_page: 100 + get do + get_review_list(params, current_user, @is_reviewer) end - { elements: elements } end - desc 'Get embargo list' - helpers RepositoryHelpers - post 'embargo_list' do + ## Get embargo selection list for submission + namespace :embargo_list do + helpers EmbargoHelpers + desc 'Get embargo list' params do optional :is_submit, type: Boolean, default: false, desc: 'Publication submission' end - is_reviewer = User.reviewer_ids.include?(current_user.id) - is_embargo_viewer = User.embargo_viewer_ids.include?(current_user.id) - is_submitter = false - if (is_reviewer || is_embargo_viewer) && params[:is_submit] == false - es = Publication.where(element_type: 'Collection', state: 'pending').order(Arel.sql("taggable_data->>'label' ASC")) - else - is_submitter = current_user.type == 'Anonymous' ? false : true - cols = if current_user.type == 'Anonymous' - Collection.where(id: current_user.sync_in_collections_users.pluck(:collection_id)).where.not(label: 'chemotion') - else - Collection.where(ancestry: current_user.publication_embargo_collection.id) - end - es = Publication.where(element_type: 'Collection', element_id: cols.pluck(:id)).order(Arel.sql("taggable_data->>'label' ASC")) unless cols&.empty? + before do + end + get do + embargo_select_list(params[:is_submit], current_user, @is_reviewer, @is_embargo_viewer) end - # es = build_publication_element_state(es) unless es.empty? - - { repository: es, current_user: { id: current_user.id, type: current_user.type, is_reviewer: is_reviewer, is_submitter: is_submitter } } end namespace :assign_embargo do + helpers EmbargoHelpers desc 'assign to an embargo bundle' params do - requires :new_embargo, type: Integer, desc: 'Collection id' - requires :element, type: Hash, desc: 'Element' + use :assign_embargo_params end after_validation do declared_params = declared(params, include_missing: false) - @p_element = declared_params[:element] - @p_embargo = declared_params[:new_embargo] - pub = Publication.find_by(element_type: @p_element['type'].classify, element_id: @p_element['id']) + @element = declared_params[:element] + @embargo_id = declared_params[:new_embargo] + pub = Publication.find_by(element_type: @element['type'], element_id: @element['id']) error!('404 Publication not found', 404) unless pub error!("404 Publication state must be #{Publication::STATE_REVIEWED}", 404) unless pub.state == Publication::STATE_REVIEWED error!('401 Unauthorized', 401) unless pub.published_by == current_user.id - if @p_embargo.to_i.positive? - e_col = Collection.find(@p_embargo.to_i) + if @embargo_id.to_i.positive? + e_col = Collection.find(@embargo_id.to_i) error!('404 This embargo has been released.', 404) unless e_col.ancestry.to_i == current_user.publication_embargo_collection.id end end post do - embargo_collection = fetch_embargo_collection(@p_embargo.to_i, current_user) if @p_embargo.to_i >= 0 - case @p_element['type'].classify - when 'Sample' - CollectionsSample - when 'Reaction' - CollectionsReaction - end.create_in_collection(@p_element['id'], [embargo_collection.id]) - { element: @p_element, - new_embargo: embargo_collection, - is_new_embargo: @p_embargo.to_i.zero?, - message: "#{@p_element['type']} [#{@p_element['title']}] has been moved to Embargo Bundle [#{embargo_collection.label}]" } + embargo = Repo::EmbargoHandler.find_or_create_embargo(@embargo_id.to_i, current_user) if @embargo_id.to_i >= 0 + assign_embargo(@element, embargo, current_user, @embargo_id.to_i.zero?) rescue StandardError => e { error: e.message } end end + ## TO BE CHECKED resource :compound do + # helpers RepositoryHelpers desc 'compound' params do requires :id, type: Integer, desc: 'Element id' @@ -494,28 +92,22 @@ def add_submission_history(root) after_validation do @pub = ElementTag.find_by(taggable_type: 'Sample', taggable_id: params[:id]) error!('404 No data found', 404) unless @pub + element_policy = ElementPolicy.new(current_user, @pub.taggable) error!('401 Unauthorized', 401) unless element_policy.read? || User.reviewer_ids.include?(current_user.id) end post do - data = @pub.taggable_data || {} - xvial = data['xvial'] || {} - xvial['num'] = params[:data] - xvial['comp_num'] = params[:xcomp] - xvial['username'] = current_user.name - xvial['userid'] = current_user.id - xvial['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') - data['xvial'] = xvial - @pub.update!(taggable_data: data) + update_compound(@pub, params, current_user) end end end resource :comment do - desc 'User comment' + # helpers RepositoryHelpers + desc 'Publication comment (public)' params do requires :id, type: Integer, desc: 'Element id' - optional :type, type: String, values: %w[Reaction Sample Container Collection] + optional :type, type: String, values: %w[Reaction Sample Container Collection] ### TO BE CHECKED (Collection) requires :pageId, type: Integer, desc: 'Page Element id' optional :pageType, type: String, values: %w[reactions molecules] optional :comment, type: String @@ -523,135 +115,66 @@ def add_submission_history(root) after_validation do @pub = Publication.find_by(element_type: params[:type], element_id: params[:id]) error!('404 No data found', 404) unless @pub - element_policy = ElementPolicy.new(current_user, @pub.element) + + element_check = params[:type] == 'Container' ? @pub.root.element : @pub.element + element_policy = ElementPolicy.new(current_user, element_check) error!('401 Unauthorized', 401) unless element_policy.read? || User.reviewer_ids.include?(current_user.id) end post 'user_comment' do PublicationMailer.mail_user_comment(current_user, params[:id], params[:type], params[:pageId], params[:pageType], params[:comment]).deliver_now end post 'reviewer' do - pub = Publication.find_by(element_type: params[:type], element_id: params[:id]) - review = pub.review || {} - review_info = review['info'] || {} - review_info['comment'] = params[:comment] - review_info['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') - review_info['username'] = current_user.name - review_info['userid'] = current_user.id - - review['info'] = review_info - pub.update!(review: review) + update_public_comment(params, current_user) end end resource :reaction do - helpers RepositoryHelpers - desc 'Return PUBLISHED serialized reaction' + helpers ReviewHelpers + desc 'Return Reviewing serialized reaction' params do requires :id, type: Integer, desc: 'Reaction id' - optional :is_public, type: Boolean, default: true end after_validation do - element = Reaction.find_by(id: params[:id]) - error!('404 No data found', 404) unless element + @reaction = Reaction.find_by(id: params[:id]) + error!('404 No data found', 404) unless @reaction - element_policy = ElementPolicy.new(current_user, element) + element_policy = ElementPolicy.new(current_user, @reaction) error!('401 Unauthorized', 401) unless element_policy.read? || User.reviewer_ids.include?(current_user.id) - pub = Publication.find_by(element_type: 'Reaction', element_id: params[:id]) - error!('404 No data found', 404) if pub.nil? + @publication = @reaction.publication + error!('404 No data found', 404) if @publication.nil? - error!('401 Unauthorized', 401) if (params[:is_public] == false && pub.state == 'completed') + error!('401 The submission has been published', 401) if @publication.state == 'completed' 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') - review_info = repo_review_info(publication, current_user&.id, false) - publication.review&.slice!('history') unless User.reviewer_ids.include?(current_user.id) || review_info[:groupleader] == true - published_user = User.find(publication.published_by) unless publication.nil? - entities = Entities::RepoReactionEntity.represent(reaction, serializable: true) - entities[:literatures] = literatures unless entities.nil? || literatures.blank? - entities[:schemes] = schemeList unless entities.nil? || schemeList.blank? - entities[:segments] = Labimotion::SegmentEntity.represent(reaction.segments) - embargo = find_embargo_collection(publication) - entities[:embargo] = embargo&.label - entities[:embargoId] = embargo&.id - { - reaction: entities, - selectEmbargo: Publication.find_by(element_type: 'Collection', element_id: embargo&.id), - pub_name: published_user&.name || '', - review_info: review_info - } + fetch_reviewing_reaction(@reaction, @publication, current_user) end end resource :sample do - helpers RepositoryHelpers + helpers ReviewHelpers desc 'Return Review serialized Sample' params do requires :id, type: Integer, desc: 'Sample id' - optional :is_public, type: Boolean, default: true end after_validation do - element = Sample.find_by(id: params[:id]) - error!('401 No data found', 401) unless element + @sample = Sample.find_by(id: params[:id]) + error!('401 No data found', 401) unless @sample - element_policy = ElementPolicy.new(current_user, element) + element_policy = ElementPolicy.new(current_user, @sample) error!('401 Unauthorized', 401) unless element_policy.read? || User.reviewer_ids.include?(current_user.id) - pub = Publication.find_by(element_type: 'Sample', element_id: params[:id]) - error!('401 No data found', 401) if pub.nil? - error!('401 Unauthorized', 401) if (params[:is_public] == false && pub.state == 'completed') + @publication = @sample.publication + error!('401 No data found', 401) if @publication.nil? + error!('401 The submission has been published', 401) if @publication.state == 'completed' end get do - sample = Sample.where(id: params[:id]).includes(:molecule, :tag).last - review_sample = { **sample.serializable_hash.deep_symbolize_keys } - review_sample[:segments] = sample.segments.present? ? Labimotion::SegmentEntity.represent(sample.segments) : [] - 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') - review_info = repo_review_info(publication, current_user&.id, false) - # preapproved = publication.review.dig('checklist', 'glr', 'status') == true - # is_leader = publication.review.dig('reviewers')&.include?(current_user&.id) - publication.review&.slice!('history') unless User.reviewer_ids.include?(current_user.id) || review_info[:groupleader] == true - published_user = User.find(publication.published_by) unless publication.nil? - literatures = get_literature(params[:id], 'Sample', params[:is_public] ? 'public' : 'detail') - # embargo = PublicationCollections.where("(elobj ->> 'element_type')::text = 'Sample' and (elobj ->> 'element_id')::integer = #{sample.id}")&.first&.label - embargo = find_embargo_collection(publication) - review_sample[:embargo] = embargo&.label - review_sample[:embargoId] = embargo&.id - label_ids = sample.tag.taggable_data['user_labels'] || [] unless sample.tag.taggable_data.nil? - user_labels = UserLabel.public_labels(label_ids) unless label_ids.nil? - { - molecule: MoleculeGuestSerializer.new(molecule).serializable_hash.deep_symbolize_keys, - sample: review_sample, - labels: user_labels, - publication: publication, - literatures: literatures, - analyses: containers, - selectEmbargo: Publication.find_by(element_type: 'Collection', element_id: embargo&.id), - doi: Entities::DoiEntity.represent(sample.doi, serializable: true), - pub_name: published_user&.name, - review_info: review_info - } + fetch_reviewing_sample(@sample, @publication, current_user) end end resource :metadata do + # helpers RepositoryHelpers desc 'metadata of publication' params do requires :id, type: Integer, desc: 'Id' @@ -663,77 +186,20 @@ def add_submission_history(root) 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 || @root_publication.review['reviewers'].include?(current_user.id) + error!('401 Unauthorized', 401) unless User.reviewer_ids.include?(current_user.id) || @root_publication.published_by == current_user.id || (@root_publication.review['reviewers'] && @root_publication.review['reviewers'].include?(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 } + metadata_preview(@root_publication, current_user) 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 + metadata_preview_zip(@root_publication, current_user) end end - namespace :review_search_options do - helpers do - def query_submitter(element_type, state) - if User.reviewer_ids.include?(current_user.id) - state_sql = state == 'All' || state.empty? ? " state in ('pending', 'reviewed', 'accepted')" : ActiveRecord::Base.send(:sanitize_sql_array, [' state=? ', state]) - type_sql = element_type == 'All' || element_type.empty? ? " element_type in ('Sample', 'Reaction')" : ActiveRecord::Base.send(:sanitize_sql_array, [' element_type=? ', element_type.chop]) - search_scope = User.where(type: 'Person').where( - <<~SQL - users.id in ( - select published_by from publications pub where ancestry is null and deleted_at is null - and #{state_sql} and #{type_sql}) - SQL - ) - .order('first_name ASC') - else - search_scope = User.where(id: current_user.id) - end - result = search_scope.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_embargo - search_scope = if User.reviewer_ids.include?(current_user.id) - Collection.where( - <<~SQL - ancestry::integer in (select id from collections cx where label = 'Embargoed Publications') - SQL - ) - else - Collection.where(ancestry: current_user.publication_embargo_collection.id) - end - result = search_scope.select( - <<~SQL - id as key, label as name, label as label - SQL - ) - .order('label ASC') - end - end + namespace :review_search_options do + helpers ReviewHelpers desc 'Find matched review values' params do requires :type, type: String, allow_blank: false, desc: 'Type', values: %w[All Submitter Embargo] @@ -741,247 +207,35 @@ def query_embargo optional :state, type: String, desc: 'Type', values: %w[All reviewed accepted pending] end get do - result = case params[:type] - when 'Submitter' - query_submitter(params[:element_type], params[:state]) - when 'Embargo' - query_embargo - else - [] - end - { result: result } - end - end - - namespace :reviewing do - helpers do - def approve_comments(root, comment, _checklist, _reviewComments, _action, _his = true) - review = root.review || {} - review_history = review['history'] || [] - current = review_history.last - current['username'] = current_user.name - current['userid'] = current_user.id - current['action'] = 'pre-approved' - current['comment'] = comment unless comment.nil? - current['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') - review_history[review_history.length - 1] = current - next_node = { action: 'reviewing', type: 'reviewed', state: 'pending' } - review_history << next_node - review['history'] = review_history - revst = review['checklist'] || {} - revst['glr'] = { status: true, user: current_user.name, updated_at: Time.now.strftime('%d-%m-%Y %H:%M:%S') } - review['checklist'] = revst - - root.update!(review: review) - end - - def save_comments(root, comment, checklist, reviewComments, action, his = true) - review = root.review || {} - review_history = review['history'] || [] - current = review_history.last || {} - current['state'] = %w[accepted declined].include?(action) ? action : root.state - current['action'] = action unless action.nil? - current['username'] = current_user.name - current['userid'] = current_user.id - current['comment'] = comment unless comment.nil? - current['type'] = root.state == Publication::STATE_PENDING ? 'reviewed' : 'submit' - current['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') - - if review_history.length == 0 - review_history[0] = current - else - review_history[review_history.length - 1] = current - end - if his ## add next_node - next_node = { action: 'revising', type: 'submit', state: 'reviewed' } if root.state == Publication::STATE_PENDING - next_node = { action: 'reviewing', type: 'reviewed', state: 'pending' } if root.state == Publication::STATE_REVIEWED - review_history << next_node - review['history'] = review_history - else - - # is_leader = review.dig('reviewers')&.include?(current_user&.id) - if root.state == Publication::STATE_PENDING && (action.nil? || action == Publication::STATE_REVIEWED) - next_node = { action: 'reviewing', type: 'reviewed', state: 'pending' } - review_history << next_node - review['history'] = review_history - end - end - if checklist&.length&.positive? - revst = review['checklist'] || {} - checklist.each do |k, v| - revst[k] = v['status'] == true ? { status: v['status'], user: current_user.name, updated_at: Time.now.strftime('%d-%m-%Y %H:%M:%S') } : { status: false } unless revst[k] && revst[k]['status'] == v['status'] - end - review['checklist'] = revst - end - review['reviewComments'] = reviewComments if reviewComments.present? - root.update!(review: review) - end - - # TODO: mv to model - def save_comment(root, comment) - review = root.review || {} - review_history = review['history'] || [] - current = review_history.last - comments = current['comments'] || {} - comment[comment.keys[0]]['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') unless comment.keys.empty? - comment[comment.keys[0]]['username'] = current_user.name - comment[comment.keys[0]]['userid'] = current_user.id - - current['comments'] = comments.deep_merge(comment || {}) - review['history'] = review_history - root.update!(review: review) - end - end - - desc 'process reviewed publication' - params do - requires :id, type: Integer, desc: 'Id' - requires :type, type: String, desc: 'Type', values: %w[sample reaction collection] - optional :comments, type: Hash - optional :comment, type: String - optional :checklist, type: Hash - 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.review.dig('reviewers')&.include?(current_user&.id)) || (@root_publication.published_by == current_user.id && @root_publication.state == Publication::STATE_REVIEWED) - - embargo = find_embargo_collection(@root_publication) unless params['type'] == 'collection' - @embargo_pub = embargo.publication if embargo.present? - end - - post :comments do - save_comments(@root_publication, params[:comment], params[:checklist], params[:reviewComments], nil, false) - element = Entities::ReactionEntity.represent(@root_publication.element) if params[:type] == 'reaction' - element = Entities::SampleEntity.represent(@root_publication.element) if params[:type] == 'sample' - element = Entities::CollectionEntity.represent(@root_publication.element) if params[:type] == 'collection' - - review_info = repo_review_info(@root_publication, current_user&.id, false) - his = @root_publication.review&.slice('history') unless User.reviewer_ids.include?(current_user.id) || @root_publication.review.dig('reviewers')&.include?(current_user.id) - { "#{params[:type]}": element, review: his || @root_publication.review, review_info: review_info } - end - - post :comment do - save_comment(@root_publication, params[:comments]) unless params[:comments].nil? - his = @root_publication.review&.slice('history') unless User.reviewer_ids.include?(current_user.id) - { review: his || @root_publication.review } - end - - post :reviewed do - save_comments(@root_publication, params[:comment], params[:checklist], params[:reviewComments], 'review') - # 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 - @embargo_pub&.refresh_embargo_metadata - element = Entities::ReactionEntity.represent(@root_publication.element) if params[:type] == 'reaction' - element = Entities::SampleEntity.represent(@root_publication.element) if params[:type] == 'sample' - review_info = repo_review_info(@root_publication, current_user&.id, false) - { "#{params[:type]}": element, review: @root_publication.review, review_info: review_info } - end - - post :submit do - save_comments(@root_publication, params[:comment], params[:checklist], params[:reviewComments], 'revision') - 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) - @embargo_pub&.refresh_embargo_metadata - element = Entities::ReactionEntity.represent(@root_publication.element) if params[:type] == 'reaction' - element = Entities::SampleEntity.represent(@root_publication.element) if params[:type] == 'sample' - his = @root_publication.review&.slice('history') unless User.reviewer_ids.include?(current_user.id) || @root_publication.review.dig('reviewers')&.include?(current_user.id) - review_info = repo_review_info(@root_publication, current_user&.id, false) - { "#{params[:type]}": element, review: his || @root_publication.review, review_info: review_info } - end - - post :approved do - approve_comments(@root_publication, params[:comment], params[:checklist], params[:reviewComments], 'approved', false) - element = Entities::ReactionEntity.represent(@root_publication.element) if params[:type] == 'reaction' - element = Entities::SampleEntity.represent(@root_publication.element) if params[:type] == 'sample' - his = @root_publication.review&.slice('history') unless User.reviewer_ids.include?(current_user.id) || @root_publication.review.dig('reviewers')&.include?(current_user.id) - review_info = repo_review_info(@root_publication, current_user&.id, false) - { "#{params[:type]}": element, review: his || @root_publication.review, review_info: review_info } - end - - post :accepted do - save_comments(@root_publication, params[:comment], params[:checklist], params[:reviewComments], 'accepted', false) - 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) - @embargo_pub&.refresh_embargo_metadata - - element = Entities::ReactionEntity.represent(@root_publication.element) if params[:type] == 'reaction' - element = Entities::SampleEntity.represent(@root_publication.element) if params[:type] == 'sample' - review_info = repo_review_info(@root_publication, current_user&.id, false) - { "#{params[:type]}": element, review: @root_publication.review, message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off', review_info: review_info } - end - post :declined do - save_comments(@root_publication, params[:comment], params[:checklist], params[:reviewComments], 'declined', false) - @root_publication.update_state('declined') - @root_publication.process_element('declined') - - ## TO BE HANDLED - remove from embargo collection - @root_publication.inform_users(Publication::STATE_DECLINED, current_user.id) - element = Entities::ReactionEntity.represent(@root_publication.element) if params[:type] == 'reaction' - element = Entities::SampleEntity.represent(@root_publication.element) if params[:type] == 'sample' - his = @root_publication.review&.slice('history') unless User.reviewer_ids.include?(current_user.id) - { "#{params[:type]}": element, review: his || @root_publication.review } + review_advanced_search(params, current_user) end end + # desc: submit sample data for publication namespace :publishSample do + helpers SubmissionHelpers 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 :reviewers, type: Array[String], default: [], desc: 'reviewers (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' + use :publish_sample_params end - after_validation do - @sample = current_user.samples.find_by(id: params[:sampleId]) + @sample = current_user.samples.find_by(id: params[:id]) + error!('You do not have permission to publish this sample', 403) unless @sample + @analyses = @sample&.analyses&.where(id: params[:analysesIds]) - @literals = Literal.where(id: params[:refs]) unless params[:refs].nil? || params[:refs].empty? ols_validation(@analyses) + error!('No analysis data available for publication', 404) if @analyses.empty? + @author_ids = if params[:addMe] [current_user.id] + coauthor_validation(params[:coauthors]) else coauthor_validation(params[:coauthors]) end - error!('401 Unauthorized', 401) unless @sample - error!('404 analyses not found', 404) if @analyses.empty? - @group_reviewers = coauthor_validation(params[:reviewers]) end - post do - @license = params[:license] - @publication_tag = create_publication_tag(current_user, @author_ids, @license) - @embargo_collection = fetch_embargo_collection(params[:embargo], current_user) if params[:embargo].present? && params[:embargo] >= 0 - pub = prepare_sample_data - pub.process_element - update_tag_doi(pub.element) - if col_pub = @embargo_collection&.publication - col_pub.refresh_embargo_metadata - end - pub.inform_users - - @sample.reload - detail_levels = ElementDetailLevelCalculator.new(user: current_user, element: @sample).detail_levels - { - sample: Entities::SampleEntity.represent(@sample, detail_levels: detail_levels), - message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' - } + declared_params = declared(params, include_missing: false) + SubmittingJob.send(perform_method, declared_params, 'Sample', @author_ids, current_user.id) + send_message_and_tag(@sample, current_user) end put :dois do @@ -996,211 +250,167 @@ def save_comment(root, comment) # desc: submit reaction data for publication namespace :publishReaction do + helpers SubmissionHelpers 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 :reviewers, type: Array[String], default: [], desc: 'reviewers (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' + use :publish_reaction_params end - after_validation do - @scheme_only = false - @reaction = current_user.reactions.find_by(id: params[:reactionId]) - error!('404 found no reaction to publish', 404) unless @reaction + @reaction = current_user.reactions.find_by(id: params[:id]) + error!('You do not have permission to publish this reaction', 404) 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) + error!('No analysis data available for publication', 404) unless @analysis_set.present? + @author_ids = if params[:addMe] [current_user.id] + coauthor_validation(params[:coauthors]) else coauthor_validation(params[:coauthors]) end - error!('404 found no analysis to publish', 404) unless @analysis_set.present? - - @group_reviewers = coauthor_validation(params[:reviewers]) - # 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], current_user) if params[:embargo].present? && params[:embargo] >= 0 - pub = prepare_reaction_data - pub.process_element - update_tag_doi(pub.element) - if col_pub = @embargo_collection&.publication - col_pub.refresh_embargo_metadata - end - pub.inform_users - - @reaction.reload - { - reaction: Entities::ReactionEntity.represent(@reaction, serializable: true), - message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' - } + declared_params = declared(params, include_missing: false) + declared_params[:scheme_only] = false + SubmittingJob.send(perform_method, declared_params, 'Reaction', @author_ids, current_user.id) + send_message_and_tag(@reaction, current_user) 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: Entities::ReactionEntity.represent(@reaction, serializable: true), - message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' - } + analysis_set_ids = @analysis_set&.map(&:id) + reserve_reaction_dois(@reaction, @analysis_set, analysis_set_ids) end end - namespace :save_repo_authors do - desc 'Save REPO authors' + # desc: submit reaction data (scheme only) for publication + namespace :publishReactionScheme do + helpers SubmissionHelpers + desc 'Publish Reaction Scheme only' params do - requires :elementId, type: Integer, desc: 'Element Id' - requires :elementType, type: String, desc: 'Element Type' - requires :taggData, type: Hash do - requires :creators, type: Array[Hash] - requires :affiliations, type: Hash - requires :contributors, type: Hash - end + use :publish_reaction_scheme_params end - after_validation do - unless User.reviewer_ids.include?(current_user.id) - @pub = Publication.find_by(element_id: params[:elementId], element_type: params[:elementType], published_by: current_user.id) - error!('404 No publication found', 404) unless @pub + @reaction = current_user.reactions.find_by(id: params[:id]) + error!('You do not have permission to publish this reaction', 404) unless @reaction + + @author_ids = if params[:addMe] + [current_user.id] + coauthor_validation(params[:coauthors]) + else + coauthor_validation(params[:coauthors]) end end - post do - pub = Publication.find_by(element_id: params[:elementId], element_type: params[:elementType]) declared_params = declared(params, include_missing: false) - - et = ElementTag.find_or_create_by(taggable_id: declared_params[:elementId], taggable_type: declared_params[:elementType]) - tagg_data = declared_params[:taggData] || {} - - tagg_data['author_ids'] = tagg_data['creators']&.map { |cr| cr['id'] } - tagg_data['affiliation_ids'] = tagg_data['creators']&.map { |cr| cr['affiliationIds'] }.flatten.uniq - tagg_data['affiliations'] = tagg_data['affiliations']&.select { |k, _| tagg_data['affiliation_ids'].include?(k.to_i) } - - pub_taggable_data = pub.taggable_data || {} - pub_taggable_data = pub_taggable_data.deep_merge(tagg_data || {}) - pub.update(taggable_data: pub_taggable_data) - - et_taggable_data = et.taggable_data || {} - pub_tag = et_taggable_data['publication'] || {} - pub_tag = pub_tag.deep_merge(tagg_data || {}) - et_taggable_data['publication'] = pub_tag - et.update(taggable_data: et_taggable_data) + declared_params[:scheme_only] = true + SubmittingJob.send(perform_method, declared_params, 'Reaction', @author_ids, current_user.id) + send_message_and_tag(@reaction, current_user) end end + namespace :reviewing do + helpers ReviewHelpers + helpers RepoCommentHelpers - # desc: submit reaction data (scheme only) for publication - namespace :publishReactionScheme do - desc 'Publish Reaction Scheme only' + desc 'process reviewed publication' 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' + use :reviewing_params end - after_validation do - @reaction = current_user.reactions.find_by(id: params[:reactionId]) - @scheme_only = true - error!('404 found no reaction to publish', 404) unless @reaction - schemeYield = 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.empty? - p.scheme_yield = py[0]['_equivalent'] if py && !py.empty? + helpers do + def process_review(action) + Repo::ReviewProcess.new(params, current_user.id, action).process end - @reaction.reactions_samples.select{ |rs| rs.type != 'ReactionsProductSample' }.map do |p| - p.equivalent = 0 + def extract_action + env['api.endpoint'].routes.first.pattern.origin[/[^\/]+$/] end - @reaction.name = '' - @reaction.purification = '{}' - @reaction.dangerous_products = '{}' - @reaction.description = { 'ops' => [{ 'insert' => '' }] } unless params[:schemeDesc] - @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? - @author_ids = if params[:addMe] - [current_user.id] + coauthor_validation(params[:coauthors]) - else - 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], current_user) if params[:embargo].present? && params[:embargo] >= 0 - pub = prepare_reaction_data - pub.process_element - pub.inform_users + before do + @root_publication = Publication.find_by(element_type: params['type'].classify,element_id: params['id']).root + reviewer_auth = User.reviewer_ids.include?(current_user.id) && @root_publication.state == Publication::STATE_PENDING + grouplead_auth = @root_publication.review.dig('reviewers')&.include?(current_user&.id) && @root_publication.state == Publication::STATE_PENDING + submitter_auth = (@root_publication.published_by == current_user.id || @root_publication.review.dig('submitters')&.include?(current_user&.id)) && @root_publication.state == Publication::STATE_REVIEWED + error!('Unauthorized. The operation cannot proceed.', 401) unless reviewer_auth || grouplead_auth || submitter_auth + end + + post :comment do + process_review(extract_action) + end + + post :comments do + process_review(extract_action) + end + + post :reviewed do + process_review(extract_action) + end + + post :submit do + process_review(extract_action) + end + + post :approved do + process_review(extract_action) + end - @reaction.reload - { - reaction: Entities::ReactionEntity.represent(@reaction, serializable: true), - message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' - } + post :accepted do + process_review(extract_action) + end + + post :declined do + process_review(extract_action) 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 + namespace :save_repo_authors do + helpers ReviewHelpers + desc 'Save REPO authors' + params do + use :save_repo_authors_params + 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! + after_validation do + if User.reviewer_ids.include?(current_user.id) + @pub = Publication.find_by(element_id: params[:elementId], element_type: params[:elementType]) + else + @pub = Publication.find_by(element_id: params[:elementId], element_type: params[:elementType], published_by: current_user.id) + end + if @pub.nil? + pub_tmp = Publication.find_by(element_id: params[:elementId], element_type: params[:elementType]) + if pub_tmp&.review&.dig('submitters')&.include?(current_user.id) + @pub = pub_tmp end end + error!('404 No publication found', 404) unless @pub + end + + post do + save_repo_authors(params, @pub, current_user) + end + end - def remove_embargo_collection(col) - col&.publication.really_destroy! - col.sync_collections_users.destroy_all - col.really_destroy! + namespace :save_repo_labels do + helpers UserLabelHelpers + desc 'Save REPO user labels' + params do + use :save_repo_labels_params + end + after_validation do + if @is_reviewer + @publication = Publication.find_by(element_id: params[:elementId], element_type: params[:elementType]) + else + @publication = Publication.find_by(element_id: params[:elementId], element_type: params[:elementType], published_by: current_user.id) end + error!('404 No publication found', 404) unless @publication end + post do + element = @publication.element + update_element_labels(@publication.element, params[:user_labels], current_user.id) + end + end + + namespace :embargo do + helpers EmbargoHelpers desc 'Generate account with chosen Embargo' params do requires :collection_id, type: Integer, desc: 'Embargo Collection Id' @@ -1208,131 +418,28 @@ def remove_embargo_collection(col) after_validation do @embargo_collection = Collection.find_by(id: params[:collection_id]) - error!('404 collection not found', 404) unless @embargo_collection + error!('Collection not found', 404) unless @embargo_collection + unless User.reviewer_ids.include?(current_user.id) @sync_emb_col = @embargo_collection.sync_collections_users.where(user_id: current_user.id)&.first - error!('404 found no collection', 404) unless @sync_emb_col + error!('Collection not found!', 404) unless @sync_emb_col end 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.created_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 } } + embargo_list(@embargo_collection, current_user) 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 + create_anonymous_user(@embargo_collection, current_user) end post :release do - begin - col_pub = @embargo_collection.publication - if col_pub.nil? || col_pub.published_by != current_user.id - { error: "only the owner of embargo #{@embargo_collection.label} can perform the release."} - else - col_pub.update(accepted_at: Time.now.utc) - col_pub.refresh_embargo_metadata - 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 - scheme_only_list = pub_list.select { |pub| pub.taggable_data['scheme_only'] == true } - if pub_list.flatten.length == scheme_only_list.flatten.length - col_pub.update(state: 'scheme_only') - else - col_pub.update(state: 'accepted') - end - - pub_list.each { |pub| element_submit(pub) } - 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.perform_now(@embargo_collection.id) - else 'development' - end - - - { message: "Embargo #{@embargo_collection.label} has been released" } - end - end - rescue StandardError => e - { error: e.message } - end + Repo::EmbargoHandler.release_embargo(@embargo_collection.id, current_user.id) 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 + Repo::EmbargoHandler.delete(@embargo_collection.id, current_user.id) end post :refresh do @@ -1343,30 +450,7 @@ def remove_embargo_collection(col) post :move do begin - # @new_embargo = params[:new_embargo] - @element = params[:element] - @new_embargo_collection = fetch_embargo_collection(params[:new_embargo]&.to_i, current_user) 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]) - - @embargo_collection&.publication&.refresh_embargo_metadata - @new_embargo_collection&.publication&.refresh_embargo_metadata - - { col_id: @embargo_collection.id, - new_embargo: @new_embargo_collection.publication, - 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}]" } + move_embargo(@embargo_collection, params, current_user) rescue StandardError => e { error: e.message } end diff --git a/app/api/chemotion/sample_api.rb b/app/api/chemotion/sample_api.rb index 2ab411638..8477c8d90 100644 --- a/app/api/chemotion/sample_api.rb +++ b/app/api/chemotion/sample_api.rb @@ -173,6 +173,7 @@ class SampleAPI < Grape::API optional :molecule_sort, type: Integer, desc: 'Sort by parameter' optional :from_date, type: Integer, desc: 'created_date from in ms' optional :to_date, type: Integer, desc: 'created_date to in ms' + optional :user_label, type: Integer, desc: 'user label' optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at' optional :product_only, type: Boolean, desc: 'query only reaction products' end @@ -220,13 +221,14 @@ class SampleAPI < Grape::API end from = params[:from_date] to = params[:to_date] + user_label = params[:user_label] by_created_at = params[:filter_created_at] || false sample_scope = sample_scope.created_time_from(Time.zone.at(from)) if from && by_created_at sample_scope = sample_scope.created_time_to(Time.zone.at(to) + 1.day) if to && by_created_at sample_scope = sample_scope.updated_time_from(Time.zone.at(from)) if from && !by_created_at sample_scope = sample_scope.updated_time_to(Time.zone.at(to) + 1.day) if to && !by_created_at - + sample_scope = sample_scope.by_user_label(user_label) if user_label sample_list = [] if params[:molecule_sort] == 1 @@ -247,7 +249,7 @@ class SampleAPI < Grape::API end else reset_pagination_page(sample_scope) - sample_scope = sample_scope.order('updated_at DESC') + sample_scope = sample_scope.order('samples.updated_at DESC') paginate(sample_scope).each do |sample| detail_levels = ElementDetailLevelCalculator.new(user: current_user, element: sample).detail_levels sample_list.push( @@ -440,6 +442,7 @@ class SampleAPI < Grape::API optional :melting_point_lowerbound, type: Float, desc: 'lower bound of sample melting point' optional :residues, type: Array optional :segments, type: Array + optional :user_labels, type: Array optional :elemental_compositions, type: Array optional :xref, type: Hash optional :stereo, type: Hash do @@ -520,6 +523,7 @@ class SampleAPI < Grape::API ) end attributes.delete(:segments) + attributes.delete(:user_labels) sample = Sample.new(attributes) @@ -545,6 +549,7 @@ class SampleAPI < Grape::API sample.container = update_datamodel(params[:container]) sample.save! + update_element_labels(sample, params[:user_labels], current_user.id) sample.save_segments(segments: params[:segments], current_user_id: current_user.id) diff --git a/app/api/chemotion/suggestion_api.rb b/app/api/chemotion/suggestion_api.rb index 0d95a5ebc..97f0fa855 100644 --- a/app/api/chemotion/suggestion_api.rb +++ b/app/api/chemotion/suggestion_api.rb @@ -175,10 +175,10 @@ def search_possibilities_by_type_user_and_collection(type) requirements: requirements, } when 'cell_lines' - dl_cl.positive? ? search_for_celllines : [] + dl_cl&.positive? ? search_for_celllines : [] else chemotion_id = suggest_pid(qry) - element_short_label = (dl_e.positive? && search_by_element_short_label.call(Labimotion::Element, qry)) || [] + element_short_label = (dl_e&.positive? && search_by_element_short_label.call(Labimotion::Element, qry)) || [] sample_name = (dl_s.positive? && search_by_field.call(Sample, :name, qry)) || [] sample_short_label = (dl_s.positive? && search_by_field.call(Sample, :short_label, qry)) || [] sample_external_label = (dl_s > -1 && search_by_field.call(Sample, :external_label, qry)) || [] @@ -200,7 +200,7 @@ def search_possibilities_by_type_user_and_collection(type) screen_name = (dl_sc > -1 && search_by_field.call(Screen, :name, qry)) || [] conditions = (dl_sc > -1 && search_by_field.call(Screen, :conditions, qry)) || [] requirements = (dl_sc > -1 && search_by_field.call(Screen, :requirements, qry)) || [] - cell_line_infos = dl_cl.positive? ? search_for_celllines : [] + cell_line_infos = dl_cl&.positive? ? search_for_celllines : {} { element_short_label: element_short_label, diff --git a/app/api/chemotion/ui_api.rb b/app/api/chemotion/ui_api.rb index f4b15ffa3..7883d33b8 100644 --- a/app/api/chemotion/ui_api.rb +++ b/app/api/chemotion/ui_api.rb @@ -41,6 +41,7 @@ def load_x_config has_radar: radar_config.present?, molecule_viewer: Matrice.molecule_viewer, collector_address: collector_address.presence, + u: Rails.configuration.u || {}, x: load_x_config, } end diff --git a/app/api/chemotion/user_api.rb b/app/api/chemotion/user_api.rb index 46bd7c96c..ff899df8c 100644 --- a/app/api/chemotion/user_api.rb +++ b/app/api/chemotion/user_api.rb @@ -26,8 +26,7 @@ class UserAPI < Grape::API desc 'list user labels' get 'list_labels' do - labels = UserLabel.where('user_id = ? or access_level >= 1', current_user.id) - .order('access_level desc, position, title') + labels = UserLabel.my_labels(current_user, false) present labels || [], with: Entities::UserLabelEntity, root: 'labels' end diff --git a/app/api/entities/reaction_entity.rb b/app/api/entities/reaction_entity.rb index 262395fca..d4d021020 100644 --- a/app/api/entities/reaction_entity.rb +++ b/app/api/entities/reaction_entity.rb @@ -101,7 +101,7 @@ def type end def comment_count - object.comments.count + 0 # object.comments.count end def variations diff --git a/app/api/entities/research_plan_entity.rb b/app/api/entities/research_plan_entity.rb index 6934c9078..539685b3d 100644 --- a/app/api/entities/research_plan_entity.rb +++ b/app/api/entities/research_plan_entity.rb @@ -53,7 +53,7 @@ def wellplates end def comment_count - object.comments.count + 0 # object.comments.count end end end diff --git a/app/api/entities/sample_entity.rb b/app/api/entities/sample_entity.rb index 352662dcd..027dece4a 100644 --- a/app/api/entities/sample_entity.rb +++ b/app/api/entities/sample_entity.rb @@ -82,14 +82,6 @@ class SampleEntity < ApplicationEntity expose_timestamps expose :is_repo_public - expose :labels - - def labels - return [] if object.tag&.taggable_data&.nil? - - label_ids = object.tag.taggable_data['user_labels'] || [] - UserLabel.public_labels(label_ids) || [] - end # expose :molecule, using: Entities::MoleculeEntity # expose :container, using: Entities::ContainerEntity @@ -156,7 +148,7 @@ def type end def comment_count - object.comments.count + 0 # object.comments.count end end end diff --git a/app/api/entities/screen_entity.rb b/app/api/entities/screen_entity.rb index 6ac20babf..f52588d2a 100644 --- a/app/api/entities/screen_entity.rb +++ b/app/api/entities/screen_entity.rb @@ -60,7 +60,7 @@ def wellplates end def comment_count - object.comments.count + 0 # object.comments.count end end end diff --git a/app/api/entities/wellplate_entity.rb b/app/api/entities/wellplate_entity.rb index 9d8fe55df..e00745be8 100644 --- a/app/api/entities/wellplate_entity.rb +++ b/app/api/entities/wellplate_entity.rb @@ -55,7 +55,7 @@ def type end def comment_count - object.comments.count + 0 # object.comments.count end end end diff --git a/app/api/helpers/embargo_helpers.rb b/app/api/helpers/embargo_helpers.rb index 55c11ec4f..ce3e8bef5 100644 --- a/app/api/helpers/embargo_helpers.rb +++ b/app/api/helpers/embargo_helpers.rb @@ -4,39 +4,137 @@ module EmbargoHelpers extend Grape::API::Helpers - def fetch_embargo_collection(cid, current_user) - 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.positive? - 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) - #embargo = Embargo.create!(name: new_embargo_col.label, collection_id: new_embargo_col.id, created_by: current_user.id) - d = Doi.create_for_element!(new_embargo_col) - Publication.create!( - state: Publication::STATE_PENDING, - element: new_embargo_col, - published_by: current_user.id, - doi: d, - taggable_data: { label: new_embargo_col.label, col_doi: d.full_doi } - ) - new_embargo_col + + def ext_embargo_list(user_id) + Publication.where(element_type: 'Collection') + .where.not(state: 'completed') + .where("review -> 'submitters' @> ?", user_id.to_s) + .pluck(:element_id) + end + + ## Get embargo selection list for submission + def embargo_select_list(is_submit, current_user, is_reviewer, is_embargo_viewer) + is_submitter = false + if (is_reviewer || is_embargo_viewer) && is_submit == false + es = Publication.where(element_type: 'Collection', state: 'pending').order(Arel.sql("taggable_data->>'label' ASC")) else - Collection.find(cid) + is_submitter = current_user.type == 'Anonymous' ? false : true + cols = if current_user.type == 'Anonymous' + Collection.where(id: current_user.sync_in_collections_users.pluck(:collection_id)).where.not(label: 'chemotion') + else + ext_col_ids = ext_embargo_list(current_user.id) || [] + Collection.where(ancestry: current_user.publication_embargo_collection.id).or(Collection.where(id: ext_col_ids)) + end + es = Publication.where(element_type: 'Collection', element_id: cols.pluck(:id)).order(Arel.sql("taggable_data->>'label' ASC")) unless cols&.empty? end + { repository: es, current_user: { id: current_user.id, type: current_user.type, is_reviewer: is_reviewer, is_submitter: is_submitter } } + rescue StandardError => e + Publication.repo_log_exception(e, { is_submit: is_submit, user_id: current_user&.id, is_reviewer: is_reviewer, is_embargo_viewer: is_embargo_viewer }) + { error: e.message } + end + + def assign_embargo(element, embargo, current_user, is_new = false) + case element['type'] + when 'Sample' + CollectionsSample + when 'Reaction' + CollectionsReaction + end.create_in_collection(element['id'], [embargo.id]) + { element: element, new_embargo: embargo, + is_new_embargo: is_new, + message: "#{element['type']} [#{element['title']}] has been assigned to Embargo Bundle [#{embargo.label}]" } + rescue StandardError => e + Publication.repo_log_exception(e, { element: element&.id, user_id: current_user&.id, is_new: is_new }) + { error: e.message } end + def embargo_list(embargo_collection, current_user) + 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.with_deleted.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' - 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 : OpenStruct.new(label: '') + 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.created_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 } } + rescue StandardError => e + Publication.repo_log_exception(e, { embargo_collection: embargo_collection&.id, user_id: current_user&.id }) + { error: e.message } end - def create_embargo() + def create_anonymous_user(embargo_collection, current_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 + Publication.repo_log_exception(e, { embargo_collection: embargo_collection&.id, user_id: current_user&.id }) + { error: e.message } + end + + def move_embargo(embargo_collection, params, current_user) + element = params[:element] + new_embargo_collection = Repo::EmbargoHandler.find_or_create_embargo(params[:new_embargo]&.to_i, current_user) 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]) + + embargo_collection&.publication&.refresh_embargo_metadata + new_embargo_collection&.publication&.refresh_embargo_metadata + { col_id: embargo_collection.id, + new_embargo: new_embargo_collection.publication, + 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 + Publication.repo_log_exception(e, { embargo_collection: embargo_collection&.id, params: params, user_id: current_user&.id }) + { error: e.message } end end diff --git a/app/api/helpers/gate_helpers.rb b/app/api/helpers/gate_helpers.rb index 21d58193c..2647e1139 100644 --- a/app/api/helpers/gate_helpers.rb +++ b/app/api/helpers/gate_helpers.rb @@ -3,7 +3,7 @@ module GateHelpers def prepare_for_receiving(request) http_token = (request.headers['Authorization'].split(' ').last if request.headers['Authorization'].present?) # rubocop: disable Style/RedundantArgument - error!('Unauthorized', 401) unless http_token + error!('Unauthorized, token not found', 401) unless http_token secret = Rails.application.secrets.secret_key_base begin @auth_token = ActiveSupport::HashWithIndifferentAccess.new( @@ -14,11 +14,11 @@ def prepare_for_receiving(request) error!("#{e}", 401) end @user = Person.find_by(email: @auth_token[:iss]) - error!('Unauthorized', 401) unless @user + error!('Unauthorized, user not found', 401) unless @user @collection = Collection.find_by( id: @auth_token[:collection], user_id: @user.id, is_shared: false, ) - error!('Unauthorized access to collection', 401) unless @collection + error!('Unauthorized, can not access to collection', 401) unless @collection @origin = @auth_token["origin"] [@user, @collection, @origin] rescue StandardError => e @@ -32,7 +32,7 @@ def save_chunk(user_id, col_id, params) tempfile = params[:chunk]&.fetch('tempfile', nil) if tempfile File.open(filename, 'ab') { |file| file.write(tempfile.read) } - end + end filename rescue StandardError => e log_exception('save_chunk', e, user_id) @@ -42,9 +42,9 @@ def save_chunk(user_id, col_id, params) tempfile&.unlink end - + def log_exception(func_name, exception, user_id = nil) - transfer_logger.error("[#{DateTime.now}] [#{func_name}] user: [#{user_id}] \n Exception: #{exception.message}") + transfer_logger.error("[#{DateTime.now}] [#{func_name}] user: [#{user_id}] \n Exception: #{exception.message}") transfer_logger.error(exception.backtrace.join("\n")) end diff --git a/app/api/helpers/public_helpers.rb b/app/api/helpers/public_helpers.rb index d19226fbb..f561f225d 100644 --- a/app/api/helpers/public_helpers.rb +++ b/app/api/helpers/public_helpers.rb @@ -5,6 +5,167 @@ module PublicHelpers extend Grape::API::Helpers include ApplicationHelper + def get_pub_reaction(id) + reaction = Reaction.where('id = ?', 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 label from publication_collections where (elobj ->> 'element_type')::text = 'Reaction' and (elobj ->> 'element_id')::integer = reactions.id) as embargo, + (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 = Repo::FetchHandler.literatures_by_cat(reaction.id,'Reaction') || [] + reaction.products.each do |p| + literatures += Repo::FetchHandler.literatures_by_cat(p.id,'Sample') + end + + pub = Publication.find_by(element_type: 'Reaction', element_id: reaction.id) + pub_info = (pub.review.present? && pub.review['info'].present? && pub.review['info']['comment']) || '' + infos = {} + ana_infos = {} + pd_infos = {} + + pub.state != Publication::STATE_COMPLETED && pub.descendants.each do |pp| + review = pp.review || {} + info = review['info'] || {} + next if info.empty? + if pp.element_type == 'Sample' + pd_infos[pp.element_id] = info['comment'] + else + ana_infos[pp.element_id] = info['comment'] + end + end + + schemeList = Repo::FetchHandler.get_reaction_table(reaction.id) + entities = Entities::RepoReactionEntity.represent(reaction, serializable: true) + entities[:products].each do |p| + label_ids = p[:tag]['taggable_data']['user_labels'] || [] unless p[:tag]['taggable_data'].nil? + p[:labels] = UserLabel.public_labels(label_ids, current_user, pub.state == Publication::STATE_COMPLETED) unless label_ids.nil? + pub_product = p + p[:xvialCom] = build_xvial_com(p[:molecule][:inchikey], current_user&.id) + pub_product_tag = pub_product[:tag]['taggable_data'] + next if pub_product_tag.nil? + + xvial = pub_product_tag['xvial'] && pub_product_tag['xvial']['num'] + next unless xvial.present? + + unless current_user.present? && User.reviewer_ids.include?(current_user.id) + pub_product_tag['xvial']['num'] = 'x' + end + p[:xvialCom][:hasSample] = true + end + label_ids = (pub.taggable_data && pub.taggable_data['user_labels']) || [] + labels = UserLabel.public_labels(label_ids, current_user, pub.state == Publication::STATE_COMPLETED) unless label_ids.nil? + + entities[:publication]['review']['history'] = [] + entities[:publication]['review'] = nil if pub.state === Publication::STATE_COMPLETED + 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[:embargo] = reaction.embargo + entities[:labels] = labels + entities[:infos] = { pub_info: pub_info, pd_infos: pd_infos, ana_infos: ana_infos } + entities[:isReviewer] = current_user.present? && User.reviewer_ids.include?(current_user.id) ? true : false + entities[:elementType] = 'reaction' + entities[:segments] = Labimotion::SegmentEntity.represent(reaction.segments) + entities + end + + def get_pub_molecule(id, adv_flag=nil, adv_type=nil, adv_val=nil, label_val=nil) + molecule = Molecule.find(id) + xvial_com = build_xvial_com(molecule.inchikey, current_user&.id) + pub_id = Collection.public_collection_id + if adv_flag.present? && adv_flag == true && adv_type.present? && adv_type == 'Authors' && 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 ('#{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| + container = 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(literals.id, literals.litype) as litype, + json_object_agg(literals.id, 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? + label_ids = s.tag.taggable_data['user_labels'] || [] unless s.tag.taggable_data.nil? + user_labels = UserLabel.public_labels(label_ids, current_user, pub.state == Publication::STATE_COMPLETED) unless label_ids.nil? + xvial = s.tag.taggable_data['xvial'] && s.tag.taggable_data['xvial']['num'] unless s.tag.taggable_data.nil? + if xvial.present? + unless current_user.present? && User.reviewer_ids.include?(current_user.id) + xvial = 'x' + end + end + comp_num = s.tag.taggable_data['xvial'] && s.tag.taggable_data['xvial']['comp_num'] unless s.tag.taggable_data.nil? + pub_info = (pub.review.present? && pub.review['info'].present? && pub.review['info']['comment']) || '' + ana_infos = {} + pub.descendants.each do |pp| + review = pp.review || {} + info = review['info'] || {} + next if info.empty? + ana_infos[pp.element_id] = info['comment'] + end + embargo = PublicationCollections.where("(elobj ->> 'element_type')::text = 'Sample' and (elobj ->> 'element_id')::integer = #{s.id}")&.first&.label + segments = Labimotion::SegmentEntity.represent(s.segments) + tag.merge(container: container, literatures: literatures, sample_svg_file: s.sample_svg_file, short_label: s.short_label, melting_point: s.melting_point, boiling_point: s.boiling_point, + sample_id: s.id, reaction_ids: reaction_ids, sid: sid, xvial: xvial, comp_num: comp_num, embargo: embargo, labels: user_labels, + showed_name: s.showed_name, pub_id: pub.id, ana_infos: ana_infos, pub_info: pub_info, segments: segments, published_at: pub.published_at, + molecular_mass: s.molecular_mass, sum_formula: s.sum_formula, decoupled: s.decoupled) + end + x = published_samples.select { |s| s[:xvial].present? } + xvial_com[:hasSample] = x.length.positive? + 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, + isReviewer: (current_user.present? && User.reviewer_ids.include?(current_user.id)) ? true : false, + xvialCom: xvial_com, + elementType: 'molecule' + } + end + def send_notification(attachment, user, status, has_error = false) data_args = { 'filename': attachment.filename, 'comment': 'the file has been updated' } level = 'success' @@ -32,14 +193,18 @@ def de_encode_json(json, key = '', viv = '', encode = true) end end - def convert_to_3d(molfile) + def represent_structure(molfile) molecule_viewer = Matrice.molecule_viewer - if molecule_viewer.blank? || molecule_viewer[:chembox_endpoint].blank? + if molecule_viewer.blank? || molecule_viewer[:chembox].blank? { molfile: molfile } else options = { timeout: 10, body: { mol: molfile }.to_json, headers: { 'Content-Type' => 'application/json' } } - response = HTTParty.post(molecule_viewer[:chembox_endpoint], options) - response.code == 200 ? { molfile: response.parsed_response } : { molfile: molfile } + response = HTTParty.post("#{molecule_viewer[:chembox]}/core/rdkit/v1/structure", options) + if response.code == 200 + { molfile: (response.parsed_response && response.parsed_response['molfile']) || molfile } + else + { molfile: molfile } + end end end diff --git a/app/api/helpers/repo_comment_helpers.rb b/app/api/helpers/repo_comment_helpers.rb new file mode 100644 index 000000000..38a172670 --- /dev/null +++ b/app/api/helpers/repo_comment_helpers.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# A helper for reviewing/submission +module RepoCommentHelpers + extend Grape::API::Helpers + def approve_comments(root, comment, _checklist, _reviewComments, _action, _his = true) + review = root.review || {} + review_history = review['history'] || [] + current = review_history.last + current['username'] = current_user.name + current['userid'] = current_user.id + current['action'] = 'pre-approved' + current['comment'] = comment unless comment.nil? + current['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') + review_history[review_history.length - 1] = current + next_node = { action: 'reviewing', type: 'reviewed', state: 'pending' } + review_history << next_node + review['history'] = review_history + revst = review['checklist'] || {} + revst['glr'] = { status: true, user: current_user.name, updated_at: Time.now.strftime('%d-%m-%Y %H:%M:%S') } + review['checklist'] = revst + + root.update!(review: review) + end + + # def save_comments(root, comment, checklist, reviewComments, action, his = true) + # review = root.review || {} + # review_history = review['history'] || [] + # current = review_history.last || {} + # current['state'] = %w[accepted declined].include?(action) ? action : root.state + # current['action'] = action unless action.nil? + # current['username'] = current_user.name + # current['userid'] = current_user.id + # current['comment'] = comment unless comment.nil? + # current['type'] = root.state == Publication::STATE_PENDING ? 'reviewed' : 'submit' + # current['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') + + # if review_history.length == 0 + # review_history[0] = current + # else + # review_history[review_history.length - 1] = current + # end + # if his ## add next_node + # next_node = { action: 'revising', type: 'submit', state: 'reviewed' } if root.state == Publication::STATE_PENDING + # next_node = { action: 'reviewing', type: 'reviewed', state: 'pending' } if root.state == Publication::STATE_REVIEWED + # review_history << next_node + # review['history'] = review_history + # else + + # # is_leader = review.dig('reviewers')&.include?(current_user&.id) + # if root.state == Publication::STATE_PENDING && (action.nil? || action == Publication::STATE_REVIEWED) + # next_node = { action: 'reviewing', type: 'reviewed', state: 'pending' } + # review_history << next_node + # review['history'] = review_history + # end + # end + # if checklist&.length&.positive? + # revst = review['checklist'] || {} + # checklist.each do |k, v| + # revst[k] = v['status'] == true ? { status: v['status'], user: current_user.name, updated_at: Time.now.strftime('%d-%m-%Y %H:%M:%S') } : { status: false } unless revst[k] && revst[k]['status'] == v['status'] + # end + # review['checklist'] = revst + # end + # review['reviewComments'] = reviewComments if reviewComments.present? + # root.update!(review: review) + # end + + # TODO: mv to model + def save_comment(root, comment) + review = root.review || {} + review_history = review['history'] || [] + current = review_history.last + comments = current['comments'] || {} + comment[comment.keys[0]]['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') unless comment.keys.empty? + comment[comment.keys[0]]['username'] = current_user.name + comment[comment.keys[0]]['userid'] = current_user.id + + current['comments'] = comments.deep_merge(comment || {}) + review['history'] = review_history + root.update!(review: review) + end +end \ No newline at end of file diff --git a/app/api/helpers/repo_params_helpers.rb b/app/api/helpers/repo_params_helpers.rb new file mode 100644 index 000000000..18250997a --- /dev/null +++ b/app/api/helpers/repo_params_helpers.rb @@ -0,0 +1,88 @@ +module RepoParamsHelpers + extend Grape::API::Helpers + + params :get_review_list_params do + requires :type, type: String, desc: 'Search Type', values: %w[All Samples Reactions] + requires :state, type: String, desc: 'Publication State', values: %w[All pending reviewed accepted] + optional :label, type: Integer, desc: 'User label' + optional :search_type, type: String, desc: 'search type', values: %w[All Name Embargo Submitter] + optional :search_value, type: String, desc: 'search value' + optional :page, type: Integer, desc: 'page' + optional :pages, type: Integer, desc: 'pages' + optional :per_page, type: Integer, desc: 'per page' + end + + params :publish_sample_params do + requires :id, type: Integer, desc: 'Sample Id' + requires :analysesIds, type: Array[Integer], desc: 'Selected analyses ids' + optional :coauthors, type: Array[Integer], default: [], desc: 'Co-author (User)' + optional :reviewers, type: Array[Integer], default: [], desc: 'reviewers (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 + + params :publish_reaction_params do + requires :id, type: Integer, desc: 'Reaction Id' + requires :analysesIds, type: Array[Integer], desc: 'Selected analyses ids' + optional :coauthors, type: Array[Integer], default: [], desc: 'Co-author (User)' + optional :reviewers, type: Array[Integer], default: [], desc: 'reviewers (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 + + params :publish_reaction_scheme_params do + requires :id, 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' + optional :reviewers, type: Array[Integer], default: [], desc: 'reviewers (User)' + 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 + + params :save_repo_authors_params do + requires :elementId, type: Integer, desc: 'Element Id' + requires :elementType, type: String, desc: 'Element Type' + optional :leaders, type: Array, default: nil, desc: 'Leaders' + optional :taggData, type: Hash do + optional :creators, type: Array[Hash] + optional :affiliations, type: Hash + optional :contributors, type: Hash + end + end + + params :assign_embargo_params do + requires :new_embargo, type: Integer, desc: 'Collection id' + requires :element, type: Hash, desc: 'Element' do + requires :id, type: Integer, desc: 'Element id' + requires :type, type: String, desc: 'Element type', values: %w[Sample Reaction] + requires :title, type: String, desc: 'Element title' + end + end + + params :save_repo_labels_params do + requires :elementId, type: Integer, desc: 'Element Id' + requires :elementType, type: String, desc: 'Element Type' + optional :user_labels, type: Array[Integer] + end + + params :reviewing_params do + requires :id, type: Integer, desc: 'Element Id' + requires :type, type: String, desc: 'Type', values: %w[sample reaction collection] + optional :comments, type: Hash + optional :comment, type: String + optional :checklist, type: Hash + optional :analysesIds, type: Array[Integer] + optional :coauthors, type: Array[Integer] + optional :reviewers, type: Array[Integer] + optional :reviewComments, type: String + end + +end \ No newline at end of file diff --git a/app/api/helpers/repository_helpers.rb b/app/api/helpers/repository_helpers.rb index d84d8f5f3..414b70e66 100644 --- a/app/api/helpers/repository_helpers.rb +++ b/app/api/helpers/repository_helpers.rb @@ -1,299 +1,71 @@ module RepositoryHelpers extend Grape::API::Helpers - def get_pub_reaction(id) - reaction = Reaction.where('id = ?', 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 label from publication_collections where (elobj ->> 'element_type')::text = 'Reaction' and (elobj ->> 'element_id')::integer = reactions.id) as embargo, - (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(reaction.id,'Reaction') || [] - reaction.products.each do |p| - literatures += get_literature(p.id,'Sample') - end - - pub = Publication.find_by(element_type: 'Reaction', element_id: reaction.id) - pub_info = (pub.review.present? && pub.review['info'].present? && pub.review['info']['comment']) || '' - infos = {} - ana_infos = {} - pd_infos = {} - - pub.state != Publication::STATE_COMPLETED && pub.descendants.each do |pp| - review = pp.review || {} - info = review['info'] || {} - next if info.empty? - if pp.element_type == 'Sample' - pd_infos[pp.element_id] = info['comment'] - else - ana_infos[pp.element_id] = info['comment'] - end - end - - schemeList = get_reaction_table(reaction.id) - entities = Entities::RepoReactionEntity.represent(reaction, serializable: true) - entities[:products].each do |p| - label_ids = p[:tag]['taggable_data']['user_labels'] || [] unless p[:tag]['taggable_data'].nil? - p[:labels] = UserLabel.public_labels(label_ids) unless label_ids.nil? - pub_product = p - p[:xvialCom] = build_xvial_com(p[:molecule][:inchikey], current_user&.id) - pub_product_tag = pub_product[:tag]['taggable_data'] - next if pub_product_tag.nil? - - xvial = pub_product_tag['xvial'] && pub_product_tag['xvial']['num'] - next unless xvial.present? - - unless current_user.present? && User.reviewer_ids.include?(current_user.id) - pub_product_tag['xvial']['num'] = 'x' - end - p[:xvialCom][:hasSample] = true - end - entities[:publication]['review']['history'] = [] - entities[:publication]['review'] = nil if pub.state === Publication::STATE_COMPLETED - 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[:embargo] = reaction.embargo - entities[:infos] = { pub_info: pub_info, pd_infos: pd_infos, ana_infos: ana_infos } - entities[:isReviewer] = current_user.present? && User.reviewer_ids.include?(current_user.id) ? true : false - entities[:elementType] = 'reaction' - entities[:segments] = Labimotion::SegmentEntity.represent(reaction.segments) - entities + def update_public_comment(params, current_user) + pub = Publication.find_by(element_type: params[:type], element_id: params[:id]) + review = pub.review || {} + review_info = review['info'] || {} + review_info['comment'] = params[:comment] + review_info['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') + review_info['username'] = current_user.name + review_info['userid'] = current_user.id + + review['info'] = review_info + pub.update!(review: review) + rescue StandardError => e + Publication.repo_log_exception(e, { params: params, user_id: current_user&.id }) + raise end - def get_pub_molecule(id, adv_flag=nil, adv_type=nil, adv_val=nil) - molecule = Molecule.find(id) - xvial_com = build_xvial_com(molecule.inchikey, current_user&.id) - pub_id = Collection.public_collection_id - if adv_flag.present? && adv_flag == true && adv_type.present? && adv_type == 'Authors' && 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 ('#{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| - container = 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(literals.id, literals.litype) as litype, - json_object_agg(literals.id, 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? - label_ids = s.tag.taggable_data['user_labels'] || [] unless s.tag.taggable_data.nil? - user_labels = UserLabel.public_labels(label_ids) unless label_ids.nil? - xvial = s.tag.taggable_data['xvial'] && s.tag.taggable_data['xvial']['num'] unless s.tag.taggable_data.nil? - if xvial.present? - unless current_user.present? && User.reviewer_ids.include?(current_user.id) - xvial = 'x' - end - end - comp_num = s.tag.taggable_data['xvial'] && s.tag.taggable_data['xvial']['comp_num'] unless s.tag.taggable_data.nil? - pub_info = (pub.review.present? && pub.review['info'].present? && pub.review['info']['comment']) || '' - ana_infos = {} - pub.descendants.each do |pp| - review = pp.review || {} - info = review['info'] || {} - next if info.empty? - ana_infos[pp.element_id] = info['comment'] - end - embargo = PublicationCollections.where("(elobj ->> 'element_type')::text = 'Sample' and (elobj ->> 'element_id')::integer = #{s.id}")&.first&.label - segments = Labimotion::SegmentEntity.represent(s.segments) - tag.merge(container: container, literatures: literatures, sample_svg_file: s.sample_svg_file, short_label: s.short_label, melting_point: s.melting_point, boiling_point: s.boiling_point, - sample_id: s.id, reaction_ids: reaction_ids, sid: sid, xvial: xvial, comp_num: comp_num, embargo: embargo, labels: user_labels, - showed_name: s.showed_name, pub_id: pub.id, ana_infos: ana_infos, pub_info: pub_info, segments: segments, published_at: pub.published_at) - end - x = published_samples.select { |s| s[:xvial].present? } - xvial_com[:hasSample] = x.length.positive? - 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, - isReviewer: (current_user.present? && User.reviewer_ids.include?(current_user.id)) ? true : false, - xvialCom: xvial_com, - elementType: 'molecule' - } + def update_compound(pub, params, current_user) + data = pub.taggable_data || {} + xvial = data['xvial'] || {} + xvial['num'] = params[:data] + xvial['comp_num'] = params[:xcomp] + xvial['username'] = current_user.name + xvial['userid'] = current_user.id + xvial['timestamp'] = Time.now.strftime('%d-%m-%Y %H:%M:%S') + data['xvial'] = xvial + pub.update!(taggable_data: data) + rescue StandardError => e + Publication.repo_log_exception(e, { pub: pub&.id, user_id: current_user&.id, params: params }) + raise end - 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 metadata_preview(root_publication, current_user) + mt = [] + root_publication = root_publication + publications = [root_publication] + root_publication.descendants + publications.each do |pub| + next if pub.element.nil? - def repo_review_info(pub, user_id, lst) - { - submitter: pub&.published_by == user_id || false, - reviewer: User.reviewer_ids&.include?(user_id) || false, - groupleader: pub&.review&.dig('reviewers')&.include?(user_id), - leaders: User.where(id: pub&.review&.dig('reviewers'))&.map{ |u| u.name }, - preapproved: pub&.review&.dig('checklist', 'glr', 'status') == true, - review_level: repo_review_level(pub&.element_id, pub&.element_type) - } - 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, literals.element_id, - json_object_agg(literals.id, literals.litype) as litype, - json_object_agg(literals.id, users.first_name || chr(32) || users.last_name) as ref_added_by - SQL - ).group('literatures.id, literals.element_type, literals.element_id').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, - (select name from molecule_names mn where mn.id = samples.molecule_name_id) as molecule_iupac_name, - molecules.iupac_name, molecules.sum_formular, - molecules.molecular_weight, samples.name, samples.short_label, - 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['real_amount_value'].zero?) ? r['target_amount_value'].to_f : r['real_amount_value'].to_f - unit = (r['real_amount_value'].nil? || r['real_amount_value'].zero?) ? 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['real_amount_value'].zero?) ? r['target_amount_value'].to_f : r['real_amount_value'].to_f - unit = (r['real_amount_value'].nil? || r['real_amount_value'].zero?) ? 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 + mt.push(element_type: pub.element_type, metadata_xml: pub.datacite_metadata_xml) end - schemeList + rescue StandardError => e + Publication.repo_log_exception(e, { root_publication: root_publication&.id, user_id: current_user&.id }) + { metadata: mt } end - def build_publication_element_state(publications) - publications.map do |c| - c.taggable_data['element_dois']&.map do |obj| - obj['state'] = Publication.find_by(id: obj['id'])&.state unless obj['state'].present? - obj + def metadata_preview_zip(root_publication, current_user) + 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| + next if pub.element.nil? + + 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 - c end + zip.rewind + zip.read + rescue StandardError => e + Publication.repo_log_exception(e, { root_publication: root_publication&.id, user_id: current_user&.id }) + raise end -end + + private + +end \ No newline at end of file diff --git a/app/api/helpers/review_helpers.rb b/app/api/helpers/review_helpers.rb new file mode 100644 index 000000000..09a390348 --- /dev/null +++ b/app/api/helpers/review_helpers.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true + +# A helper for reviewing publications +# It includes the following methods: +# 1. get_review_list: Get the list of publications to review +# 2. fetch_reviewing_reaction: Fetch the details of a reaction for reviewing +# 3. fetch_reviewing_sample: Fetch the details of a sample for reviewing + +module ReviewHelpers + extend Grape::API::Helpers + + def get_review_list(params, current_user, is_reviewer = false) + type = params[:type].blank? || params[:type] == 'All' ? %w[Sample Reaction] : params[:type].chop! + state = params[:state].empty? || params[:state] == 'All' ? [Publication::STATE_PENDING, Publication::STATE_REVIEWED, Publication::STATE_ACCEPTED] : params[:state] + pub_scope = Publication.where(state: state, ancestry: nil, element_type: type) + pub_scope = pub_scope.where("published_by = ? OR (review -> 'reviewers')::jsonb @> '?' OR (review -> 'submitters')::jsonb @> '?'", current_user.id, current_user.id, current_user.id) unless is_reviewer + unless params[:search_value].blank? || params[:search_value] == 'All' + case params[:search_type] + when 'Submitter' + pub_scope = pub_scope.where(published_by: params[:search_value]) + when 'Embargo' + embargo_search = <<~SQL + (element_type = 'Reaction' and element_id in (select reaction_id from collections_reactions cr where cr.deleted_at is null and cr.collection_id = ?)) + or + (element_type = 'Sample' and element_id in (select sample_id from collections_samples cs where cs.deleted_at is null and cs.collection_id = ?)) + SQL + embargo_search = ActiveRecord::Base.send(:sanitize_sql_array, [embargo_search, params[:search_value], params[:search_value]]) + pub_scope = pub_scope.where(embargo_search) + when 'Name' + r_name_sql = " r.short_label like '%#{ActiveRecord::Base.send(:sanitize_sql_like, params[:search_value])}%' " + s_name_sql = " s.short_label like '%#{ActiveRecord::Base.send(:sanitize_sql_like, params[:search_value])}%' " + name_search = <<~SQL + (element_type = 'Reaction' and element_id in (select id from reactions r where #{r_name_sql})) + or + (element_type = 'Sample' and element_id in (select id from samples s where #{s_name_sql})) + SQL + pub_scope = pub_scope.where(name_search) + end + end + pub_scope = pub_scope.where("taggable_data->'user_labels' @> '?'", params[:label]) if params[:label].present? + list = pub_scope.order('publications.updated_at desc') + elements = [] + paginate(list).each do |e| + element_type = e.element&.class&.name + next if element_type.nil? + + u = User.with_deleted.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' + review_info = Repo::FetchHandler.repo_review_info(e, current_user&.id) + checklist = e.review && e.review['checklist'] if is_reviewer || review_info[:groupleader] == true + scheme_only = element_type == 'Reaction' && e.taggable_data && e.taggable_data['scheme_only'] + + label_ids = (e.taggable_data && e.taggable_data['user_labels']) || [] + labels = UserLabel.public_labels(label_ids, current_user, e.state == Publication::STATE_COMPLETED) unless label_ids.nil? + elements.push( + id: e.element_id, svg: svg_file, type: element_type, title: title, checklist: checklist || {}, review_info: review_info, isReviewer: is_reviewer, + published_by: u&.name, submitter_id: u&.id, submit_at: e.created_at, state: e.state, embargo: Repo::FetchHandler.find_embargo_collection(e).label, scheme_only: scheme_only, labels: labels + ) + end + { elements: elements } + rescue StandardError => e + Publication.repo_log_exception(e, { params: params, user_id: current_user&.id, is_reviewer: is_reviewer }) + { error: e.message } + end + + def fetch_reviewing_reaction(reaction, publication, current_user) + 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 = Repo::FetchHandler.literatures_by_cat(reaction.id, 'Reaction', 'detail') || [] + reaction.products.each do |p| + literatures += Repo::FetchHandler.literatures_by_cat(p.id, 'Sample', 'detail') + end + schemeList = Repo::FetchHandler.get_reaction_table(reaction.id) + review_info = Repo::FetchHandler.repo_review_info(publication, current_user&.id) + publication.review&.slice!('history') unless User.reviewer_ids.include?(current_user.id) || review_info[:groupleader] == true + published_user = User.with_deleted.find(publication.published_by) unless publication.nil? + entities = Entities::RepoReactionEntity.represent(reaction, serializable: true) + entities[:literatures] = literatures unless entities.nil? || literatures.blank? + entities[:schemes] = schemeList unless entities.nil? || schemeList.blank? + entities[:segments] = Labimotion::SegmentEntity.represent(reaction.segments) + embargo = Repo::FetchHandler.find_embargo_collection(publication) + entities[:embargo] = embargo&.label + entities[:embargoId] = embargo&.id + label_ids = publication.taggable_data['user_labels'] || [] unless publication.taggable_data.nil? + user_labels = UserLabel.public_labels(label_ids, current_user, publication.state == Publication::STATE_COMPLETED) unless label_ids.nil? + entities[:labels] = user_labels + { + reaction: entities, + selectEmbargo: Publication.find_by(element_type: 'Collection', element_id: embargo&.id), + pub_name: published_user&.name || '', + review_info: review_info + } + rescue StandardError => e + Publication.repo_log_exception(e, { reaction: reaction&.id, publication: publication&.id, current_user: current_user&.id }) + { error: e.message } + end + + def fetch_reviewing_sample(sample, publication, current_user) + review_sample = { **sample.serializable_hash.deep_symbolize_keys } + review_sample[:segments] = sample.segments.present? ? Labimotion::SegmentEntity.represent(sample.segments) : [] + containers = Entities::ContainerEntity.represent(sample.container) + publication = Publication.find_by(element_id: params[:id], element_type: 'Sample') + review_info = review_info = Repo::FetchHandler.repo_review_info(publication, current_user&.id) + # preapproved = publication.review.dig('checklist', 'glr', 'status') == true + # is_leader = publication.review.dig('reviewers')&.include?(current_user&.id) + publication.review&.slice!('history') unless User.reviewer_ids.include?(current_user.id) || review_info[:groupleader] == true + published_user = User.with_deleted.find(publication.published_by) unless publication.nil? + literatures = Repo::FetchHandler.literatures_by_cat(params[:id], 'Sample', 'detail') + # embargo = PublicationCollections.where("(elobj ->> 'element_type')::text = 'Sample' and (elobj ->> 'element_id')::integer = #{sample.id}")&.first&.label + embargo = Repo::FetchHandler.find_embargo_collection(publication) + review_sample[:embargo] = embargo&.label + review_sample[:embargoId] = embargo&.id + review_sample[:user_labels] = publication.taggable_data['user_labels'] || [] unless publication.taggable_data.nil? + review_sample[:showed_name] = sample.showed_name + label_ids = publication.taggable_data['user_labels'] || [] unless publication.taggable_data.nil? + user_labels = UserLabel.public_labels(label_ids, current_user, publication.state == Publication::STATE_COMPLETED) unless label_ids.nil? + { + molecule: MoleculeGuestSerializer.new(sample.molecule).serializable_hash.deep_symbolize_keys, + sample: review_sample, + labels: user_labels, + publication: publication, + literatures: literatures, + analyses: containers, + selectEmbargo: Publication.find_by(element_type: 'Collection', element_id: embargo&.id), + doi: Entities::DoiEntity.represent(sample.doi, serializable: true), + pub_name: published_user&.name, + review_info: review_info + } + rescue StandardError => e + Publication.repo_log_exception(e, { sample: sample&.id, publication: publication&.id, current_user: current_user&.id }) + { error: e.message } + end + + def save_repo_authors(declared_params, pub, current_user) + et = ElementTag.find_or_create_by(taggable_id: declared_params[:elementId], taggable_type: declared_params[:elementType]) + tagg_data = declared_params[:taggData] || {} + leaders = declared_params[:leaders] + + if tagg_data.present? + tagg_data['author_ids'] = tagg_data['creators']&.map { |cr| cr['id'] } + tagg_data['affiliation_ids'] = [tagg_data['creators']&.map { |cr| cr['affiliationIds'] }.flatten.uniq] + tagg_data['affiliations'] = tagg_data['affiliations']&.select { |k, _| tagg_data['affiliation_ids'].include?(k.to_i) } + + pub_taggable_data = pub.taggable_data || {} + pub_taggable_data = pub_taggable_data.deep_merge(tagg_data || {}) + pub.update(taggable_data: pub_taggable_data) + + et_taggable_data = et.taggable_data || {} + pub_tag = et_taggable_data['publication'] || {} + pub_tag = pub_tag.deep_merge(tagg_data || {}) + et_taggable_data['publication'] = pub_tag + et.update(taggable_data: et_taggable_data) + end + if !leaders.nil? + review = pub.review || {} + orig_leaders = review['reviewers'] || [] + curr_leaders = leaders.map { |l| l['id'] } + + del_leaders = orig_leaders - curr_leaders + new_leaders = curr_leaders - orig_leaders + pub.update(review: review.deep_merge({ 'reviewers' => curr_leaders })) + reassign_leaders(pub, current_user, del_leaders, new_leaders) if del_leaders.present? || new_leaders.present? + end + pub + rescue StandardError => e + Publication.repo_log_exception(e, { declared_params: declared_params, pub: pub&.id, current_user: current_user&.id }) + { error: e.message } + end + + def reassign_leaders(pub, current_user, del_leaders, new_leaders) + pub_user = User.with_deleted.find(pub.published_by) + element = pub.element + return false unless pub_user && element + + if new_leaders.present? + new_leader_list = User.where(id: new_leaders, type: 'Person') + new_leader_list&.each do |user| + col = user.find_or_create_grouplead_collection + case pub.element_type + when 'Sample' + CollectionsSample + when 'Reaction' + CollectionsReaction + end.create_in_collection([element.id], [col.id]) + end + end + + if del_leaders.present? + del_leader_list = User.where(id: del_leaders, type: 'Person') + del_leader_list&.each do |user| + col = user.find_or_create_grouplead_collection + case pub.element_type + when 'Sample' + CollectionsSample + when 'Reaction' + CollectionsReaction + end.remove_in_collection([element.id], [col.id]) + end + end + rescue StandardError => e + Publication.repo_log_exception(e, { pub: pub&.id, current_user: current_user&.id, del_leaders: del_leaders, new_leaders: new_leaders }) + raise e + end + + def review_advanced_search(params, current_user) + result = case params[:type] + when 'Submitter' + query_submitter(params[:element_type], params[:state], current_user) + when 'Embargo' + query_embargo(current_user) + else + [] + end + { result: result } + rescue StandardError => e + Publication.repo_log_exception(e, { params: params, current_user: current_user&.id }) + { error: e.message } + end + + + private + + def query_submitter(element_type, state, current_user) + if User.reviewer_ids.include?(current_user.id) + state_sql = state == 'All' || state.empty? ? " state in ('pending', 'reviewed', 'accepted')" : ActiveRecord::Base.send(:sanitize_sql_array, [' state=? ', state]) + type_sql = element_type == 'All' || element_type.empty? ? " element_type in ('Sample', 'Reaction')" : ActiveRecord::Base.send(:sanitize_sql_array, [' element_type=? ', element_type.chop]) + search_scope = User.where(type: 'Person').where( + <<~SQL + users.id in ( + select published_by from publications pub where ancestry is null and deleted_at is null + and #{state_sql} and #{type_sql}) + SQL + ) + .order('first_name ASC') + else + search_scope = User.where(id: current_user.id) + end + result = search_scope.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 + ) + rescue StandardError => e + Publication.repo_log_exception(e, { element_type: element_type, state: state, current_user: current_user&.id }) + { error: e.message } + end + + def query_embargo(current_user) + search_scope = if User.reviewer_ids.include?(current_user.id) + Collection.where( + <<~SQL + ancestry::integer in (select id from collections cx where label = 'Embargoed Publications') + SQL + ) + else + Collection.where(ancestry: current_user.publication_embargo_collection.id) + end + result = search_scope.select( + <<~SQL + id as key, label as name, label as label + SQL + ).order('label ASC') + rescue StandardError => e + Publication.repo_log_exception(e, { current_user: current_user&.id }) + { error: e.message } + end + +end diff --git a/app/api/helpers/submission_helpers.rb b/app/api/helpers/submission_helpers.rb index 22eb8c80b..6d7c342ba 100644 --- a/app/api/helpers/submission_helpers.rb +++ b/app/api/helpers/submission_helpers.rb @@ -4,132 +4,95 @@ module SubmissionHelpers extend Grape::API::Helpers + def tag_as_submitted(element) + return unless element.is_a?(Sample) || element.is_a?(Reaction) + + et = element.tag + return if et.taggable_data.nil? + + et.update!(taggable_data: (et.taggable_data || {}).merge(publish_pending: true)) + rescue StandardError => e + Publication.repo_log_exception(e, { element: element&.id }) + nil + end + + def reserve_reaction_dois(reaction, analysis_set, analysis_set_ids) + 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: Entities::ReactionEntity.represent(reaction, serializable: true), + message: ENV['PUBLISH_MODE'] ? "publication on: #{ENV['PUBLISH_MODE']}" : 'publication off' + } + rescue StandardError => e + Publication.repo_log_exception(e, { reaction: reaction&.id, analysis_set: analysis_set, analysis_set_ids: analysis_set_ids }) + nil + end + 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 + rescue StandardError => e + Publication.repo_log_exception(e, { analyses: analyses }) + nil end def coauthor_validation(coauthors) coauthor_ids = [] coauthors&.each do |coa| - val = coa.strip + val = coa usr = 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 usr.nil? coauthor_ids << usr.id end coauthor_ids + rescue StandardError => e + Publication.repo_log_exception(e, { coauthors: coauthors }) + nil end - - def update_tag_doi(element) - unless element.nil? || element&.doi.nil? || element&.tag.nil? - mds = Datacite::Mds.new - et = element.tag - tag_data = (et.taggable_data && et.taggable_data['publication']) || {} - tag_data['doi'] = "#{mds.doi_prefix}/#{element&.doi.suffix}" - et.update!( - taggable_data: (et.taggable_data || {}).merge(publication: tag_data) - ) - if element&.class&.name == 'Reaction' - element&.publication.children.each do |child| - next unless child&.element&.class&.name == 'Sample' - - update_tag_doi(child.element) - end - end - end + def perform_method + method = ENV['PUBLISH_MODE'] == 'production' ? :perform_later : :perform_now + method end - def accept_new_sample(root, sample) - pub_s = Publication.create!( - state: Publication::STATE_PENDING, - element: sample, - doi: sample&.doi, - published_by: root.published_by, - parent: root, - taggable_data: root.taggable_data + def send_message_and_tag(element, user) + tag_as_submitted(element) + Message.create_msg_notification( + channel_id: Channel.find_by(subject: Channel::SUBMITTING)&.id, + message_from: user.id, + autoDismiss: 5, + message_content: { 'data': "Your submission for #{element.class.name} [#{element.short_label}] is currently being processed. You will recieve a notification upon completion." }, ) - sample.analyses.each do |sa| - accept_new_analysis(pub_s, sa) - end + element.reload + rescue StandardError => e + Publication.repo_log_exception(e, { element: element&.id, user: user&.id }) end - def accept_new_analysis(root, analysis, nil_analysis = true) - if nil_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 - end - analysis.children.where(container_type: 'dataset').each do |ds| - ds.attachments.each do |att| - if MimeMagic.by_path(att.filename)&.type&.start_with?('image') - 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.read_file.force_encoding("utf-8")) - end - end - end - 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 + + private + - 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 - 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, 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, sa, Publication.find_by(element: sa).nil?) - end - - when 'Reaction' - root.element.products.each do |pd| - Publication.find_by(element_type: 'Sample', element_id: pd.id)&.destroy! if pd.analyses&.length == 0 - next if pd.analyses&.length == 0 - pd.reserve_suffix - pd.reserve_suffix_analyses(pd.analyses) - pd.reload - prod_pub = Publication.find_by(element: pd) - if prod_pub.nil? - accept_new_sample(root, pd) - else - pd.analyses.each do |rpa| - accept_new_analysis(prod_pub, rpa, 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 - } - update_tag_doi(root.element) - end end diff --git a/app/api/helpers/user_label_helpers.rb b/app/api/helpers/user_label_helpers.rb index 9345122dd..5d08aaab2 100644 --- a/app/api/helpers/user_label_helpers.rb +++ b/app/api/helpers/user_label_helpers.rb @@ -5,7 +5,16 @@ def update_element_labels(element, user_labels, current_user_id) tag = ElementTag.find_by(taggable: element) data = tag.taggable_data || {} private_labels = UserLabel.where(id: data['user_labels'], access_level: [0, 1]).where.not(user_id: current_user_id).pluck(:id) - data['user_labels'] = ((user_labels || []) + private_labels)&.uniq + if !User.reviewer_ids.include?(current_user_id) + review_labels = UserLabel.where(id: data['user_labels'], access_level: 3).pluck(:id) + end + data['user_labels'] = ((user_labels || []) + private_labels + (review_labels || []))&.uniq tag.save! + + ## For Chemotion Repository + if element.respond_to?(:publication) && pub = element.publication + pub.update_user_labels(data['user_labels'], current_user_id) if pub.present? + end end + end \ No newline at end of file diff --git a/app/assets/stylesheets/components/select.scss b/app/assets/stylesheets/components/select.scss index 812ec9214..e3081f1ee 100644 --- a/app/assets/stylesheets/components/select.scss +++ b/app/assets/stylesheets/components/select.scss @@ -1,6 +1,6 @@ .header-group-select { .Select-control { - width: 200px; + width: 120px; height: 27px; cursor: pointer; box-shadow: inset 0 2px 2px #e9e9e9; diff --git a/app/assets/stylesheets/repo-extension.scss b/app/assets/stylesheets/repo-extension.scss new file mode 100644 index 000000000..fb82e2e5f --- /dev/null +++ b/app/assets/stylesheets/repo-extension.scss @@ -0,0 +1,15 @@ +.ext-icon { + width: auto; + height: 4vh; + max-height: 4vh; + padding-right: 8px; +} + +.ext-icon-list { + margin-right: 10px; + float: right; + img { + height: 2.2vh !important; + max-height: 2.2vh !important; + } +} diff --git a/app/assets/stylesheets/repo_home.scss b/app/assets/stylesheets/repo_home.scss index 5535e33a9..dffce9d99 100644 --- a/app/assets/stylesheets/repo_home.scss +++ b/app/assets/stylesheets/repo_home.scss @@ -132,6 +132,7 @@ $bs-successcolor: #5cb85c; .repo-registed-compound-desc { color: $bs-primarycolor; font-style: italic; + font-size: small; i { color: black; cursor: pointer; @@ -705,6 +706,11 @@ $bs-successcolor: #5cb85c; } } +@mixin preview-btn { + position: absolute; + top: 80%; +} + .repo-analysis-header { display: inline-block; width: 100%; @@ -723,10 +729,16 @@ $bs-successcolor: #5cb85c; .preview-table:hover { border: 1px solid $statisticcolor; } - .spectra { - position: absolute; - top: 80%; - left: 76%; + .btn0 { + display: none; + } + .btn1 { + @include preview-btn; + right: 0px; + } + .btn2 { + @include preview-btn; + right: 30px; } } .abstract { @@ -1105,16 +1117,16 @@ $bs-successcolor: #5cb85c; a { padding: 15px 15px 10px 0px !important; } -} - -.white-nav-item:hover { - text-decoration: underline; - text-decoration-color: #2e6da4; - text-decoration-style: solid; - text-decoration-thickness: 4px; - text-underline-offset: 20%; - color: #2e6da4; - font-weight: bolder; + &:hover a, + &:hover b { + text-decoration: underline !important; + text-decoration-color: #2e6da4 !important; + text-decoration-style: solid !important; + text-decoration-thickness: 4px !important; + text-underline-offset: 20% !important; + color: #2e6da4 !important; + font-weight: bolder !important; + } } .hub-menu { @@ -1150,3 +1162,12 @@ $bs-successcolor: #5cb85c; justify-content: center; margin-top: 8px; } + +.repo-nfdi-award { + display: inline-flex;; + align-items: center; + justify-content: center; + > img { + width: 80%; + } +} diff --git a/app/assets/stylesheets/structure_viewer.scss b/app/assets/stylesheets/structure_viewer.scss index 17f431f69..9f5cca2aa 100644 --- a/app/assets/stylesheets/structure_viewer.scss +++ b/app/assets/stylesheets/structure_viewer.scss @@ -35,3 +35,9 @@ border: 0; } } + +.structure-editor-container { + position: absolute; + top: 6px; + right: 46px; +} diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index efea6ebec..98ecd3363 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -19,8 +19,6 @@ def mydb; end def editor; end - def jsmol; end - def sfn_cb code = params[:code] sf_verifer = request.env.dig('action_dispatch.request.unsigned_session_cookie', 'omniauth.pkce.verifier') diff --git a/app/jobs/chemotion_embargo_pubchem_job.rb b/app/jobs/chemotion_embargo_pubchem_job.rb index adc05be8d..0054c98c9 100644 --- a/app/jobs/chemotion_embargo_pubchem_job.rb +++ b/app/jobs/chemotion_embargo_pubchem_job.rb @@ -146,7 +146,15 @@ def send_pubchem def send_email if ENV['PUBLISH_MODE'] == 'production' - PublicationMailer.mail_publish_embargo_release(@embargo_collection.id).deliver_now + begin + PublicationMailer.mail_publish_embargo_release(@embargo_collection.id).deliver_now + rescue StandardError => e + Delayed::Worker.logger.error <<~TXT + --------- ChemotionEmbargoPubchemJob send_email error -------------- + Error Message: #{e.message} + -------------------------------------------------------------------- + TXT + end end Message.create_msg_notification( channel_subject: Channel::PUBLICATION_REVIEW, diff --git a/app/jobs/chemotion_repo_reviewing_job.rb b/app/jobs/chemotion_repo_reviewing_job.rb index ec9423cd6..fa4cac43d 100644 --- a/app/jobs/chemotion_repo_reviewing_job.rb +++ b/app/jobs/chemotion_repo_reviewing_job.rb @@ -28,7 +28,7 @@ def notify_users channel_subject: Channel::PUBLICATION_REVIEW, message_from: submitter } - sgl = publication.review.dig('reviewers').nil? ? [submitter] : publication.review.dig('reviewers') + [submitter] + sgl = publication.review&.dig('reviewers').nil? ? [submitter] : publication.review&.dig('reviewers') + [submitter] case publication.state when Publication::STATE_PENDING diff --git a/app/jobs/submitting_job.rb b/app/jobs/submitting_job.rb new file mode 100644 index 000000000..6f70501f9 --- /dev/null +++ b/app/jobs/submitting_job.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class SubmittingJob < ApplicationJob + include ActiveJob::Status + queue_as :submitting + + def max_attempts + 1 + end + + def perform(params, type, author_ids, current_user_id) + if params[:scheme_only] + scheme_yield = params[:products]&.map { |v| v.slice(:id, :_equivalent) } || [] + scheme_params = { + scheme_yield: scheme_yield, + temperature: params[:temperature], + duration: params[:duration], + schemeDesc: params[:schemeDesc] + } + else + scheme_params = {} + end + + Repo::Submission.new( + type: type, + id: params[:id], + author_ids: author_ids, + group_leaders: params[:reviewers], + analyses_ids: params[:analysesIds], + refs: params[:refs], + license: params[:license], + embargo: params[:embargo], + scheme_only: params[:scheme_only] || false, + scheme_params: scheme_params, + user_id: current_user_id + ).submitting + end +end \ No newline at end of file diff --git a/app/models/channel.rb b/app/models/channel.rb index 147c024bf..e92dc23f5 100644 --- a/app/models/channel.rb +++ b/app/models/channel.rb @@ -43,6 +43,7 @@ class Channel < ApplicationRecord # REPOSITORY ONLY PUBLICATION_REVIEW = 'Publication Review' PUBLICATION_PUBLISHED = 'Publication Published' + SUBMITTING = 'Publication Submission' class << self def build_message(**args) diff --git a/app/models/concerns/collectable.rb b/app/models/concerns/collectable.rb index 1789c502a..3936fa0e3 100644 --- a/app/models/concerns/collectable.rb +++ b/app/models/concerns/collectable.rb @@ -6,7 +6,7 @@ module Collectable scope :for_user_n_groups, ->(user_ids) { joins(:collections).where('collections.user_id IN (?)', user_ids).references(:collections) } scope :by_collection_id, ->(id) { joins(:collections).where('collections.id = ?', id) } scope :search_by, ->(search_by_method, arg) { public_send("search_by_#{search_by_method}", arg) } - + scope :by_user_label, ->(id) { joins(:tag).where("element_tags.taggable_data->'user_labels' @> '?'", id) } # TODO: Filters are not working properly # the following scopes are not working as I would expect # in the ui the selection is a date but in the api we are getting a timestamp, diff --git a/app/models/concerns/element_codes.rb b/app/models/concerns/element_codes.rb index 3ceda5f43..9a33a4e12 100644 --- a/app/models/concerns/element_codes.rb +++ b/app/models/concerns/element_codes.rb @@ -13,6 +13,9 @@ def code_logs return [] if source_class == 'container' && containable_type == 'Labimotion::Element' CodeLog.where(source: source_class).where(source_id: id).order(created_at: 'DESC') + rescue StandardError => e + Rails.logger.error e.message + [] end def code_log() code_logs.first end diff --git a/app/models/concerns/embargo_col.rb b/app/models/concerns/embargo_col.rb index bde82f7ee..995387daa 100644 --- a/app/models/concerns/embargo_col.rb +++ b/app/models/concerns/embargo_col.rb @@ -3,8 +3,9 @@ module EmbargoCol def refresh_embargo_metadata return if element_type != 'Collection' || element.nil? - ps = Publication.where(element_type: 'Sample', ancestry: nil, element_id: element.samples&.pluck(:id)) - pr = Publication.where(element_type: 'Reaction', ancestry: nil, element_id: element.reactions&.pluck(:id)) + # Fetch publications for samples and reactions with preloaded doi + ps = Publication.where(element_type: 'Sample', ancestry: nil, element_id: element.samples&.pluck(:id)).includes(:doi) + pr = Publication.where(element_type: 'Reaction', ancestry: nil, element_id: element.reactions&.pluck(:id)).includes(:doi) creators = [] author_ids = [] @@ -14,16 +15,15 @@ def refresh_embargo_metadata dois = [] (ps + pr)&.each do |pu| - if pu.taggable_data['scheme_only'] == true - else - eids.push(pu.id) - dois.push({ id: pu.id, element_type: pu.element_type, element_id: pu.element_id, doi: pu.doi.full_doi, state: pu.state }) if pu.doi.present? - ctag = pu.taggable_data || {} - creators << ctag["creators"] - author_ids << ctag["author_ids"] - affiliation_ids << ctag["affiliation_ids"] - contributors = ctag["contributors"] - end + next if pu.taggable_data['scheme_only'] == true + + eids.push(pu.id) + dois.push({ id: pu.id, element_type: pu.element_type, element_id: pu.element_id, doi: pu.doi.full_doi, state: pu.state }) if pu.doi.present? + ctag = pu.taggable_data || {} + creators << ctag["creators"] + author_ids << ctag["author_ids"] + affiliation_ids << ctag["affiliation_ids"] + contributors = ctag["contributors"] end et = ElementTag.find_or_create_by(taggable_id: element_id, taggable_type: element_type) diff --git a/app/models/concerns/metadata_jsonld.rb b/app/models/concerns/metadata_jsonld.rb index dab3bb694..027db2669 100644 --- a/app/models/concerns/metadata_jsonld.rb +++ b/app/models/concerns/metadata_jsonld.rb @@ -12,21 +12,21 @@ def json_ld(ext = nil) if element_type == 'Sample' json_ld_sample_root elsif element_type == 'Reaction' - json_ld_reaction(ext) + json_ld_reaction(self, ext) elsif element_type == 'Container' json_ld_container end end - def json_ld_sample_root(pub = self) - json = json_ld_study - json['about'] = [json_ld_sample] + def json_ld_sample_root(pub = self, is_root = true) + json = json_ld_study(pub, is_root) + json['about'] = [json_ld_sample(pub, nil, is_root)] json end - def json_ld_study(pub = self) + def json_ld_study(pub = self, is_root = true) json = {} - json['@context'] = 'https://schema.org' + json['@context'] = 'https://schema.org' if is_root == true json['@type'] = 'Study' json['@id'] = "https://doi.org/#{doi.full_doi}" json['dct:conformsTo'] = { @@ -39,13 +39,13 @@ def json_ld_study(pub = self) json['author'] = json_ld_authors(pub.taggable_data) json['contributor'] = json_ld_contributor(pub.taggable_data["contributors"]) json['citation'] = json_ld_citations(pub.element.literatures, pub.element.id) - json['includedInDataCatalog'] = json_ld_data_catalog(pub) + json['includedInDataCatalog'] = json_ld_data_catalog(pub) if is_root == true json end def json_ld_data_catalog(pub = self) json = {} - json['@context'] = 'https://schema.org' + ## json['@context'] = 'https://schema.org' json['@type'] = 'DataCatalog' json['@id'] = 'https://www.chemotion-repository.net' json['description'] = 'Repository for samples, reactions and related research data.' @@ -137,12 +137,12 @@ def conforms_to } end - def json_ld_sample(pub = self, ext = nil) + def json_ld_sample(pub = self, ext = nil, is_root = true) # metadata_xml json = {} - json['@context'] = 'https://schema.org' + json['@context'] = 'https://schema.org' if is_root == true json['@type'] = 'ChemicalSubstance' - json['@id'] = "https://doi.org/#{pub.doi.full_doi}" + json['@id'] = pub.doi.full_doi json['identifier'] = "CRS-#{pub.id}" json['url'] = "https://www.chemotion-repository.net/inchikey/#{pub.doi.suffix}" json['name'] = pub.element.molecule_name&.name @@ -152,7 +152,8 @@ def json_ld_sample(pub = self, ext = nil) json['description'] = json_ld_description(pub.element.description) #json['author'] = json_ld_authors(pub.taggable_data) json['hasBioChemEntityPart'] = json_ld_moelcule_entity(pub.element) - json['subjectOf'] = json_ld_subjectOf(pub) + json['subjectOf'] = json_ld_subjectOf(pub) if is_root == true + json['isPartOf'] = is_part_of(pub) if pub.parent.present? #json_object = JSON.parse(json) #JSON.pretty_generate(json_object) @@ -166,27 +167,27 @@ def json_ld_sample(pub = self, ext = nil) # formatted_json end - def json_ld_reaction(ext = nil) + def json_ld_reaction(pub= self, ext = nil, is_root = true) json = {} - json['@context'] = 'https://schema.org' + json['@context'] = 'https://schema.org' if is_root == true json['@type'] = 'Study' - json['@id'] = "https://doi.org/#{doi.full_doi}" - json['identifier'] = "CRR-#{id}" - json['url'] = "https://www.chemotion-repository.net/inchikey/#{doi.suffix}" + json['@id'] = pub.doi.full_doi + json['identifier'] = "CRR-#{pub.id}" + json['url'] = "https://www.chemotion-repository.net/inchikey/#{pub.doi.suffix}" json['additionalType'] = 'Reaction' - json['name'] = element.rinchi_short_key - json['creator'] = json_ld_authors(taggable_data) + json['name'] = pub.element.rinchi_short_key + json['creator'] = json_ld_authors(pub.taggable_data) json['author'] = json['creator'] - json['description'] = json_ld_description(element.description) - json['license'] = rights_data[:rightsURI] - json['datePublished'] = published_at&.strftime('%Y-%m-%d') - json['dateCreated'] = created_at&.strftime('%Y-%m-%d') + json['description'] = json_ld_description(pub.element.description) + json['license'] = pub.rights_data[:rightsURI] + json['datePublished'] = pub.published_at&.strftime('%Y-%m-%d') + json['dateCreated'] = pub.created_at&.strftime('%Y-%m-%d') json['publisher'] = json_ld_publisher json['provider'] = json_ld_publisher json['keywords'] = 'chemical reaction: structures conditions' - json['citation'] = json_ld_citations(element.literatures, element.id) - json['subjectOf'] = json_ld_reaction_has_part(ext) + json['citation'] = json_ld_citations(pub.element.literatures, pub.element.id) + json['subjectOf'] = json_ld_reaction_has_part(pub, ext, is_root) if is_root == true if ext == 'LLM' json = Metadata::Jsonldllm.reaction_ext(json, element) @@ -210,17 +211,17 @@ def json_ld_lab_protocol json end - def json_ld_reaction_has_part(ext = nil) + def json_ld_reaction_has_part(root, ext = nil, is_root = true) json = [] - children&.each do |pub| + root.children&.each do |pub| json.push(json_ld_sample(pub, ext)) if pub.element_type == 'Sample' json.push(json_ld_analysis(pub, false)) if pub.element_type == 'Container' end - if ext == 'LLM' && element&.samples.present? - element&.samples&.each do |sample| + if ext == 'LLM' && root.element&.samples.present? + root.element&.samples&.each do |sample| next if sample.publication.present? || !sample.collections&.pluck(:id).include?(Collection.public_collection&.id) - json.push(Metadata::Jsonldllm.all_samples(element, sample)) + json.push(Metadata::Jsonldllm.all_samples(root.element, sample)) end end @@ -236,13 +237,15 @@ def json_ld_description(desc) #xml_data = Nokogiri::XML(metadata_xml) #desc = xml_data.search('description')&.text&.strip #desc + rescue StandardError => e + Rails.logger.error ["API call - json_ld_description:", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR) + '' end def json_ld_container json_ld_analysis(self, true) end - def json_ld_subjectOf(pub = self) arr = [] # arr.push(json_ld_creative_work(pub)) @@ -254,23 +257,103 @@ def json_ld_subjectOf(pub = self) def json_ld_analysis(pub = self, root = true) json = {} - json['@context'] = 'https://schema.org' + json['@context'] = 'https://schema.org' if root == true json['@type'] = 'Dataset' - json['@id'] = "https://doi.org/#{pub.doi.full_doi}" + json['@id'] = pub.doi.full_doi json['identifier'] = "CRD-#{pub.id}" json['url'] = "https://www.chemotion-repository.net/inchikey/#{pub.doi.suffix}" + json['dct:conformsTo'] = { + "@id": 'https://schema.org/Dataset', + "@type": 'CreativeWork' + } json['publisher'] = json_ld_publisher json['license'] = pub.rights_data[:rightsURI] - json['name'] = pub.element.extended_metadata['kind'] || '' if pub&.element&.extended_metadata.present? + json['name'] = (pub.element.extended_metadata['kind'] || '').split(' | ')&.last if pub&.element&.extended_metadata.present? measureInfo = json_ld_measurement_technique(pub) if pub&.element&.extended_metadata.present? && ENV['OLS_SERVICE'].present? + variable_measured = json_ld_variable_measured(pub) json['measurementTechnique'] = measureInfo if measureInfo.present? + json['variableMeasured'] = variable_measured if variable_measured.present? json['creator'] = json_ld_authors(pub.taggable_data) json['author'] = json['creator'] json['description'] = json_ld_analysis_description(pub) - json['includedInDataCatalog'] = json_ld_data_catalog(pub) if root == true + if root == true + json['includedInDataCatalog'] = json_ld_data_catalog(pub) + json['isPartOf'] = is_part_of(pub) + end + json + end + + def is_part_of(pub = self) + + json = {} + if pub.parent.present? + json = json_ld_reaction(pub.parent, nil, false) if pub.parent.element_type == 'Reaction' + json = json_ld_sample_root(pub.parent, false) if pub.parent.element_type == 'Sample' + end json end + def get_val(field) + return '' if field.blank? + case field['type'] + when Labimotion::FieldType::SYSTEM_DEFINED + unit = Labimotion::Units::FIELDS.find { |o| o[:field] == field['option_layers'] }&.fetch(:units, []).find { |u| u[:key] == field['value_system'] }&.fetch(:label, '') + "#{field['value']} #{unit}" + when Labimotion::FieldType::TEXT, Labimotion::FieldType::INTEGER, Labimotion::FieldType::SELECT, Labimotion::FieldType::INTEGER + field['value'] + else + '' + end + end + + def get_ols_short_form(field, klass, key) + short_form = field.fetch('ontology', {}).fetch('short_form', '') + return short_form if short_form.present? + + klass_prop = klass['properties_release'] + klass_layer = klass_prop['layers'][key] + klass_field = klass_layer && klass_layer['fields']&.find { |f| f['field'] == field['field'] } + return klass_field && klass_field['ontology']&.fetch('short_form', '') + end + + def json_ld_variable_measured(pub = self) + arr = [] + analysis = pub.element + containers = Container.where(parent_id: analysis.id, container_type: 'dataset') + containers.each do |container| + ds = container.dataset + ols_id = ds&.dataset_klass&.ols_term_id + next if ds.nil? || ols_id.nil? + # mcon = Rails.configuration.try(:m)&.dataset&.find { |ss| ss[:ols_term] == ols_id } + # con_layers = mcon[:layers].pluck(:identifier) if mcon.present? + # next if mcon.nil? || con_layers.nil? + + klass = Labimotion::DatasetKlass.find_by(ols_term_id: ols_id) + + ds&.properties.fetch('layers', nil)&.keys.each do |key| + # next unless con_layers&.include?(key) + # mcon_fields = mcon[:layers].find { |ss| ss[:identifier] == key }&.fetch(:fields, [])&.map { |field| field[:identifier] } + # next if mcon_fields.nil? + + ds&.properties['layers'][key].fetch('fields', []).each do |field| + # next unless mcon_fields&.include?(field['field']) + # short_form = field.fetch('ontology', {}).fetch('short_form', '') + short_form = get_ols_short_form(field, klass, key) + val = get_val(field) + next if field['value'].blank? || short_form.blank? || val.blank? + + json = {} + json['@type'] = 'PropertyValue' + json['name'] = field["label"] + json['propertyID'] = short_form if short_form.present? + json['value'] = val + arr.push(json) unless json.empty? + end + end + end + arr + end + def json_ld_measurement_technique(pub = self) json = {} term_id = pub.element.extended_metadata['kind']&.split('|')&.first&.strip @@ -296,11 +379,12 @@ def json_ld_analysis_description(pub) kind = 'dataset for ' + (element.extended_metadata['kind'] || '')&.split('|').pop + '\n' desc = element.extended_metadata['description'] || '' + '\n' content = element.extended_metadata['content'].nil? ? '' : REXML::Text.new(Nokogiri::HTML( Chemotion::QuillToHtml.new.convert(element.extended_metadata['content'])).text, false, nil, false).to_s - kind + desc + content + rescue StandardError => e + Rails.logger.error ["API call - json_ld_analysis_description:", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR) + kind + desc end - def json_ld_citations(literatures, id) json = [] literatures.each do |lit| diff --git a/app/models/concerns/publishing.rb b/app/models/concerns/publishing.rb index 8a2c6285d..7ee4d5e45 100644 --- a/app/models/concerns/publishing.rb +++ b/app/models/concerns/publishing.rb @@ -68,7 +68,6 @@ def reserve_suffix_analyses(as = Container.none) ### for all def full_doi return nil unless (d = Doi.find_by(doiable: self)) - d.full_doi end diff --git a/app/models/publication.rb b/app/models/publication.rb index 834649369..975c63cb5 100644 --- a/app/models/publication.rb +++ b/app/models/publication.rb @@ -76,7 +76,7 @@ def puttextcontent(content, remotefile, &block) STATE_RETRACTED = 'retracted' def embargoed?(root_publication = root) - cid = User.find(root_publication.published_by).publication_embargo_collection.id + cid = User.with_deleted.find(root_publication.published_by).publication_embargo_collection.id embargo_col = root_publication.element.collections.select { |c| c['ancestry'].to_i == cid } embargo_col.present? ? true : false end @@ -103,6 +103,7 @@ def process_element(new_state = state) when Publication::STATE_ACCEPTED move_to_accepted_collection group_review_collection + publish_user_labels when Publication::STATE_DECLINED declined_reverse_original_element declined_move_collections @@ -112,7 +113,7 @@ def process_element(new_state = state) # WARNING: for STATE_ACCEPTED the method does more than just notify users (see ChemotionRepoPublishingJob) # TODO: separate publishing responsability - def inform_users(new_state = state, current_user_id = 0) + def process_new_state_job(new_state = state, current_user_id = 0) method = if ENV['PUBLISH_MODE'] == 'production' && Rails.env.production? :perform_later elsif ENV['PUBLISH_MODE'] == 'staging' @@ -130,9 +131,32 @@ def inform_users(new_state = state, current_user_id = 0) klass.set(queue: "#{queue_name} #{id}").send(method, id, new_state, current_user_id) end + def publish_user_labels + tag = element&.tag + return if tag.nil? + + data = tag.taggable_data || {} + return if data['user_labels'].blank? + + public_labels = UserLabel.where(id: data['user_labels'], access_level: 2).pluck(:id) + data['user_labels'] = public_labels + tag.save! + + pub_data = taggable_data || {} + pub_data['user_labels'] = public_labels + update_columns(taggable_data: pub_data) + end + + def update_user_labels(user_labels, current_user_id) + data = taggable_data || {} + private_labels = UserLabel.where(id: data['user_labels'], access_level: [0, 1]).where.not(user_id: current_user_id).pluck(:id) + data['user_labels'] = ((user_labels || []) + private_labels)&.uniq + update_columns(taggable_data: data) + end + # remove publication element from editable collections def move_to_accepted_collection - pub_user = User.find(published_by) + pub_user = User.with_deleted.find(published_by) return false unless pub_user && element return true unless embargoed? @@ -157,7 +181,7 @@ def move_to_accepted_collection # move publication element from reviewer editable collection to submitter editable collection # move publication element from submitter readable collection to reviewer readable collection def move_to_review_collection - pub_user = User.find(published_by) + pub_user = User.with_deleted.find(published_by) return false unless pub_user && element case element_type @@ -173,7 +197,7 @@ def move_to_review_collection end def move_to_pending_collection - pub_user = User.find(published_by) + pub_user = User.with_deleted.find(published_by) return false unless pub_user && element case element_type @@ -203,11 +227,11 @@ def declined_reverse_original_element end def group_review_collection - pub_user = User.find(published_by) + pub_user = User.with_deleted.find(published_by) return false unless pub_user && element group_reviewers = review && review['reviewers'] - reviewers = User.where(id: group_reviewers) if group_reviewers.present? + reviewers = User.where(id: group_reviewers, type: 'Person') if group_reviewers.present? return false if reviewers&.empty? reviewers&.each do |user| @@ -237,7 +261,7 @@ def declined_reverse_original_reaction_elements end def declined_move_collections - all_col_id = User.find(published_by).all_collection&.id + all_col_id = User.with_deleted.find(published_by).all_collection&.id return unless element && all_col_id col_ids = element&.collections&.pluck(:id) @@ -285,6 +309,11 @@ def publication_logger @@publication_logger ||= Logger.new(File.join(Rails.root, 'log', 'publication.log')) end + + def self.repository_logger + @@repository_logger ||= Logger.new(Rails.root.join('log/repository.log')) + end + def doi_bag d = doi case element_type @@ -716,6 +745,21 @@ def log_invalid_transition(to_state) logger("CANNOT TRANSITION from #{state} to #{to_state}") end + def self.repo_log_exception(exception, options = {}) + Publication.repository_logger.error(self.class.name); + Publication.repository_logger.error("options [#{options}] \n ") + Publication.repository_logger.error("exception: #{exception.message} \n") + Publication.repository_logger.error(exception.backtrace.join("\n")) + + # send message to admin + Message.create_msg_notification( + channel_id: Channel.find_by(subject: Channel::SUBMITTING)&.id, + message_from: User.find_by(name_abbreviation: 'CHI')&.id, + autoDismiss: 5, + message_content: { 'data': "Repository Error Log: #{exception.message}" }, + ) + end + def logger(message_arr) message = [message_arr].flatten.join("\n") publication_logger.info( diff --git a/app/models/user_label.rb b/app/models/user_label.rb index 7d4841f92..2954035f0 100644 --- a/app/models/user_label.rb +++ b/app/models/user_label.rb @@ -17,10 +17,36 @@ class UserLabel < ApplicationRecord acts_as_paranoid - def self.public_labels(label_ids) + def self.public_labels(label_ids, current_user, is_public) return [] if label_ids.blank? - labels = UserLabel.where(id: label_ids, access_level: [1,2]) - .order('access_level desc, position, title') - labels&.map{|l| {title: l.title, description: l.description, color: l.color} } ||[] + + if is_public + labels = UserLabel.where(id: label_ids, access_level: 2).order('access_level desc, position, title') + else + if User.reviewer_ids.include?(current_user.id) + labels = UserLabel.where('id in (?) and ((user_id = ? AND access_level = 0) OR access_level IN (?))', label_ids, current_user.id, [1, 2, 3]) + .order('access_level desc, position, title') + else + labels = UserLabel.where('id in (?) and ((user_id = ? AND access_level = 0) OR access_level IN (?))', label_ids, current_user.id, [1, 2]) + .order('access_level desc, position, title') + end + end + + labels&.map{|l| {id: l.id, title: l.title, description: l.description, color: l.color, access_level: l.access_level} } ||[] + end + + def self.my_labels(current_user, is_public) + if is_public + labels = UserLabel.where('access_level IN (?)', current_user.id, [2]) + .order('access_level desc, position, title') + else + if User.reviewer_ids.include?(current_user.id) + labels = UserLabel.where('(user_id = ? AND access_level in (0, 1)) OR access_level IN (2, 3)', current_user.id) + .order('access_level desc, position, title') + else + labels = UserLabel.where('(user_id = ? AND access_level in (0, 1)) OR access_level = 2', current_user.id) + .order('access_level desc, position, title') + end + end end end diff --git a/app/packs/src/apps/home/Home.js b/app/packs/src/apps/home/Home.js index 36a586e7b..516337524 100644 --- a/app/packs/src/apps/home/Home.js +++ b/app/packs/src/apps/home/Home.js @@ -2,12 +2,9 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Grid, Row } from 'react-bootstrap'; import Aviator from 'aviator'; - import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; - import initPublicRoutes from 'src/libHome/homeRoutes'; - import Navigation from 'src/libHome/Navigation'; import Notifications from 'src/components/Notifications'; import RepoEmbargo from 'src/repoHome/RepoEmbargo'; @@ -35,19 +32,18 @@ import LoadingModal from 'src/components/common/LoadingModal'; import PublicActions from 'src/stores/alt/repo/actions/PublicActions'; import RepoGenericHub from 'src/repoHome/RepoGenericHub'; -import embedMatomo from 'src/components/chemrepo/matomo'; +import SysInfo from 'src/components/chemrepo/SysInfo'; class Home extends Component { constructor(props) { super(); this.state = { - guestPage: null + guestPage: null, }; this.onChange = this.onChange.bind(this); } componentDidMount() { - embedMatomo(); PublicStore.listen(this.onChange); RStore.listen(this.onChange); PublicActions.initialize(); @@ -69,7 +65,8 @@ class Home extends Component { } renderGuestPage() { - switch (this.state.guestPage) { + const { guestPage, listType } = this.state; + switch (guestPage) { case 'genericHub': return ; case 'moleculeArchive': @@ -91,7 +88,7 @@ class Home extends Component { case 'contact': return ; case 'publications': - return ; + return ; case 'review': return ; case 'collection': @@ -115,7 +112,8 @@ class Home extends Component { } renderNavFooter() { - switch (this.state.guestPage) { + const { guestPage } = this.state; + switch (guestPage) { case 'publications': case 'review': case 'embargo': @@ -134,15 +132,14 @@ class Home extends Component { render() { return (
+
- - {this.renderGuestPage()} - + {this.renderGuestPage()} {this.renderNavFooter()}
diff --git a/app/packs/src/apps/home/index.js b/app/packs/src/apps/home/index.js deleted file mode 100644 index da53cab94..000000000 --- a/app/packs/src/apps/home/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Home from 'src/apps/home/Home'; - -document.addEventListener('DOMContentLoaded', () => { - const domElement = document.getElementById('Home'); - if (domElement) { ReactDOM.render(, domElement); } -}); diff --git a/app/packs/src/apps/mydb/elements/details/ElementDetails.js b/app/packs/src/apps/mydb/elements/details/ElementDetails.js index fc9385906..719da02ad 100644 --- a/app/packs/src/apps/mydb/elements/details/ElementDetails.js +++ b/app/packs/src/apps/mydb/elements/details/ElementDetails.js @@ -174,10 +174,17 @@ export default class ElementDetails extends Component { content(_el) { const el = _el; - el.sealed = getPublicationId(el) ? true : false; + const isPending = el?.tag?.taggable_data?.publish_pending; + + el.sealed = isPending || !!getPublicationId(el); if (el && el.klassType === 'GenericEl' && el.type != null) { - return ; + return ( + + ); } switch (el.type) { diff --git a/app/packs/src/apps/mydb/elements/details/literature/LiteratureCommon.js b/app/packs/src/apps/mydb/elements/details/literature/LiteratureCommon.js index 99e9d8f47..88fac8dc0 100644 --- a/app/packs/src/apps/mydb/elements/details/literature/LiteratureCommon.js +++ b/app/packs/src/apps/mydb/elements/details/literature/LiteratureCommon.js @@ -138,13 +138,18 @@ const literatureContent = (literature, onlyText) => { indexData = indexData.substr(0, indexData.lastIndexOf(',')); litBibtex = litBibtex.replace(indexData, indexData.replace(/[^a-zA-Z0-9\-_]/g, '')); } - const citation = new Cite(litBibtex); + let citation; + try { + citation = new Cite(litBibtex); + } catch (error) { + console.error('An error occurred:', error); + } if (onlyText) { - content = citation.format('bibliography', { format: 'text', template: 'apa' }); + content = citation?.format('bibliography', { format: 'text', template: 'apa' }); } else { content = (
- {citation.format('bibliography', { format: 'text', template: 'apa' })} + {citation?.format('bibliography', { format: 'text', template: 'apa' })}
); } diff --git a/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js b/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js index 39c6a5c6a..40fdf338f 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js @@ -49,6 +49,7 @@ import CommentActions from 'src/stores/alt/actions/CommentActions'; import CommentModal from 'src/components/common/CommentModal'; import { commentActivation } from 'src/utilities/CommentHelper'; import { formatTimeStampsOfElement } from 'src/utilities/timezoneHelper'; +import { ShowUserLabels } from 'src/components/UserLabels'; import { PublishedTag, @@ -526,6 +527,7 @@ export default class ReactionDetails extends Component {
{colLabel} {rsPlanLabel} + { schemeOnly ?   : '' } diff --git a/app/packs/src/apps/mydb/elements/details/reactions/propertiesTab/ReactionDetailsProperties.js b/app/packs/src/apps/mydb/elements/details/reactions/propertiesTab/ReactionDetailsProperties.js index fce129f6f..db9b04bb0 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/propertiesTab/ReactionDetailsProperties.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/propertiesTab/ReactionDetailsProperties.js @@ -14,6 +14,7 @@ import { solventsTL } from 'src/utilities/reactionPredefined'; import OlsTreeSelect from 'src/components/OlsComponent'; import { permitOn } from 'src/components/common/uis'; import HelpInfo from 'src/components/common/HelpInfo'; +import { EditUserLabels } from 'src/components/UserLabels'; export default class ReactionDetailsProperties extends Component { constructor(props) { @@ -166,6 +167,11 @@ export default class ReactionDetailsProperties extends Component { + + + + +
diff --git a/app/packs/src/apps/mydb/elements/details/reactions/schemeTab/Material.js b/app/packs/src/apps/mydb/elements/details/reactions/schemeTab/Material.js index 8216b7a20..417321204 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/schemeTab/Material.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/schemeTab/Material.js @@ -403,7 +403,7 @@ class Material extends Component { createParagraph(m) { const { materialGroup } = this.props; - let molName = m.molecule_name_hash.label; + let molName = m.molecule_name_hash?.label; if (!molName) { molName = m.molecule.iupac_name; } if (!molName) { molName = m.molecule.sum_formular; } diff --git a/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js b/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js index b71806b15..b4c48594a 100644 --- a/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js +++ b/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js @@ -519,27 +519,6 @@ export default class SampleDetails extends React.Component { this.forceUpdate(); } - svgOrLoading(sample) { - let svgPath = ''; - if (this.state.loadingMolecule) { - svgPath = '/images/wild_card/loading-bubbles.svg'; - } else { - svgPath = sample.svgPath; - } - let className = svgPath ? 'svg-container' : 'svg-container-empty' - return ( - sample.can_update - ?
- - -
- :
- -
- ); - } - handleChemIdentSectionToggle() { this.setState({ showChemicalIdentifiers: !this.state.showChemicalIdentifiers @@ -981,7 +960,7 @@ export default class SampleDetails extends React.Component { decoupleMolecule={this.decoupleMolecule} /> - + {this.elementalPropertiesItem(sample)} {this.chemicalIdentifiersItem(sample)} @@ -1556,32 +1535,45 @@ export default class SampleDetails extends React.Component { svgOrLoading(sample) { let svgPath = ''; + const { svgPath: sampleSvgPath } = sample; if (this.state.loadingMolecule) { svgPath = '/images/wild_card/loading-bubbles.svg'; } else { - svgPath = sample.svgPath; + svgPath = sampleSvgPath; } const className = svgPath ? 'svg-container' : 'svg-container-empty'; - return ( - sample.can_update - ? ( -
- - -
- ) - : ( -
- -
- ) + return sample.can_update ? ( + <> +
+ + +
+ + + ) : ( +
+ + +
); } @@ -1870,12 +1862,6 @@ export default class SampleDetails extends React.Component { tabTitles={tabTitlesMap} onTabPositionChanged={this.onTabPositionChanged} /> - {this.state.sfn ? : null} {tabContents} diff --git a/app/packs/src/apps/mydb/elements/details/samples/analysesTab/SampleDetailsContainers.js b/app/packs/src/apps/mydb/elements/details/samples/analysesTab/SampleDetailsContainers.js index 968bd88af..317745c14 100644 --- a/app/packs/src/apps/mydb/elements/details/samples/analysesTab/SampleDetailsContainers.js +++ b/app/packs/src/apps/mydb/elements/details/samples/analysesTab/SampleDetailsContainers.js @@ -45,9 +45,6 @@ export default class SampleDetailsContainers extends Component { TextTemplateActions.fetchTextTemplates('sample'); } - // componentWillReceiveProps(nextProps) { - // } - componentWillUnmount() { UIStore.unlisten(this.onUIStoreChange); } diff --git a/app/packs/src/apps/mydb/elements/details/screens/ScreenDetails.js b/app/packs/src/apps/mydb/elements/details/screens/ScreenDetails.js index 2e99d2eb2..02c84db68 100644 --- a/app/packs/src/apps/mydb/elements/details/screens/ScreenDetails.js +++ b/app/packs/src/apps/mydb/elements/details/screens/ScreenDetails.js @@ -62,7 +62,8 @@ export default class ScreenDetails extends Component { } } - componentWillReceiveProps(nextProps) { + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(nextProps) { const { screen } = nextProps; this.setState({ screen }); } @@ -71,12 +72,6 @@ export default class ScreenDetails extends Component { UIStore.unlisten(this.onUIStoreChange); } - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps(nextProps) { - const { screen } = nextProps; - this.setState({ screen }); - } - onUIStoreChange(state) { if (state.screen.activeTab !== this.state.activeTab) { this.setState({ diff --git a/app/packs/src/apps/mydb/elements/details/screens/researchPlansTab/EmbeddedResearchPlanDetails.js b/app/packs/src/apps/mydb/elements/details/screens/researchPlansTab/EmbeddedResearchPlanDetails.js index b8d04c8dd..686415122 100644 --- a/app/packs/src/apps/mydb/elements/details/screens/researchPlansTab/EmbeddedResearchPlanDetails.js +++ b/app/packs/src/apps/mydb/elements/details/screens/researchPlansTab/EmbeddedResearchPlanDetails.js @@ -56,7 +56,7 @@ export default class EmbeddedResearchPlanDetails extends Component { this.handleBodyAdd = this.handleBodyAdd.bind(this); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { let { researchPlan, expanded } = nextProps; if (!(researchPlan instanceof ResearchPlan)) { const rResearchPlan = new ResearchPlan(researchPlan); diff --git a/app/packs/src/apps/mydb/elements/list/ElementsTable.js b/app/packs/src/apps/mydb/elements/list/ElementsTable.js index 231c709ae..dd2e589c8 100644 --- a/app/packs/src/apps/mydb/elements/list/ElementsTable.js +++ b/app/packs/src/apps/mydb/elements/list/ElementsTable.js @@ -16,7 +16,7 @@ import ElementStore from 'src/stores/alt/stores/ElementStore'; import ElementAllCheckbox from 'src/apps/mydb/elements/list/ElementAllCheckbox'; import ElementsTableEntries from 'src/apps/mydb/elements/list/ElementsTableEntries'; import ElementsTableSampleEntries from 'src/apps/mydb/elements/list/ElementsTableSampleEntries'; - +import { SearchUserLabels } from 'src/components/UserLabels'; import UserStore from 'src/stores/alt/stores/UserStore'; import ElementsTableGroupedEntries from 'src/apps/mydb/elements/list/ElementsTableGroupedEntries'; import Select from 'react-select'; @@ -50,6 +50,7 @@ export default class ElementsTable extends React.Component { this.changeDateFilter = this.changeDateFilter.bind(this); this.toggleProductOnly = this.toggleProductOnly.bind(this); + this.setUserLabel = this.setUserLabel.bind(this); this.setFromDate = this.setFromDate.bind(this); this.setToDate = this.setToDate.bind(this); this.timer = null; @@ -108,7 +109,7 @@ export default class ElementsTable extends React.Component { } const { checkedIds, uncheckedIds, checkedAll } = state[type]; const { - filterCreatedAt, fromDate, toDate, number_of_results, currentSearchByID, productOnly + filterCreatedAt, fromDate, toDate, userLabel, number_of_results, currentSearchByID, productOnly } = state; // check if element details of any type are open at the moment @@ -120,7 +121,7 @@ export default class ElementsTable extends React.Component { const { currentStateProductOnly, searchResult } = this.state; const stateChange = ( checkedIds || uncheckedIds || checkedAll || currentId || filterCreatedAt - || fromDate || toDate || productOnly !== currentStateProductOnly + || fromDate || toDate || userLabel || productOnly !== currentStateProductOnly || isSearchResult !== searchResult ); const moleculeSort = isSearchResult ? true : ElementStore.getState().moleculeSort; @@ -135,7 +136,8 @@ export default class ElementsTable extends React.Component { currentId, number_of_results, fromDate, - toDate + toDate, + userLabel, }, productOnly, searchResult: isSearchResult, @@ -164,6 +166,12 @@ export default class ElementsTable extends React.Component { if (elementsDidChange || currentElementDidChange) { this.setState(nextState); } } + + setUserLabel(label) { + const { userLabel } = this.state; + if (userLabel !== label) UIActions.setUserLabel(label); + } + setFromDate(date) { const { fromDate } = this.state; if (fromDate !== date) UIActions.setFromDate(date); @@ -573,15 +581,25 @@ export default class ElementsTable extends React.Component { renderHeader = () => { const { filterCreatedAt, ui } = this.state; const { type, showReport, genericEl } = this.props; - const { fromDate, toDate } = ui; + const { fromDate, toDate, userLabel } = ui; + let searchLabel = ; let typeSpecificHeader = ; if (type === 'sample') { typeSpecificHeader = this.renderSamplesHeader(); + searchLabel = ( + + ); } else if (type === 'reaction') { typeSpecificHeader = this.renderReactionsHeader(); + searchLabel = ( + + ); } else if (genericEl) { typeSpecificHeader = this.renderGenericElementsHeader(); + searchLabel = ( + + ); } const filterTitle = filterCreatedAt === true @@ -610,6 +628,7 @@ export default class ElementsTable extends React.Component { flexWrap: 'wrap' }} > + {searchLabel}
diff --git a/app/packs/src/components/Quill2Viewer.js b/app/packs/src/components/Quill2Viewer.js new file mode 100644 index 000000000..2c694a1b2 --- /dev/null +++ b/app/packs/src/components/Quill2Viewer.js @@ -0,0 +1,52 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import Quill from 'quill2'; +import { isEqual } from 'lodash'; +import { keepSupSub } from 'src/utilities/quillFormat'; + +function Quill2Viewer({ value, preview }) { + const quillViewerRef = useRef(null); + const viewerRef = useRef(null); + + useEffect(() => { + if (!viewerRef.current) { + const defaultOptions = { + theme: 'bubble', + readOnly: true, + modules: { + toolbar: null, + }, + }; + viewerRef.current = new Quill(quillViewerRef.current, defaultOptions); + const oriValue = value; + const initialValue = preview ? keepSupSub(oriValue) : oriValue; + viewerRef.current.setContents(initialValue); + } + }, []); + + useEffect(() => { + if (viewerRef.current && !isEqual(viewerRef.current.getContents(), value)) { + viewerRef.current.setContents(value); + } + }, [value]); + + return preview ? ( +
+
+
+ ) : ( + + ); +} + +Quill2Viewer.propTypes = { + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + preview: PropTypes.bool, +}; + +Quill2Viewer.defaultProps = { + value: [], + preview: false, +}; + +export default Quill2Viewer; diff --git a/app/packs/src/components/UserLabels.js b/app/packs/src/components/UserLabels.js index 473f26c31..496815eef 100644 --- a/app/packs/src/components/UserLabels.js +++ b/app/packs/src/components/UserLabels.js @@ -3,8 +3,18 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { - Modal, Checkbox, Table, Col, Badge, Panel, ButtonGroup, Button, - Form, FormGroup, FormControl, ControlLabel, InputGroup, + Modal, + Table, + Col, + Badge, + Panel, + ButtonGroup, + Button, + Form, + FormGroup, + FormControl, + ControlLabel, + InputGroup, } from 'react-bootstrap'; import { CirclePicker } from 'react-color'; import Select from 'react-select'; @@ -12,9 +22,6 @@ import UsersFetcher from 'src/fetchers/UsersFetcher'; import NotificationActions from 'src/stores/alt/actions/NotificationActions'; import UserActions from 'src/stores/alt/actions/UserActions'; import UserStore from 'src/stores/alt/stores/UserStore'; -import MatrixCheck from 'src/components/common/MatrixCheck'; - -const UL_FUNC_NAME = 'userLabel'; class UserLabelModal extends Component { constructor(props) { @@ -23,7 +30,7 @@ class UserLabelModal extends Component { labels: [], label: {}, showDetails: false, - defaultColor: '#428BCA' + defaultColor: '#428BCA', }; this.onChange = this.onChange.bind(this); this.handelNewLabel = this.handelNewLabel.bind(this); @@ -113,31 +120,33 @@ class UserLabelModal extends Component { onChange(state) { const { currentUser, labels } = state; - const list = (labels || []).filter( - (r) => r.access_level === 2 || r.user_id === (currentUser && currentUser.id) - ) || []; + const list = + (labels || []).filter( + r => + r.access_level === 2 || r.user_id === (currentUser && currentUser.id) + ) || []; this.setState({ - labels: list + labels: list, }); } handelNewLabel() { this.setState({ label: {}, - showDetails: true + showDetails: true, }); } renderUserLabels() { const { labels } = this.state; if (labels == null || labels.length === 0) { - return (
); + return
; } - return (labels || []).map((g) => { + return (labels || []).map(g => { const badgeStyle = { - backgroundColor: g.color || this.state.defaultColor + backgroundColor: g.color || this.state.defaultColor, }; let accessLabel = ''; switch (g.access_level) { @@ -150,11 +159,12 @@ class UserLabelModal extends Component { case 2: accessLabel = 'Global'; break; + case 3: + accessLabel = 'Review'; + break; default: accessLabel = ''; } - const currentUser = UserStore.getState()?.currentUser; - const isReviewer = (currentUser?.is_reviewer) || false; return ( @@ -228,6 +238,7 @@ class UserLabelModal extends Component { const isReviewer = (currentUser?.is_reviewer) || false; if (isReviewer) { + accessList.unshift({ label: 'Review - Reviewer only', value: 3 }); accessList.unshift({ label: 'Global - Open to everyone', value: 2 }); } @@ -239,6 +250,7 @@ class UserLabelModal extends Component { this.handleSelectChange(e)} + /> + +
+ ); + } +} + +class ReviewUserLabels extends React.Component { + constructor(props) { + super(props); + this.state = { + currentUser: (UserStore.getState() && UserStore.getState().currentUser) || {}, + labels: this.props.labels || (UserStore.getState() && UserStore.getState().labels) || [], + selectedLabels: this.props.element.user_labels || [], + }; + this.onChange = this.onChange.bind(this); + this.handleSelectChange = this.handleSelectChange.bind(this); + } + + componentDidMount() { + UserStore.listen(this.onChange); + } + + componentWillUnmount() { + UserStore.unlisten(this.onChange); + } + + handleSelectChange(val) { + const { element } = this.props; + if (val) { + const ids = val.map(v => v.value); + if (ids != null) { + // element.setUserLabels(ids); + this.props.fnCb(element, ids); + } + this.setState({ selectedLabels: val }); + } + } + + onChange(state) { + const { currentUser, labels } = state; + this.setState({ + currentUser, + labels + }); + } + + render() { + let { selectedLabels } = this.state; + const { currentUser, labels } = this.state; + const { element } = this.props; + const curLableIds = element.user_labels || []; + + // const reviewLabel = labels.filter(l => l.access_level === 3); + const reviewLabel = labels; + + const defaultLabels = (labels || []) + .filter( + (r) => (curLableIds || []).includes(r.id) + && (r.access_level > 0 || r.user_id === currentUser.id) + ) .map((ll) => ({ value: ll.id, label: ( - {ll.title} + + {ll.title} + ), - })) || []; + })); + + if (selectedLabels == null) { + selectedLabels = defaultLabels; + } + + const labelOptions = + (reviewLabel || []) + .filter(r => r.access_level > 0 || r.user_id === currentUser.id) + .map(ll => ({ + value: ll.id, + label: ( + + {ll.title} + + ), + })) || []; return (
- My Labels this.handleSelectChange(e)} + /> +
+ ); + } +} + UserLabelModal.propTypes = { showLabelModal: PropTypes.bool.isRequired, onHide: PropTypes.func.isRequired }; EditUserLabels.propTypes = { - element: PropTypes.object.isRequired + element: PropTypes.object.isRequired, + fnCb: PropTypes.func.isRequired, +}; + +ReviewUserLabels.propTypes = { + element: PropTypes.object.isRequired, + fnCb: PropTypes.func.isRequired, }; ShowUserLabels.propTypes = { element: PropTypes.object.isRequired }; -export { UserLabelModal, EditUserLabels, ShowUserLabels }; + +SearchUserLabels.propTypes = { + fnCb: PropTypes.func.isRequired, + // eslint-disable-next-line react/require-default-props + userLabel: PropTypes.number, + className: PropTypes.string, + isPublish: PropTypes.bool, +}; + +SearchUserLabels.defaultProps = { + className: 'header-group-select', + isPublish: false, +}; + +export { + UserLabelModal, + EditUserLabels, + ReviewUserLabels, + ShowUserLabels, + SearchUserLabels, +}; diff --git a/app/packs/src/components/chemrepo/ExactMass.js b/app/packs/src/components/chemrepo/ExactMass.js new file mode 100644 index 000000000..3ddf3b881 --- /dev/null +++ b/app/packs/src/components/chemrepo/ExactMass.js @@ -0,0 +1,27 @@ +/* eslint-disable react/destructuring-assignment */ +import React from 'react'; +import RepoConst from 'src/components/chemrepo/common/RepoConst'; + +function FormatEM(em) { + if (em) { + return ( + + {em.toFixed(6)} g⋅mol-1 + + ); + } + return null; +} + +function ExactMass(sample, molecule) { + const { decoupled = false, molecular_mass: molecularMass } = sample ?? {}; + const { inchikey = '', exact_molecular_weight: exactMolecularWeight } = + molecule ?? {}; + const mass = + decoupled && inchikey !== RepoConst.INCHIKEY_DUMMY + ? molecularMass + : exactMolecularWeight; + return FormatEM(mass); +} + +export default ExactMass; diff --git a/app/packs/src/components/chemrepo/ExtIcon.js b/app/packs/src/components/chemrepo/ExtIcon.js new file mode 100644 index 000000000..8bac9e37c --- /dev/null +++ b/app/packs/src/components/chemrepo/ExtIcon.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PublicStore from 'src/stores/alt/repo/stores/PublicStore'; +import UIStore from 'src/stores/alt/stores/UIStore'; + +function getCollectionIcon(_collectionLabel) { + const uiExtension = UIStore.getState().u || PublicStore.getState().u; + const { collectionIcons } = uiExtension; + if (!_collectionLabel || !collectionIcons) return null; + return collectionIcons.find(c => c.label === _collectionLabel)?.icons[0]; +} + +function ExtIcon(_collectionLabel) { + const icon = getCollectionIcon(_collectionLabel); + if (!icon || !icon.filename) return null; + const { filename, title } = icon; + + return ( + {filename} + ); +} + +function ExtInfo(_collectionLabel) { + const icon = getCollectionIcon(_collectionLabel); + if (!icon || !icon.info) return null; + const { info } = icon; + + return ( +
+ Additional Information: + {info} +
+ ); +} + +export { ExtIcon, ExtInfo }; diff --git a/app/packs/src/components/chemrepo/PublicAnchor.js b/app/packs/src/components/chemrepo/PublicAnchor.js index d5d231e6c..58e513069 100644 --- a/app/packs/src/components/chemrepo/PublicAnchor.js +++ b/app/packs/src/components/chemrepo/PublicAnchor.js @@ -1,15 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -const PublicAnchor = (props) => { +function PublicAnchor(props) { const { doi, isPublished } = props; if (!isPublished || typeof doi !== 'string') return null; const anchorId = doi?.split('/').pop() || ''; - return ; -}; + return ; +} PublicAnchor.propTypes = { - doi: PropTypes.string.isRequired, + doi: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, isPublished: PropTypes.bool.isRequired, }; diff --git a/app/packs/src/components/chemrepo/PublicLabels.js b/app/packs/src/components/chemrepo/PublicLabels.js index 3839d6967..44145c4cf 100644 --- a/app/packs/src/components/chemrepo/PublicLabels.js +++ b/app/packs/src/components/chemrepo/PublicLabels.js @@ -2,31 +2,32 @@ import React from 'react'; import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap'; import uuid from 'uuid'; -const PublicLabels = (_labels) => { +function PublicLabels(_labels) { + // eslint-disable-next-line react/destructuring-assignment if (!_labels || !Array.isArray(_labels) || _labels.length < 1) return null; const renderTooltip = (description, title) => ( - - {description || title} - + {description || title} ); + // eslint-disable-next-line react/destructuring-assignment const output = _labels.map(_label => ( - + {_label.title} - + )); - return ( -
- {output} -
- ); -}; + return
{output}
; +} export default PublicLabels; diff --git a/app/packs/src/components/chemrepo/PublicReactionTlc.js b/app/packs/src/components/chemrepo/PublicReactionTlc.js index da3575324..606d5af78 100644 --- a/app/packs/src/components/chemrepo/PublicReactionTlc.js +++ b/app/packs/src/components/chemrepo/PublicReactionTlc.js @@ -15,7 +15,6 @@ const PublicReactionTlc = ({ } = reaction; tlcDescription = tlcDescription || ''; tlcSolvents = tlcSolvents || ''; - rfValue = rfValue && rfValue !== '0' ? rfValue : ''; if (isPublished && !tlcDescription && !tlcSolvents && !rfValue) return null; return ( diff --git a/app/packs/src/components/chemrepo/PublicSample.js b/app/packs/src/components/chemrepo/PublicSample.js index 49d22a750..2305a10ff 100644 --- a/app/packs/src/components/chemrepo/PublicSample.js +++ b/app/packs/src/components/chemrepo/PublicSample.js @@ -3,6 +3,8 @@ import { Button } from 'react-bootstrap'; import { Citation, literatureContent, RefByUserInfo } from 'src/apps/mydb/elements/details/literature/LiteratureCommon'; import { ChemotionId, CommentBtn, Doi } from 'src/repoHome/RepoCommon'; import { formatPhysicalProps } from 'src/components/chemrepo/publication-utils'; +import RepoConst from 'src/components/chemrepo/common/RepoConst'; +import DecoupleInfo from 'src/repoHome/DecoupleInfo'; const PublicSample = (_props) => { const { @@ -18,9 +20,9 @@ const PublicSample = (_props) => { )) : []; let sampleTypeDescription = 'Consists of molecule with defined structure'; - if (sample.decoupled && element.molecule.inchikey === 'DUMMY') { + if (sample.decoupled && element.molecule.inchikey === RepoConst.INCHIKEY_DUMMY) { sampleTypeDescription = 'Includes only undefined structural components'; - } else if (sample.decoupled && element.molecule.inchikey !== 'DUMMY') { + } else if (sample.decoupled && element.molecule.inchikey !== RepoConst.INCHIKEY_DUMMY) { sampleTypeDescription = 'Includes a fragment with defined structure'; } @@ -65,6 +67,8 @@ const PublicSample = (_props) => { return (
Sample type: {sampleTypeDescription} + +
{embargo} diff --git a/app/packs/src/components/chemrepo/PublishCommon.js b/app/packs/src/components/chemrepo/PublishCommon.js index 7a7e46300..61424b95e 100644 --- a/app/packs/src/components/chemrepo/PublishCommon.js +++ b/app/packs/src/components/chemrepo/PublishCommon.js @@ -19,6 +19,8 @@ const labelStyle = { const handleClick = (e, id, clickType) => { e.preventDefault(); e.stopPropagation(); + if (typeof id === 'undefined' || id === null) return; + const uri = Aviator.getCurrentURI(); const uriArray = uri.split(/\//); switch (clickType) { @@ -216,23 +218,27 @@ const PublishedTag = ({ element, fnUnseal }) => { ? `${tagType} is being reviewed` : `${tagType} has been published`; const publishedId = getPublicationId(element); - return publishedId ? ( - - {tip}} - > -