Skip to content

Instantly share code, notes, and snippets.

@jkeck
Created May 4, 2012 21:59
Show Gist options
  • Save jkeck/2598002 to your computer and use it in GitHub Desktop.
Save jkeck/2598002 to your computer and use it in GitHub Desktop.
Nearby on shelf
These are the flat files for Nearby on Shelf as implemented in SearchWorks.
I would like to turn this into an installable gem, however as a stop-gap measure for those interested in implementing nearby for themselves (or wants to run w/ this code an refactoring into a gem).
Some major areas of consideration are:
1) The current code gets sort keys and call numbers out of a delimited item_display field. These should be single configurable fields for each key.
2) jQuery for scrolling the nearby UI. We should really find a reliable 3rd party library for this kind of carrousel behavior.
3) There is some code revolving around checking barcodes. This should be refactored out in some way but I can't think of how.
4) The part of the NearbyOnShelf class initializer that takes action based on an "ajax" parameter seems wrong to me, but I can't think of a proper solution off the top of my head.
5) Several pieces of this will need to have a way of injecting HTML into the Blacklight UI. Changes to both Blacklight and this Nearby on Shelf code will be necessary to gem-ify this codebase.
Files:
lib/nearby_on_shelf.rb Needs to be taken out of the Stanford module. You may need to include a solr_helper here.
app/controllers/browse_controller.rb This file has to take into account our grid and list layouts, which won't be necessary in core Blacklight.
app/views/browse/nearby.html.erb Simple wrapper view
app/views/browse/_nearby.html.erb Main container for nearby sidebar. This file also includes the jQuery for scrolling.
app/views/browse/_nearby_item.html.erb The partial for individual items in the nearby UI. There are some helpers/constants here for processing the data that I'm going to omit, so you should just add your own fields/parsing here. (e.g. nearby_author helper method or Constants::LIB_TRANSLATIONS constant)
<%- nearby_response ||= @nearby_response -%>
<%- unless nearby_response.items.nil? -%>
<div id="nearby_wrapper">
<ol id="nearby_objects" class="nrby_obj">
<%- nearby_response.items.each do |item| -%>
<%= render "browse/nearby_item", :item => item %>
<%- end -%>
</ol>
</div>
<div id="browse_controls">
<div id="previous">
<a href="" onclick="return false;">
<span class="page_prev">Previous</span>
</a>
</div>
<div id="full_view">
<%= link_to("#{image_tag("fullpage.png")} Show full page".html_safe, browse_index_path(:start => params[:id] ? params[:id] : params[:original_id])) %>
</div>
<div id="next">
<a href="" onclick="return false;">
<span class="page_next">Next</span>
</a>
</div>
</div>
<script type="text/javascript">
$("div#next").click(function(){
var text = $("ol#nearby_objects").children("li:last").children().children("div.callnum_sort").attr("title");
$.get("<%= browse_nearby_path %>?original_id=<%= params[:id] ? params[:id] : params[:original_id] %>&amp;start=" + text + "&amp;field=shelfkey&amp;num=5", function(data){
var $resp = $(data);
$resp.filter("div#nearby_wrapper").children("ol").each(function(){
$("ol#nearby_objects").after($(this));
$("ol.nrby_obj:last").attr("style","position:absolute;");
$("ol.nrby_obj:last").animate({top:"0"},500,function(){
$("ol.nrby_obj:first").remove();
$("ol.nrby_obj").each(function(){
$(this).attr("style","position:relative;");
});
});
});
});
return false;
});
$("div#previous").click(function(){
var text = $("ol#nearby_objects").children("li:first").children().children("div.callnum_reverse_sort").attr("title");
$.get("<%= browse_nearby_path %>?original_id=<%= params[:id] ? params[:id] : params[:original_id] %>&amp;start=" + text + "&amp;field=reverse_shelfkey&amp;num=5", function(data){
var $resp = $(data);
$resp.filter("div#nearby_wrapper").children("ol").each(function(){
$(this).attr("style","position:absolute;top:-500px;z-index:1;");
$("ol#nearby_objects:first").attr("style","position:relative;");
$("ol#nearby_objects").before($(this));
$(this).animate({top:"0"},500,function(){
$("ol.nrby_obj:last").remove();
$("ol.nrby_obj").each(function(){
$(this).attr("style","position:relative;");
});
});
});
});
return false;
});
</script>
<%- end -%>
<li <%= (item[:doc][:id] == params[:id] or item[:doc][:id] == params[:original_id]) ? "class='current_item'" : nil %>>
<div class="nearby_item">
<div class="title">
<%- if (item[:doc][:id] == params[:id] or item[:doc][:id] == params[:original_id]) -%>
<%= h(truncate(nearby_title(item[:doc]),:length=>38)) %>
<%- else -%>
<%= link_to_document(item[:doc], :label => truncate(nearby_title(item[:doc]),:length=>38).to_s) %>
<%- end -%>
</div>
<div class="year"><%= item[:doc][:pub_date] unless item[:doc][:pub_date].nil? %></div>
<div class="author">
<%- if nearby_author(item[:doc]).nil? -%>
<br/>
<%- else -%>
<%= nearby_author(item[:doc]) %>
<%- end -%>
</div>
<div class="callnum">
<%- unless item[:holding].nil? -%>
<%= h(Constants::LIB_TRANSLATIONS[item[:holding][:library]]) %> &raquo; <%= item[:holding][:callnumber] %>
<%- else -%>
<br/>
<%- end -%>
</div>
<div style="display:none;" class="callnum_sort" title="<%= URI.encode(item[:holding][:shelfkey]) %>"></div>
<div style="display:none;" class="callnum_reverse_sort" title="<%= URI.encode(item[:holding][:reverse_shelfkey]) %>"></div>
</div>
</li>
class BrowseController < ApplicationController
include Blacklight::Catalog
def index
@response, @original_doc = get_solr_response_for_doc_id(params[:start])
if params[:barcode]
preferred_barcode = params[:barcode]
else
preferred_barcode = @original_doc[:preferred_barcode]
end
save_current_search_params
if params[:view] and params[:view] == "list"
@document_list = Stanford::NearbyOnShelf.new("static",{:item_display => @original_doc[:item_display],:preferred_barcode=>preferred_barcode, :before => 19, :after => 20, :page => params[:page]}).items
render "catalog/_list_list"
else
@document_list = Stanford::NearbyOnShelf.new("static",{:item_display => @original_doc[:item_display],:preferred_barcode=>preferred_barcode, :before => 9, :after => 10, :page => params[:page]}).items
render "catalog/_gallery_list"
end
end
def nearby
if params[:preferred_barcode].to_s.empty?
@nearby_response = Stanford::NearbyOnShelf.new("ajax",{:start => params[:start], :field => params[:field], :num => params[:num]})
else
@nearby_response = Stanford::NearbyOnShelf.new("bcode",{:item_display=>params[:item_display],:preferred_barcode=>params[:preferred_barcode], :before => 2, :after => 2})
end
render :layout => false
end
end
<%- nearby_response ||= @nearby_response -%>
<%= render "browse/nearby", :nearby_response => nearby_response %>
module Stanford
class NearbyOnShelf
attr_reader :items
def initialize(type,options)
if type == "ajax"
@items = get_next_spines_from_field(options[:start],options[:field],options[:num],nil)
else
@items = get_nearby_items(options[:item_display],options[:preferred_barcode],options[:before],options[:after],options[:page])
end
end
protected
def get_nearby_items(itm_display, barcode, before, after, page)
items=[]
item_display = get_item_display(itm_display,barcode)
if !item_display.nil?
my_shelfkey = get_shelfkey(item_display)
my_reverse_shelfkey = get_reverse_shelfkey(item_display)
if page.nil? or page.to_i == 0
# get preceding bookspines
items << get_next_spines_from_field(my_reverse_shelfkey, "reverse_shelfkey", before, nil)
# TODO: can we avoid this extra call to Solr but keep the code this clean?
# What is the purpose of this call? To just return the original document?
items << get_spines_from_field_values([my_shelfkey], "shelfkey").uniq
# get following bookspines
items << get_next_spines_from_field(my_shelfkey, "shelfkey", after, nil)
else
if page.to_i < 0 # page is negative so we need to get the preceding docs
items << get_next_spines_from_field(my_reverse_shelfkey, "reverse_shelfkey", (before.to_i+1)*2, page.to_i)
elsif page.to_i > 0 # page is possitive, so we need to get the following bookspines
items << get_next_spines_from_field(my_shelfkey, "shelfkey", after.to_i*2, page.to_i)
end
end
items.flatten
end
end # get_nearby_items
# given a shelfkey or reverse shelfkey (for a lopped call number), get the
# text for the next "n" nearby items
def get_next_spines_from_field(starting_value, field_name, how_many, page)
number_of_items = how_many
unless page.nil?
if page < 0
page = page.to_s[1,page.to_s.length]
end
number_of_items = how_many.to_i * page.to_i+1
end
desired_values = get_next_terms_for_field(starting_value, field_name, number_of_items)
unless page.nil? or page.to_i == 0
desired_values = desired_values.values_at((desired_values.length-how_many.to_i)..desired_values.length)
end
get_spines_from_field_values(desired_values, field_name)
end
# return an array of the next terms in the index for the indicated field and
# starting term. Returned array does NOT include starting term. Queries Solr (duh).
def get_next_terms_for_field(starting_term, field_name, how_many=3)
result = []
# terms is array of one element hashes with key=term and value=count
terms_array = get_next_terms(starting_term, field_name, how_many.to_i+1)
terms_array.each { |term_hash|
result << term_hash.keys[0] unless term_hash.keys[0] == starting_term
}
result
end
# create an array of sorted html list items containing the appropriate display text
# (analogous to what would be visible if you were looking at the spine of
# a book on a shelf) from relevant solr docs, given a particular solr
# field and value for which to retrieve spine info.
# Each html list item must match a desired value
def get_spines_from_field_values(desired_values, field)
spines_hash = {}
docs = get_docs_for_field_values(desired_values, field)
docs.each do |doc|
hsh = get_spine_hash_from_doc(doc, desired_values, field)
spines_hash.merge!(hsh)
end
result = []
spines_hash.keys.sort.each { |sortkey|
result << spines_hash[sortkey]
}
result
end
# create a hash with
# key = sorting key for the spine,
# value = the html list item containing appropriate display text
# (analogous to what would be visible if you were looking at the spine of
# a book on a shelf) from a solr doc.
# spine is: <li> title [(pub year)] [<br/> author] <br/> callnum </li>
# Each element of the hash must match a desired value in the
# desired_values array for the indicated piece (shelfkey or reverse shelfkey)
def get_spine_hash_from_doc(doc, desired_values, field)
result_hash = {}
return if doc[:item_display].nil?
# This winnows down the holdings hashs on only ones where the desired values includes the shelfkey or reverse shelfkey using a very quick select statment
# The resulting array looke like [[:"36105123456789",{:barcode=>"36105123456789",:callnumber=>"PS3156 .A53"}]]
item_array = doc.holdings_from_solr.select{|k,v| ( (field == "shelfkey" and desired_values.include?(v[:shelfkey]) ) or ( field == "reverse_shelfkey" and desired_values.include?(v[:reverse_shelfkey]) ) ) }
temp_holdings_hash = {}
unless item_array.empty?
# putting items back into a hash for readibility
item_array.each do |value|
temp_holdings_hash[value.first] = value.last
end
# looping through the resulting temp hash of holdings to build proper sort keys and then return a hash that conains a solr document for every item in the hash
temp_holdings_hash.each do |key,value|
# create sorting key for spine
# shelfkey asc, then by sorting title asc, then by pub date desc
# notice that shelfkey and sort_title need to be a constant length
# separator of " -|- " is for human readability only
sort_key = "#{value[:shelfkey][0,100].ljust(100)} -|- "
sort_key << "#{doc[:title_sort][0,100].ljust(100)} -|- " unless doc[:title_sort].nil?
# pub_year must be inverted for descending sort
if doc[:pub_date].nil? || doc[:pub_date].length == 0
sort_key << '9999'
else
sort_key << doc[:pub_date].tr('0123456789', '9876543210')
end
# Adding ckey to sort to make sure we collapse things that have the same callnumber, title, pub date, AND ckey
sort_key << " -|- #{doc[:id][0,20].ljust(20)}"
# We were adding the library to the sortkey. However; if we don't add the library we can easily collapse items that have the same
# call number (shelfkey), title, pub date, and ckey but are housed in different libraries.
#sort_key << " -|- #{value[:library][0,40].ljust(40)}"
result_hash[sort_key] = {:doc=>doc,:holding=>value}
end # end each item display
end
return result_hash
end
# given a document and the barcode of an item in the document, return the
# item_display field corresponding to the barcode, or nil if there is no
# such item
def get_item_display(item_display, barcode)
item = ""
if barcode.nil? || barcode.length == 0
return nil
end
item_display.each do |item_disp|
item = item_disp if item_disp =~ /^#{CGI::escape(barcode)}/
end
return item unless item == ""
end
# return the shelfkey (lopped) piece of the item_display field
def get_shelfkey(item_display)
get_item_display_piece(item_display, 6)
end
# return the reverse shelfkey (lopped) piece of the item_display field
def get_reverse_shelfkey(item_display)
get_item_display_piece(item_display, 7)
end
def get_item_display_piece(item_display, index)
if (item_display)
item_array = item_display.split('-|-')
return item_array[index].strip unless item_array[index].nil?
end
nil
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment