bento_search 0.5.0 → 0.6.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 (47) hide show
  1. data/README.md +6 -5
  2. data/app/assets/javascripts/bento_search/ajax_load.js +42 -16
  3. data/app/assets/stylesheets/bento_search/suggested_styles.css +9 -0
  4. data/app/controllers/bento_search/search_controller.rb +15 -6
  5. data/app/helpers/bento_search_helper.rb +24 -8
  6. data/app/item_decorators/bento_search/no_links.rb +13 -0
  7. data/app/models/bento_search/openurl_creator.rb +18 -8
  8. data/app/models/bento_search/registrar.rb +2 -6
  9. data/app/models/bento_search/result_item.rb +43 -3
  10. data/app/models/bento_search/results.rb +4 -0
  11. data/app/models/bento_search/search_engine.rb +25 -23
  12. data/app/search_engines/bento_search/ebsco_host_engine.rb +42 -17
  13. data/app/search_engines/bento_search/google_books_engine.rb +2 -0
  14. data/app/search_engines/bento_search/google_site_search_engine.rb +177 -0
  15. data/app/search_engines/bento_search/mock_engine.rb +5 -0
  16. data/app/search_engines/bento_search/primo_engine.rb +23 -2
  17. data/app/search_engines/bento_search/scopus_engine.rb +4 -1
  18. data/app/search_engines/bento_search/summon_engine.rb +4 -14
  19. data/app/search_engines/bento_search/worldcat_sru_dc_engine.rb +293 -0
  20. data/app/views/bento_search/_std_item.html.erb +4 -5
  21. data/app/views/bento_search/_wrap_with_count.html.erb +20 -0
  22. data/app/views/bento_search/search/search.html.erb +15 -1
  23. data/config/locales/en.yml +6 -4
  24. data/lib/bento_search/util.rb +13 -0
  25. data/lib/bento_search/version.rb +1 -1
  26. data/test/dummy/log/development.log +1 -0
  27. data/test/dummy/log/test.log +24357 -0
  28. data/test/functional/bento_search/search_controller_test.rb +39 -0
  29. data/test/helper/bento_search_helper_test.rb +47 -5
  30. data/test/unit/ebsco_host_engine_test.rb +15 -0
  31. data/test/unit/google_books_engine_test.rb +1 -0
  32. data/test/unit/google_site_search_test.rb +122 -0
  33. data/test/unit/item_decorators_test.rb +12 -1
  34. data/test/unit/openurl_creator_test.rb +19 -3
  35. data/test/unit/primo_engine_test.rb +5 -3
  36. data/test/unit/result_item_test.rb +36 -1
  37. data/test/unit/search_engine_test.rb +27 -4
  38. data/test/unit/worldcat_sru_dc_engine_test.rb +120 -0
  39. data/test/vcr_cassettes/google_site/basic_smoke_test.yml +254 -0
  40. data/test/vcr_cassettes/google_site/empty_result_set.yml +53 -0
  41. data/test/vcr_cassettes/google_site/pagination_object_is_correct_for_actual_page_when_you_ask_for_too_far.yml +260 -0
  42. data/test/vcr_cassettes/google_site/with_highlighting.yml +265 -0
  43. data/test/vcr_cassettes/google_site/without_highlighting.yml +267 -0
  44. data/test/vcr_cassettes/primo/proper_tags_for_snippets.yml +517 -502
  45. data/test/vcr_cassettes/primo/search_smoke_test.yml +1 -1
  46. data/test/vcr_cassettes/worldcat_sru_dc/smoke_test.yml +628 -0
  47. metadata +40 -4
data/README.md CHANGED
@@ -13,7 +13,7 @@ Rails3 and tested only under ruby 1.9.3.
13
13
  * It is focused on use cases for academic libraries, but may be useful in generic
14
14
  cases too. Initially, engine adapters are planned to be provided for:
15
15
  Google Books, Scopus, SerialSolutions Summon, Ex Libris Primo,
16
- EBSCO Discovery Service, and EBSCO traditional 'EIT' api. Most
16
+ EBSCO Discovery Service, EBSCO traditional 'EIT' api, Google Site Search. Most
17
17
  of these search engines require a vendor license to use.
18
18
 
19
19
  * bento_search could be considered building blocks for a type of 'federated
@@ -42,6 +42,7 @@ BentoSearch::SearchEngine. http://rubydoc.info/gems/bento_search/frames/
42
42
 
43
43
  An example app using BentoSearch and showing it's features is
44
44
  available at http://github.com/jrochkind/sample_megasearch
45
+ There is a short screencast showing that sample app in action here: http://screencast.com/t/JLS0lclrBZU
45
46
 
46
47
  ## Usage Examples
47
48
 
@@ -104,8 +105,8 @@ field type names:
104
105
  This will raise if an engine doesn't support that semantic search field.
105
106
  You can find out what fields a particular engine supports.
106
107
 
107
- BentoSearch::GoogleBooksEngine.search_keys # => internal keys
108
- BentoSearch::GoogleBooksEngine.semantic_search_keys
108
+ google_books_engine.search_keys # => internal keys
109
+ google_books_engine.semantic_search_keys
109
110
 
110
111
  You can also provide all arguments in a single hash when it's convenient
111
112
  to do so:
@@ -116,9 +117,9 @@ to do so:
116
117
 
117
118
  An engine advertises what sort types it supports:
118
119
 
119
- BentoSearch::GoogleBooksEngine.sort_definitions
120
+ google_books_engine.sort_keys
120
121
 
121
- That returns a hash, where the keys are sort identifiers, where possible
122
+ An array of sort identifiers, where possible
122
123
  chosen from a standard list of semantics. (See list in config/i18n/en.yml,
123
124
  bento_search.sort_keys).
124
125
 
@@ -1,22 +1,48 @@
1
+ var BentoSearch = BentoSearch || {}
2
+
3
+ // Pass in a DOM node that has a data-ajax-url attribute.
4
+ // Will AJAX load bento search results inside that node.
5
+ // optional second arg success callback function.
6
+ BentoSearch.ajax_load = function(node, success_callback) {
7
+ div = $(node);
8
+
9
+ if (div.length == 0) {
10
+ //we've got nothing
11
+ return
12
+ }
13
+
14
+
15
+ // We find the "waiting"/spinner section already rendered,
16
+ // and show it. We experimented with generating the spinner/waiting
17
+ // purely in JS, instead of rendering a hidden one server-side. But
18
+ // it was too weird and unreliable to do that, sorry.
19
+ div.find(".bento_search_ajax_loading").show();
20
+
21
+
22
+ // Now load the actual external content from html5 data-bento-ajax-url
23
+ $.ajax({
24
+ url: div.data("bentoAjaxUrl"),
25
+ success: function(response, status, xhr) {
26
+ if (success_callback) {
27
+ success_callback.apply(div, response);
28
+ }
29
+ div.replaceWith(response);
30
+ },
31
+ error: function(xhr, status, errorThrown) {
32
+ var msg = "Sorry but there was an error: ";
33
+ div.html(msg + xhr.status + " " + xhr.statusText + ", " + status);
34
+ }
35
+ });
36
+
37
+
38
+ }
39
+
1
40
  jQuery(document).ready(function($) {
2
41
  //Intentionally wait for window.load, not just onready, to
3
42
  //prevent interfering with rest of page load.
4
43
  $(window).bind("load", function() {
5
- $(".bento_search_ajax_wait").each(function(i, div) {
6
- div = $(div);
7
- // from html5 data-bento-ajax-url
8
- $.ajax({
9
- url: div.data("bentoAjaxUrl"),
10
- success: function(response, status, xhr) {
11
- div.replaceWith(response);
12
- },
13
- error: function(xhr, status, errorThrown) {
14
- var msg = "Sorry but there was an error: ";
15
- div.html(msg + xhr.status + " " + xhr.statusText + ", " + status);
16
- }
17
- });
18
-
44
+ $("*[data-bento-search-load=ajax_auto]").each(function(i, div) {
45
+ BentoSearch.ajax_load(div);
19
46
  });
20
- });
21
-
47
+ });
22
48
  });
@@ -20,3 +20,12 @@
20
20
  margin: 2em;
21
21
  }
22
22
 
23
+ /* we put title in an h4, but if it's a link too, it
24
+ doesn't really need to be bold, link style is already
25
+ visible enough. */
26
+ .bento_item_title a {
27
+ font-weight: normal;
28
+ }
29
+ .bento_item_title a .bento_search_highlight {
30
+ font-style: inherit;
31
+ }
@@ -40,25 +40,34 @@ module BentoSearch
40
40
 
41
41
  # returns partial HTML results, suitable for
42
42
  # AJAX to insert into DOM.
43
- # arguments for engine.search are taken from URI request params.
44
- # (TODO: Is this a security issue, do we need to whitelist em? )
45
- def search
46
- engine = BentoSearch.get_engine(params[:engine_id])
43
+ # arguments for engine.search are taken from URI request params, whitelisted
44
+ def search
45
+ engine = BentoSearch.get_engine(params[:engine_id])
46
+ # put it in an iVar mainly for testing purposes.
47
+ @engine = engine
47
48
 
48
49
 
49
50
  unless engine.configuration.allow_routable_results == true
50
51
  raise AccessDenied.new("engine needs to be registered with :allow_routable_results => true")
51
52
  end
52
53
 
53
- @results = engine.search(params.to_hash.symbolize_keys)
54
+ @results = engine.search safe_search_args(engine, params)
55
+ # template name of a partial with 'yield' to use to wrap the results
56
+ @partial_wrapper = @results.display_configuration.lookup!("ajax.wrapper_template")
54
57
 
55
- render :layout => false # partial HTML results
58
+ # partial HTML results
59
+ render "bento_search/search/search", :layout => false
60
+
56
61
  end
57
62
 
58
63
 
59
64
 
60
65
  protected
61
66
 
67
+ def safe_search_args(engine, params)
68
+ params.to_hash.symbolize_keys.slice( *engine.public_settable_search_args )
69
+ end
70
+
62
71
  def deny_access(exception)
63
72
  render :text => exception.message, :status => 403
64
73
  end
@@ -40,13 +40,26 @@ module BentoSearchHelper
40
40
 
41
41
  end
42
42
 
43
- if (!results && load_mode == :ajax_auto)
43
+ if (!results && [:ajax_auto, :ajax_triggered].include?(load_mode))
44
44
  raise ArgumentError.new("`:load => :ajax` requires a registered engine with an id") unless engine.configuration.id
45
- content_tag(:div, :class => "bento_search_ajax_wait",
46
- :"data-bento-ajax-url" => to_bento_search_url( {:engine_id => engine.configuration.id}.merge(options) )) do
47
- image_tag("bento_search/large_loader.gif", :alt => I18n.translate("bento_search.ajax_loading")) +
48
- content_tag("noscript") do
49
- "Can not load results without javascript"
45
+ content_tag(:div,
46
+ :class => "bento_search_ajax_wait",
47
+ :"data-bento-search-load" => load_mode.to_s,
48
+ :"data-bento-ajax-url" => to_bento_search_url( {:engine_id => engine.configuration.id}.merge(options) )) do
49
+
50
+ # An initially hidden div with loading msg/spinner that will be shown
51
+ # by js on ajax load
52
+ content_tag("noscript") do
53
+ I18n.t("bento_search.ajax_noscript")
54
+ end +
55
+ content_tag(:div,
56
+ :class => "bento_search_ajax_loading",
57
+ :style => "display:none") do
58
+
59
+ image_tag("bento_search/large_loader.gif",
60
+ :alt => I18n.translate("bento_search.ajax_loading"),
61
+ )
62
+
50
63
  end
51
64
  end
52
65
  else
@@ -55,7 +68,7 @@ module BentoSearchHelper
55
68
  if results.failed?
56
69
  render :partial => "bento_search/search_error", :locals => {:results => results}
57
70
  elsif results.length > 0
58
- render :partial => "bento_search/std_item", :collection => results
71
+ render :partial => "bento_search/std_item", :collection => results, :as => :item
59
72
  else
60
73
  content_tag(:div, :class=> "bento_search_no_results") do
61
74
  I18n.translate("bento_search.no_results")
@@ -126,10 +139,13 @@ module BentoSearchHelper
126
139
  # returns a hash of label => key suitable for passing to rails
127
140
  # options_for_select. (Yes, it works backwards from how you'd expect).
128
141
  # Label is looked up using I18n, at bento_search.sort_keys.*
142
+ #
143
+ # If no i18n is found, titleized version of key itself is used as somewhat
144
+ # reasonable default.
129
145
  def bento_sort_hash_for(engine)
130
146
  Hash[
131
147
  engine.sort_definitions.keys.collect do |key|
132
- [I18n.translate(key, :scope => "bento_search.sort_keys"), key]
148
+ [I18n.translate(key.to_s, :scope => "bento_search.sort_keys", :default => key.to_s.titleize), key.to_s]
133
149
  end
134
150
  ]
135
151
  end
@@ -0,0 +1,13 @@
1
+ # An item decorator that just erases all links, main link and other
2
+ # links.
3
+ module BentoSearch::NoLinks
4
+
5
+ def link
6
+ nil
7
+ end
8
+
9
+ def other_links
10
+ []
11
+ end
12
+
13
+ end
@@ -16,6 +16,8 @@ module BentoSearch
16
16
  # In some cases nil can be returned, if no reasonable OpenURL can
17
17
  # be created from the ResultItem.
18
18
  class OpenurlCreator
19
+ include ActionView::Helpers::SanitizeHelper # for strip_tags
20
+
19
21
  attr_accessor :result_item
20
22
 
21
23
  def initialize(ri)
@@ -42,9 +44,9 @@ module BentoSearch
42
44
  r.set_metadata("genre", self.genre)
43
45
 
44
46
  if result_item.authors.length > 0
45
- r.set_metadata("aufirst", result_item.authors.first.first)
46
- r.set_metadata("aulast", result_item.authors.first.last)
47
- r.set_metadata("au", result_item.author_display(result_item.authors.first))
47
+ r.set_metadata("aufirst", ensure_no_tags(result_item.authors.first.first))
48
+ r.set_metadata("aulast", ensure_no_tags(result_item.authors.first.last))
49
+ r.set_metadata("au", result_item.author_display(ensure_no_tags result_item.authors.first))
48
50
  end
49
51
 
50
52
  r.set_metadata("date", result_item.year.to_s)
@@ -52,18 +54,18 @@ module BentoSearch
52
54
  r.set_metadata("issue", result_item.issue.to_s)
53
55
  r.set_metadata("spage", result_item.start_page.to_s)
54
56
  r.set_metadata("epage", result_item.end_page.to_s)
55
- r.set_metadata("jtitle", result_item.journal_title)
57
+ r.set_metadata("jtitle", ensure_no_tags(result_item.source_title))
56
58
  r.set_metadata("issn", result_item.issn)
57
59
  r.set_metadata("isbn", result_item.isbn)
58
- r.set_metadata("pub", result_item.publisher)
60
+ r.set_metadata("pub", ensure_no_tags(result_item.publisher))
59
61
 
60
62
  case result_item.format
61
63
  when "Book"
62
- r.set_metadata("btitle", result_item.complete_title)
64
+ r.set_metadata("btitle", ensure_no_tags(result_item.complete_title))
63
65
  when "Article", :conference_paper
64
- r.set_metadata("atitle", result_item.complete_title)
66
+ r.set_metadata("atitle", ensure_no_tags(result_item.complete_title))
65
67
  else
66
- r.set_metadata("title", result_item.complete_title)
68
+ r.set_metadata("title", ensure_no_tags(result_item.complete_title))
67
69
  end
68
70
 
69
71
  return context_object
@@ -124,5 +126,13 @@ module BentoSearch
124
126
  end
125
127
 
126
128
 
129
+ # If the input is not marked html_safe?, just return it. Otherwise
130
+ # strip html tags from it.
131
+ def ensure_no_tags(str)
132
+ return str unless str.html_safe?
133
+
134
+ strip_tags(str)
135
+ end
136
+
127
137
  end
128
138
  end
@@ -57,12 +57,8 @@ class BentoSearch::Registrar
57
57
  # Turn a string into a constant/class object, lexical lookup
58
58
  # within BentoSearch module. Can use whatever would be legal
59
59
  # in ruby, "A", "A::B", "::A::B" (force top-level lookup), etc.
60
- def constantize(klass_string)
61
- unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ klass_string
62
- raise NameError, "#{klass_string.inspect} is not a valid constant name!"
63
- end
64
-
65
- BentoSearch.module_eval(klass_string, __FILE__, __LINE__)
60
+ def constantize(klass_string)
61
+ BentoSearch::Util.constantize(klass_string)
66
62
  end
67
63
 
68
64
 
@@ -1,3 +1,5 @@
1
+ require 'language_list'
2
+
1
3
  module BentoSearch
2
4
  # Data object representing a single hit from a search, normalized
3
5
  # with common data fields. Usually held in a BentoSearch::Results object.
@@ -24,6 +26,10 @@ module BentoSearch
24
26
  self.custom_data ||= {}
25
27
  end
26
28
 
29
+ # If set to true, item will refuse to generate an openurl,
30
+ # returning nil from #to_openurl or #openurl_kev
31
+ attr_accessor :openurl_disabled
32
+
27
33
  # Array (possibly empty) of BentoSearch::Link objects
28
34
  # representing additional links. Often SearchEngine's themselves
29
35
  # won't include any of these, but Decorators will be used
@@ -81,6 +87,29 @@ module BentoSearch
81
87
  # format.
82
88
  attr_accessor :format_str
83
89
 
90
+ # Language of materials. Producer can set language_code to an ISO 639-1 (two
91
+ # letter) or 639-3 (three letter) language code. If you do this, you don't
92
+ # need to set language_str, it'll be automatically looked up. (Providing
93
+ # language name in English at present, i18n later maybe).
94
+ #
95
+ # Or, if you don't know the language code (or there isn't one?), you can set
96
+ # language_str manually to a presumably english user-displayable string.
97
+ # Manually set language_str will over-ride display string calculated from
98
+ # language_code.
99
+ #
100
+ # Consumers can look at language_code or language_str regardless (although
101
+ # either or both may be nil). You can use language_list gem to normalize to a
102
+ # 2- or 3-letter from language_code that could be either.
103
+ attr_accessor :language_code
104
+ attr_writer :language_str
105
+ def language_str
106
+ @language_str || language_code.try do |code|
107
+ LanguageList::LanguageInfo.find(code).try do |lang_obj|
108
+ lang_obj.name
109
+ end
110
+ end
111
+ end
112
+
84
113
  # year published. a ruby int
85
114
  # PART of:.
86
115
  # * schema.org CreativeWork "datePublished", year portion
@@ -93,9 +122,18 @@ module BentoSearch
93
122
  attr_accessor :start_page
94
123
  attr_accessor :end_page
95
124
 
96
- attr_accessor :journal_title
125
+ # source_title is often used for journal_title (and aliased
126
+ # as #journal_title, although that may go away), but can
127
+ # also be used for other 'container' titles. Book title for
128
+ # a book chapter. Even web site or URL for a web page.
129
+ attr_accessor :source_title
130
+ alias_method :journal_title, :source_title
131
+ alias_method :'journal_title=', :'source_title='
132
+
133
+
97
134
  attr_accessor :issn
98
135
  attr_accessor :isbn
136
+ attr_accessor :oclcnum # OCLC accession number, WorldCat.
99
137
 
100
138
  attr_accessor :doi
101
139
 
@@ -125,6 +163,8 @@ module BentoSearch
125
163
 
126
164
  # Returns a ruby OpenURL::ContextObject (NISO Z39.88).
127
165
  def to_openurl
166
+ return nil if openurl_disabled
167
+
128
168
  BentoSearch::OpenurlCreator.new(self).to_openurl
129
169
  end
130
170
 
@@ -180,9 +220,9 @@ module BentoSearch
180
220
  result_elements.push "<span class='year'>#{year}</span>"
181
221
  end
182
222
 
183
- result_elements.push(journal_title) unless journal_title.blank?
223
+ result_elements.push(source_title) unless source_title.blank?
184
224
 
185
- if journal_title.blank? && ! publisher.blank?
225
+ if source_title.blank? && ! publisher.blank?
186
226
  result_elements.push html_escape publisher
187
227
  end
188
228
 
@@ -11,6 +11,10 @@ module BentoSearch
11
11
  attr_accessor :start
12
12
  # per_page setting, can be used for pagination.
13
13
  attr_accessor :per_page
14
+
15
+ # simply copied over from search engine configuration :display key,
16
+ # useful for making config available at display time in a DRY way.
17
+ attr_accessor :display_configuration
14
18
 
15
19
  # If error is non-nil, it's an error condition with no real results.
16
20
  # error should be a hash with these (and possibly other) keys, although
@@ -50,7 +50,8 @@ module BentoSearch
50
50
  # framework:
51
51
  #
52
52
  # [item_decorators]
53
- # Array of Modules that will be decorated on to each individual search
53
+ # Array of Modules (or strings specifying modules, helpful to keep
54
+ # config serializable) that will be decorated on to each individual search
54
55
  # BentoSearch::ResultItem. These can be used to, via configuration, change
55
56
  # the links associated with items, change certain item behaviors, or massage
56
57
  # item metadata. (Needs more documentation).
@@ -125,6 +126,14 @@ module BentoSearch
125
126
  # handles configuration loading, mostly. Argument is a
126
127
  # Confstruct::Configuration or Hash.
127
128
  def initialize(aConfiguration = Confstruct::Configuration.new)
129
+ # To work around weird confstruct bug, we need to change
130
+ # a hash to a Confstruct ourselves.
131
+ # https://github.com/mbklein/confstruct/issues/14
132
+ unless aConfiguration.kind_of? Confstruct::Configuration
133
+ aConfiguration = Confstruct::Configuration.new aConfiguration
134
+ end
135
+
136
+
128
137
  # init, from copy of default, or new
129
138
  if self.class.default_configuration
130
139
  self.configuration = Confstruct::Configuration.new(self.class.default_configuration)
@@ -136,6 +145,7 @@ module BentoSearch
136
145
 
137
146
  # global defaults?
138
147
  self.configuration[:item_decorators] ||= []
148
+ self.configuration[:for_display] ||= {}
139
149
 
140
150
  # check for required keys -- have to be present, and not nil
141
151
  if self.class.required_configuration
@@ -208,6 +218,8 @@ module BentoSearch
208
218
  results.engine_id = configuration.id
209
219
 
210
220
  results.timing = (Time.now - start_t)
221
+
222
+ results.display_configuration = configuration.for_display
211
223
 
212
224
  return results
213
225
  rescue *auto_rescue_exceptions => e
@@ -303,15 +315,26 @@ module BentoSearch
303
315
  alias_method :parse_search_arguments, :normalized_search_arguments
304
316
 
305
317
 
306
-
318
+ # Used mainly/only by the AJAX results loading.
319
+ # an array WHITELIST of attributes that can be sent as non-verified
320
+ # request params and used to execute a search. For instance, 'auth' is
321
+ # NOT on there, you can't trust a web request as to 'auth' status.
322
+ # individual engines may over-ride, call super, and add additional
323
+ # engine-specific attributes.
324
+ def public_settable_search_args
325
+ [:query, :search_field, :semantic_search_field, :sort, :page, :start, :per_page]
326
+ end
307
327
 
308
328
 
309
329
  protected
310
330
 
311
331
  # Extend each result with each specified decorator module
332
+ # configuration.item_decorators is an array of either Module constants,
333
+ # or strings specifying module constants.
312
334
  def decorate(results)
313
335
  results.each do |result|
314
336
  configuration.item_decorators.each do |decorator|
337
+ decorator = (decorator.kind_of? Module) ? decorator : BentoSearch::Util.constantize(decorator)
315
338
  result.extend decorator
316
339
  end
317
340
  end
@@ -337,27 +360,6 @@ module BentoSearch
337
360
 
338
361
  module ClassMethods
339
362
 
340
- # If support fielded search, over-ride to specify fields
341
- # supported. Returns a hash, key is engine-specific internal
342
- # search field, value is nil or a hash of metadata about
343
- # the search field, including semantic mapping.
344
- #
345
- # def search_field_definitions
346
- # { "intitle" => {:semantic => :title}}
347
- # end
348
- def search_field_definitions
349
- {}
350
- end
351
-
352
-
353
- # Returns list of string internal search_field's that can
354
- # be supplied to search(:search_field => x)
355
- def search_keys
356
- return search_field_definitions.keys
357
- end
358
-
359
-
360
-
361
363
  # Over-ride returning a hash or Confstruct with
362
364
  # any configuration values you want by default.
363
365
  # actual user-specified config values will be deep-merged