bento_search 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +131 -74
- data/app/assets/javascripts/bento_search/ajax_load.js +12 -4
- data/app/assets/stylesheets/bento_search/suggested_styles.css +4 -4
- data/app/helpers/bento_search_helper.rb +114 -27
- data/app/item_decorators/bento_search/decorator_base.rb +53 -0
- data/app/item_decorators/bento_search/ebscohost/conditional_openurl_main_link.rb +36 -0
- data/app/item_decorators/bento_search/no_links.rb +3 -2
- data/app/item_decorators/bento_search/only_premade_openurl.rb +4 -0
- data/app/item_decorators/bento_search/openurl_add_other_link.rb +4 -0
- data/app/item_decorators/bento_search/openurl_main_link.rb +4 -0
- data/app/item_decorators/bento_search/standard_decorator.rb +122 -0
- data/app/models/bento_search/multi_searcher.rb +13 -6
- data/app/models/bento_search/openurl_creator.rb +25 -5
- data/app/models/bento_search/result_item.rb +25 -83
- data/app/models/bento_search/results/pagination.rb +8 -2
- data/app/models/bento_search/search_engine.rb +29 -23
- data/app/search_engines/bento_search/ebsco_host_engine.rb +161 -25
- data/app/search_engines/bento_search/eds_engine.rb +1 -44
- data/app/search_engines/bento_search/google_books_engine.rb +61 -14
- data/app/search_engines/bento_search/google_site_search_engine.rb +3 -1
- data/app/search_engines/bento_search/mock_engine.rb +4 -0
- data/app/search_engines/bento_search/primo_engine.rb +2 -3
- data/app/search_engines/bento_search/scopus_engine.rb +1 -0
- data/app/search_engines/bento_search/summon_engine.rb +5 -1
- data/app/search_engines/bento_search/worldcat_sru_dc_engine.rb +36 -8
- data/app/views/bento_search/_item_title.html.erb +29 -0
- data/app/views/bento_search/_no_results.html.erb +3 -0
- data/app/views/bento_search/_search_error.html.erb +19 -15
- data/app/views/bento_search/_std_item.html.erb +55 -30
- data/app/views/bento_search/search/search.html.erb +7 -0
- data/config/locales/en.yml +22 -0
- data/lib/bento_search/util.rb +63 -1
- data/lib/bento_search/version.rb +1 -1
- data/test/decorator/decorator_base_test.rb +72 -0
- data/test/decorator/standard_decorator_test.rb +55 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/log/development.log +12 -0
- data/test/dummy/log/test.log +119757 -0
- data/test/functional/bento_search/search_controller_test.rb +28 -0
- data/test/helper/bento_search_helper_test.rb +71 -0
- data/test/helper/bento_truncate_helper_test.rb +71 -0
- data/test/unit/ebsco_host_engine_test.rb +110 -3
- data/test/unit/google_books_engine_test.rb +22 -14
- data/test/unit/google_site_search_test.rb +11 -4
- data/test/unit/item_decorators_test.rb +6 -65
- data/test/unit/openurl_creator_test.rb +87 -8
- data/test/unit/result_item_test.rb +1 -11
- data/test/unit/search_engine_base_test.rb +25 -2
- data/test/unit/search_engine_test.rb +16 -0
- data/test/unit/summon_engine_test.rb +3 -0
- data/test/vcr_cassettes/ebscohost/another_dissertation.yml +148 -0
- data/test/vcr_cassettes/ebscohost/dissertation_example.yml +218 -0
- data/test/vcr_cassettes/ebscohost/fulltext_info.yml +1306 -0
- data/test/vcr_cassettes/ebscohost/live_book_example.yml +130 -0
- data/test/vcr_cassettes/ebscohost/live_dissertation.yml +148 -0
- data/test/vcr_cassettes/ebscohost/live_pathological_book_item_example.yml +215 -0
- data/test/vcr_cassettes/google_site/gets_format_string.yml +232 -0
- data/test/vcr_cassettes/max_out_pagination.yml +155 -0
- data/test/vcr_cassettes/worldcat_sru_dc/max_out_pagination.yml +167 -0
- data/test/view/std_item_test.rb +25 -8
- metadata +45 -12
- data/test/unit/result_item_display_test.rb +0 -39
- data/test/unit/worldcat_sru_dc_engine_test.rb +0 -120
@@ -16,6 +16,17 @@ module BentoSearch
|
|
16
16
|
#
|
17
17
|
# Configuration :api_key STRONGLY recommended, or google will severely
|
18
18
|
# rate-limit you.
|
19
|
+
#
|
20
|
+
# == Custom Data
|
21
|
+
# GBS API's "viewability" value is stored at item.custom_data[:viewability]
|
22
|
+
# PARTIAL, ALL_PAGES, NO_PAGES or UNKNOWN.
|
23
|
+
# https://developers.google.com/books/docs/v1/reference/volumes#resource
|
24
|
+
#
|
25
|
+
# #link_is_fulltext? is also set appropriately.
|
26
|
+
#
|
27
|
+
# You may want to use a custom decorator to display the viewability
|
28
|
+
# status somehow (in display_format? In an other_link?). See wiki
|
29
|
+
# for info on decorators.
|
19
30
|
class GoogleBooksEngine
|
20
31
|
include BentoSearch::SearchEngine
|
21
32
|
include ActionView::Helpers::SanitizeHelper
|
@@ -66,32 +77,67 @@ module BentoSearch
|
|
66
77
|
results.total_items = json["totalItems"]
|
67
78
|
|
68
79
|
|
69
|
-
(json["items"] || []).each do |
|
70
|
-
|
80
|
+
(json["items"] || []).each do |item_response|
|
81
|
+
v_info = item_response["volumeInfo"] || {}
|
71
82
|
|
72
83
|
item = ResultItem.new
|
73
84
|
results << item
|
74
85
|
|
75
|
-
item.title =
|
76
|
-
item.subtitle =
|
77
|
-
item.publisher =
|
78
|
-
|
79
|
-
|
80
|
-
item.
|
81
|
-
item.
|
86
|
+
item.title = v_info["title"]
|
87
|
+
item.subtitle = v_info["subtitle"]
|
88
|
+
item.publisher = v_info["publisher"]
|
89
|
+
# previewLink gives you your search results highlighted, preferable
|
90
|
+
# if it exists.
|
91
|
+
item.link = v_info["previewLink"] || v_info["canonicalVolumeLink"]
|
92
|
+
item.abstract = sanitize v_info["description"]
|
93
|
+
item.year = get_year v_info["publishedDate"]
|
94
|
+
# sometimes we have yyyy-mm, but we need a date to make a ruby Date,
|
95
|
+
# we'll just say the 1st.
|
96
|
+
item.publication_date = case v_info["publishedDate"]
|
97
|
+
when /(\d\d\d\d)-(\d\d)/ then Date.parse "#{$1}-#{$2}-01"
|
98
|
+
when /(\d\d\d\d)-(\d\d)-(\d\d)/ then Date.parse v_info["published_date"]
|
99
|
+
else nil
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
item.format = if v_info["printType"] == "MAGAZINE"
|
82
104
|
:serial
|
83
105
|
else
|
84
106
|
"Book"
|
85
107
|
end
|
86
108
|
|
87
|
-
|
109
|
+
|
110
|
+
|
111
|
+
item.language_code = v_info["language"]
|
88
112
|
|
89
|
-
(
|
113
|
+
(v_info["authors"] || []).each do |author_name|
|
90
114
|
item.authors << Author.new(:display => author_name)
|
91
115
|
end
|
116
|
+
|
117
|
+
# Find ISBN's, prefer ISBN-13
|
118
|
+
item.isbn = (v_info["industryIdentifiers"] || []).find {|node| node["type"] == "ISBN_13"}.try {|node| node["identifier"]}
|
119
|
+
unless item.isbn
|
120
|
+
# Look for ISBN-10 okay
|
121
|
+
item.isbn = (v_info["industryIdentifiers"] || []).find {|node| node["type"] == "ISBN_10"}.try {|node| node["identifier"]}
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# only VERY occasionally does a GBS hit have an OCLC number, but let's look
|
126
|
+
# just in case.
|
127
|
+
item.oclcnum = (v_info["industryIdentifiers"] || []).
|
128
|
+
find {|node| node["type"] == "OTHER" && node["identifier"].starts_with?("OCLC:") }.
|
129
|
+
try do |node|
|
130
|
+
node =~ /OCLC:(.*)/ ? $1 : nil
|
131
|
+
end
|
132
|
+
|
133
|
+
# save viewability status in custom_data. PARTIAL, ALL_PAGES, NO_PAGES or UNKNOWN.
|
134
|
+
# https://developers.google.com/books/docs/v1/reference/volumes#resource
|
135
|
+
item.custom_data[:viewability] = item_response["accessInfo"].try {|h| h["viewability"]}
|
136
|
+
item.link_is_fulltext = (item.custom_data[:viewability] == "ALL_PAGES") if item.custom_data[:viewability]
|
92
137
|
end
|
93
138
|
|
94
139
|
|
140
|
+
|
95
141
|
return results
|
96
142
|
end
|
97
143
|
|
@@ -106,8 +152,9 @@ module BentoSearch
|
|
106
152
|
100
|
107
153
|
end
|
108
154
|
|
109
|
-
def search_field_definitions
|
110
|
-
{
|
155
|
+
def search_field_definitions
|
156
|
+
{ nil => {:semantic => :general},
|
157
|
+
"intitle" => {:semantic => :title},
|
111
158
|
"inauthor" => {:semantic => :author},
|
112
159
|
"inpublisher" => {:semantic => :publisher},
|
113
160
|
"subject" => {:semantic => :subject},
|
@@ -155,7 +202,7 @@ module BentoSearch
|
|
155
202
|
if arguments[:sort] &&
|
156
203
|
(defn = sort_definitions[arguments[:sort]]) &&
|
157
204
|
(value = defn[:implementation])
|
158
|
-
query_url += "&
|
205
|
+
query_url += "&orderBy=#{CGI.escape(value)}"
|
159
206
|
end
|
160
207
|
|
161
208
|
|
@@ -77,8 +77,10 @@ class BentoSearch::GoogleSiteSearchEngine
|
|
77
77
|
else
|
78
78
|
item.title = json_item["title"]
|
79
79
|
item.abstract = json_item["snippet"]
|
80
|
-
item.source_title
|
80
|
+
item.source_title = json_item["formattedUrl"]
|
81
81
|
end
|
82
|
+
|
83
|
+
item.format_str = json_item["fileFormat"]
|
82
84
|
|
83
85
|
item.link = json_item["link"]
|
84
86
|
|
@@ -150,9 +150,7 @@ class BentoSearch::PrimoEngine
|
|
150
150
|
|
151
151
|
item.format = map_format fmt_str
|
152
152
|
end
|
153
|
-
|
154
|
-
#TODO highlighting
|
155
|
-
|
153
|
+
|
156
154
|
results << item
|
157
155
|
end
|
158
156
|
|
@@ -281,6 +279,7 @@ class BentoSearch::PrimoEngine
|
|
281
279
|
def search_field_definitions
|
282
280
|
# others are avail too, this is not exhaustive.
|
283
281
|
{
|
282
|
+
nil => {:semantic => :general},
|
284
283
|
"creator" => {:semantic => :author},
|
285
284
|
"title" => {:semantic => :title},
|
286
285
|
"sub" => {:semantic => :subject},
|
@@ -125,7 +125,11 @@ class BentoSearch::SummonEngine
|
|
125
125
|
item.subtitle = handle_highlighting( first_if_present doc_hash["Subtitle"] )# TODO is this right?
|
126
126
|
item.custom_data["raw_subtitle"] = handle_highlighting( first_if_present(doc_hash["Subtitle"]), :strip => true )
|
127
127
|
|
128
|
-
item.link
|
128
|
+
item.link = doc_hash["link"]
|
129
|
+
# Don't understand difference between hasFullText and
|
130
|
+
# isFullTextHit. ??. We'll use hasFullText for now, that's
|
131
|
+
# the documented one.
|
132
|
+
item.link_is_fulltext = doc_hash["hasFullText"]
|
129
133
|
|
130
134
|
if configuration.use_summon_openurl
|
131
135
|
item.openurl_kev_co = doc_hash["openUrl"] # Summon conveniently gives us pre-made OpenURL
|
@@ -19,6 +19,9 @@ require 'httpclient'
|
|
19
19
|
# readable semantic #format is often defaulted to "Book", which may not
|
20
20
|
# always be right.
|
21
21
|
#
|
22
|
+
# WorldCat doesn't let you paginate past start_record 9999. If client asks,
|
23
|
+
# this engine will silenly reset to 9999.
|
24
|
+
#
|
22
25
|
# == API Docs
|
23
26
|
# * http://oclc.org/developer/documentation/worldcat-search-api/using-api
|
24
27
|
# * http://oclc.org/developer/documentation/worldcat-search-api/sru
|
@@ -38,7 +41,7 @@ require 'httpclient'
|
|
38
41
|
#
|
39
42
|
# == Extra search args
|
40
43
|
#
|
41
|
-
# [auth] default false. Set to true to
|
44
|
+
# [auth] default false. Set to true to specify current user is authenticated
|
42
45
|
# and servicelevel=full for OCLC. Overrides config 'auth' value.
|
43
46
|
#
|
44
47
|
class BentoSearch::WorldcatSruDcEngine
|
@@ -47,27 +50,42 @@ class BentoSearch::WorldcatSruDcEngine
|
|
47
50
|
extend HTTPClientPatch::IncludeClient
|
48
51
|
include_http_client
|
49
52
|
|
53
|
+
MaxStartRecord = 9999 # at least as of Sep 2012, worldcat errors if you ask for pagination beyond this
|
54
|
+
|
50
55
|
def search_implementation(args)
|
51
56
|
url = construct_query_url(args)
|
52
57
|
|
53
58
|
results = BentoSearch::Results.new
|
54
|
-
|
59
|
+
|
55
60
|
response = http_client.get(url)
|
56
61
|
|
62
|
+
# check for http errors
|
57
63
|
if response.status != 200
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
64
|
+
results.error ||= {}
|
65
|
+
results.error[:status] = response.status
|
66
|
+
results.error[:info] = response.body
|
67
|
+
results.error[:url] = url
|
68
|
+
|
69
|
+
return results
|
62
70
|
end
|
63
71
|
|
64
72
|
xml = Nokogiri::XML(response.body)
|
65
73
|
# namespaces only get in the way
|
66
74
|
xml.remove_namespaces!
|
67
75
|
|
76
|
+
|
68
77
|
results.total_items = xml.at_xpath("//numberOfRecords").try {|n| n.text.to_i }
|
69
78
|
|
70
79
|
|
80
|
+
# check for SRU fatal errors, no results AND a diagnostic message
|
81
|
+
# is a fatal error always, I think.
|
82
|
+
if (results.total_items == 0 &&
|
83
|
+
error_xml = xml.at_xpath("./searchRetrieveResponse/diagnostics/diagnostic"))
|
84
|
+
|
85
|
+
results.error ||= {}
|
86
|
+
results.error[:info] = error_xml.children.to_xml
|
87
|
+
end
|
88
|
+
|
71
89
|
(xml.xpath("/searchRetrieveResponse/records/record/recordData/oclcdcs") || []).each do |record|
|
72
90
|
item = BentoSearch::ResultItem.new
|
73
91
|
|
@@ -125,13 +143,22 @@ class BentoSearch::WorldcatSruDcEngine
|
|
125
143
|
return results
|
126
144
|
end
|
127
145
|
|
146
|
+
# Note, if pagination start record is beyond what we think is worldcat's
|
147
|
+
# max, it will silently reset to max, and mutate the args passed in
|
148
|
+
# so pagination appears to be at max too!
|
128
149
|
def construct_query_url(args)
|
129
150
|
url = configuration.base_url
|
130
151
|
url += "&wskey=#{CGI.escape configuration.api_key}"
|
131
152
|
url += "&recordSchema=#{CGI.escape 'info:srw/schema/1/dc'}"
|
132
153
|
|
133
|
-
|
154
|
+
|
134
155
|
url += "&maximumRecords=#{args[:per_page]}" if args[:per_page]
|
156
|
+
|
157
|
+
# pagination, WorldCat 'start' is 1-based, ours is 0-based. Catch max.
|
158
|
+
if args[:start] && args[:start] > (MaxStartRecord-1)
|
159
|
+
args[:start] = MaxStartRecord - 1
|
160
|
+
args[:page] = (args[:start] / (args[:per_page] || 10)) + 1
|
161
|
+
end
|
135
162
|
url += "&startRecord=#{args[:start] + 1}" if args[:start]
|
136
163
|
|
137
164
|
url += "&query=#{CGI.escape construct_cql_query(args)}"
|
@@ -264,8 +291,9 @@ class BentoSearch::WorldcatSruDcEngine
|
|
264
291
|
# we're listing now are available even at 'default' service level.
|
265
292
|
def search_field_definitions
|
266
293
|
{
|
267
|
-
|
294
|
+
nil => {:semantic => :general},
|
268
295
|
"srw.ti" => {:semantic => :title},
|
296
|
+
"srw.au" => {:semantic => :author},
|
269
297
|
"srw.su" => {:semantic => :subject},
|
270
298
|
"srw.bn" => {:semantic => :isbn},
|
271
299
|
# Oddly no ISSN index, all we get is 'number'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<%#
|
2
|
+
# Prepare a title in an H4, with formats in parens in a <small> (for
|
3
|
+
# bootstrap), linked, etc.
|
4
|
+
#
|
5
|
+
# Pass in local `item` with BentoSearch::ResultItem (can use :as arg to
|
6
|
+
# Rails render).
|
7
|
+
#
|
8
|
+
# Optionally pass in a local "index" with result set index to display
|
9
|
+
# in front of title. 1. 2. etc.
|
10
|
+
#
|
11
|
+
# %>
|
12
|
+
<h4 class="bento_item_title">
|
13
|
+
<% if local_assigns[:index] %>
|
14
|
+
<span class="bento_index"><%= index%>.</span>
|
15
|
+
<% end %>
|
16
|
+
|
17
|
+
<%= link_to_unless(item.link.blank?, item.complete_title, item.link) %>
|
18
|
+
|
19
|
+
<% if item.display_format.present? || item.display_language.present? %>
|
20
|
+
|
21
|
+
<small class="bento_item_about">
|
22
|
+
<%# sorry, no whitespace so parens are flush %>
|
23
|
+
(<%- if item.display_format.present? -%><span class="bento_format"><%= item.display_format -%></span><%- end -%><%- if item.display_language.present? -%><span class="bento_language"> in <%= item.display_language -%></span><%- end -%>)
|
24
|
+
</small>
|
25
|
+
|
26
|
+
<% end %>
|
27
|
+
</h4>
|
28
|
+
|
29
|
+
|
@@ -2,21 +2,25 @@
|
|
2
2
|
<p>
|
3
3
|
<%= t("bento_search.search_error") %>
|
4
4
|
</p>
|
5
|
+
|
6
|
+
<% if Rails.application.config.consider_all_requests_local %>
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
8
|
+
<div style="overflow-x:scroll">
|
9
|
+
<p>
|
10
|
+
<%= results.error %>
|
11
|
+
</p>
|
12
|
+
|
13
|
+
|
14
|
+
<% if results.error["exception"] %>
|
15
|
+
<ul>
|
16
|
+
<% results.error["exception"].backtrace.each do |line| %>
|
17
|
+
<li><%= line %></li>
|
18
|
+
<% end %>
|
19
|
+
<ul>
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<% end %>
|
21
25
|
|
22
26
|
</div>
|
@@ -1,38 +1,63 @@
|
|
1
|
-
<% # must pass in
|
2
|
-
#
|
1
|
+
<% # must pass in locals:
|
2
|
+
# * 'item' that's a BentoSearch::ResultItem
|
3
|
+
# * 'results' that's the BentoSearch::Results (optional, actually)
|
4
|
+
# * 'item_counter', 1-based collection counter, passed in automatically
|
5
|
+
# by rails render collection (little known rails feature),
|
6
|
+
# can be used with results.start to calculate actual result set
|
7
|
+
# index.
|
8
|
+
#
|
3
9
|
# Custom partials meant to take this place of this one should
|
4
10
|
# use same convention, local 'item'.
|
11
|
+
#
|
12
|
+
# By default we're passing index to item_title partial to display
|
13
|
+
# counter for results, not sure if that's going to be generally
|
14
|
+
# wanted, but to begin with I'm often taking what I need locally
|
15
|
+
# based on user-testing and stuff for my use cases, and making
|
16
|
+
# it default.
|
5
17
|
%>
|
6
18
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
<div class="bento_item_body">
|
11
|
-
|
19
|
+
|
20
|
+
|
21
|
+
<% bento_decorate(item) do |item| %>
|
12
22
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
23
|
+
<div class="bento_item">
|
24
|
+
<%= render :partial => "bento_search/item_title", :object => item, :as => 'item', :locals => {:index => bento_item_counter(local_assigns[:item_counter], local_assigns[:results]) } %>
|
25
|
+
|
26
|
+
<div class="bento_item_body">
|
27
|
+
|
28
|
+
|
29
|
+
<% if item.authors.length > 0 || item.year.present? %>
|
30
|
+
<p class="bento_item_row first_about">
|
31
|
+
|
32
|
+
<% if item.authors.length > 0 %>
|
33
|
+
<%= bento_item_authors(item) %>
|
34
|
+
<% end %>
|
18
35
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
36
|
+
<% if item.year.present? %>
|
37
|
+
<span class="date"> — <%= item.year %> </span>
|
38
|
+
<% end %>
|
39
|
+
|
40
|
+
</p>
|
41
|
+
<% end %>
|
42
|
+
|
43
|
+
<% if item.abstract %>
|
44
|
+
<p class="bento_item_row abstract">
|
45
|
+
<%= bento_truncate( item.abstract ) %>
|
46
|
+
</p>
|
47
|
+
<% end %>
|
48
|
+
|
49
|
+
<% if item.published_in %>
|
50
|
+
<p class="bento_item_row second_about">
|
51
|
+
<%= item.published_in %>
|
52
|
+
</p>
|
53
|
+
<% end %>
|
54
|
+
|
55
|
+
<% if item.other_links.present? %>
|
56
|
+
<p class="bento_item_other_links">
|
57
|
+
<%= render :partial => "bento_search/link", :collection => item.other_links %>
|
58
|
+
</p>
|
59
|
+
<% end %>
|
60
|
+
</div>
|
30
61
|
|
31
|
-
<% if item.other_links.present? %>
|
32
|
-
<p class="bento_item_other_links">
|
33
|
-
<%= render :partial => "bento_search/link", :collection => item.other_links %>
|
34
|
-
</p>
|
35
|
-
<% end %>
|
36
62
|
</div>
|
37
|
-
|
38
|
-
</div>
|
63
|
+
<% end %>
|