From b1108f97ac211103476963ed3a787ab42ba2bc6a Mon Sep 17 00:00:00 2001 From: Audrey Hamelers Date: Wed, 12 Jun 2024 18:26:05 +0200 Subject: [PATCH 1/8] basically functional customizable admin dashboard, most filters (ugly) --- .../admin_dashboard_controller.rb | 141 ++++++++++++++++++ app/models/stash_datacite/affiliation.rb | 4 +- app/models/stash_engine/resource.rb | 10 +- app/models/stash_engine/tenant.rb | 7 + app/policies/stash_engine/tenant_policy.rb | 4 +- .../admin_dashboard/_fields.html.erb | 48 ++++++ .../admin_dashboard/_filter_date.html.erb | 10 ++ .../admin_dashboard/_filters.html.erb | 26 ++++ .../admin_dashboard/_table_header.html.erb | 62 ++++++++ .../admin_dashboard/_table_row.html.erb | 71 +++++++++ .../admin_dashboard/index.csv.erb | 60 ++++++++ .../admin_dashboard/index.html.erb | 45 ++++++ config/routes.rb | 1 + .../stash/repo/merritt_oaidc_builder_spec.rb | 10 +- .../models/stash_datacite/affiliation_spec.rb | 3 +- 15 files changed, 480 insertions(+), 22 deletions(-) create mode 100644 app/controllers/stash_engine/admin_dashboard_controller.rb create mode 100644 app/views/stash_engine/admin_dashboard/_fields.html.erb create mode 100644 app/views/stash_engine/admin_dashboard/_filter_date.html.erb create mode 100644 app/views/stash_engine/admin_dashboard/_filters.html.erb create mode 100644 app/views/stash_engine/admin_dashboard/_table_header.html.erb create mode 100644 app/views/stash_engine/admin_dashboard/_table_row.html.erb create mode 100644 app/views/stash_engine/admin_dashboard/index.csv.erb create mode 100644 app/views/stash_engine/admin_dashboard/index.html.erb diff --git a/app/controllers/stash_engine/admin_dashboard_controller.rb b/app/controllers/stash_engine/admin_dashboard_controller.rb new file mode 100644 index 0000000000..b31ff77485 --- /dev/null +++ b/app/controllers/stash_engine/admin_dashboard_controller.rb @@ -0,0 +1,141 @@ +module StashEngine + class AdminDashboardController < ApplicationController + helper SortableTableHelper + before_action :require_user_login + before_action :setup_paging, only: :index + before_action :setup_search, only: :index + # before_action :load, only: %i[popup note_popup edit] + + # rubocop:disable Metrics/MethodLength + def index + @datasets = StashEngine::Resource.latest_per_dataset + .left_outer_joins(identifier: %i[counter_stat internal_data]) + .preload(identifier: %i[counter_stat internal_data]) + .left_outer_joins(:last_curation_activity) + .preload(:last_curation_activity) + .left_outer_joins(:authors) + .preload(:authors) + .joins(" + left outer join stash_engine_curation_activities seca on seca.id = ( + select ca.id from stash_engine_curation_activities ca where ca.resource_id = stash_engine_resources.id + and ca.status in ('submitted', 'peer_review') + order by ca.created_at limit 1 + )") + .joins("left outer join ( + select stash_engine_users.* from stash_engine_users + inner join stash_engine_roles on stash_engine_users.id = stash_engine_roles.user_id + and role in ('curator', 'superuser') + ) curator on curator.id = stash_engine_resources.current_editor_id") + .joins("left outer join stash_engine_journals on stash_engine_internal_data.data_type = 'publicationISSN' + and stash_engine_journals.issn like CONCAT('%', stash_engine_internal_data.value ,'%')") + .select(" + distinct stash_engine_resources.*, stash_engine_curation_activities.status, + stash_engine_counter_stats.unique_investigation_count, stash_engine_counter_stats.citation_count, + stash_engine_counter_stats.unique_request_count, seca.created_at as submit_date, + stash_engine_journals.title as journal_title, stash_engine_journals.sponsor_id, stash_engine_journals.issn, + CONCAT_WS(' ', curator.first_name, curator.last_name) as curator_name, + (select GROUP_CONCAT(distinct CONCAT_WS(', ', sea.author_last_name, sea.author_first_name) ORDER BY sea.author_order, sea.id separator '; ') + from stash_engine_authors sea where sea.resource_id = stash_engine_resources.id) as author_string, + MATCH(stash_engine_identifiers.search_words) AGAINST('#{@search_string}') as relevance + ") + + add_fields + add_filters + date_filters + + order_string = 'relevance desc' + if params[:sort].present? + order_list = %w[title author_string status total_file_size unique_investigation_count curator_name + updated_at submit_date publication_date] + order_string = helpers.sortable_table_order(whitelist: order_list) + order_string += ', relevance desc' if @search_string.present? + end + + @datasets = @datasets.order(order_string) + @datasets = @datasets.page(@page).per(@page_size) + + respond_to do |format| + format.html + format.csv do + headers['Content-Disposition'] = "attachment; filename=#{Time.new.strftime('%F')}_report.csv" + end + end + end + # rubocop:enable Metrics/MethodLength + + private + + def setup_paging + if request.format.csv? + @page = 1 + @page_size = 2_000 + return + end + @page = params[:page] || '1' + @page_size = if params[:page_size].blank? || params[:page_size].to_i == 0 + 10 + else + params[:page_size].to_i + end + end + + def setup_search + @search_string = params[:q] || '' + @filters = params[:filters] || session[:admin_search_filters] || {} + session[:admin_search_filters] = params[:filters] if params[:filters].present? + @fields = params[:fields] || session[:admin_search_fields] + session[:admin_search_fields] = params[:fields] if params[:fields].present? + return unless @fields.blank? + + @fields = %w[doi keywords authors status metrics submit_date publication_date] + @fields << 'curator' if current_user.min_curator? + end + + def add_fields + @datasets = @datasets.preload(:funders) if @fields.include?('funders') + @datasets = @datasets.preload(:subjects) if @fields.include?('keywords') + @datasets = @datasets.preload(authors: :affiliations).preload(:tenant) if @fields.include?('affiliations') + @datasets = @datasets.preload(tenant: :ror_orgs).preload(authors: { affiliations: :ror_org }) if @fields.include?('countries') + end + + # rubocop:disable Style/MultilineIfModifier + def add_filters + @datasets = @datasets.where('stash_engine_curation_activities.status': @filters[:status]) if @filters[:status].present? + @datasets = @datasets.where( + 'curator.id': Integer(@filters[:curator], exception: false) ? @filters[:curator] : nil + ) if @filters[:curator].present? + @datasets = @datasets.where('stash_engine_journals.sponsor_id': @filters[:sponsor]) if @filters[:sponsor].present? + @datasets = @datasets.where("MATCH(stash_engine_identifiers.search_words) AGAINST('#{@search_string}') > 0") unless @search_string.blank? + return unless @filters[:member].present? && StashEngine::Tenant.find_by(id: @filters[:member]).present? + + tenant_orgs = StashEngine::Tenant.find(@filters[:member]).ror_ids + @datasets = @datasets.left_outer_joins(authors: :affiliations).left_outer_joins(:funders) + @datasets = @datasets.where( + 'stash_engine_resources.tenant_id = ? or stash_engine_identifiers.payment_id = ? + or dcs_affiliations.ror_id in (?) or dcs_contributors.name_identifier_id in (?)', + @filters[:member], @filters[:member], tenant_orgs, tenant_orgs + ) + end + + def date_filters + @datasets = @datasets.where( + "stash_engine_curation_activities.updated_at #{date_string(@filters[:updated_at])}" + ) unless @filters[:updated_at].nil? || @filters[:updated_at].values.all?(&:blank?) + @datasets = @datasets.where( + "seca.created_at #{date_string(@filters[:submit_date])}" + ) unless @filters[:submit_date].nil? || @filters[:submit_date].values.all?(&:blank?) + @datasets = @datasets.where( + "stash_engine_resources.publication_date #{date_string(@filters[:publication_date])}" + ) unless @filters[:publication_date].nil? || @filters[:publication_date].values.all?(&:blank?) + end + # rubocop:enable Style/MultilineIfModifier + + def date_string(date_hash) + from = date_hash[:start_date] + to = date_hash[:end_date] + return "< '#{to}'" if from.blank? + + "BETWEEN '#{from}' AND #{to.blank? ? 'now()' : "'#{to}'"}" + end + end +end diff --git a/app/models/stash_datacite/affiliation.rb b/app/models/stash_datacite/affiliation.rb index 9f7afcbf88..18603cab3f 100644 --- a/app/models/stash_datacite/affiliation.rb +++ b/app/models/stash_datacite/affiliation.rb @@ -24,6 +24,7 @@ class Affiliation < ApplicationRecord self.table_name = 'dcs_affiliations' has_and_belongs_to_many :authors, class_name: 'StashEngine::Author', join_table: 'dcs_affiliations_authors' has_and_belongs_to_many :contributors, class_name: 'StashDatacite::Contributor' + belongs_to :ror_org, class_name: 'StashEngine::RorOrg', primary_key: 'ror_id', foreign_key: 'ror_id', optional: true validates :long_name, presence: true @@ -42,9 +43,6 @@ def smart_name(show_asterisk: false) end def country_name - return nil if ror_id.blank? - - ror_org = StashEngine::RorOrg.find_by_ror_id(ror_id) return nil if ror_org.nil? || ror_org.country.nil? ror_org.country diff --git a/app/models/stash_engine/resource.rb b/app/models/stash_engine/resource.rb index 2e12d90655..fdcf0672e2 100644 --- a/app/models/stash_engine/resource.rb +++ b/app/models/stash_engine/resource.rb @@ -83,6 +83,7 @@ class Resource < ApplicationRecord # rubocop:disable Metrics/ClassLength has_one :language, class_name: 'StashDatacite::Language', dependent: :destroy has_many :descriptions, class_name: 'StashDatacite::Description', dependent: :destroy has_many :contributors, class_name: 'StashDatacite::Contributor', dependent: :destroy + has_many :funders, -> { where(contributor_type: 'funder') }, class_name: 'StashDatacite::Contributor' has_many :datacite_dates, class_name: 'StashDatacite::DataciteDate', dependent: :destroy has_many :descriptions, class_name: 'StashDatacite::Description', dependent: :destroy has_many :geolocations, class_name: 'StashDatacite::Geolocation', dependent: :destroy @@ -609,15 +610,6 @@ def previous_curated_resource .order(id: :desc).first end - # ------------------------------------------------------------ - # Ownership - - def tenant - return nil unless tenant_id - - Tenant.find(tenant_id) - end - # ----------------------------------------------------------- # Permissions diff --git a/app/models/stash_engine/tenant.rb b/app/models/stash_engine/tenant.rb index bc128c1e8f..84c02d95c7 100644 --- a/app/models/stash_engine/tenant.rb +++ b/app/models/stash_engine/tenant.rb @@ -62,6 +62,13 @@ def ror_ids tenant_ror_orgs.map(&:ror_id) end + def country_name + ror_org = ror_orgs.first + return nil if ror_org.nil? || ror_org.country.nil? + + ror_org.country + end + def omniauth_login_path(params = nil) @omniauth_login_path ||= send(:"#{authentication.strategy}_login_path", params) end diff --git a/app/policies/stash_engine/tenant_policy.rb b/app/policies/stash_engine/tenant_policy.rb index eec088372e..a43459d9e4 100644 --- a/app/policies/stash_engine/tenant_policy.rb +++ b/app/policies/stash_engine/tenant_policy.rb @@ -21,8 +21,10 @@ def initialize(user, scope) def resolve if @user.tenant_limited? @scope.enabled.joins(:tenant_ror_orgs).where(tenant_ror_orgs: { ror_id: user.tenant.ror_ids }).distinct - else + elsif @user.system_user? @scope.all + else + [] end end diff --git a/app/views/stash_engine/admin_dashboard/_fields.html.erb b/app/views/stash_engine/admin_dashboard/_fields.html.erb new file mode 100644 index 0000000000..9dbc22b3c2 --- /dev/null +++ b/app/views/stash_engine/admin_dashboard/_fields.html.erb @@ -0,0 +1,48 @@ +<% +def check(id) + h = {funders: 'Grant funders', awards: 'Award IDs', sponsor: 'Journal sponsor', dpc: 'DPC paid by', updated_at: 'Last modified', submit_date: 'Submitted', publication_date: 'Published'} + label = h[id.to_sym] + label ||= id.length < 4 ? id.upcase : id.capitalize + + '
' + + check_box_tag('fields[]', id, @fields.include?(id), {id: id}) + + label_tag('fields[]', label, {for: id})+ + '
' +end +%> + +
+

Fields

+
+
+ Description: + <%= check('doi').html_safe %> + <%= check('keywords').html_safe %> +
+
+ <%= check('authors').html_safe %> + <%= check('affiliations').html_safe %> +
<%= check('countries').html_safe %>
+
+
+ <%= check('status').html_safe %> + <%= check('metrics').html_safe %> + <%= check('size').html_safe %> +
+
+ <%= check('journal').html_safe %> + <%= check('sponsor').html_safe %> + <%= check('dpc').html_safe %> +
+
+ <%= check('funders').html_safe %> +
<%= check('awards').html_safe %>
+ <% if current_user.min_curator? %><%= check('curator').html_safe %><% end %> +
+
+ <%= check('updated_at').html_safe %> + <%= check('submit_date').html_safe %> + <%= check('publication_date').html_safe %> +
+
+
\ No newline at end of file diff --git a/app/views/stash_engine/admin_dashboard/_filter_date.html.erb b/app/views/stash_engine/admin_dashboard/_filter_date.html.erb new file mode 100644 index 0000000000..1126f82297 --- /dev/null +++ b/app/views/stash_engine/admin_dashboard/_filter_date.html.erb @@ -0,0 +1,10 @@ +<%# locals: id, label %> +
+
+ <%= label %> + + <%= date_field_tag("filters[#{id}][start_date]", @filters.dig(id.to_sym, :start_date), class: 'c-input__text', id: "#{id}start") %> + + <%= date_field_tag("filters[#{id}][end_date]", @filters.dig(id.to_sym, :end_date), class: 'c-input__text', id: "#{id}end") %> +
+
\ No newline at end of file diff --git a/app/views/stash_engine/admin_dashboard/_filters.html.erb b/app/views/stash_engine/admin_dashboard/_filters.html.erb new file mode 100644 index 0000000000..833c2ceed2 --- /dev/null +++ b/app/views/stash_engine/admin_dashboard/_filters.html.erb @@ -0,0 +1,26 @@ +<% +def selecter(id, options) + h = { member: 'Dryad member', sponsor: 'Journal sponsor' } + label = h[id.to_sym] || id.capitalize + + '
' + + "" + + select_tag("filters[#{id}]", options_for_select(options, @filters.dig(id.to_sym)), id: "filter-#{id}", class: 'c-input__select') + + '
' +end +%> + +

Filter

+
+ <%= selecter('member', [['', '']] + institution_select).html_safe %> + <%= selecter('status', [['', '']] + status_select).html_safe %> + <% if current_user.min_curator? %><%= selecter('curator', [['', ''], ['Unassigned', 'unassigned']] + editor_select).html_safe %><% end %> + <%# journal search select %> + <%= selecter('sponsor', [['', '']] + sponsor_select).html_safe %> + <%# funder search select %> + <%# affiliation search select %> + + <%= render partial: 'filter_date', locals: { id: 'updated_at', label: 'Last modified' } %> + <%= render partial: 'filter_date', locals: { id: 'submit_date', label: 'Submitted' } %> + <%= render partial: 'filter_date', locals: { id: 'publication_date', label: 'Published' } %> +
\ No newline at end of file diff --git a/app/views/stash_engine/admin_dashboard/_table_header.html.erb b/app/views/stash_engine/admin_dashboard/_table_header.html.erb new file mode 100644 index 0000000000..82fa050fa2 --- /dev/null +++ b/app/views/stash_engine/admin_dashboard/_table_header.html.erb @@ -0,0 +1,62 @@ + + + + <%= sortable_column_head sort_field: 'title', title: 'Description' %> + + <% if @fields.include?('authors') %> + + <%= sortable_column_head sort_field: 'author_string', title: 'Authors' %> + + <% end %> + <% if @fields.include?('affiliations') || @fields.include?('countries') %> + <%= @fields.include?('affiliations') ? 'Affiliations' : 'Countries' %> + <% end %> + <% if @fields.include?('status') %> + + <%= sortable_column_head sort_field: 'status', title: 'Status' %> + + <% end %> + <% if @fields.include?('size') %> + + <%= sortable_column_head sort_field: 'total_file_size', title: 'Size' %> + + <% end %> + <% if @fields.include?('metrics') %> + + <%= sortable_column_head sort_field: 'unique_investigation_count', title: 'Metrics' %> + + <% end %> + <% if @fields.include?('funders') || @fields.include?('awards') %> + <%= @fields.include?('funders') ? 'Grant funders' : 'Award IDs' %> + <% end %> + <% if @fields.include?('journal') %> + Journal + <% end %> + <% if @fields.include?('sponsor') %> + Journal sponsor + <% end %> + <% if @fields.include?('curator') %> + + <%= sortable_column_head sort_field: 'curator_name', title: 'Curator' %> + + <% end %> + <% if @fields.include?('dpc') %> + DPC paid by + <% end %> + <% if @fields.include?('updated_at') %> + + <%= sortable_column_head sort_field: 'updated_at', title: 'Last modified' %> + + <% end %> + <% if @fields.include?('submit_date') %> + + <%= sortable_column_head sort_field: 'submit_date', title: 'Submitted' %> + + <% end %> + <% if @fields.include?('publication_date') %> + + <%= sortable_column_head sort_field: 'publication_date', title: 'Published' %> + + <% end %> + + \ No newline at end of file diff --git a/app/views/stash_engine/admin_dashboard/_table_row.html.erb b/app/views/stash_engine/admin_dashboard/_table_row.html.erb new file mode 100644 index 0000000000..77c104e0e9 --- /dev/null +++ b/app/views/stash_engine/admin_dashboard/_table_row.html.erb @@ -0,0 +1,71 @@ +<% @datasets.each do |dataset| %> + + +
+
+ <%= link_to dataset.title, stash_url_helpers.show_path(id: dataset.identifier_str), target: '_blank' %> + <% if @fields.include?('doi') %> +
DOI: <%= dataset.identifier.identifier %>
+ <% end %> + <% if @fields.include?('keywords') && dataset.subjects.present? %> +
<%= dataset.subjects.map(&:subject).join(', ') %>
+ <% end %> +
+
+ + <% if @fields.include?('authors') %> + <%= dataset.author_string %> + <% end %> + <% if @fields.include?('affiliations') || @fields.include?('countries') %> + <% affs = dataset.authors.map(&:affiliations).flatten.uniq.each_with_object([]) { |a, arr| + if a.ror_id + arr << a + end + arr }.map { |aff| ([@fields.include?('affiliations') ? aff.smart_name : nil] + [@fields.include?('countries') ? aff.country_name : nil]).reject(&:blank?).join(', ') }%> + <%= ([dataset.tenant_id == 'dryad' ? nil : ([@fields.include?('affiliations') ? dataset.tenant&.short_name : nil] + [@fields.include?('countries') ? dataset.tenant&.country_name : nil]).reject(&:blank?).join(', ')].flatten).concat(affs).uniq.reject(&:blank?).join('; ') %> + <% end %> + <% if @fields.include?('status') %> + <%= StashEngine::CurationActivity.readable_status(dataset.status) %> + <% end %> + <% if @fields.include?('size') %> + <%= filesize(dataset.total_file_size) %> + <% end %> + <% if @fields.include?('metrics') %> + <%= dataset.unique_investigation_count.blank? || dataset.unique_investigation_count < dataset.unique_request_count ? dataset.unique_request_count || 0 : dataset.unique_investigation_count %> views
+ <%= dataset.unique_request_count || 0 %> downloads
+ <%= dataset.citation_count || 0 %> citations + + <% end %> + <% if @fields.include?('funders') || @fields.include?('awards') %> + <%= dataset.funders.map { |f| ([@fields.include?('funders') ? f.contributor_name : nil] + [@fields.include?('awards') ? f.award_number : nil]).reject(&:blank?).join(', ')}.uniq.reject(&:blank?).join('; ') %> + <% end %> + <% if @fields.include?('journal') %> + <%= dataset.journal_title %> + <% end %> + <% if @fields.include?('sponsor') %> + <%= dataset.sponsor_id && StashEngine::JournalOrganization.where(id: dataset.sponsor_id).first&.name %> + <% end %> + <% if @fields.include?('curator') %> + <%= dataset.curator_name %> + <% end %> + <% if @fields.include?('dpc') %> + + <% dpc = '' + dpc = dataset.tenant&.short_name if dataset.identifier.payment_id = dataset.tenant_id + dpc = dataset.journal_title if dataset&.issn&.include?(dataset.identifier.payment_id) + dpc = dataset.identifier.payment_id.split("funder:").last.split("|").first if dataset.identifier.payment_id.starts_with?('funder') + %> + <%= dpc %> + + <% end %> + <% if @fields.include?('updated_at') %> + <%= formatted_datetime(dataset.last_curation_activity.updated_at) %> + <% end %> + <% if @fields.include?('submit_date') %> + <%= formatted_datetime(dataset.submit_date) %> + <% end %> + <% if @fields.include?('publication_date') %> + <%= formatted_datetime(dataset.publication_date) %> + <% end %> + +<% end %> \ No newline at end of file diff --git a/app/views/stash_engine/admin_dashboard/index.csv.erb b/app/views/stash_engine/admin_dashboard/index.csv.erb new file mode 100644 index 0000000000..364bb56028 --- /dev/null +++ b/app/views/stash_engine/admin_dashboard/index.csv.erb @@ -0,0 +1,60 @@ +<% +head = 'Title' +head += ',DOI' if @fields.include?('doi') +head += ',Keywords' if @fields.include?('keywords') +head += ',Authors' if @fields.include?('authors') +if @fields.include?('affiliations') || @fields.include?('countries') + head += @fields.include?('affiliations') ? ',Affiliations' : ',Countries' +end +head += ',Status' if @fields.include?('status') +head += ',Size' if @fields.include?('size') +head += ',Metrics' if @fields.include?('metrics') +if @fields.include?('funders') || @fields.include?('awards') + head += @fields.include?('funders') ? ',Grant funders' : ',Award IDs' +end +head += ',Journal' if @fields.include?('journal') +head += ',Journal sponsor' if @fields.include?('sponsor') +head += ',Curator' if @fields.include?('curator') +head += ',DPC paid by' if @fields.include?('dpc') +head += ',Last modified' if @fields.include?('updated_at') +head += ',Submitted' if @fields.include?('submit_date') +head += ',Published' if @fields.include?('publication_date') +%> +<%= head %> +<% @datasets.each do |dataset| + row = [dataset.title] + row << dataset.identifier.identifier if @fields.include?('doi') + row << dataset.subjects.map(&:subject).join(', ') if @fields.include?('keywords') + row << dataset.author_string if @fields.include?('authors') + if @fields.include?('affiliations') || @fields.include?('countries') + affs = dataset.authors.map(&:affiliations).flatten.uniq.each_with_object([]) { |a, arr| + if a.ror_id + arr << a + end + arr }.map { |aff| ([@fields.include?('affiliations') ? aff.smart_name : nil] + [@fields.include?('countries')? aff.country_name : nil]).reject(&:blank?).join(', ') } + row << ([dataset.tenant_id == 'dryad' ? nil : ([@fields.include?('affiliations') ? dataset.tenant&.short_name : nil] + [@fields.include?('countries') ? dataset.tenant&.country_name : nil]).reject(&:blank?).join(', ')].flatten).concat(affs).uniq.reject(&:blank?).join('; ') + end + row << StashEngine::CurationActivity.readable_status(dataset.status) if @fields.include?('status') + row << filesize(dataset.total_file_size) if @fields.include?('size') + if @fields.include?('metrics') + row << "#{dataset.unique_investigation_count.blank? || dataset.unique_investigation_count < dataset.unique_request_count ? dataset.unique_request_count || 0 : dataset.unique_investigation_count} views, #{dataset.unique_request_count || 0} downloads, #{dataset.citation_count || 0} citations" + end + if @fields.include?('funders') || @fields.include?('awards') + row << dataset.funders.map { |f| ([@fields.include?('funders') ? f.contributor_name : nil] + [@fields.include?('awards') ? f.award_number : nil]).reject(&:blank?).join(', ')}.uniq.reject(&:blank?).join('; ') + end + row << dataset.journal_title if @fields.include?('journal') + row << dataset.sponsor_id && StashEngine::JournalOrganization.where(id: dataset.sponsor_id).first&.name if @fields.include?('sponsor') + row << dataset.curator_name if @fields.include?('curator') + if @fields.include?('dpc') + dpc = '' + dpc = dataset.tenant&.short_name if dataset.identifier.payment_id = dataset.tenant_id + dpc = dataset.journal_title if dataset&.issn&.include?(dataset.identifier.payment_id) + dpc = dataset.identifier.payment_id.split("funder:").last.split("|").first if dataset.identifier.payment_id.starts_with?('funder') + row << dpc + end + row << dataset.last_curation_activity.updated_at if @fields.include?('updated_at') + row << dataset.submit_date if @fields.include?('submit_date') + row << dataset.publication_date if @fields.include?('publication_date') +%> +<%= row.to_csv(row_sep: nil).html_safe %> +<% end %> \ No newline at end of file diff --git a/app/views/stash_engine/admin_dashboard/index.html.erb b/app/views/stash_engine/admin_dashboard/index.html.erb new file mode 100644 index 0000000000..98f04bd35e --- /dev/null +++ b/app/views/stash_engine/admin_dashboard/index.html.erb @@ -0,0 +1,45 @@ +<% @page_title = "Admin dashboard" %> + +

Admin dashboard

+ +<%= form_with(url: stash_url_helpers.admin_dashboard_path, method: 'post', id: 'search_form') do %> + <%= render partial: 'fields' %> + <%= render partial: 'filters' %> +

+ + <%= search_field_tag(:q, @search_string, class: 'c-input__text', id: 'search-string' ) %> + <%= submit_tag('Apply', name: nil, class: 'o-button__submit', style: 'margin-right: 0;' ) %> +

+<% end %> + +

+ <%= @datasets.total_count %> results + <%= link_to 'Export all as CSV', stash_url_helpers.admin_dashboard_path(request.parameters.except(:action, :controller, :fields, :authenticity_token, :fields, :filters).merge(format: :csv)), class: 'o-link__buttonlink' %> +

+
+ + <%= render partial: 'table_header' %> + + <%= render partial: 'table_row' %> + +
+
+ + diff --git a/config/routes.rb b/config/routes.rb index 9030cf2de6..eae130405d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -302,6 +302,7 @@ post 'publisher_admin/:id', to: 'journal_organization_admin#edit', as: 'publisher_edit' # admin_datasets, aka "Curator Dashboard" + match 'admin_dashboard', to: 'admin_dashboard#index', via: %i[get post] get 'ds_admin', to: 'admin_datasets#index' get 'ds_admin/:id/create_salesforce_case', to: 'admin_datasets#create_salesforce_case', as: 'create_salesforce_case' get 'ds_admin/:id/activity_log', to: 'admin_datasets#activity_log', as: 'activity_log' diff --git a/spec/models/stash/repo/merritt_oaidc_builder_spec.rb b/spec/models/stash/repo/merritt_oaidc_builder_spec.rb index 01f602a176..a9807479b5 100644 --- a/spec/models/stash/repo/merritt_oaidc_builder_spec.rb +++ b/spec/models/stash/repo/merritt_oaidc_builder_spec.rb @@ -6,19 +6,15 @@ module Builders attr_reader :tenant before(:each) do - user = StashEngine::User.create( - email: 'lmuckenhaupt@example.edu', - tenant_id: 'dataone' - ) + user = create(:user, email: 'lmuckenhaupt@example.edu', tenant_id: 'dataone') dc4_xml = File.read('spec/data/stash-merritt/mrt-datacite.xml') dcs_resource = Datacite::Mapping::Resource.parse_xml(dc4_xml) stash_wrapper_xml = File.read('spec/data/stash-merritt/stash-wrapper.xml') stash_wrapper = Stash::Wrapper::StashWrapper.parse_xml(stash_wrapper_xml) - @tenant = double(StashEngine::Tenant) - allow(tenant).to receive(:short_name).and_return('DataONE') - allow(StashEngine::Tenant).to receive(:find).with('dataone').and_return(tenant) + @tenant = StashEngine::Tenant.find('dataone') + @tenant.update(short_name: 'DataONE') @resource = StashDatacite::ResourceBuilder.new( user_id: user.id, diff --git a/spec/models/stash_datacite/affiliation_spec.rb b/spec/models/stash_datacite/affiliation_spec.rb index a976e663bd..399ed70c9a 100644 --- a/spec/models/stash_datacite/affiliation_spec.rb +++ b/spec/models/stash_datacite/affiliation_spec.rb @@ -56,11 +56,10 @@ module StashDatacite before(:each) do @affil = StashDatacite::Affiliation.create(long_name: 'Bertelsmann Music Group', ror_id: '12345') @ror_org = StashEngine::RorOrg.new(ror_id: '12345', name: 'Bertelsmann Music Group') - allow(StashEngine::RorOrg).to receive(:find_by_ror_id).and_return(@ror_org) end it 'returns the correct country_name when given a country object' do - @ror_org.country = 'East Timor' + @ror_org.update(country: 'East Timor') expect(@affil.country_name).to eql('East Timor') end end From b4c15a823b87c89f2517186d2ebeedb9aef92dc9 Mon Sep 17 00:00:00 2001 From: Audrey Hamelers Date: Thu, 13 Jun 2024 17:21:00 +0200 Subject: [PATCH 2/8] search select combobox --- .../javascripts/stash_engine/combobox.js | 108 ++++++++++++++++++ app/assets/stylesheets/scss/_select.scss | 51 +++++++++ .../stash_datacite/contributors_controller.rb | 20 +--- .../admin_dashboard_controller.rb | 27 +++-- .../stash_engine/user_admin_controller.rb | 4 +- .../admin_dashboard/_filters.html.erb | 37 +++++- .../shared/_search_select.html.erb | 55 +++++++++ .../user_admin/_admin_role_fieldset.html.erb | 7 +- .../user_admin/_admin_role_form.html.erb | 32 +++++- spec/features/stash_engine/user_admin_spec.rb | 4 +- 10 files changed, 305 insertions(+), 40 deletions(-) create mode 100644 app/assets/javascripts/stash_engine/combobox.js create mode 100644 app/views/stash_engine/shared/_search_select.html.erb diff --git a/app/assets/javascripts/stash_engine/combobox.js b/app/assets/javascripts/stash_engine/combobox.js new file mode 100644 index 0000000000..ed54dc4e54 --- /dev/null +++ b/app/assets/javascripts/stash_engine/combobox.js @@ -0,0 +1,108 @@ +'use strict'; + +class ComboboxAutocomplete { + constructor(combobox, textbox, list, fill, selection) { + this.combobox = combobox + this.textbox = textbox + this.list = list + this.fill = fill + this.selection = selection + + this.textbox.addEventListener('beforeinput', this.open.bind(this)) + this.textbox.addEventListener('input', this.enter.bind(this)) + + this.textbox.addEventListener('blur', this.unFocus.bind(this), true) + this.list.addEventListener('blur', this.unFocus.bind(this), true) + this.combobox.addEventListener('blur', this.unFocus.bind(this), true) + + this.textbox.addEventListener('keydown', this.keyPressed.bind(this)) + this.list.addEventListener('keydown', this.listPressed.bind(this), true) + + this.list.addEventListener('click', this.saveOption.bind(this), true) + } + open() { + this.fill() + this.list.removeAttribute('hidden') + this.textbox.setAttribute('aria-expanded', true) + } + enter() { + this.fill() + if (this.textbox.value.length == 0) { + this.selection({value: '', label: ''}) + } + } + close(){ + this.list.setAttribute('hidden', true) + this.textbox.setAttribute('aria-expanded', false) + } + unFocus(e) { + if (!this.combobox.contains(e.relatedTarget)) { + this.close() + } + } + selectOption({value, label}) { + const [prev] = this.list.getElementsByClassName('selected-option') + if (prev) { + prev.classList.remove('selected-option') + prev.setAttribute('aria-selected', false) + } + const selected = this.list.querySelector(`[data-value="${value}"]`) + selected.classList.add('selected-option') + selected.setAttribute('aria-selected', true) + this.selection({value, label}) + this.textbox.value = label + } + saveOption(e) { + e.preventDefault() + e.stopPropagation() + this.selectOption(e.target.dataset) + this.textbox.focus() + this.close() + } + listPressed(e) { + this.keyPressed(e) + } + keyPressed(e, option) { + switch (e.key) { + case 'Enter': + if (e.target.hasAttribute('data-value')) this.saveOption(e) + else if (e.target.id === this.textbox.id) { + this.open() + e.preventDefault() + } + break + case 'ArrowDown': + case 'ArrowRight': + if (e.target.id === this.textbox.id) { + if (this.list.hasAttribute('hidden')) this.open() + this.list.firstChild.focus() + e.preventDefault() + } else if (e.target.nextSibling) { + e.target.nextSibling.focus() + e.preventDefault() + } + break + case 'ArrowUp': + if (e.target.id !== this.textbox.id && e.target.previousSibling) { + e.target.previousSibling.focus() + e.preventDefault() + } + break + case 'Home': + if (e.target.id !== this.textbox.id) { + this.list.firstChild.focus() + e.preventDefault() + } + break + case 'End': + if (e.target.id !== this.textbox.id) { + this.list.lastChild.focus() + e.preventDefault() + } + break + default: + break + } + } + +} diff --git a/app/assets/stylesheets/scss/_select.scss b/app/assets/stylesheets/scss/_select.scss index 6948fa1cdc..8c4c303300 100644 --- a/app/assets/stylesheets/scss/_select.scss +++ b/app/assets/stylesheets/scss/_select.scss @@ -3,3 +3,54 @@ .o-select { margin: 0 0 20px; } + +.searchselect { + position: relative; + flex-grow: 2; + + & > input { + width: 100%; + } + + &:focus-within input { + outline: 2px solid $design-medium-blue-color; + } + + ul { + margin: 0; + padding: 2px 0; + list-style-type: none; + position: absolute; + top: 32px; + right: -2px; + left: -2px; + z-index: 10; + width: calc(100% + 4px); + max-height: 50vh; + overflow-y: auto; + background-color: $design-extra-light-gray-color; + + li { + padding: 6px 10px; + cursor: default; + &[role='option'] { + padding-left: 3ch; + &:before { + content: ''; + display: inline-block; + width: 2.5ch; + margin-left: -2.5ch; + font-family: FontAwesome; + } + &.selected-option:before { + content: '\f00c'; + } + &:hover, + &:focus { + color: $design-white-color; + background-color: $design-dark-blue-color; + } + } + } + } +} diff --git a/app/controllers/stash_datacite/contributors_controller.rb b/app/controllers/stash_datacite/contributors_controller.rb index 68e3f6c29e..23d7cde3ca 100644 --- a/app/controllers/stash_datacite/contributors_controller.rb +++ b/app/controllers/stash_datacite/contributors_controller.rb @@ -64,24 +64,16 @@ def reorder end end - # GET /contributors/autocomplete?term={query_term} + # GET /contributors/autocomplete?query={query_term} def autocomplete - return if params.blank? - - partial_term = params['term'] + partial_term = params['query'] if partial_term.blank? render json: nil else - # clean the partial_term of unwanted characters so it doesn't cause errors when calling the CrossRef API - partial_term.gsub!(%r{[/\-\\()~!@%&"\[\]\^:]}, ' ') - response = HTTParty.get('https://api.crossref.org/funders', - query: { query: partial_term }, - headers: { 'Content-Type' => 'application/json' }) - return if response.parsed_response.blank? - return if response.parsed_response['message'].blank? - - result_list = response.parsed_response['message']['items'] - render json: bubble_up_exact_matches(result_list: result_list, term: partial_term) + @affiliations = StashEngine::RorOrg.distinct.joins( + "inner join dcs_contributors on identifier_type = 'ror' and contributor_type = 'funder' and name_identifier_id = ror_id" + ).find_by_ror_name(partial_term) + render json: @affiliations end end diff --git a/app/controllers/stash_engine/admin_dashboard_controller.rb b/app/controllers/stash_engine/admin_dashboard_controller.rb index b31ff77485..98bfb4659e 100644 --- a/app/controllers/stash_engine/admin_dashboard_controller.rb +++ b/app/controllers/stash_engine/admin_dashboard_controller.rb @@ -100,21 +100,19 @@ def add_fields # rubocop:disable Style/MultilineIfModifier def add_filters + if @filters[:member].present? || @filters.dig(:funder, :value).present? || @filters.dig(:affiliation, :value).present? + @datasets = @datasets.left_outer_joins(authors: :affiliations).left_outer_joins(:funders) + end + tenant_filter @datasets = @datasets.where('stash_engine_curation_activities.status': @filters[:status]) if @filters[:status].present? @datasets = @datasets.where( 'curator.id': Integer(@filters[:curator], exception: false) ? @filters[:curator] : nil ) if @filters[:curator].present? + @datasets = @datasets.where('stash_engine_journals.id': @filters.dig(:journal, :value)) if @filters.dig(:journal, :value).present? @datasets = @datasets.where('stash_engine_journals.sponsor_id': @filters[:sponsor]) if @filters[:sponsor].present? @datasets = @datasets.where("MATCH(stash_engine_identifiers.search_words) AGAINST('#{@search_string}') > 0") unless @search_string.blank? - return unless @filters[:member].present? && StashEngine::Tenant.find_by(id: @filters[:member]).present? - - tenant_orgs = StashEngine::Tenant.find(@filters[:member]).ror_ids - @datasets = @datasets.left_outer_joins(authors: :affiliations).left_outer_joins(:funders) - @datasets = @datasets.where( - 'stash_engine_resources.tenant_id = ? or stash_engine_identifiers.payment_id = ? - or dcs_affiliations.ror_id in (?) or dcs_contributors.name_identifier_id in (?)', - @filters[:member], @filters[:member], tenant_orgs, tenant_orgs - ) + @datasets = @datasets.where('dcs_contributors.name_identifier_id': @filters.dig(:funder, :value)) if @filters.dig(:funder, :value).present? + @datasets = @datasets.where('dcs_affiliations.ror_id': @filters.dig(:affiliation, :value)) if @filters.dig(:affiliation, :value).present? end def date_filters @@ -130,6 +128,17 @@ def date_filters end # rubocop:enable Style/MultilineIfModifier + def tenant_filter + return unless @filters[:member].present? && StashEngine::Tenant.find_by(id: @filters[:member]).present? + + tenant_orgs = StashEngine::Tenant.find(@filters[:member]).ror_ids + @datasets = @datasets.where( + 'stash_engine_resources.tenant_id = ? or stash_engine_identifiers.payment_id = ? + or dcs_affiliations.ror_id in (?) or dcs_contributors.name_identifier_id in (?)', + @filters[:member], @filters[:member], tenant_orgs, tenant_orgs + ) + end + def date_string(date_hash) from = date_hash[:start_date] to = date_hash[:end_date] diff --git a/app/controllers/stash_engine/user_admin_controller.rb b/app/controllers/stash_engine/user_admin_controller.rb index b38fa18efe..77e3d5611e 100644 --- a/app/controllers/stash_engine/user_admin_controller.rb +++ b/app/controllers/stash_engine/user_admin_controller.rb @@ -93,7 +93,7 @@ def set_role # set publisher role save_role(role_params[:publisher_role], @publisher_role, StashEngine::JournalOrganization.find_by(id: role_params[:publisher])) # set journal role - save_role(role_params[:journal_role], @journal_role, StashEngine::Journal.find_by(id: role_params[:journal])) + save_role(role_params[:journal_role], @journal_role, StashEngine::Journal.find_by(id: role_params.dig(:journal, :value))) # set funder role save_role(role_params[:funder_role], @funder_role, StashEngine::Funder.find_by(id: role_params[:funder])) # reload roles @@ -192,7 +192,7 @@ def edit_params end def role_params - params.permit(:role, :tenant_role, :publisher, :publisher_role, :journal, :journal_role, :funder, :funder_role) + params.permit(:role, :tenant_role, :publisher, :publisher_role, :funder, :funder_role, :journal_role, journal: %i[value label]) end end end diff --git a/app/views/stash_engine/admin_dashboard/_filters.html.erb b/app/views/stash_engine/admin_dashboard/_filters.html.erb index 833c2ceed2..a69e9f01af 100644 --- a/app/views/stash_engine/admin_dashboard/_filters.html.erb +++ b/app/views/stash_engine/admin_dashboard/_filters.html.erb @@ -15,10 +15,41 @@ end <%= selecter('member', [['', '']] + institution_select).html_safe %> <%= selecter('status', [['', '']] + status_select).html_safe %> <% if current_user.min_curator? %><%= selecter('curator', [['', ''], ['Unassigned', 'unassigned']] + editor_select).html_safe %><% end %> - <%# journal search select %> + <% # locals: field_name, id, selected, options_path, placeholder %> +
+ <%= render partial: 'stash_engine/shared/search_select', locals: { + id: 'journal', + label: 'Journal', + field_name: 'filters[journal]', + options_path: '/stash_datacite/publications/autocomplete?term=', + options_label: 'title', + options_value: 'id', + selected: @filters.dig(:journal) + } %> +
<%= selecter('sponsor', [['', '']] + sponsor_select).html_safe %> - <%# funder search select %> - <%# affiliation search select %> +
+ <%= render partial: 'stash_engine/shared/search_select', locals: { + id: 'funder', + label: 'Funder', + field_name: 'filters[funder]', + options_path: '/stash_datacite/contributors/autocomplete?query=', + options_label: 'name', + options_value: 'id', + selected: @filters.dig(:funder) + } %> +
+
+ <%= render partial: 'stash_engine/shared/search_select', locals: { + id: 'affiliation', + label: 'Affiliation', + field_name: 'filters[affiliation]', + options_path: '/stash_datacite/affiliations/autocomplete?query=', + options_label: 'name', + options_value: 'id', + selected: @filters.dig(:affiliation) + } %> +
<%= render partial: 'filter_date', locals: { id: 'updated_at', label: 'Last modified' } %> <%= render partial: 'filter_date', locals: { id: 'submit_date', label: 'Submitted' } %> diff --git a/app/views/stash_engine/shared/_search_select.html.erb b/app/views/stash_engine/shared/_search_select.html.erb new file mode 100644 index 0000000000..acaf48623c --- /dev/null +++ b/app/views/stash_engine/shared/_search_select.html.erb @@ -0,0 +1,55 @@ +<% # locals: field_name, label, id, selected, options_path, options_label, options_value %> + +
+ + + <%= text_field_tag(nil, selected&.dig(:label), id: "searchselect-#{id}__input", class: 'c-input__select', placeholder: 'Find as you type...', 'aria-autocomplete': 'both', 'aria-controls': "searchselect-#{id}__list", 'aria-expanded': 'false', role: 'combobox') %> + + +
+ \ No newline at end of file diff --git a/app/views/stash_engine/user_admin/_admin_role_fieldset.html.erb b/app/views/stash_engine/user_admin/_admin_role_fieldset.html.erb index e7955e7134..4ba3485af0 100644 --- a/app/views/stash_engine/user_admin/_admin_role_fieldset.html.erb +++ b/app/views/stash_engine/user_admin/_admin_role_fieldset.html.erb @@ -6,12 +6,7 @@ <%= select_label %> <%= @user.tenant.short_name %> <% else %> <%= label_tag(label, select_label, class: 'c-input__label') %> - <% if options.length < 10 %> - <%= select_tag(label, options_for_select(options, user_role&.role_object_id), class: 'c-input__select') %> - <% else %> - <%= text_field_tag(label, options.find {|o| o[1].to_i == user_role&.role_object_id&.to_i }&.first, list: "#{label}-options", class: 'c-input__select', placeholder: 'Begin typing...') %> - <%= options_for_select(options, user_role&.role_object_id) %> - <% end %> + <%= select_tag(label, options_for_select(options, user_role&.role_object_id), class: 'c-input__select') %> <% end %>