diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index ca7236d3..d6df14e2 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -57,6 +57,10 @@ const SearchView = { $(this).find("select:nth(1)")); }); + $("[name=item_type]").on("change", function() { + $(this).parents("form:first").submit(); + }); + // When the Simple Search or Advanced Search submit button is clicked, // clear all form fields in the other tab pane, so they don't get sent // along as well. diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 342b86d5..aacbea95 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -15,7 +15,8 @@ class SearchController < ApplicationController def index @permitted_params = params.permit(Search::SIMPLE_SEARCH_PARAMS + Search::advanced_search_params + - Search::RESULTS_PARAMS) + Search::RESULTS_PARAMS + + [:item_type]) @start = [@permitted_params[:start].to_i.abs, max_start].min @window = window_size @items = EntityRelation.new. @@ -24,9 +25,28 @@ def index start(@start). limit(@window) if institution_host? - @items = @items.institution(current_institution) + @items.institution(current_institution) + if policy(Item).show_private? + case params[:item_type] + when "private" + @items. + filter_range("#{Item::IndexFields::EMBARGOES}.#{Embargo::IndexFields::ALL_ACCESS_EXPIRES_AT}", + :gt, + Time.now.strftime("%Y-%m-%d")). + must_not(Item::IndexFields::STAGE, Item::Stages::WITHDRAWN) + when "rejected" + @items.filter(Item::IndexFields::STAGE, Item::Stages::REJECTED) + when "withdrawn" + @items.filter(Item::IndexFields::STAGE, Item::Stages::WITHDRAWN) + when "deleted" + @items.include_buried. + filter(Item::IndexFields::STAGE, Item::Stages::BURIED) + end + else + @items = policy_scope(@items, policy_scope_class: ItemPolicy::Scope) + end else - @items = @items.metadata_profile(MetadataProfile.global) + @items.metadata_profile(MetadataProfile.global) end if @permitted_params[:sort].present? @items.order(@permitted_params[:sort] => @@ -34,7 +54,6 @@ def index end process_search_query(@items) - @items = policy_scope(@items, policy_scope_class: ItemPolicy::Scope) @count = @items.count @facets = @items.facets @current_page = @items.page diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a07cb721..b10090c3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -318,6 +318,30 @@ def date_range_picker def facets_as_cards(facets, permitted_params) return nil unless facets html = StringIO.new + if institution_host? && policy(Item).show_private? + # This is a "fake" facet that is presented like the other facets in the + # UI, but doesn't work the same way. + facet = Facet.new + facet.name = "Item Type" + facet.field = "" + facet.terms << FacetTerm.new(name: "public", + label: "Public", + facet: facet) + facet.terms << FacetTerm.new(name: "private", + label: "Private", + facet: facet) + facet.terms << FacetTerm.new(name: "rejected", + label: "Rejected", + facet: facet) + facet.terms << FacetTerm.new(name: "withdrawn", + label: "Withdrawn", + facet: facet) + facet.terms << FacetTerm.new(name: "deleted", + label: "Deleted", + facet: facet) + html << facet_card(facet, permitted_params, name: "item_type", + control: "radio", checked_value: "public") + end facets.select{ |f| f.terms.any? }.each do |facet| html << facet_card(facet, permitted_params) end @@ -747,11 +771,15 @@ def resource_list_row(resource, use_resource_host: true, show_institution: false, show_private_normally: false) - embargoed_item = !show_private_normally && - resource.kind_of?(Item) && - resource.embargoed_for?(user: current_user, - client_hostname: request_context.client_hostname, - client_ip: request_context.client_ip) + embargoed_item = withdrawn_item = rejected_item = buried_item = false + if !show_private_normally && resource.kind_of?(Item) + embargoed_item = resource.embargoed_for?(user: current_user, + client_hostname: request_context.client_hostname, + client_ip: request_context.client_ip) + withdrawn_item = resource.withdrawn? + buried_item = resource.buried? + rejected_item = resource.rejected? + end thumb = thumbnail_for(resource) if use_resource_host resource_url = polymorphic_url(resource, host: resource.institution.fqdn) @@ -777,7 +805,7 @@ def resource_list_row(resource, html << "" html << "
" html << "
" - if embargoed_item + if embargoed_item && !policy(resource).show? html << resource.title else html << link_to(resource.title, resource_url) @@ -789,6 +817,15 @@ def resource_list_row(resource, if embargoed_item html << " EMBARGOED" end + if withdrawn_item + html << " WITHDRAWN" + end + if rejected_item + html << " REJECTED" + end + if buried_item + html << " DELETED" + end html << "
" if resource.kind_of?(Item) @@ -954,28 +991,43 @@ def toast!(title:, message:, icon: nil) ## # @param facet [Facet] # @param permitted_params [ActionController::Parameters] + # @param name [String] + # @param control [String] Input type (`checkbox` or `radio`). + # @param checked_value [String] Value to pre-check. # - def facet_card(facet, permitted_params) + def facet_card(facet, + permitted_params, + name: "fq[]", + control: "checkbox", + checked_value: nil) panel = StringIO.new panel << "
" panel << "
#{facet.name}
" panel << '
' panel << '
    ' facet.terms.each do |term| - checked = (permitted_params[:fq]&.include?(term.query)) ? "checked" : nil - checked_params = term.removed_from_params(permitted_params.deep_dup).except(:start) - unchecked_params = term.added_to_params(permitted_params.deep_dup).except(:start) - term_label = truncate(sanitize(term.label, tags: []), length: 80) + term_label = truncate(sanitize(term.label, tags: []), length: 80) + query = term.query.gsub('"', '"') + if name == "fq[]" + checked = (permitted_params[:fq]&.include?(term.query)) ? "checked" : nil + checked_params = term.removed_from_params(permitted_params.deep_dup).except(:start) + unchecked_params = term.added_to_params(permitted_params.deep_dup).except(:start) + elsif query == checked_value + checked = "checked" + else + checked = (params[name.to_sym] == query) ? "checked" : "" + end panel << '
  • ' panel << '' panel << '
  • ' end diff --git a/app/policies/item_policy.rb b/app/policies/item_policy.rb index 8e46331e..f99be2eb 100644 --- a/app/policies/item_policy.rb +++ b/app/policies/item_policy.rb @@ -324,6 +324,10 @@ def show_metadata end end + def show_private + review + end + def show_properties show_access end diff --git a/app/search/entity_relation.rb b/app/search/entity_relation.rb index 52827eec..30e5c520 100644 --- a/app/search/entity_relation.rb +++ b/app/search/entity_relation.rb @@ -12,6 +12,19 @@ def initialize @must_nots << [Item::IndexFields::STAGE, Item::Stages::BURIED] end + ## + # Buried items are excluded from results by default--this method removes that + # exclusion. + # + # @return [self] + # + def include_buried + @must_nots.delete([Unit::IndexFields::BURIED, true]) + @must_nots.delete([Collection::IndexFields::BURIED, true]) + @must_nots.delete([Item::IndexFields::STAGE, Item::Stages::BURIED]) + self + end + ## # Filters out items with current all-access embargoes. # diff --git a/test/policies/item_policy_test.rb b/test/policies/item_policy_test.rb index 4f68e732..9febdc8b 100644 --- a/test/policies/item_policy_test.rb +++ b/test/policies/item_policy_test.rb @@ -1728,6 +1728,65 @@ class ScopeTest < ActiveSupport::TestCase assert !policy.show_metadata? end + # show_private?() + + test "show_private?() does not authorize a nil user" do + context = RequestContext.new(user: nil, + institution: @item.institution) + policy = ItemPolicy.new(context, @item) + assert !policy.show_private? + end + + test "show_private?() does not authorize an incorrect scope" do + context = RequestContext.new(user: users(:southwest_admin), + institution: institutions(:northeast)) + policy = ItemPolicy.new(context, @item) + assert !policy.show_private? + end + + test "show_private?() does not authorize non-sysadmins" do + user = users(:southwest) + context = RequestContext.new(user: user, + institution: @item.institution) + policy = ItemPolicy.new(context, @item) + assert !policy.show_private? + end + + test "show_private?() authorizes sysadmins" do + user = users(:southwest_sysadmin) + context = RequestContext.new(user: user, + institution: @item.institution) + policy = ItemPolicy.new(context, @item) + assert policy.show_private? + end + + test "show_private?() authorizes administrators of the same institution" do + user = users(:southwest_admin) + context = RequestContext.new(user: user, + institution: @item.institution) + policy = ItemPolicy.new(context, @item) + assert policy.show_private? + end + + test "show_private?() does not authorize administrators of a different + institution" do + user = users(:southwest_admin) + context = RequestContext.new(user: user, + institution: institutions(:northeast)) + policy = ItemPolicy.new(context, @item) + assert !policy.show_private? + end + + test "show_private?() respects role limits" do + # sysadmin user limited to an insufficient role + user = users(:southwest_sysadmin) + context = RequestContext.new(user: user, + institution: @item.institution, + role_limit: Role::COLLECTION_SUBMITTER) + policy = ItemPolicy.new(context, @item) + assert !policy.show_private? + end + # show_properties?() test "show_properties?() does not authorize a nil user" do