Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div id="appliedParams" class="clearfix constraints-container">
<%= render 'start_over' %>
<%= link_back_to_catalog class: 'btn btn-outline-secondary' %>
</div>
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions app/components/blacklight/search_context_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 46 additions & 6 deletions app/controllers/concerns/blacklight/search_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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
19 changes: 12 additions & 7 deletions app/helpers/blacklight/url_helper_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions app/views/catalog/_show_main_content.html.erb
Original file line number Diff line number Diff line change
@@ -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| %>
Expand Down
3 changes: 3 additions & 0 deletions app/views/catalog/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<% content_for(:head) do %>
<meta name="blacklight-search-storage" content="<%= blacklight_config.track_search_session_config.storage %>">
<% end %>
<% content_for(:sidebar) do %>
<%= render 'search_sidebar' %>
<% end %>
Expand Down
7 changes: 1 addition & 6 deletions app/views/catalog/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
<% if current_search_session %>
<div id="appliedParams" class="clearfix constraints-container">
<%= render 'start_over' %>
<%= link_back_to_catalog class: 'btn btn-outline-secondary' %>
</div>
<% 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' %>

Expand Down
20 changes: 20 additions & 0 deletions lib/blacklight/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 }
Expand Down
45 changes: 45 additions & 0 deletions lib/blacklight/configuration/session_tracking_config.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/blacklight/routes/searchable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down
2 changes: 1 addition & 1 deletion spec/components/blacklight/document_component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions spec/components/blacklight/search_context_component_spec.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions spec/controllers/bookmarks_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions spec/controllers/catalog_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading