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
@@ -0,0 +1,53 @@
1
+ require 'delegate'
2
+
3
+ module BentoSearch
4
+ # A delegator with an ActionView context.
5
+ # You can access the ActionView context at _h , to call Rails helper
6
+ # methods (framework or app specific, whatever should be avail at
7
+ # given context)
8
+ #
9
+ # inside a method in a decorator, `_h.content_tag` or `_h.html_escape`
10
+ # or `_h.url_for` etc.
11
+ #
12
+ # (Except you can't call html_escape that way becuase Rails makes it
13
+ # private for some reason, wtf. We provide an html_escape)
14
+ #
15
+ # Inside a decorator, access #_base to get undecorated base model.
16
+ class DecoratorBase < SimpleDelegator
17
+
18
+ def initialize(base, view_context)
19
+ super(base)
20
+
21
+
22
+ # This worked to make html_escape avail at _h.html_escape, but
23
+ # yfeldblum warned it messes up the method lookup cache, so
24
+ # we just provide a straight #html_escape instead.
25
+ #if view_context.respond_to?(:html_escape, true)
26
+ # thanks yfeldblum in #rails for this simple way to make
27
+ # html_escape public, which I don't entirely understand myself. :)
28
+
29
+ # class << view_context
30
+ # public :html_escape
31
+ # end
32
+ #end
33
+
34
+ @view_context = view_context
35
+ @base = base
36
+ end
37
+
38
+ def _h
39
+ @view_context
40
+ end
41
+
42
+ def _base
43
+ @base
44
+ end
45
+
46
+ # _h.html_escape won't work because Rails makes html_escape
47
+ # private for some weird reason. We provide our own here instead.
48
+ def html_escape(*args, &block)
49
+ ERB::Util.html_escape(*args, &block)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ # DEPRECATED. Just write the logic into a custom Decorator yourself.
2
+ # See wiki on decorators.
3
+ #
4
+ #
5
+ # For ebscohost connector, example of an Item Decorator that replaces the main
6
+ # 'link' with an openurl ONLY if there is NOT fulltext avail from EBSCO.
7
+ #
8
+ # This example uses crazy metaprogramming to dynamically create
9
+ # a module configured with your base url etc. You don't need to use
10
+ # crazy method like that; just define your own local decorator doing
11
+ # exactly what you need, it's meant to be simple.
12
+ #
13
+ # config.item_decorators = [ BentoSearch::Ebscohost::ConditionalOpenurlMainLink[:base_url => "http://resolve.somewhere.edu/foo", :extra_query => "&foo=bar"] ]
14
+ #
15
+ module BentoSearch::Ebscohost::ConditionalOpenurlMainLink
16
+ def self.[](options)
17
+ base_url = options[:base_url]
18
+ extra_query = options[:extra_query] || ""
19
+ Module.new do
20
+
21
+ define_method :link do
22
+ if custom_data["fulltext_formats"]
23
+ super()
24
+ elsif (ou = to_openurl)
25
+ "#{base_url}?#{ou.kev}#{extra_query}"
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+
34
+
35
+
36
+ end
@@ -1,5 +1,6 @@
1
- # An item decorator that just erases all links, main link and other
2
- # links.
1
+ # An item decorator mix-in that just erases all links, main link and other
2
+ # links. May be convenient to `include BentoSearch::NoLinks` in your
3
+ # custom decorator, although you could always just write this yourself too.
3
4
  module BentoSearch::NoLinks
4
5
 
5
6
  def link
@@ -1,3 +1,7 @@
1
+ # DEPRECATED. Just write the logic into a custom Decorator yourself.
2
+ # See wiki on decorators.
3
+
4
+
1
5
  require 'openurl'
2
6
 
3
7
  # A Decorator that will make #to_openurl refuse to construct
@@ -1,3 +1,7 @@
1
+ # DEPRECATED. Just write the logic into a custom Decorator yourself.
2
+ # See wiki on decorators.
3
+ #
4
+ #
1
5
  # Example of an Item Decorator that ADDs an 'other link' with an openurl.
2
6
  #
3
7
  # This example uses crazy metaprogramming to dynamically create
@@ -1,3 +1,7 @@
1
+ # DEPRECATED. Just write the logic into a custom Decorator yourself.
2
+ # See wiki on decorators.
3
+
4
+
1
5
  # Example of an Item Decorator that replaces the main 'link'
2
6
  # with an openurl.
3
7
  #
@@ -0,0 +1,122 @@
1
+ module BentoSearch
2
+ class StandardDecorator < DecoratorBase
3
+
4
+
5
+ # How to display a BentoSearch::Author object as a name
6
+ def author_display(author)
7
+ if (author.first && author.last)
8
+ "#{author.last}, #{author.first.slice(0,1)}"
9
+ elsif author.display
10
+ author.display
11
+ elsif author.last
12
+ author.last
13
+ else
14
+ nil
15
+ end
16
+ end
17
+
18
+ # Put together title and subtitle if neccesary.
19
+ def complete_title
20
+ t = self.title
21
+ if self.subtitle
22
+ t = safe_join([t, ": ", self.subtitle], "")
23
+ end
24
+
25
+ if t.blank?
26
+ t = I18n.translate("bento_search.missing_title")
27
+ end
28
+
29
+ return t
30
+ end
31
+
32
+
33
+
34
+
35
+
36
+ # A simple user-displayable citation, _without_ author/title.
37
+ # the journal, year, vol, iss, page; or publisher and year; etc.
38
+ # Constructed from individual details. Not formal APA or MLA or anything,
39
+ # just a rough and ready display.
40
+ #
41
+ # TODO: Should this be moved to a rails helper method? Not sure.
42
+ def published_in
43
+ result_elements = []
44
+
45
+ result_elements.push("<span class='source_label'>#{I18n.t("bento_search.published_in")}</span><span class='source_title'>#{html_escape source_title}</span>".html_safe) unless source_title.blank?
46
+
47
+ if source_title.blank? && ! publisher.blank?
48
+ result_elements.push html_escape publisher
49
+ end
50
+
51
+ result_elements.push("#{I18n.t('bento_search.volume')} #{volume}") if volume.present?
52
+
53
+ result_elements.push("#{I18n.t('bento_search.issue')} #{issue}") if issue.present?
54
+
55
+ if (! start_page.blank?) && (! end_page.blank?)
56
+ result_elements.push html_escape "#{I18n.t('bento_search.pages')} #{start_page}-#{end_page}"
57
+ elsif ! start_page.blank?
58
+ result_elements.push html_escape "#{I18n.t('bento_search.page')} #{start_page}"
59
+ end
60
+
61
+ return nil if result_elements.empty?
62
+
63
+ return result_elements.join(", ").html_safe
64
+ end
65
+
66
+ # A display method, this is like #langauge_str, but will be nil if
67
+ # the language_code matches the current default locale, used
68
+ # for printing language only when not "English" normally.
69
+ #
70
+ #(Sorry, will be 'Spanish' never 'Espa~nol", we don't
71
+ # have a data source for language names in other languages right now. )
72
+ def display_language
73
+ return nil unless self.language_code
74
+
75
+ default = I18n.locale.try {|l| l.to_s.gsub(/\-.*$/, '')} || "en"
76
+
77
+ this_doc = self.language_obj.try(:iso_639_1)
78
+
79
+ return nil if this_doc == default
80
+
81
+ self.language_str
82
+ end
83
+
84
+ # format string to display to user. Uses #format_str if present,
85
+ # otherwise finds an i18n label from #format. Returns nil if none
86
+ # available.
87
+ def display_format
88
+ value = self.format_str ||
89
+ I18n.t(self.format, :scope => [:bento_search, :format], :default => self.format.to_s.titleize)
90
+
91
+ return value.blank? ? nil : value
92
+ end
93
+
94
+
95
+ ###################
96
+ # turn into a representative OpenURL
97
+ #
98
+ # use to_openurl_kev to go straight there,
99
+ # or to_openurl to get a ruby OpenURL object.
100
+ ###################
101
+
102
+
103
+ # Returns a ruby OpenURL::ContextObject (NISO Z39.88).
104
+ # or nil if none avail.
105
+ def to_openurl
106
+ return nil if openurl_disabled
107
+
108
+ BentoSearch::OpenurlCreator.new(self).to_openurl
109
+ end
110
+
111
+ # Returns a kev encoded openurl, that is a URL query string representing
112
+ # openurl. Or nil if none available.
113
+ #
114
+ # Right now just calls #to_openurl.kev, can conceivably
115
+ # be modified to do things more efficient, without a ruby openurl
116
+ # obj. Law of demeter, represent.
117
+ def to_openurl_kev
118
+ to_openurl.try(:kev)
119
+ end
120
+
121
+ end
122
+ end
@@ -2,7 +2,8 @@ require 'celluloid'
2
2
 
3
3
  # Based on Celluloid, concurrently runs multiple searches in
4
4
  # seperate threads. You must include 'celluloid' gem dependency
5
- # into your local app to use this class.
5
+ # into your local app to use this class. Requires celluloid 0.12.0
6
+ # or above (for new preferred async syntax).
6
7
  #
7
8
  # I am not an expert at use of Celluloid, it's possible there's a better
8
9
  # way to do this all, but seems to work.
@@ -13,7 +14,7 @@ require 'celluloid'
13
14
  # searcher = BentoBox::MultiSearcher.new(:gbs, :scopus)
14
15
  #
15
16
  # start the concurrent searches, params same as engine.search
16
- # searcher.start( query_params )
17
+ # searcher.search( query_params )
17
18
  #
18
19
  # retrieve results, blocking until each is completed:
19
20
  # searcher.results
@@ -25,7 +26,12 @@ require 'celluloid'
25
26
  #
26
27
  # important to call results at some point after calling start, in order
27
28
  # to make sure Celluloid::Actors are properly terminated to avoid
28
- # resource leakage.
29
+ # resource leakage. May want to do it in an ensure block.
30
+ #
31
+ # Note that celluloid uses multi-threading in such a way that you
32
+ # may have to set config.cache_classes=true even in development
33
+ # to avoid problems. Rails class reloading is not thread-safe.
34
+ #
29
35
  #
30
36
  # TODO: have a method that returns Futures instead of only supplying the blocking
31
37
  # results method? Several tricks, including making sure to properly terminate actors.
@@ -46,14 +52,15 @@ class BentoSearch::MultiSearcher
46
52
  end
47
53
 
48
54
  # Starts all searches, returns self so you can chain method calls if you like.
49
- def start(*search_args)
55
+ def search(*search_args)
50
56
  @engines.each do |engine|
51
57
  a = Actor.new(engine)
52
58
  @actors << a
53
- a.start! *search_args
59
+ a.async.start *search_args
54
60
  end
55
61
  return self
56
62
  end
63
+ alias_method :start, :search # backwards compat
57
64
 
58
65
  # Call after #start. Blocks until each included engine is finished
59
66
  # then returns a Hash keyed by engine registered id, value is a
@@ -87,7 +94,7 @@ class BentoSearch::MultiSearcher
87
94
  self.engine = a_engine
88
95
  end
89
96
 
90
- # call as start! with bang, to invoke async.
97
+ # call as .async.start, to invoke async.
91
98
  def start(*search_args)
92
99
  begin
93
100
  @results = self.engine.search(*search_args)
@@ -1,4 +1,5 @@
1
1
  require 'openurl'
2
+ require 'cgi'
2
3
 
3
4
  module BentoSearch
4
5
 
@@ -7,7 +8,7 @@ module BentoSearch
7
8
  # a NISO Z39.88 OpenURL context object, useful for using
8
9
  # with linking software that expects such. http://en.wikipedia.org/wiki/OpenURL
9
10
  #
10
- # co = OpenurlCreator.new( result_item ).to_open_url
11
+ # co = OpenurlCreator.new( decorated_result_item ).to_open_url
11
12
  # # => ruby OpenURL::ContextObject object.
12
13
  #
13
14
  # co.kev
@@ -20,6 +21,9 @@ module BentoSearch
20
21
 
21
22
  attr_accessor :result_item
22
23
 
24
+ # Pass in a DECORATED result_item, eg StandardDecorator.new(result_item, nil)
25
+ # Need the display logic methods in the decorator, not just a raw
26
+ # result_item.
23
27
  def initialize(ri)
24
28
  self.result_item = ri
25
29
  end
@@ -38,7 +42,13 @@ module BentoSearch
38
42
  r.set_format( self.format )
39
43
 
40
44
  if result_item.doi
41
- r.add_identifier("info:doi:#{result_item.doi}")
45
+ r.add_identifier("info:doi/#{result_item.doi}")
46
+ end
47
+
48
+ if result_item.oclcnum
49
+ r.add_identifier("info:oclcnum/#{result_item.oclcnum}")
50
+ # and do the one that's not actually legal practice, but is common
51
+ r.set_metadata("oclcnum", result_item.oclcnum)
42
52
  end
43
53
 
44
54
  r.set_metadata("genre", self.genre)
@@ -49,7 +59,13 @@ module BentoSearch
49
59
  r.set_metadata("au", result_item.author_display(ensure_no_tags result_item.authors.first))
50
60
  end
51
61
 
52
- r.set_metadata("date", result_item.year.to_s)
62
+ if result_item.publication_date
63
+ r.set_metadata("date", result_item.publication_date.iso8601)
64
+ else
65
+ r.set_metadata("date", result_item.year.to_s)
66
+ end
67
+
68
+
53
69
  r.set_metadata("volume", result_item.volume.to_s)
54
70
  r.set_metadata("issue", result_item.issue.to_s)
55
71
  r.set_metadata("spage", result_item.start_page.to_s)
@@ -59,9 +75,13 @@ module BentoSearch
59
75
  r.set_metadata("isbn", result_item.isbn)
60
76
  r.set_metadata("pub", ensure_no_tags(result_item.publisher))
61
77
 
78
+
62
79
  case result_item.format
63
80
  when "Book"
64
81
  r.set_metadata("btitle", ensure_no_tags(result_item.complete_title))
82
+ when :book_item
83
+ r.set_metadata("btitle", result_item.source_title)
84
+ r.set_metadata("atitle", result_item.title)
65
85
  when "Article", :conference_paper
66
86
  r.set_metadata("atitle", ensure_no_tags(result_item.complete_title))
67
87
  else
@@ -116,8 +136,8 @@ module BentoSearch
116
136
  # with much actual software, as a neutral default.
117
137
  def format
118
138
  case result_item.format
119
- when "Book"
120
- "book"
139
+ when "Book", :book_item
140
+ "book"
121
141
  when :dissertation
122
142
  "dissertation"
123
143
  else
@@ -51,6 +51,15 @@ module BentoSearch
51
51
  # * schema.org CreativeWork: 'url'
52
52
  attr_accessor :link
53
53
 
54
+ # does the #link correspond to fulltext? true or false -- or nil
55
+ # for unknown/non-applicable. Not all engines will set.
56
+ def link_is_fulltext?
57
+ @link_is_fulltext
58
+ end
59
+ def link_is_fulltext=(v)
60
+ @link_is_fulltext = v
61
+ end
62
+
54
63
  # normalized controlled vocab title, important this is supplied
55
64
  # if possible for OpenURL generation and other features.
56
65
  #
@@ -98,7 +107,8 @@ module BentoSearch
98
107
  # language_code.
99
108
  #
100
109
  # 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
110
+ # either or both may be nil). You can get a language_list gem obj from
111
+ # language_obj, and use to normalize to a
102
112
  # 2- or 3-letter from language_code that could be either.
103
113
  attr_accessor :language_code
104
114
  attr_writer :language_str
@@ -109,13 +119,24 @@ module BentoSearch
109
119
  end
110
120
  end
111
121
  end
122
+ # Returns a LanguageList gem language object, from #language_code,
123
+ # convenience
124
+ def language_obj
125
+ return nil unless self.language_code
126
+
127
+ @language_obj ||= LanguageList::LanguageInfo.find( self.language_code )
128
+ end
112
129
 
113
130
  # year published. a ruby int
114
131
  # PART of:.
115
132
  # * schema.org CreativeWork "datePublished", year portion
116
133
  # * dcterms.issued, year portion
117
134
  # * prism:coverDate, year portion
135
+ #
136
+ # See also publication_date when you have a complete date
118
137
  attr_accessor :year
138
+ # ruby stdlib Date object.
139
+ attr_accessor :publication_date
119
140
 
120
141
  attr_accessor :volume
121
142
  attr_accessor :issue
@@ -160,89 +181,10 @@ module BentoSearch
160
181
  # for internal use.
161
182
  attr_accessor :custom_data
162
183
 
184
+ # Copied over from engine configuration usually, a string
185
+ # qualified name of a decorator class. Can be nil for default.
186
+ attr_accessor :decorator
163
187
 
164
- # Returns a ruby OpenURL::ContextObject (NISO Z39.88).
165
- def to_openurl
166
- return nil if openurl_disabled
167
-
168
- BentoSearch::OpenurlCreator.new(self).to_openurl
169
- end
170
-
171
- ##################
172
- # Presentation related methods.
173
- # yes, it really makes sense to include them here, they can be overridden
174
- # by decorators.
175
- # May extract these to their own base decorator module at some point,
176
- # but the OO hieararchy would be basically the same either way.
177
- #######################
178
-
179
-
180
- # How to display a BentoSearch::Author object as a name
181
- def author_display(author)
182
- if (author.first && author.last)
183
- "#{author.last}, #{author.first.slice(0,1)}"
184
- elsif author.display
185
- author.display
186
- elsif author.last
187
- author.last
188
- else
189
- nil
190
- end
191
- end
192
-
193
- # Put together title and subtitle if neccesary.
194
- def complete_title
195
- t = self.title
196
- if self.subtitle
197
- t = safe_join([t, ": ", self.subtitle], "")
198
- end
199
-
200
- if t.blank?
201
- t = I18n.translate("bento_search.missing_title")
202
- end
203
-
204
- return t
205
- end
206
-
207
-
208
-
209
- # A simple user-displayable citation, _without_ author/title.
210
- # the journal, year, vol, iss, page; or publisher and year; etc.
211
- # Constructed from individual details. Not formal APA or MLA or anything,
212
- # just a rough and ready display.
213
- #
214
- # TODO: Should this be moved to a rails helper method? Not sure.
215
- def published_in
216
- result_elements = []
217
-
218
- unless year.blank?
219
- # wrap year in a span so we can bold it.
220
- result_elements.push "<span class='year'>#{year}</span>"
221
- end
222
-
223
- result_elements.push(source_title) unless source_title.blank?
224
-
225
- if source_title.blank? && ! publisher.blank?
226
- result_elements.push html_escape publisher
227
- end
228
-
229
- if (! volume.blank?) && (! issue.blank?)
230
- result_elements.push html_escape "#{volume}(#{issue})"
231
- else
232
- result_elements.push html_escape volume unless volume.blank?
233
- result_elements.push html_escape issue unless issue.blank?
234
- end
235
-
236
- if (! start_page.blank?) && (! end_page.blank?)
237
- result_elements.push html_escape "pp. #{start_page}-#{end_page}"
238
- elsif ! start_page.blank?
239
- result_elements.push html_escape "p. #{start_page}"
240
- end
241
-
242
- return nil if result_elements.empty?
243
-
244
- return result_elements.join(", ").html_safe
245
- end
246
188
 
247
189
  end
248
190
  end