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.
Files changed (63) hide show
  1. data/README.md +131 -74
  2. data/app/assets/javascripts/bento_search/ajax_load.js +12 -4
  3. data/app/assets/stylesheets/bento_search/suggested_styles.css +4 -4
  4. data/app/helpers/bento_search_helper.rb +114 -27
  5. data/app/item_decorators/bento_search/decorator_base.rb +53 -0
  6. data/app/item_decorators/bento_search/ebscohost/conditional_openurl_main_link.rb +36 -0
  7. data/app/item_decorators/bento_search/no_links.rb +3 -2
  8. data/app/item_decorators/bento_search/only_premade_openurl.rb +4 -0
  9. data/app/item_decorators/bento_search/openurl_add_other_link.rb +4 -0
  10. data/app/item_decorators/bento_search/openurl_main_link.rb +4 -0
  11. data/app/item_decorators/bento_search/standard_decorator.rb +122 -0
  12. data/app/models/bento_search/multi_searcher.rb +13 -6
  13. data/app/models/bento_search/openurl_creator.rb +25 -5
  14. data/app/models/bento_search/result_item.rb +25 -83
  15. data/app/models/bento_search/results/pagination.rb +8 -2
  16. data/app/models/bento_search/search_engine.rb +29 -23
  17. data/app/search_engines/bento_search/ebsco_host_engine.rb +161 -25
  18. data/app/search_engines/bento_search/eds_engine.rb +1 -44
  19. data/app/search_engines/bento_search/google_books_engine.rb +61 -14
  20. data/app/search_engines/bento_search/google_site_search_engine.rb +3 -1
  21. data/app/search_engines/bento_search/mock_engine.rb +4 -0
  22. data/app/search_engines/bento_search/primo_engine.rb +2 -3
  23. data/app/search_engines/bento_search/scopus_engine.rb +1 -0
  24. data/app/search_engines/bento_search/summon_engine.rb +5 -1
  25. data/app/search_engines/bento_search/worldcat_sru_dc_engine.rb +36 -8
  26. data/app/views/bento_search/_item_title.html.erb +29 -0
  27. data/app/views/bento_search/_no_results.html.erb +3 -0
  28. data/app/views/bento_search/_search_error.html.erb +19 -15
  29. data/app/views/bento_search/_std_item.html.erb +55 -30
  30. data/app/views/bento_search/search/search.html.erb +7 -0
  31. data/config/locales/en.yml +22 -0
  32. data/lib/bento_search/util.rb +63 -1
  33. data/lib/bento_search/version.rb +1 -1
  34. data/test/decorator/decorator_base_test.rb +72 -0
  35. data/test/decorator/standard_decorator_test.rb +55 -0
  36. data/test/dummy/db/development.sqlite3 +0 -0
  37. data/test/dummy/log/development.log +12 -0
  38. data/test/dummy/log/test.log +119757 -0
  39. data/test/functional/bento_search/search_controller_test.rb +28 -0
  40. data/test/helper/bento_search_helper_test.rb +71 -0
  41. data/test/helper/bento_truncate_helper_test.rb +71 -0
  42. data/test/unit/ebsco_host_engine_test.rb +110 -3
  43. data/test/unit/google_books_engine_test.rb +22 -14
  44. data/test/unit/google_site_search_test.rb +11 -4
  45. data/test/unit/item_decorators_test.rb +6 -65
  46. data/test/unit/openurl_creator_test.rb +87 -8
  47. data/test/unit/result_item_test.rb +1 -11
  48. data/test/unit/search_engine_base_test.rb +25 -2
  49. data/test/unit/search_engine_test.rb +16 -0
  50. data/test/unit/summon_engine_test.rb +3 -0
  51. data/test/vcr_cassettes/ebscohost/another_dissertation.yml +148 -0
  52. data/test/vcr_cassettes/ebscohost/dissertation_example.yml +218 -0
  53. data/test/vcr_cassettes/ebscohost/fulltext_info.yml +1306 -0
  54. data/test/vcr_cassettes/ebscohost/live_book_example.yml +130 -0
  55. data/test/vcr_cassettes/ebscohost/live_dissertation.yml +148 -0
  56. data/test/vcr_cassettes/ebscohost/live_pathological_book_item_example.yml +215 -0
  57. data/test/vcr_cassettes/google_site/gets_format_string.yml +232 -0
  58. data/test/vcr_cassettes/max_out_pagination.yml +155 -0
  59. data/test/vcr_cassettes/worldcat_sru_dc/max_out_pagination.yml +167 -0
  60. data/test/view/std_item_test.rb +25 -8
  61. metadata +45 -12
  62. data/test/unit/result_item_display_test.rb +0 -39
  63. 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 |j_item|
70
- j_item = j_item["volumeInfo"] if j_item["volumeInfo"]
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 = j_item["title"]
76
- item.subtitle = j_item["subtitle"]
77
- item.publisher = j_item["publisher"]
78
- item.link = j_item["canonicalVolumeLink"]
79
- item.abstract = sanitize j_item["description"]
80
- item.year = get_year j_item["publishedDate"]
81
- item.format = if j_item["printType"] == "MAGAZINE"
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
- item.language_code = j_item["language"]
109
+
110
+
111
+ item.language_code = v_info["language"]
88
112
 
89
- (j_item["authors"] || []).each do |author_name|
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
- { "intitle" => {:semantic => :title},
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 += "&sort=#{CGI.escape(value)}"
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 = json_item["formattedUrl"]
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
 
@@ -53,4 +53,8 @@ class BentoSearch::MockEngine
53
53
  configuration.sort_definitions || {}
54
54
  end
55
55
 
56
+ def search_field_definitions
57
+ configuration.search_field_definitions || {}
58
+ end
59
+
56
60
  end
@@ -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},
@@ -186,6 +186,7 @@ module BentoSearch
186
186
 
187
187
  def search_field_definitions
188
188
  {
189
+ nil => {:semantic => :general},
189
190
  "AUTH" => {:semantic => :author},
190
191
  "TITLE" => {:semantic => :title},
191
192
  # controlled and author-assigned keywords
@@ -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 = doc_hash["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 assume all users are authenticated
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
- response.error ||= {}
59
- response.error[:status] = response.status
60
- response.error[:info] = response.body
61
- response.error[:url] = url
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
- # pagination, WorldCat 'start' is 1-based, ours is 0-based.
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
- "srw.au" => {:semantic => :author},
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
+
@@ -0,0 +1,3 @@
1
+ <p class="bento_search_no_results">
2
+ <%= I18n.translate("bento_search.no_results") %>
3
+ </p>
@@ -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
- <div style="overflow-x:scroll">
7
- <p>
8
- <%= results.error %>
9
- </p>
10
- <p>
11
-
12
- <% if Rails.application.config.consider_all_requests_local && results.error["exception"] %>
13
- <ul>
14
- <% results.error["exception"].backtrace.each do |line| %>
15
- <li><%= line %></li>
16
- <% end %>
17
- <ul>
18
- <% end %>
19
- </p>
20
- </div>
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 local 'item' that's a BentoSearch::ResultItem
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
- <div class="bento_item">
8
- <%= bento_item_title(item) %>
9
-
10
- <div class="bento_item_body">
11
-
19
+
20
+
21
+ <% bento_decorate(item) do |item| %>
12
22
 
13
- <% if item.authors.length > 0 %>
14
- <p class="bento_item_authors">
15
- <%= bento_item_authors(item) %>
16
- </p>
17
- <% end %>
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
- <% if item.published_in %>
20
- <p class="bento_item_row published_in">
21
- <span class="bento_value"><%= item.published_in %> </span>
22
- </p>
23
- <% end %>
24
-
25
- <% if item.abstract %>
26
- <p class="bento_item_abstract">
27
- <%= bento_abstract_truncate( item.abstract ) %>
28
- </p>
29
- <% end %>
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 %>