diff --git a/app/components/blacklight/search_context/server_applied_params_component.html.erb b/app/components/blacklight/search_context/server_applied_params_component.html.erb
new file mode 100644
index 0000000000..7254cce572
--- /dev/null
+++ b/app/components/blacklight/search_context/server_applied_params_component.html.erb
@@ -0,0 +1,4 @@
+
+ <%= render 'start_over' %>
+ <%= link_back_to_catalog class: 'btn btn-outline-secondary' %>
+
diff --git a/app/components/blacklight/search_context/server_applied_params_component.rb b/app/components/blacklight/search_context/server_applied_params_component.rb
new file mode 100644
index 0000000000..d7783c714d
--- /dev/null
+++ b/app/components/blacklight/search_context/server_applied_params_component.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Blacklight
+ module SearchContext
+ class ServerAppliedParamsComponent < Blacklight::Component
+ delegate :current_search_session, :link_back_to_catalog, to: :helpers
+
+ def render?
+ current_search_session
+ end
+ end
+ end
+end
diff --git a/app/components/blacklight/search_context_component.rb b/app/components/blacklight/search_context_component.rb
index e7d8bb59a3..d227e4925f 100644
--- a/app/components/blacklight/search_context_component.rb
+++ b/app/components/blacklight/search_context_component.rb
@@ -4,13 +4,14 @@ module Blacklight
class SearchContextComponent < Blacklight::Component
with_collection_parameter :search_context
- def initialize(search_context:, search_session:)
+ def initialize(search_context:, search_session:, current_document: nil)
@search_context = search_context
@search_session = search_session
+ @current_document_id = current_document&.id
end
def render?
- @search_context.present? && (@search_context[:prev] || @search_context[:next])
+ @search_context.present? && (@search_context[:prev] || @search_context[:next]) && (@current_document_id && @search_session['document_id'] == @current_document_id)
end
def item_page_entry_info
diff --git a/app/controllers/concerns/blacklight/search_context.rb b/app/controllers/concerns/blacklight/search_context.rb
index 33955c4952..ccee37d6bf 100644
--- a/app/controllers/concerns/blacklight/search_context.rb
+++ b/app/controllers/concerns/blacklight/search_context.rb
@@ -17,6 +17,28 @@ def record_search_parameters opts = { only: :index }
end
end
+ # GET previous and next document json for the document specified by
+ # the counter param in current search
+ def page_links
+ counter_param = params.delete(:counter)
+ @page_link_data = {}
+ if counter_param
+ index = counter_param.to_i - 1
+ response, documents = search_service.previous_and_next_documents_for_search index, search_state.reset_search
+ if documents.detect(&:present?)
+ @page_link_data[:prev] = page_links_document_path(documents.first, index)
+ @page_link_data[:next] = page_links_document_path(documents.last, index + 2)
+ end
+ if response&.total && response.total.positive?
+ @page_link_data[:counterRaw] = counter_param
+ @page_link_data[:counterDelimited] = helpers.number_with_delimiter(counter_param.to_i)
+ @page_link_data[:totalRaw] = response.total
+ @page_link_data[:totalDelimited] = helpers.number_with_delimiter(response.total)
+ end
+ end
+ render json: @page_link_data
+ end
+
private
# sets up the session[:search] hash if it doesn't already exist
@@ -79,6 +101,8 @@ def agent_is_crawler?
end
def find_or_initialize_search_session_from_params params
+ return unless blacklight_config.track_search_session_config.storage == 'server'
+
params_copy = params.reject { |k, v| blacklisted_search_session_params.include?(k.to_sym) || v.blank? }
return if params_copy.reject { |k, _v| [:action, :controller].include? k.to_sym }.blank?
@@ -109,15 +133,31 @@ def blacklisted_search_session_params
# calls setup_previous_document then setup_next_document.
# used in the show action for single view pagination.
def setup_next_and_previous_documents
- if search_session['counter'] && current_search_session
- index = search_session['counter'].to_i - 1
- response, documents = search_service.previous_and_next_documents_for_search index, search_state.reset(current_search_session.query_params)
+ return { counter: params[:counter] } if setup_next_and_previous_on_client?
+ return nil unless setup_next_and_previous_on_server?
- search_session['total'] = response.total
- { prev: documents.first, next: documents.last }
- end
+ index = search_session['counter'].to_i - 1
+ response, documents = search_service.previous_and_next_documents_for_search index, search_state.reset(current_search_session.query_params)
+
+ search_session['total'] = response.total
+ { prev: documents.first, next: documents.last }
rescue Blacklight::Exceptions::InvalidRequest => e
logger&.warn "Unable to setup next and previous documents: #{e}"
nil
end
+
+ def setup_next_and_previous_on_server?
+ search_session['counter'] && current_search_session && blacklight_config.track_search_session_config.storage == 'server'
+ end
+
+ def setup_next_and_previous_on_client?
+ params[:counter] && blacklight_config.track_search_session_config.storage == 'client'
+ end
+
+ def page_links_document_path(document, counter)
+ return nil unless document
+ return search_state.url_for_document(document, counter: counter) if blacklight_config.view_config(:show).route
+
+ solr_document_path(document, counter: counter)
+ end
end
diff --git a/app/helpers/blacklight/url_helper_behavior.rb b/app/helpers/blacklight/url_helper_behavior.rb
index 16b5ddb0b0..bbcc9832ba 100644
--- a/app/helpers/blacklight/url_helper_behavior.rb
+++ b/app/helpers/blacklight/url_helper_behavior.rb
@@ -80,21 +80,24 @@ def link_to_next_document(next_document, classes: 'next', **addl_link_opts)
# @example
# session_tracking_params(SolrDocument.new(id: 123), 7)
# => { data: { context_href: '/catalog/123/track?counter=7&search_id=999' } }
- def session_tracking_params document, counter
- path = session_tracking_path(document, per_page: params.fetch(:per_page, search_session['per_page']), counter: counter, search_id: current_search_session.try(:id), document_id: document&.id)
-
- if path.nil?
- return {}
+ def session_tracking_params document, counter, per_page: search_session['per_page'], search_id: current_search_session&.id
+ path_params = { per_page: params.fetch(:per_page, per_page), counter: counter, search_id: search_id }
+ if blacklight_config.track_search_session_config.storage == 'server'
+ path_params[:document_id] = document&.id
+ path_params[:search_id] = search_id
end
+ path = session_tracking_path(document, path_params)
+ return {} if path.nil?
- { data: { context_href: path, turbo_prefetch: false } }
+ context_method = blacklight_config.track_search_session_config.storage == 'client' ? 'get' : 'post'
+ { data: { context_href: path, context_method: context_method, turbo_prefetch: false } }
end
private :session_tracking_params
##
# Get the URL for tracking search sessions across pages using polymorphic routing
def session_tracking_path document, params = {}
- return if document.nil? || !blacklight_config&.track_search_session
+ return if document.nil? || !blacklight_config.track_search_session_config.storage
if main_app.respond_to?(controller_tracking_method)
return main_app.public_send(controller_tracking_method, params.merge(id: document))
@@ -105,6 +108,8 @@ def session_tracking_path document, params = {}
end
def controller_tracking_method
+ return blacklight_config.track_search_session_config.url_helper if blacklight_config.track_search_session_config.url_helper
+
"track_#{controller_name}_path"
end
diff --git a/app/views/catalog/_show_main_content.html.erb b/app/views/catalog/_show_main_content.html.erb
index 8190d7b5d4..b50c6bfacd 100644
--- a/app/views/catalog/_show_main_content.html.erb
+++ b/app/views/catalog/_show_main_content.html.erb
@@ -1,6 +1,6 @@
-<%= render(Blacklight::SearchContextComponent.new(search_context: @search_context, search_session: search_session)) if search_session['document_id'] == @document.id %>
+<%= render blacklight_config.track_search_session_config.item_pagination_component.new(search_context: @search_context, search_session: search_session, current_document: @document) if blacklight_config.track_search_session_config.item_pagination_component %>
-<% @page_title = t('blacklight.search.show.title', document_title: Deprecation.silence(Blacklight::BlacklightHelperBehavior) { document_show_html_title }, application_name: application_name).html_safe %>
+<% @page_title = t('blacklight.search.show.title', document_title: document_presenter(@document).html_title, application_name: application_name).html_safe %>
<% content_for(:head) { render_link_rel_alternates } %>
<%= render (blacklight_config.view_config(:show).document_component || Blacklight::DocumentComponent).new(presenter: document_presenter(@document), component: :div, title_component: :h1, show: true) do |component| %>
diff --git a/app/views/catalog/index.html.erb b/app/views/catalog/index.html.erb
index c0af2dbe12..5dd4f3bf61 100644
--- a/app/views/catalog/index.html.erb
+++ b/app/views/catalog/index.html.erb
@@ -1,3 +1,6 @@
+<% content_for(:head) do %>
+
+<% end %>
<% content_for(:sidebar) do %>
<%= render 'search_sidebar' %>
<% end %>
diff --git a/app/views/catalog/show.html.erb b/app/views/catalog/show.html.erb
index 39533c8543..c09f46956b 100644
--- a/app/views/catalog/show.html.erb
+++ b/app/views/catalog/show.html.erb
@@ -1,9 +1,4 @@
-<% if current_search_session %>
-
- <%= render 'start_over' %>
- <%= link_back_to_catalog class: 'btn btn-outline-secondary' %>
-
-<% end %>
+<%= render blacklight_config.track_search_session_config.applied_params_component.new if blacklight_config.track_search_session_config.applied_params_component %>
<%= render 'show_main_content' %>
diff --git a/lib/blacklight/configuration.rb b/lib/blacklight/configuration.rb
index 970913764d..5361b1a1ba 100644
--- a/lib/blacklight/configuration.rb
+++ b/lib/blacklight/configuration.rb
@@ -306,6 +306,11 @@ def default_per_page
# @return [Boolean]
property :track_search_session, default: true
+ # @!attribute track_search_session_config
+ # @since v7.35.0
+ # @return [Blacklight::Configuration::SessionTrackingConfig]
+ property :track_search_session_config, default: nil
+
# @!attribute advanced_search
# @since v7.15.0
# @return [#enabled]
@@ -618,6 +623,21 @@ def show_fields_for(document_or_display_types)
fields.merge(show_fields)
end
+ def track_search_session=(val)
+ self.track_search_session_config = Blacklight::Configuration::SessionTrackingConfig.new(storage: val ? 'server' : false)
+ super
+ end
+
+ def track_search_session_config
+ return track_search_session if track_search_session.is_a?(Blacklight::Configuration::SessionTrackingConfig)
+
+ stored_config = super
+
+ return stored_config if stored_config
+
+ self.track_search_session_config = Blacklight::Configuration::SessionTrackingConfig.new(storage: track_search_session ? 'server' : false)
+ end
+
# @!visibility private
def freeze
each { |_k, v| v.is_a?(OpenStruct) && v.freeze }
diff --git a/lib/blacklight/configuration/session_tracking_config.rb b/lib/blacklight/configuration/session_tracking_config.rb
new file mode 100644
index 0000000000..b7d87f7ee1
--- /dev/null
+++ b/lib/blacklight/configuration/session_tracking_config.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class Blacklight::Configuration
+ class SessionTrackingConfig < Blacklight::OpenStructWithHashAccess
+ # @!attribute storage
+ # @return [String, FalseClass] 'server': use server-side tracking; 'client': delegate search tracking and prev/next navigation to client
+ # @!attribute applied_params_component
+ # @return [Class] component class used to render a facet group
+ # @!attribute item_pagination_component
+ # @return [Class] component class used to render the constraints
+
+ def initialize(property_hash = {})
+ super({ storage: 'server' }.merge(property_hash))
+ end
+
+ def applied_params_component
+ super || default_applied_params_component(storage)
+ end
+
+ def item_pagination_component
+ super || default_item_pagination_component(storage)
+ end
+
+ def url_helper
+ super || default_url_helper(storage)
+ end
+
+ def default_applied_params_component(storage)
+ return Blacklight::SearchContext::ServerAppliedParamsComponent if storage == 'server'
+
+ nil
+ end
+
+ def default_item_pagination_component(storage)
+ return Blacklight::SearchContextComponent if storage == 'server'
+
+ nil
+ end
+
+ # extension point for alternative storage types
+ def default_url_helper(_storage)
+ nil
+ end
+ end
+end
diff --git a/lib/blacklight/routes/searchable.rb b/lib/blacklight/routes/searchable.rb
index 7dbcd36c85..ec2c287903 100644
--- a/lib/blacklight/routes/searchable.rb
+++ b/lib/blacklight/routes/searchable.rb
@@ -9,6 +9,7 @@ def initialize(defaults = {})
def call(mapper, _options = {})
mapper.match '/', action: 'index', as: 'search', via: [:get, :post]
mapper.get '/advanced', action: 'advanced_search', as: 'advanced_search'
+ mapper.get '/page_links', action: 'page_links', as: 'page_links'
mapper.post ":id/track", action: 'track', as: 'track'
mapper.get ":id/raw", action: 'raw', as: 'raw', defaults: { format: 'json' }
diff --git a/spec/components/blacklight/document_component_spec.rb b/spec/components/blacklight/document_component_spec.rb
index 74dada0616..12713882d0 100644
--- a/spec/components/blacklight/document_component_spec.rb
+++ b/spec/components/blacklight/document_component_spec.rb
@@ -26,7 +26,7 @@
let(:blacklight_config) do
CatalogController.blacklight_config.deep_copy.tap do |config|
- config.track_search_session = false
+ config.track_search_session_config.storage = false
config.index.thumbnail_field = 'thumbnail_path_ss'
config.index.document_actions[:bookmark].partial = '/catalog/bookmark_control'
end
diff --git a/spec/components/blacklight/search_context/server_applied_params_component_spec.rb b/spec/components/blacklight/search_context/server_applied_params_component_spec.rb
new file mode 100644
index 0000000000..ff21468e7f
--- /dev/null
+++ b/spec/components/blacklight/search_context/server_applied_params_component_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Blacklight::SearchContext::ServerAppliedParamsComponent, type: :component do
+ subject(:render) { instance.render_in(view_context) }
+
+ let(:instance) { described_class.new }
+ let(:current_search_session) { nil }
+ let(:view_context) { controller.view_context }
+
+ before do
+ view_context.view_paths.unshift(RSpec::Rails::ViewExampleGroup::StubResolverCache.resolver_for('application/_start_over.html.erb' => 'start over'))
+ allow(view_context).to receive(:current_search_session).and_return current_search_session
+ allow(view_context).to receive(:link_back_to_catalog).with(any_args)
+ end
+
+ it 'is blank without current session' do
+ expect(render).to be_blank
+ end
+
+ context 'with current session' do
+ let(:current_search_session) { double(query_params: { q: 'abc' }) }
+
+ it 'is not blank' do
+ expect(render).not_to be_blank
+ end
+ end
+end
diff --git a/spec/components/blacklight/search_context_component_spec.rb b/spec/components/blacklight/search_context_component_spec.rb
new file mode 100644
index 0000000000..7e2230e243
--- /dev/null
+++ b/spec/components/blacklight/search_context_component_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Blacklight::SearchContextComponent, type: :component do
+ subject(:render) { render_inline(instance) }
+
+ let(:current_document_id) { 9 }
+ let(:current_document) { SolrDocument.new(id: current_document_id) }
+ let(:search_session) { { 'document_id' => current_document_id } }
+ let(:current_search_session) { double(id: current_document_id) }
+ let(:instance) { described_class.new(search_context: search_context, search_session: search_session, current_document: current_document) }
+
+ before do
+ allow(controller).to receive(:current_search_session).and_return(current_search_session)
+ allow(controller).to receive(:view_context).and_return(controller.view_context)
+ allow(controller.view_context).to receive(:current_search_session).and_return(current_search_session)
+ allow(controller.view_context).to receive(:search_session).and_return(search_session)
+ end
+
+ context 'when there is no next or previous' do
+ let(:search_context) { {} }
+
+ it "does not render content" do
+ expect(render.to_html).to be_blank
+ end
+ end
+
+ context 'when there is next and previous' do
+ let(:search_context) { { next: next_doc, prev: prev_doc } }
+ let(:prev_doc) { SolrDocument.new(id: '777') }
+ let(:next_doc) { SolrDocument.new(id: '888') }
+
+ before do
+ # allow(controller).to receive(:controller_tracking_method).and_return('track_catalog_path')
+ allow(controller).to receive(:controller_name).and_return('catalog')
+
+ allow(controller).to receive(:link_to_previous_document).and_return('')
+ allow(controller).to receive(:link_to_next_document).and_return('')
+ end
+
+ it "renders content" do
+ expect(render.css('.pagination-search-widgets').to_html).not_to be_blank
+ end
+
+ context "session and document are out of sync" do
+ let(:current_document) { SolrDocument.new(id: current_document_id + 1) }
+
+ it "does not render content" do
+ expect(render.to_html).to be_blank
+ end
+ end
+ end
+end
diff --git a/spec/controllers/bookmarks_controller_spec.rb b/spec/controllers/bookmarks_controller_spec.rb
index 05df6104ca..7e4bf8fdd4 100644
--- a/spec/controllers/bookmarks_controller_spec.rb
+++ b/spec/controllers/bookmarks_controller_spec.rb
@@ -8,6 +8,7 @@
it 'opts out of search session tracking' do
expect(@controller.blacklight_config.track_search_session).to eq false
+ expect(@controller.blacklight_config.track_search_session_config.storage).to be false
end
end
diff --git a/spec/controllers/catalog_controller_spec.rb b/spec/controllers/catalog_controller_spec.rb
index 2bef7d51a7..ac7abe207f 100644
--- a/spec/controllers/catalog_controller_spec.rb
+++ b/spec/controllers/catalog_controller_spec.rb
@@ -919,6 +919,21 @@ def export_as_mock
end
end
end
+
+ describe "page_links" do
+ it "has prev/next docs and result set data for non-empty result sets", integration: true do
+ get :page_links, params: { f: { "format" => 'Book' }, counter: 2 }
+ expect(assigns(:page_link_data)).not_to be_empty
+ expect(assigns(:page_link_data).fetch(:prev, nil)).to end_with('counter=1')
+ expect(assigns(:page_link_data).fetch(:next, nil)).to end_with('counter=3')
+ expect(assigns(:page_link_data).fetch(:totalRaw, nil)).to be 30
+ end
+
+ it "is empty for empty result sets", integration: true do
+ get :page_links, params: { f: { "format" => 'empty-result-set' }, counter: 1 }
+ expect(assigns(:page_link_data)).to be_empty
+ end
+ end
end
# there must be at least one facet, and each facet must have at least one value
diff --git a/spec/lib/blacklight/configuration/session_tracking_config_spec.rb b/spec/lib/blacklight/configuration/session_tracking_config_spec.rb
new file mode 100644
index 0000000000..ad0f648e9d
--- /dev/null
+++ b/spec/lib/blacklight/configuration/session_tracking_config_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.describe Blacklight::Configuration::SessionTrackingConfig do
+ subject(:config) { described_class.new }
+
+ it "defaults @storage to 'server'" do
+ expect(config.storage).to eq 'server'
+ end
+
+ context "@storage is set to 'server'" do
+ before do
+ config.storage = 'server'
+ end
+
+ it "defaults components values" do
+ expect(config.applied_params_component).to eq Blacklight::SearchContext::ServerAppliedParamsComponent
+ end
+
+ it "defaults tracking url helper" do
+ expect(config.url_helper).to be_nil
+ end
+ end
+
+ context "@storage is set to false" do
+ before do
+ config.storage = false
+ end
+
+ it "defaults components values" do
+ expect(config.applied_params_component).to be_nil
+ end
+
+ it "defaults tracking url helper" do
+ expect(config.url_helper).to be_nil
+ end
+ end
+end
diff --git a/spec/routing/catalog_routing_spec.rb b/spec/routing/catalog_routing_spec.rb
index 1a11c69aa3..8f9599161a 100644
--- a/spec/routing/catalog_routing_spec.rb
+++ b/spec/routing/catalog_routing_spec.rb
@@ -23,6 +23,10 @@
it "maps { :controller => 'catalog', :action => 'show', :id => 666 } to /catalog/666" do
expect(get: "/catalog/666").to route_to(controller: 'catalog', action: 'show', id: "666")
end
+
+ it "maps GET {:controller => 'catalog', :action => 'prev_next_documents'} to /catalog/page_links" do
+ expect(get: "/catalog/page_links").to route_to(controller: 'catalog', action: 'page_links')
+ end
end
describe "solr_document_path for SolrDocument", test: true do