bento_search 0.5.0 → 0.6.0

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