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.
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 %>