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
data/README.md CHANGED
@@ -7,16 +7,46 @@ be some breaking api changes before 1.0, but probably not too many, it's
7
7
  looking pretty good).
8
8
 
9
9
  bento_search provides an abstraction/normalization layer for querying and
10
- displaying results for external search engines, in Ruby on Rails. Requires
10
+ displaying results from external search engines, in Ruby on Rails. Requires
11
11
  Rails3 and tested only under ruby 1.9.3.
12
12
 
13
- * It is focused on use cases for academic libraries, but may be useful in generic
14
- cases too. Initially, engine adapters are planned to be provided for:
15
- Google Books, Scopus, SerialSolutions Summon, Ex Libris Primo,
16
- EBSCO Discovery Service, EBSCO traditional 'EIT' api, Google Site Search. Most
17
- of these search engines require a vendor license to use.
13
+ ### Goals: To help you
18
14
 
19
- * bento_search could be considered building blocks for a type of 'federated
15
+ * **Get up and running as quickly as possible** with searching and displaying
16
+ results from a third-party service. Solutions to idiosyncracies and
17
+ undocumented workarounds are encoded in a shared codebase, which abstracts
18
+ everything to a good, simple code API giving you building blocks to focus
19
+ on your needs, not the search service's problems.
20
+ * Let you switch out one search service for another in an already built
21
+ application with as little code rewriting as possible. **Avoid vendor lock-in**.
22
+ * Give you the harness to **write adapters for new search services**, without
23
+ having to rewrite common general functionality, just focus on the interface
24
+ with the new API you want to support.
25
+
26
+ bento_search is focused on use cases for academic libraries, which is mainly
27
+ evidenced by the search engine adapters currently included, and by the
28
+ generalized domain models including fields that matter in our domain (issn,
29
+ vol/issue/page, etc), and some targetted functionality (OpenURL generation).
30
+ But it ought to be useful for more general basic use
31
+ cases too (we include a google site search adapter for instance).
32
+
33
+ Adapters currently included in bento_search
34
+
35
+ * Google Books (requires free api key)
36
+ * Scopus (requires license)
37
+ * Serial Solution Summon (requires license)
38
+ * Ex Libris Primo (requires license)
39
+ * EBSCO Discovery Service (requires license)
40
+ * EBSCOHost 'traditional' API (requires license)
41
+ * WorldCat Search (requires OCLC membership to get api key)
42
+ * Google Site Search (requires sign-up for more than 100 searches/day)
43
+
44
+
45
+
46
+
47
+ ### Scope of functionality
48
+
49
+ bento_search could be considered building blocks for a type of 'federated
20
50
  search' functionality, but it does not and will never support merging results
21
51
  from multiple engines into one result set. It is meant to support displaying the
22
52
  first few results from multiple engines on one page, "bento box" style (as
@@ -24,18 +54,18 @@ named by Tito Sierra@NCSU), as well as more expanded single-search-on-a-page
24
54
  uses.
25
55
 
26
56
  * bento_search provides abstract functionality for pagination, sorting,
27
- and single-field-specified queries. Faceting, limiting, and 'advanced'
28
- multi-field searches are not yet supported, but planned. Not all
29
- search engine adapters support all features. Search engine adapters can
57
+ and single-field-specified queries. Faceting, generalized limiting, and 'advanced'
58
+ multi-field searches are not yet supported, but possibly will be built
59
+ out in the future.
60
+
61
+ Not all search engine adapters support all features. Some engines offer
62
+ engine-specific features, such as limiting. Search engine adapters can
30
63
  declare search fields and sort options with 'semantics', so you can for
31
64
  instance search or sort by 'title' across search engines without regard
32
65
  to internal engine-specific field names.
33
66
 
34
67
  bento_search is designed to allow code to be written agnostic of the search
35
- provider, so you can switch out the search provider, minimizing dependent
36
- code in your app that needs to be rewritten. As well as letting you get
37
- started quick without reinventing the wheel and figuring out poorly
38
- documented vendor API's yourself.
68
+ provider, so you can switch out the search provider.
39
69
 
40
70
  See code-level api documentation for more details, especially at
41
71
  BentoSearch::SearchEngine. http://rubydoc.info/gems/bento_search/frames/
@@ -53,9 +83,11 @@ are a few standard keys (see BentoSearch::SearchEngine), and others that
53
83
  may be engine-specific. Some engine-specific keys (such as api auth keys)
54
84
  may be required for certain engines.
55
85
 
86
+ ~~~~ruby
56
87
  engine = BentoSearch::GoogleBooksEngine.new(:api_key => "my_gbs_api_key")
57
88
  results = engine.search("a query")
58
-
89
+ ~~~~
90
+
59
91
  `results` are a BentoSearch::Results object, which acts like an array of
60
92
  BentoSearch::Item objects, along with some meta-information about the
61
93
  search itself (pagination keys, etc). BentoSearch::Results and Item fields
@@ -72,16 +104,20 @@ required for certain functionality (like out-of-the-box AJAX loading).
72
104
 
73
105
  In an initializer in your app, like say `./config/initializers/bento_search.rb`:
74
106
 
107
+ ~~~~ruby
75
108
  BentoSearch.register_engine("gbs") do |conf|
76
109
  conf.engine = "BentoSearch::GoogleBooksEngine"
77
110
  conf.api_key = "my_google_api_key"
78
111
  # any other configuration
79
112
  end
113
+ ~~~~
80
114
 
81
115
  Then you can refer to it, for instance in a controller, by the id you registered:
82
116
 
117
+ ~~~~ruby
83
118
  @results = BentoSearch.get_engine("gbs").search("my query")
84
-
119
+ ~~~~
120
+
85
121
  ### Display results
86
122
 
87
123
  You can of course write your own code to display a BentoSearch::Results object
@@ -89,47 +125,70 @@ however you like. But BentoSearch comes with a helper method for displaying
89
125
  a list of BentoSearch::Results in a standard way, using the bento_search
90
126
  helper method.
91
127
 
92
- <%= bento_search(@results) %>
128
+ ~~~~ruby
129
+ <%= bento_search @results %>
130
+ ~~~~
131
+
132
+ See also the [Customizing Results Display wiki page](https://github.com/jrochkind/bento_search/wiki/Customizing-Results-Display).
93
133
 
94
134
  ### Fielded searching.
95
135
 
96
136
  You can search by an internal engine-specific field name:
97
137
 
138
+ ~~~~ruby
98
139
  google_books_engine.search("smith", :search_field => "inauthor")
99
-
140
+ ~~~~
141
+
100
142
  Or, if the engine provides it, you can search by normalized semantic search
101
143
  field type names:
102
144
 
103
- google_books_engine.search("smith", :semantic_earch_field => :title)
104
-
105
- This will raise if an engine doesn't support that semantic search field.
145
+ ~~~~ruby
146
+ google_books_engine.search("smith", :semantic_search_field => :title)
147
+ ~~~~
148
+
106
149
  You can find out what fields a particular engine supports.
107
150
 
151
+ ~~~~ruby
108
152
  google_books_engine.search_keys # => internal keys
109
153
  google_books_engine.semantic_search_keys
154
+ ~~~~
155
+
156
+ A helper method for generating an html select of search field options is
157
+ available in `bento_field_hash_for`, check it out.
110
158
 
111
159
  You can also provide all arguments in a single hash when it's convenient
112
160
  to do so:
113
161
 
162
+ ~~~~ruby
114
163
  google_books_engine.search(:query => "smith", :search_field => "inauthor")
164
+ ~~~~
165
+
166
+ Search fields that are not recognized (semantic or internal) will normally
167
+ be ignored, but set `:unrecognized_search_field => :raise` in configuration
168
+ or search arg to get an ArgumentError instead.
115
169
 
116
170
  ### Sorting
117
171
 
118
172
  An engine advertises what sort types it supports:
119
173
 
174
+ ~~~~ruby
120
175
  google_books_engine.sort_keys
121
-
176
+ ~~~~
177
+
122
178
  An array of sort identifiers, where possible
123
- chosen from a standard list of semantics. (See list in config/i18n/en.yml,
124
- bento_search.sort_keys).
179
+ chosen from a standard list of semantics. (See list in `./config/i18n/en.yml`,
180
+ `bento_search.sort_keys`).
125
181
 
182
+ ~~~~ruby
126
183
  google_books_engine.search("my query", :sort => "date_desc")
127
-
128
- For help creating your UI, you can use built-in helper method:
184
+ ~~~~
129
185
 
130
- bento_sort_hash_for(engine)
131
- #=> returns a Hash suitable as first argument for rails
132
- # options_for_select helper, with sort options and labels from I18n.
186
+ For help creating your UI, you can use built-in helper method, perhaps with Rails helper
187
+ options_for_select:
188
+
189
+ ~~~~ruby
190
+ <%= options_for_select( bento_sort_hash_for(engine), params[:sort] ) %>
191
+ ~~~~
133
192
 
134
193
 
135
194
  ### Pagination
@@ -138,13 +197,17 @@ You can tell the search engine how many items you want per-page, and
138
197
  use _either_ `:start` (0-based item offset) or `:page` (1-based page
139
198
  offset) keys to paginate into the results.
140
199
 
200
+ ~~~~ruby
141
201
  results = google_books_engine.search("my query", :per_page => 20, :start => 40)
142
202
  results = google_books_engine.search("my query", :per_page => 20, :page => 2) # means same as above
143
-
203
+ ~~~~
204
+
144
205
  An engine instance advertises it's maximum per-page values.
145
206
 
207
+ ~~~~ruby
146
208
  google_books_engine.max_per_page
147
-
209
+ ~~~~
210
+
148
211
  bento_search fixes the default per_page at 10.
149
212
 
150
213
  For help creating your UI, you can ask a BentoSearch::Results for
@@ -153,8 +216,10 @@ object which should be suitable for passing to [kaminari](https://github.com/ama
153
216
  `paginate`, or else have convenient methods for roll your own pagination UI.
154
217
  Kaminari's paginate method:
155
218
 
219
+ ~~~~ruby
156
220
  <%= paginate results.pagination %>
157
-
221
+ ~~~~
222
+
158
223
  ### Concurrent searching
159
224
 
160
225
  If you're going to search 2 or more search engines at once, you'll want to execute
@@ -172,8 +237,8 @@ to help you do this easily. Say, in a controller:
172
237
  # constructor takes id's registered with BentoSearch.register_engine
173
238
  searcher = BentoSearch::MultiSearcher.new(:gbs, :scopus, :summon)
174
239
 
175
- # Call 'start' with any parameters you would give to an_engine.search
176
- searcher.start("my query", :semantic_search_field => :author, :sort => "title")
240
+ # Call 'search' with any parameters you would give to an_engine.search
241
+ searcher.search("my query", :semantic_search_field => :author, :sort => "title")
177
242
 
178
243
  # At this point, all searches are executing asynchronously in seperate threads.
179
244
  # To get the results, blocking until all complete:
@@ -189,8 +254,10 @@ in the main thread (like search a local store of some kind outside of
189
254
  bento_search)
190
255
 
191
256
  You will need to add the 'celluloid' gem to your app to use this feature,
192
- BentoSearch doesn't automatically include the celluloid dependency right now
193
- (should it?).
257
+ BentoSearch doesn't automatically include the celluloid dependency. Note
258
+ that Celluloid uses multi-threading in such a way that you might need
259
+ to turn Rails config.cache_classes=true even in development.
260
+
194
261
 
195
262
  For more info, see BentoSearch::MultiSearcher.
196
263
 
@@ -199,38 +266,16 @@ For more info, see BentoSearch::MultiSearcher.
199
266
  BentoSearch provides some basic support for initially displaying a placeholder
200
267
  progress spinner, and having Javascript call back to get the actual results.
201
268
 
202
- * **Setup Pre-requisites**
203
- * In your `./config/routes.rb`, you need `BentoSearch::Routes.new(self).draw` in order
204
- to route to the ajax loader.
205
- * In your asset pipeline, you must have `//= require 'bento_search/ajax_load`
206
- to get JS for ajax loading. (or require 'bento_search' to get all bento_search JS)
207
- * **Note** that this is not a panacea for a very slow search engine -- if the
208
- search results take 20 seconds to come in, when the AJAX call back happens,
209
- your Rails process _will_ be blocked from serving any other requests for that 20
210
- seconds. In fact, that makes this feature of very limited applicability in general,
211
- think carefully about what this will do for you.
212
- * **Beware** that there are some authorization considerations if your search
213
- engine is not publically configurable, see BentoSearch::SearchController
214
- for more details.
215
-
216
- You have have registered a configured engine globally, and given it the special
217
- `:allow_routable_results` key.
218
-
219
- BentoSearch.register_engine("gbs") do |conf|
220
- conf.api_key = "x"
221
- conf.allow_routable_results = true
222
- end
223
-
224
- Now you can use the `bento_search` helper method with the registered id
225
- and query, instead of with results as before, and with an option for
226
- ajax auto-load.
227
-
228
- <%= bento_search("gbs", :query => "my query",
229
- :semantic_search_field => :title,
230
- :load => :ajax_auto) %>
231
-
269
+ It's not a panacea for pathologically slow search results, and can be tricky
270
+ for results that need access controls. But it can be useful
271
+ in some situations, both for automatic on-page-load ajax loading, and triggered
272
+ ajax loading.
232
273
 
274
+ See the [wiki page](https://github.com/jrochkind/bento_search/wiki/AJAX-results-loading)
275
+ for more info.
233
276
 
277
+
278
+
234
279
  ### Item Decorators, and Links
235
280
 
236
281
  You can configure Decorators, in the form of plain old ruby modules, to be
@@ -244,15 +289,27 @@ link resolver.
244
289
  BentoSearch::Items can have a main link associated with them (generally
245
290
  hyperlinked from title), as well as a list of additional links. Most engines
246
291
  do not provide additional links by default, custom local Decorators would
247
- be used to add them.
292
+ be used to add them. See wiki for more info on decorators, and BentoSearch::Link
293
+ for fields.
294
+
295
+ ## OpenURL and metadata
248
296
 
249
- BentoSearch.register_engine("something") do |conf|
250
- conf.engine = SomeEngine
251
- conf.item_decorators = [ SomeModule, OtherModule]
252
- end
297
+ Academic library uses often need openurl links from scholarly citations. One of
298
+ the design goals of bento_search is to produce standardized normalized BentoSearch::ResultItem
299
+ models, with sufficient semantics for translation to other formats.
300
+
301
+ See ResultItem#to_openurl_kev (string URL query encoding of OpenURL), and
302
+ ResultItem#to_openurl (a [ruby OpenURL gem](https://github.com/openurl/openurl) object).
253
303
 
254
- See BentoSearch::Link for more info on links. (TODO: Better docs/examples
255
- on decorators).
304
+ Quality may vary, depending on how well the particular engine adapter captures semantics,
305
+ especially the format/type of results (See bento_search's internal format/type vocabulary
306
+ documented at ResultItem#format). As well as how well the #to_openurl routine
307
+ handles all edge cases (OpenURL can be weird). As edge cases are discovered, they
308
+ can be solved.
309
+
310
+ See `./app/item_decorators/bento_search/openurl_add_other_link.rb` for an example
311
+ of using item decorators to add a link to your openurl resover to an item when
312
+ displayed.
256
313
 
257
314
  ## Planned Features
258
315
 
@@ -267,7 +324,6 @@ Probably:
267
324
  also may be supported by some engines that do not support facetting).
268
325
  * Support for multi-field, multi-entry-box 'advanced search' UI's, in
269
326
  a normalized cross-engine way.
270
- * More mindless support for displaying pagination UI with kaminari.
271
327
 
272
328
  Other needs or suggestions?
273
329
 
@@ -304,3 +360,4 @@ welcome. See more info on writing a BentoSearch::SearchEngine in the inline
304
360
  docs in that file.
305
361
 
306
362
 
363
+
@@ -4,7 +4,7 @@ var BentoSearch = BentoSearch || {}
4
4
  // Will AJAX load bento search results inside that node.
5
5
  // optional second arg success callback function.
6
6
  BentoSearch.ajax_load = function(node, success_callback) {
7
- div = $(node);
7
+ var div = $(node);
8
8
 
9
9
  if (div.length == 0) {
10
10
  //we've got nothing
@@ -22,11 +22,19 @@ BentoSearch.ajax_load = function(node, success_callback) {
22
22
  // Now load the actual external content from html5 data-bento-ajax-url
23
23
  $.ajax({
24
24
  url: div.data("bentoAjaxUrl"),
25
- success: function(response, status, xhr) {
25
+ success: function(response, status, xhr) {
26
+ var do_replace = true;
26
27
  if (success_callback) {
27
- success_callback.apply(div, response);
28
+ // We need to make the response into a DOM so the callback
29
+ // can deal with it better. Wrapped in a div, so it makes
30
+ // jquery happy even if there isn't a single parent element.
31
+ response = $("<div>" + response + "</div>");
32
+
33
+ do_replace = success_callback.apply(div, [response]);
28
34
  }
29
- div.replaceWith(response);
35
+ if (do_replace != false) {
36
+ div.replaceWith(response);
37
+ }
30
38
  },
31
39
  error: function(xhr, status, errorThrown) {
32
40
  var msg = "Sorry but there was an error: ";
@@ -3,10 +3,6 @@
3
3
  or just use this as documentation for suggestions to implement yourself. */
4
4
 
5
5
 
6
- /* year in bold in citations */
7
- .bento_item_row.published_in .year {
8
- font-weight: bold;
9
- }
10
6
 
11
7
  /* highlighted element in title is already in a <b> tag, but
12
8
  title is likely to be bold already. italisize it too. */
@@ -29,3 +25,7 @@
29
25
  .bento_item_title a .bento_search_highlight {
30
26
  font-style: inherit;
31
27
  }
28
+
29
+ .bento_item_body .source_title {
30
+ font-style: italic;
31
+ }
@@ -1,3 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'nokogiri'
4
+
1
5
  # Rails helper module provided by BentoSearch meant to be included
2
6
  # in host app's helpers.
3
7
  module BentoSearchHelper
@@ -25,6 +29,7 @@ module BentoSearchHelper
25
29
  # bento_results("google_books", :query => "cancer", :load => :ajax_auto)
26
30
  #
27
31
  def bento_search(search_arg, options = {})
32
+
28
33
  results = search_arg if search_arg.kind_of? BentoSearch::Results
29
34
 
30
35
  load_mode = options.delete(:load)
@@ -35,6 +40,7 @@ module BentoSearchHelper
35
40
  engine = (if search_arg.kind_of? BentoSearch::SearchEngine
36
41
  search_arg
37
42
  else
43
+ raise ArgumentError.new("Need Results, engine, or registered engine_id as first argument to #bento_search") unless search_arg
38
44
  BentoSearch.get_engine(search_arg.to_s)
39
45
  end)
40
46
 
@@ -66,16 +72,32 @@ module BentoSearchHelper
66
72
  results = engine.search(options) unless results
67
73
 
68
74
  if results.failed?
69
- render :partial => "bento_search/search_error", :locals => {:results => results}
70
- elsif results.length > 0
71
- render :partial => "bento_search/std_item", :collection => results, :as => :item
75
+ partial = (results.display_configuration.error_partial if results.display_configuration) || "bento_search/search_error"
76
+ render :partial => partial, :locals => {:results => results}
77
+ elsif results.length > 0
78
+ partial = (results.display_configuration.item_partial if results.display_configuration) || "bento_search/std_item"
79
+ render :partial => partial, :collection => results, :as => :item, :locals => {:results => results}
72
80
  else
73
81
  content_tag(:div, :class=> "bento_search_no_results") do
74
- I18n.translate("bento_search.no_results")
82
+ partial = (results.display_configuration.no_results_partial if results.display_configuration) || "bento_search/no_results"
83
+ render :partial => partial, :locals => {:results => results}
75
84
  end
76
85
  end
77
86
  end
78
87
  end
88
+
89
+ # Wrap a ResultItem in a decorator! For now hard-coded to
90
+ # BentoSearch::StandardDecorator
91
+ def bento_decorate(result_item)
92
+ # What decorator class? If specified as string in #decorator,
93
+ # look it up as a class object, else default.
94
+ decorator_class = result_item.decorator.try {|name| BentoSearch::Util.constantize(name) } || BentoSearch::StandardDecorator
95
+
96
+ # in a helper method, 'self' is a view_context already I think?
97
+ decorated = decorator_class.new(result_item, self)
98
+ yield(decorated) if block_given?
99
+ return decorated
100
+ end
79
101
 
80
102
 
81
103
  ##
@@ -89,33 +111,53 @@ module BentoSearchHelper
89
111
  #
90
112
  ##
91
113
 
92
- def bento_abstract_truncate(str)
93
- # if it's html safe, we can't truncate it, we don't have an HTML-aware
94
- # truncation routine right now, that avoids leaving tags open etc.
95
- return str if str.html_safe?
114
+ # Like rails truncate helper, and taking the same options, but html_safe.
115
+ #
116
+ # If input string is NOT marked html_safe?, simply passes to rails truncate helper.
117
+ # If a string IS marked html_safe?, uses nokogiri to parse it, and truncate
118
+ # actual displayed text to max_length, while keeping html structure valid.
119
+ #
120
+ # Default omission marker is unicode elipsis
121
+ #
122
+ # :length option will also default to 280, what we think is a good
123
+ # length for abstract/snippet display
124
+ def bento_truncate(str, options = {})
125
+ options.reverse_merge!(:omission => "…", :length => 280, :separator => ' ')
126
+
127
+ # works for non-html of course, but for html a quick check
128
+ # to avoid expensive nokogiri parse if the whole string, even
129
+ # with tags, is still less than max length.
130
+ return str if str.length < options[:length]
96
131
 
97
- truncate(str, :length => 280, :separator => " ")
132
+ if str.html_safe?
133
+ noko = Nokogiri::HTML::DocumentFragment.parse(str)
134
+ BentoSearch::Util.nokogiri_truncate(noko, options[:length], options[:omission], options[:separator]).inner_html.html_safe
135
+ else
136
+ return truncate(str, options)
137
+ end
98
138
  end
139
+
140
+
99
141
 
100
- # Prepare a title in an H4, with formats in parens in a <small> (for
101
- # bootstrap), linked, etc.
142
+ # Deprecated, made more sense to put this in a partial. Just
143
+ # call partial directly:
144
+ # <%= render :partial => "bento_search/item_title", :object => item, :as => 'item' %>
102
145
  def bento_item_title(item)
103
- content_tag("h4", :class => "bento_item_title") do
104
- safe_join([
105
- link_to_unless( item.link.blank?, item.complete_title, item.link ),
106
- if item.format.present? || item.format_str.present?
107
- content_tag("small", :class => "bento_item_about") do
108
- " (" +
109
- if item.format_str
110
- item.format_str
111
- else
112
- t(item.format, :scope => [:bento_search, :format], :default => item.format.to_s.titleize)
113
- end + ")"
114
- end
115
- end
116
- ], '')
117
- end
146
+ render :partial => "bento_search/item_title", :object => item, :as => 'item'
118
147
  end
148
+ deprecate :bento_item_title
149
+
150
+ # pass in 0-based rails current collection counter and a BentoSearch::Results,
151
+ # calculates a user-displayable result set index label.
152
+ #
153
+ # Only non-trivial thing is both inputs are allowed to be nil; if either
154
+ # is nil, nil is returned.
155
+ def bento_item_counter(counter, results)
156
+ return nil if counter.nil? || results.nil? || results.start.nil?
157
+
158
+ return counter + results.start + 1
159
+ end
160
+
119
161
 
120
162
  # first 3 authors, each in a <span>, using item.author_display, seperated by
121
163
  # semi-colons.
@@ -125,7 +167,7 @@ module BentoSearchHelper
125
167
  first_three = item.authors.slice(0,3)
126
168
 
127
169
  first_three.each_with_index do |author, index|
128
- parts << content_tag("span", :class => "bento_item_author") do
170
+ parts << content_tag("span", :class => "authors") do
129
171
  item.author_display(author)
130
172
  end
131
173
  if (index + 1) < first_three.length
@@ -133,6 +175,10 @@ module BentoSearchHelper
133
175
  end
134
176
  end
135
177
 
178
+ if item.authors.length > 3
179
+ parts << I18n.t("bento_search.authors_et_al")
180
+ end
181
+
136
182
  return safe_join(parts, "")
137
183
  end
138
184
 
@@ -150,5 +196,46 @@ module BentoSearchHelper
150
196
  ]
151
197
  end
152
198
 
199
+ # Returns a hash of label => key suitable for passing to rails
200
+ # options_for_select. ONLY includes fields with :semantics set at
201
+ # present. Key will be _semantic_ key name.
202
+ # For engine-specific fields, you're on you're own, sorry!
203
+ #
204
+ # If first arg is an engine instance, will
205
+ # be search fields supported by that engine. If first arg is nil,
206
+ # will be any field in our i18n lists for search fields.
207
+ #
208
+ # Can pass in options :only or :except to customize list. Values
209
+ # in :only and :except will match on internal field names OR
210
+ # semantic field names (hopefully this convenience ambiguity
211
+ # won't cause problems)
212
+ #
213
+ # eg:
214
+ # <%= select_tag 'search_field', options_for_select(bento_field_hash_for(@engine), params[:search_field]) %>
215
+ def bento_field_hash_for(engine, options = {})
216
+ if engine.nil?
217
+ hash = I18n.t("bento_search.search_fields").invert
218
+ else
219
+ hash = Hash[ engine.search_field_definitions.collect do |k, defn|
220
+ if defn[:semantic] && (label = I18n.t(defn[:semantic], :scope => "bento_search.search_fields", :default => defn[:semantic].to_s.titlecase ))
221
+ [label, defn[:semantic].to_s]
222
+ end
223
+ end]
224
+ end
225
+
226
+ # :only/:except
227
+ if options[:only]
228
+ keys = [options[:only]].flatten.collect(&:to_s)
229
+ hash.delete_if {|key, value| ! keys.include?(value) }
230
+ end
231
+
232
+ if options[:except]
233
+ keys = [options[:except]].flatten.collect(&:to_s)
234
+ hash.delete_if {|key, value| keys.include?(value) }
235
+ end
236
+
237
+ return hash
238
+ end
239
+
153
240
 
154
241
  end