bento_search 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 %>
|