umlaut 3.0.0beta4 → 3.0.0beta5

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.
@@ -0,0 +1,8 @@
1
+ /* simple_visible_toggle.js. Used for toggling visibility of error information. Can possibly be combined with more powerful expand_contract_toggle.js */
2
+ jQuery(document).ready(function($) {
3
+
4
+ $("a.simple_visible_toggle").live("click", function() {
5
+ $(this).next().toggle();
6
+ });
7
+
8
+ });
@@ -0,0 +1,181 @@
1
+ /* update_html.js: Provide functions to update content on page with background responses from Umlaut. Used by Umlaut itself, as well as by third party callers.*/
2
+ (function($) {
3
+
4
+ function SectionTarget(config) {
5
+ //Add properties from config to ourself
6
+ $.extend(this, config);
7
+
8
+ //Defaults
9
+ if (this.selector == undefined)
10
+ this.selector = "#" + this.umlaut_section_id;
11
+ if (this.position == undefined)
12
+ this.position = "html";
13
+
14
+ }
15
+ //Callback default to no-op function please.
16
+ var noop = function() {};
17
+ SectionTarget.prototype.before_update = noop;
18
+ SectionTarget.prototype.after_update = noop;
19
+ SectionTarget.prototype.complete = noop;
20
+
21
+ SectionTarget.prototype.ensure_placement_destination = function() {
22
+ if ( this.selector == undefined) {
23
+ return null;
24
+ }
25
+
26
+ //Already have it cached?
27
+ if ( this.host_div_element ) {
28
+ return this.host_div_element;
29
+ }
30
+
31
+ var new_div = $('<div class="umlaut" style="display:none"></div>');
32
+ // Find the first thing matched by selector, and call the
33
+ // method specified in "action" string on it, giving it our
34
+ // HTML to replace. This works because our actions are
35
+ // all arguments that will take one method: html, before, after, append,
36
+ // prepend.
37
+ $(this.selector).eq(0)[ this.position ]( new_div );
38
+
39
+ //Cache for later
40
+ this.host_div_element = new_div;
41
+ return this.host_div_element;
42
+ };
43
+
44
+
45
+ // Define an object constructor on the global window object
46
+ // For our UmlautHtmlUpdater object.
47
+ function HtmlUpdater(umlaut_base, context_object) {
48
+ if (context_object == undefined)
49
+ context_object = "";
50
+
51
+ umlaut_base = umlaut_base.replace(/\/$/,'');
52
+ this.umlaut_uri = umlaut_base + '/resolve/partial_html_sections?umlaut.response_format=json&' + context_object;
53
+
54
+ this.section_targets = [];
55
+
56
+ this.add_section_target = function(config) {
57
+ this.section_targets.push( new SectionTarget(config) );
58
+ };
59
+
60
+ //default no-op call-backs
61
+ this.complete = noop;
62
+ this.before_update = noop;
63
+ this.after_update = noop;
64
+
65
+
66
+ //Code for seeing if a URI is same origin or not borrowed from jQuery
67
+ this.is_remote_url = function(url) {
68
+ var regexp = /^(\w+:)?\/\/([^\/?#]+)/;
69
+ var parts = regexp.exec( url );
70
+ return (parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host));
71
+ }
72
+
73
+ this.update = function() {
74
+ // Need to capture because we won't have 'this' inside the ajax
75
+ // success handler.
76
+ var myself = this;
77
+ var dataType = this.is_remote_url( this.umlaut_uri ) ? "jsonp" : "json";
78
+ $.ajax({
79
+ url: myself.umlaut_uri,
80
+ dataType: dataType,
81
+ jsonp: "umlaut.jsonp",
82
+ error: function() {
83
+ $.error("Problem loading background elements.");
84
+ },
85
+ success: function(umlaut_response) {
86
+ for (var i = 0; i < myself.section_targets.length; i++) {
87
+ var section_target = myself.section_targets[i];
88
+
89
+ var umlaut_html_section = myself.find_umlaut_response_section(umlaut_response, section_target.umlaut_section_id);
90
+
91
+ if (umlaut_html_section == undefined) {
92
+ continue;
93
+ }
94
+ var count = null;
95
+ if (typeof umlaut_html_section.response_count != "undefined") {
96
+ count = parseInt(umlaut_html_section.response_count.value);
97
+ }
98
+ var existing_element = section_target.ensure_placement_destination();
99
+ var new_element = $('<div class="umlaut" style="display:none" class="' + section_target.umlaut_section_id +'"></div>');
100
+ new_element.html(umlaut_html_section.html_content);
101
+
102
+
103
+ var should_continue = section_target.before_update(new_element, count, section_target);
104
+ if (should_continue != false) {
105
+ should_continue = myself.before_update(new_element, count, section_target);
106
+ }
107
+
108
+ if (should_continue != false) {
109
+ existing_element.replaceWith(new_element);
110
+
111
+ section_target.host_div_element = new_element;
112
+
113
+ new_element.show();
114
+
115
+ section_target.after_update(new_element, count, section_target);
116
+ myself.after_update(new_element, count, section_target);
117
+
118
+ }
119
+ }
120
+
121
+ //Do we need to update again?
122
+ if (umlaut_response.partial_html_sections.in_progress) {
123
+ //Fix our update URI to be the one umlaut suggests
124
+ //Except strip out the umlaut.jsonp parameter, jquery is
125
+ //going to add that back in as desired.
126
+ myself.umlaut_uri =
127
+ umlaut_response.partial_html_sections.in_progress.refresh_url.replace(/[?;&]umlaut\.jsonp=[^;&]+/, '');
128
+
129
+
130
+ var refresh_seconds =
131
+ umlaut_response.partial_html_sections.in_progress.requested_wait_seconds;
132
+ window.setTimeout(function() { myself.update(); }, refresh_seconds * 1000);
133
+
134
+ } else {
135
+ myself.complete();
136
+ for (var i = 0; i < myself.section_targets.length; i++) {
137
+ var section_target = myself.section_targets[i];
138
+ section_target.complete(section_target);
139
+ }
140
+ }
141
+
142
+ }
143
+ });
144
+ };
145
+ this.find_umlaut_response_section = function(response, id) {
146
+ return $.grep(response.partial_html_sections.html_section, function(section) {
147
+ return section.id == id;
148
+ })[0];
149
+ };
150
+
151
+ };
152
+
153
+ //Put it in a global object, leave space for other things in "Umlaut" later.
154
+ if (window.Umlaut == undefined)
155
+ window.Umlaut = new Object();
156
+ window.Umlaut.HtmlUpdater = HtmlUpdater;
157
+
158
+ /* LEGACY Loader was recommended for loading Umlaut JS behaviors
159
+ in an external page, for JQuery Content Utility.
160
+
161
+ var loader = new Umlaut.Loader();
162
+ loader.load();
163
+
164
+ We will provide just enough code to keep that from
165
+ error'ing (and halting js execution), although at present it does not
166
+ actually load the JS behaviors using new style, app wont' have
167
+ JS behaviors. */
168
+
169
+ window.Umlaut.Loader = function() {
170
+ this.load = function(option_list) {
171
+ // log problem in browsers that support it.
172
+ if (typeof console != "undefined" && typeof console.log != "undefined") {
173
+ console.log("WARN: Umlaut.Loader no longer supported in Umlaut 3.x, you may have not loaded Umlaut JS Behaviors as desired. See Umlaut documentation for new way.");
174
+
175
+ }
176
+ }
177
+ }
178
+
179
+
180
+ })(jQuery);
181
+
@@ -1,19 +1,27 @@
1
- /* expand_contract_toggle.js: Support for show more/hide more in lists of umlaut content. */
1
+ /* expand_contract_toggle.js: Support for show more/hide more in lists of umlaut content.
2
+
3
+ The JS needs to swap out the image for expand/contract toggle. AND we need
4
+ the URL it swaps in to be an ABSOLUTE url when we're using partial html
5
+ widget.
6
+
7
+ So we swap in a non-fingerprinted URL, even if the original was asset
8
+ pipline fingerprinted. sorry, best way to make it work!
9
+ */
2
10
  jQuery(document).ready(function($) {
3
11
 
4
12
  $(".expand_contract_toggle").live("click", function() {
5
13
  var content = $(this).next(".expand_contract_content");
6
14
  var icon = $(this).parent().find('img.toggle_icon');
7
15
 
8
- if (content.is(":visible")) {
9
- icon.attr("src", '<%= image_path("list_closed.png") %>');
16
+ if (content.is(":visible")) {
17
+ icon.attr("src", icon.attr("src").replace(/list_open[^.]*\.png/, "list_closed.png"));
10
18
  $(this).find(".expand_contract_action_label").text("Show ");
11
19
 
12
20
  content.hide();
13
21
 
14
22
  }
15
23
  else {
16
- icon.attr("src", '<%= image_path("list_open.png") %>');
24
+ icon.attr("src", icon.attr("src").replace(/list_closed[^.]*\.png/, "list_open.png"));
17
25
  $(this).find(".expand_contract_action_label").text("Hide ");
18
26
  content.show();
19
27
  }
@@ -58,7 +58,10 @@
58
58
  };
59
59
 
60
60
  //default no-op call-backs
61
- this.complete = function() {};
61
+ this.complete = noop;
62
+ this.before_update = noop;
63
+ this.after_update = noop;
64
+
62
65
 
63
66
  //Code for seeing if a URI is same origin or not borrowed from jQuery
64
67
  this.is_remote_url = function(url) {
@@ -98,6 +101,9 @@
98
101
 
99
102
 
100
103
  var should_continue = section_target.before_update(new_element, count, section_target);
104
+ if (should_continue != false) {
105
+ should_continue = myself.before_update(new_element, count, section_target);
106
+ }
101
107
 
102
108
  if (should_continue != false) {
103
109
  existing_element.replaceWith(new_element);
@@ -106,7 +112,8 @@
106
112
 
107
113
  new_element.show();
108
114
 
109
- section_target.after_update(new_element, count, section_target)
115
+ section_target.after_update(new_element, count, section_target);
116
+ myself.after_update(new_element, count, section_target);
110
117
 
111
118
  }
112
119
  }
@@ -163,7 +170,8 @@
163
170
  this.load = function(option_list) {
164
171
  // log problem in browsers that support it.
165
172
  if (typeof console != "undefined" && typeof console.log != "undefined") {
166
- console.log("WARN: Umlaut.Loader no longer supported in Umlaut 3.x, you have not loaded Umlaut JS Behaviors. See Umlaut documentation for new way.");
173
+ console.log("WARN: Umlaut.Loader no longer supported in Umlaut 3.x, you may have not loaded Umlaut JS Behaviors as desired. See Umlaut documentation for new way.");
174
+
167
175
  }
168
176
  }
169
177
  }
@@ -158,9 +158,7 @@ module ResolveHelper
158
158
  # <li>Item Number: <%= index %>: <%= item.title %></li>
159
159
  # <% end %>
160
160
  def list_with_limit(id, list, options = {}, &block)
161
-
162
-
163
-
161
+
164
162
  # backwards compatible to when third argument was just a number
165
163
  # for limit.
166
164
  options = {:limit => options} unless options.kind_of?(Hash)
@@ -172,17 +170,17 @@ module ResolveHelper
172
170
  content = "".html_safe
173
171
  content <<
174
172
  content_tag(:ul, :class => options[:ul_class]) do
175
- list.enum_for(:each_with_index).collect do |item, index|
176
- capture(item, index, &block) unless list.length > options[:limit] && index >= options[:limit]-2
173
+ list.slice(0, options[:limit]).enum_for(:each_with_index).collect do |item, index|
174
+ yield(item, index)
177
175
  end.join(" \n ").html_safe
178
176
  end
179
177
 
180
178
  if (list.length > options[:limit] )
181
179
  content <<
182
- expand_contract_section("#{list.length - options[:limit] + 1} more", id) do
180
+ expand_contract_section("#{list.length - options[:limit] } more", id) do
183
181
  content_tag(:ul, :class=>options[:ul_class]) do
184
- list.slice(options[:limit]-1..list.length-1).enum_for(:each_with_index).each do |item, index|
185
- yield(item, index)
182
+ list.slice(options[:limit]..list.length-1).enum_for(:each_with_index).each do |item, index|
183
+ yield(item, index + options[:limit])
186
184
  end.join(" \n ").html_safe
187
185
  end
188
186
  end
@@ -1,8 +1,8 @@
1
1
  <%
2
2
 
3
- #debugger
4
3
  all_responses = fulltext
5
-
4
+
5
+
6
6
  (title_level_fulltxt, fulltxt) =
7
7
  all_responses.partition {|i| i.view_data[:coverage_checked] == false || i.view_data[:can_link_to_article] == false}
8
8
  # order em with full fulltxt before title_level_fulltxt
@@ -15,7 +15,6 @@
15
15
 
16
16
 
17
17
  <div class="umlaut_section_content">
18
-
19
18
  <%= list_with_limit('umlaut_fulltext',
20
19
  all_responses,
21
20
  :ul_class => "response_list",
@@ -36,7 +35,6 @@
36
35
  </div>
37
36
  <ul class="response_list">
38
37
  <% end %>
39
-
40
38
  <%= render(:partial => "standard_response_item", :object=> item) %>
41
39
  <% end %>
42
40
 
@@ -0,0 +1,327 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+ require 'base64'
4
+ require 'marc'
5
+
6
+ # Searches a Blacklight with the cql extension installed.
7
+ #
8
+ #
9
+ # Params include:
10
+ # [base_url]
11
+ # required. Complete URL to catalog.atom action. Eg "https://blacklight.mse.jhu.edu/catalog.atom"
12
+ # [bl_fields]
13
+ # required with at least some entries if you want this to do anything. Describe the names of given semantic fields in your BL instance.
14
+ # * issn
15
+ # * isbn
16
+ # * lccn
17
+ # * oclcnum
18
+ # * id (defaults to 'id')
19
+ # * title
20
+ # * author
21
+ # * serials_limit_clause => not an index name, full URL clause for a limit to apply to known serials searches, for instance "f[format][]=Serial"
22
+ # [identifier_search]
23
+ # Do catalog search on issn/isbn/oclcnum/lccn/bibId. Default true.
24
+ # [keyword_search]
25
+ # Do catalog search on title/author keywords where applicable. Generally only used when identifier_search finds no hits, if identifier_search is on. Default true.
26
+ # [keyword_per_page]
27
+ # How many records to fetch from blacklight when doing keyword searches.
28
+ # [exclude_holdings]
29
+ # Can be used to exclude certain 'dummy' holdings that have certain collection, location, or other values. Eg:
30
+ # exclude_holdings:
31
+ # collection_str:
32
+ # - World Wide Web
33
+ # - Internet
34
+ # [rft_id_bibnum_prefixes]
35
+ # Array of URI prefixes in an rft_id that indicate that the actual solr id comes next. For instance, if your blacklight will send "http://blacklight.com/catalog/some_id" in an rft_id, then include "http://blacklight.com/catalog/". Optional.
36
+ class Blacklight < Service
37
+ required_config_params :base_url, :display_name
38
+ attr_reader :base_url, :cql_search_field
39
+ attr_reader :bl_fields, :issn
40
+
41
+ include UmlautHttp
42
+ include MetadataHelper
43
+ include MarcHelper
44
+ include XmlSchemaHelper
45
+
46
+ def initialize(config)
47
+ # defaults
48
+ # If you are sending an OpenURL from a library service, you may
49
+ # have the HIP bibnum, and include it in the OpenURL as, eg.
50
+ # rft_id=http://catalog.library.jhu.edu/bib/343434 (except URL-encoded)
51
+ # Then you'd set rft_id_bibnum_prefix to http://catalog.library.jhu.edu/bib/
52
+ @rft_id_bibnum_prefixes = []
53
+ @cql_search_field = "cql"
54
+ @keyword_per_page = 10
55
+ @identifier_search = true
56
+ @keyword_search = true
57
+ @link_to_search = true
58
+ super(config)
59
+ @bl_fields = { "id" => "id "}.merge(@bl_fields)
60
+ end
61
+
62
+ # Standard method, used by background service updater. See Service docs.
63
+ def service_types_generated
64
+ types = [ ServiceTypeValue[:fulltext], ServiceTypeValue[:holding], ServiceTypeValue[:table_of_contents], ServiceTypeValue[:relevant_link] ]
65
+
66
+ return types
67
+ end
68
+
69
+
70
+ def handle(request)
71
+ ids_processed = []
72
+ holdings_added = 0
73
+
74
+ if (@identifier_search && url = blacklight_precise_search_url(request) )
75
+ doc = Nokogiri::XML( http_fetch(url).body )
76
+
77
+ ids_processed.concat( bib_ids_from_atom_entries( doc.xpath("atom:feed/atom:entry", xml_ns) ) )
78
+
79
+ # namespaces make xpath harder than it should be, but css
80
+ # selector still easy, thanks nokogiri! Grab the marc from our
81
+ # results.
82
+ marc_matches = doc.xpath("atom:feed/atom:entry/atom:content[@type='application/marc']", xml_ns).collect do |encoded_marc21|
83
+ MARC::Reader.decode( Base64.decode64(encoded_marc21.text) )
84
+ end
85
+
86
+ add_856_links(request, marc_matches )
87
+
88
+ # Got to make a second fetch for dlf_expanded info, cause BL doens't
89
+ # (yet) let us ask for more than one at once
90
+ holdings_url = blacklight_precise_search_url( request, "dlf_expanded" )
91
+ holdings_added += add_holdings( holdings_url ) if holdings_url
92
+ end
93
+ #keyword search.
94
+ if (@keyword_search &&
95
+ url = blacklight_keyword_search_url(request))
96
+
97
+ doc = Nokogiri::XML( http_fetch(url).body )
98
+ # filter out matches whose titles don't really match at all, or
99
+ # which have already been seen in identifier search.
100
+ entries = filter_keyword_entries( doc.xpath("atom:feed/atom:entry", xml_ns) , :exclude_ids => ids_processed, :remove_subtitle => (! title_is_serial?(request.referent)) )
101
+
102
+ marc_by_atom_id = {}
103
+
104
+ # Grab the marc from our entries. Important not to do a // xpath
105
+ # search, or we'll wind up matching parent elements not actually
106
+ # included in our 'entries' list.
107
+ marc_matches = entries.xpath("atom:content[@type='application/marc']", xml_ns).collect do |encoded_marc21|
108
+ marc = MARC::Reader.decode( Base64.decode64(encoded_marc21.text) )
109
+
110
+ marc_by_atom_id[ encoded_marc21.at_xpath("ancestor::atom:entry/atom:id/text()", xml_ns).to_s ] = marc
111
+
112
+ marc
113
+ end
114
+
115
+ # We've filtered out those we consider just plain bad
116
+ # matches, everything else we're going to call
117
+ # an approximate match. Sort so that those with
118
+ # a date close to our request date are first.
119
+ if ( year = get_year(request.referent))
120
+ marc_matches = marc_matches.partition {|marc| get_years(marc).include?( year )}.flatten
121
+ end
122
+ # And add in the 856's
123
+ add_856_links(request, marc_matches, :match_reliability => ServiceResponse::MatchUnsure)
124
+
125
+ # Fetch and add in the holdings
126
+ url = blacklight_url_for_ids(bib_ids_from_atom_entries(entries))
127
+
128
+ holdings_added += add_holdings( url, :match_reliability => ServiceResponse::MatchUnsure, :marc_data => marc_by_atom_id ) if url
129
+
130
+ if (@link_to_search && holdings_added ==0)
131
+ hit_count = doc.at_xpath("atom:feed/opensearch:totalResults/text()", xml_ns).to_s.to_i
132
+ html_result_url = doc.at_xpath("atom:feed/atom:link[@rel='alternate'][@type='text/html']/attribute::href", xml_ns).to_s
133
+
134
+ if hit_count > 0
135
+ request.add_service_response(
136
+ :service => self,
137
+ :source_name => @display_name,
138
+ :count => hit_count,
139
+ :display_text => "#{hit_count} possible #{case; when hit_count > 1 ; 'matches' ; else; 'match' ; end} in #{@display_name}",
140
+ :url => html_result_url,
141
+ :service_type_value => :holding_search )
142
+ end
143
+ end
144
+ end
145
+
146
+
147
+
148
+
149
+ return request.dispatched(self, true)
150
+
151
+
152
+ end
153
+
154
+ # Send a CQL request for any identifiers present.
155
+ # Ask for for an atom response with embedded marc21 back.
156
+ def blacklight_precise_search_url(request, format = "marc")
157
+ # Add search clauses for our identifiers, if we have them and have a configured search field for them.
158
+ clauses = []
159
+ added = []
160
+ ["lccn", "isbn", "oclcnum"].each do |key|
161
+ if bl_fields[key] && request.referent.send(key)
162
+ clauses.push( "#{bl_fields[key]} = \"#{request.referent.send(key)}\"")
163
+ added << key
164
+ end
165
+ end
166
+ # Only add ISSN if we don't have an ISBN, reduces false matches
167
+ if ( !added.include?("isbn") &&
168
+ bl_fields["issn"] &&
169
+ request.referent.issn)
170
+ clauses.push("#{bl_fields["issn"]} = \"#{request.referent.issn}\"")
171
+ end
172
+
173
+
174
+ # Add Solr document identifier if we can get one from the URL
175
+
176
+ if (id = get_solr_id(request.referent))
177
+ clauses.push("#{bl_fields['id']} = \"#{id}\"")
178
+ end
179
+
180
+ # if we have nothing, we can do no search.
181
+ return nil if clauses.length == 0
182
+
183
+ cql = clauses.join(" OR ")
184
+
185
+ return base_url + "?search_field=#{@cql_search_field}&content_format=#{format}&q=#{CGI.escape(cql)}"
186
+ end
187
+
188
+ # Construct a CQL search against blacklight for author and title,
189
+ # possibly with serial limit. Ask for Atom with embedded MARC back.
190
+ def blacklight_keyword_search_url(request, options = {})
191
+ options[:format] ||= "atom"
192
+ options[:content_format] ||= "marc"
193
+
194
+ clauses = []
195
+
196
+ # We need both title and author to search keyword style, or
197
+ # we get too many false positives. Except serials we'll do
198
+ # title only. sigh, logic tree.
199
+ title = get_search_title(request.referent)
200
+ author = get_top_level_creator(request.referent)
201
+ return nil unless title && (author || (@bl_fields["serials_limit_clause"] && title_is_serial?(request.referent)))
202
+ # phrase search for title, just raw dismax for author
203
+ # Embed quotes inside the quoted value, need to backslash-quote for CQL,
204
+ # and backslash the backslashes for ruby literal.
205
+ clauses.push("#{@bl_fields["title"]} = \"\\\"#{title}\\\"\"")
206
+ clauses.push("#{@bl_fields["author"]} = \"#{author}\"") if author
207
+
208
+
209
+
210
+ url = base_url + "?search_field=#{@cql_search_field}&content_format=#{options[:content_format]}&q=#{CGI.escape(clauses.join(" AND "))}"
211
+
212
+ if (@bl_fields["serials_limit_clause"] &&
213
+ title_is_serial?(request.referent))
214
+ url += "&" + @bl_fields["serials_limit_clause"]
215
+ end
216
+
217
+ return url
218
+ end
219
+
220
+ # Takes a url that will return atom response of dlf_expanded content.
221
+ # Adds Umlaut "holding" ServiceResponses for dlf_expanded, as appropriate.
222
+ # Returns number of holdings added.
223
+ def add_holdings(holdings_url, options = {})
224
+ options[:match_reliability] ||= ServiceResponse::MatchExact
225
+ options[:marc_data] ||= {}
226
+
227
+ atom = Nokogiri::XML( http_fetch(holdings_url).body )
228
+ content_entries = atom.search("/atom:feed/atom:entry/atom:content", xml_ns)
229
+
230
+ # For each atom entry, find the dlf_expanded record. For each dlf_expanded
231
+ # record, take all of it's holdingsrec's if it has them, or all of it's
232
+ # items if it doesn't, and add them to list. We wind up with a list
233
+ # of mixed holdingsrec's and items.
234
+ holdings_xml = content_entries.collect do |dlf_expanded|
235
+ copies = dlf_expanded.xpath("dlf:record/dlf:holdings/dlf:holdingset/dlf:holdingsrec", xml_ns)
236
+ copies.length > 0 ? copies : dlf_expanded.xpath("dlf:record/dlf:items/dlf:item", xml_ns)
237
+ end.flatten
238
+
239
+ service_data = holdings_xml.collect do | xml_metadata |
240
+ atom_entry = xml_metadata.at_xpath("ancestor::atom:entry", xml_ns)
241
+ atom_id = atom_entry.at_xpath("atom:id/text()", xml_ns).to_s
242
+
243
+ edition_str = edition_statement(options[:marc_data][atom_id])
244
+ url = atom_entry.at_xpath("atom:link[@rel='alternate'][@type='text/html']/attribute::href", xml_ns).to_s
245
+
246
+ xml_to_holdings( xml_metadata ).merge(
247
+ :service => self,
248
+ :match_reliability => options[:match_reliability],
249
+ :edition_str => edition_str,
250
+ :url => url
251
+ )
252
+ end
253
+
254
+ # strip out holdings that aren't really holdings
255
+ service_data.delete_if do |data|
256
+ @exclude_holdings.collect do |key, values|
257
+ values.include?(data[key.to_sym])
258
+ end.include?(true)
259
+ end
260
+
261
+ # Sort by "collection"
262
+ service_data.sort do |a, b|
263
+ a[:collection_str] <=> b[:collection_str]
264
+ end
265
+
266
+ service_data.each do |data|
267
+ request.add_service_response(data.merge(:service => self, :service_type_value =>"holding"))
268
+ end
269
+
270
+ return service_data.length
271
+ end
272
+
273
+ def filter_keyword_entries(atom_entries, options = {})
274
+ options[:exclude_ids] ||= []
275
+ options[:remove_subtitle] ||= true
276
+ request_title_forms = [
277
+ raw_search_title(request.referent).downcase,
278
+ normalize_title( raw_search_title(request.referent) )
279
+ ]
280
+ request_title_forms << normalize_title( raw_search_title(request.referent), :remove_subtitle => true) if options[:remove_subtitle]
281
+ request_title_forms.compact
282
+
283
+ # Only keep entries with title match, and that aren't in the
284
+ # exclude_ids list.
285
+ good_entries = atom_entries.find_all do |atom_entry|
286
+ title = atom_entry.xpath("atom:title/text()", xml_ns).to_s
287
+
288
+ entry_title_forms = [
289
+ title.downcase,
290
+ normalize_title(title)
291
+ ]
292
+ entry_title_forms << normalize_title(title, :remove_subtitle=>true) if options[:remove_subtitle]
293
+ entry_title_forms.compact
294
+
295
+ ((entry_title_forms & request_title_forms).length > 0 &&
296
+ (bib_ids_from_atom_entries(atom_entry) & options[:exclude_ids]).length == 0)
297
+ end
298
+ return Nokogiri::XML::NodeSet.new( atom_entries.document, good_entries)
299
+ end
300
+
301
+ def bib_ids_from_atom_entries(entries)
302
+ entries.xpath("atom:id/text()", xml_ns).to_a.collect do |atom_id|
303
+ atom_id.to_s =~ /([^\/]+)$/
304
+ $1
305
+ end.compact
306
+ end
307
+
308
+ def blacklight_url_for_ids(ids, format="dlf_expanded")
309
+ return nil unless ids.length > 0
310
+
311
+ return base_url + "?search_field=#{@cql_search_field}&content_format=#{format}&q=" + CGI.escape("#{@bl_fields["id"]} any \"#{ids.join(" ")}\"")
312
+ end
313
+
314
+
315
+ def get_solr_id(rft)
316
+ rft.identifiers.each do |id|
317
+ @rft_id_bibnum_prefixes.each do |prefix|
318
+ if id[0, prefix.length] == prefix
319
+ return id[prefix.length, id.length]
320
+ end
321
+ end
322
+ end
323
+
324
+ return nil
325
+ end
326
+
327
+ end
@@ -33,6 +33,9 @@
33
33
  class Scopus < Service
34
34
  require 'open-uri'
35
35
  require 'multi_json'
36
+
37
+ include ActionView::Helpers::SanitizeHelper
38
+
36
39
  include MetadataHelper
37
40
  include UmlautHttp
38
41
 
@@ -237,7 +240,8 @@ class Scopus < Service
237
240
  request.add_service_response(
238
241
  :service=>self,
239
242
  :display_text => "Abstract from #{@display_name}",
240
- :content => first_hit["abstract"],
243
+ :content => sanitize(first_hit["abstract"]),
244
+ :content_html_safe => true,
241
245
  :url => detail_url(first_hit),
242
246
  :service_type_value => :abstract)
243
247
  end
@@ -0,0 +1,164 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Umlaut
3
+ # Class to inject Umlaut routes, design copied from Blacklight project.
4
+ # you would do a:
5
+ # Umlaut::Routes.new(self, optional_args).draw
6
+ # in local
7
+ # app routes.rb, that line is generated into local app by Umlaut generator.
8
+ # options include :only and :except to limit what route groups are generated.
9
+ class Routes
10
+
11
+ def initialize(router, options ={})
12
+ @router = router
13
+ @options = options
14
+ end
15
+
16
+ def draw
17
+ route_sets.each do |r|
18
+ self.send(r)
19
+ end
20
+ end
21
+
22
+ protected
23
+
24
+ def add_routes &blk
25
+ @router.instance_exec(@options, &blk)
26
+ end
27
+
28
+ def route_sets
29
+ # :admin is not included by default, needs to be turned on.
30
+ (@options[:only] || default_route_sets) - (@options[:except] || []) + (@options[:admin] == true ? [:admin] : [])
31
+ end
32
+
33
+ def default_route_sets
34
+ [:root, :permalinks, :a_z, :resolve, :open_search, :link_router, :export_email, :resources, :search, :javascript]
35
+ end
36
+
37
+ module RouteSets
38
+ # for now include root generation in Umlaut auto-generation
39
+ def root
40
+ add_routes do |options|
41
+ root :to => "search#index"
42
+ end
43
+ end
44
+
45
+ def permalinks
46
+ add_routes do |options|
47
+ match 'go/:id' => 'store#index'
48
+ end
49
+ end
50
+
51
+ # some special direct links to A-Z type searches, including
52
+ # legacy redirects for SFX-style urls, to catch any bookmarks.
53
+ def a_z
54
+ add_routes do |options|
55
+ # Special one for alpha list
56
+ match 'journal_list/:id/:page' => 'search#journal_list', :defaults => { :page => 1, :id => 'A' }
57
+
58
+
59
+ # Catch redirected from SFX A-Z and citation linker urls
60
+ # v2 A-Z links redirected to umlaut, point to journal_list
61
+ # code in journal_list filter picks out SFX URL vars for
62
+ # letter.
63
+ match '/resolve/azlist/default' => 'search#journal_list', :page => 1, :id => 'A'
64
+
65
+ # SFX v3 A-Z list url format
66
+ match 'resolve/az' => 'search#journal_list', :page => 1, :id => 'A'
67
+ end
68
+ end
69
+
70
+ # This is a legacy wild controller route that's not recommended for RESTful applications.
71
+ # Note: This route will make all actions in every controller accessible via GET requests.
72
+ # match ':controller(/:action(/:id(.:format)))'
73
+
74
+ def resolve
75
+ add_routes do |options|
76
+ # ResolveController still uses rails 2.0 style 'wildcard' routes,
77
+ # TODO tighten this up to only match what oughta be matched.
78
+ # Note: This route will make all actions in this controller accessible via GET requests.
79
+
80
+ match 'resolve(/:action(/:id(.:format)))' => "resolve"
81
+ end
82
+ end
83
+
84
+ def open_search
85
+ add_routes do |options|
86
+ # OpenSearchController still uses rails 2.0 style 'wildcard' routes,
87
+ # TODO tighten this up to only match what oughta be matched.
88
+ # Note: This route will make all actions in this controller accessible via GET requests.
89
+
90
+ match 'open_search(/:action(/:id(.:format)))' => "open_search"
91
+ end
92
+ end
93
+
94
+ def link_router
95
+ add_routes do |options|
96
+ # LinkRouterController still uses rails 2.0 style 'wildcard' routes,
97
+ # TODO tighten this up to only match what oughta be matched.
98
+ # Note: This route will make all actions in this controller accessible via GET requests.
99
+
100
+ match 'link_router(/:action(/:id(.:format)))' => "link_router"
101
+ end
102
+ end
103
+
104
+ def export_email
105
+ add_routes do |options|
106
+ # ExportEmailController still uses rails 2.0 style 'wildcard' routes,
107
+ # TODO tighten this up to only match what oughta be matched.
108
+ # Note: This route will make all actions in this controller accessible via GET requests.
109
+
110
+ match 'export_email(/:action(/:id(.:format)))' => "export_email"
111
+ end
112
+ end
113
+
114
+ def resources
115
+ add_routes do |options|
116
+ # ResourceController still uses rails 2.0 style 'wildcard' routes,
117
+ # TODO tighten this up to only match what oughta be matched.
118
+ # Note: This route will make all actions in this controller accessible via GET requests.
119
+
120
+ match 'resource(/:action(/:id(.:format)))' => "resource"
121
+ end
122
+ end
123
+
124
+ def search
125
+ add_routes do |options|
126
+ # SearchController still uses rails 2.0 style 'wildcard' routes,
127
+ # TODO tighten this up to only match what oughta be matched.
128
+ # Note: This route will make all actions in this controller accessible via GET requests.
129
+
130
+ match 'search(/:action(/:id(.:format)))' => "search"
131
+ end
132
+ end
133
+
134
+ def javascript
135
+ add_routes do |options|
136
+ # Legacy location for update_html.js used by JQuery Content Utility
137
+ # to embed JS on external sites. Redirect to new location.
138
+ # Intentionally non-fingerprinted, most efficient thing
139
+ # we can do in this case is let the web server take care
140
+ # of Last-modified-by etc headers.
141
+ match 'javascripts/jquery/umlaut/update_html.js' => redirect("/assets/umlaut/update_html.js", :status => 301)
142
+
143
+ # The loader doens't work _exactly_ like the new umlaut-ui.js, but
144
+ # it's close enough that it'll work better redirecting than just
145
+ # 404'ing.
146
+ match 'js_helper/loader' => redirect("/assets/umlaut_ui.js")
147
+
148
+
149
+ match 'images/spinner.gif' => redirect("/assets/spinner.gif")
150
+ end
151
+ end
152
+
153
+ def admin
154
+ add_routes do |options|
155
+ namespace "admin" do
156
+ match 'service_errors(/:service_id)' => "service_errors#index"
157
+ end
158
+ end
159
+ end
160
+
161
+ end
162
+ include RouteSets
163
+ end
164
+ end
data/lib/umlaut/routes.rb CHANGED
@@ -140,6 +140,12 @@ module Umlaut
140
140
  # of Last-modified-by etc headers.
141
141
  match 'javascripts/jquery/umlaut/update_html.js' => redirect("/assets/umlaut/update_html.js", :status => 301)
142
142
 
143
+ # The loader doens't work _exactly_ like the new umlaut-ui.js, but
144
+ # it's close enough that it'll work better redirecting than just
145
+ # 404'ing.
146
+ match 'js_helper/loader' => redirect("/assets/umlaut_ui.js")
147
+
148
+
143
149
  match 'images/spinner.gif' => redirect("/assets/spinner.gif")
144
150
  end
145
151
  end
@@ -1,3 +1,3 @@
1
1
  module Umlaut
2
- VERSION = "3.0.0beta4"
2
+ VERSION = "3.0.0beta5"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: umlaut
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0beta4
4
+ version: 3.0.0beta5
5
5
  prerelease: 5
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-02 00:00:00.000000000 Z
12
+ date: 2012-04-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &223216140 !ruby/object:Gem::Requirement
16
+ requirement: &17186100 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 3.2.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *223216140
24
+ version_requirements: *17186100
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: jquery-rails
27
- requirement: &223215720 !ruby/object:Gem::Requirement
27
+ requirement: &17185560 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *223215720
35
+ version_requirements: *17185560
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: nokogiri
38
- requirement: &224032220 !ruby/object:Gem::Requirement
38
+ requirement: &17184800 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - =
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 1.5.0
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *224032220
46
+ version_requirements: *17184800
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: openurl
49
- requirement: &224031720 !ruby/object:Gem::Requirement
49
+ requirement: &17184180 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.3.0
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *224031720
57
+ version_requirements: *17184180
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: marc
60
- requirement: &224031240 !ruby/object:Gem::Requirement
60
+ requirement: &17183420 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.4.3
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *224031240
68
+ version_requirements: *17183420
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: isbn
71
- requirement: &224030800 !ruby/object:Gem::Requirement
71
+ requirement: &17182120 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *224030800
79
+ version_requirements: *17182120
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: htmlentities
82
- requirement: &224030200 !ruby/object:Gem::Requirement
82
+ requirement: &17180260 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :runtime
89
89
  prerelease: false
90
- version_requirements: *224030200
90
+ version_requirements: *17180260
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: multi_json
93
- requirement: &224029580 !ruby/object:Gem::Requirement
93
+ requirement: &17179300 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0'
99
99
  type: :runtime
100
100
  prerelease: false
101
- version_requirements: *224029580
101
+ version_requirements: *17179300
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: confstruct
104
- requirement: &224028780 !ruby/object:Gem::Requirement
104
+ requirement: &17132240 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ~>
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: '0.2'
110
110
  type: :runtime
111
111
  prerelease: false
112
- version_requirements: *224028780
112
+ version_requirements: *17132240
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: exlibris-primo
115
- requirement: &224028280 !ruby/object:Gem::Requirement
115
+ requirement: &17130980 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ~>
@@ -120,10 +120,10 @@ dependencies:
120
120
  version: 0.1.0
121
121
  type: :runtime
122
122
  prerelease: false
123
- version_requirements: *224028280
123
+ version_requirements: *17130980
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: single_test
126
- requirement: &224027780 !ruby/object:Gem::Requirement
126
+ requirement: &17129720 !ruby/object:Gem::Requirement
127
127
  none: false
128
128
  requirements:
129
129
  - - ~>
@@ -131,10 +131,10 @@ dependencies:
131
131
  version: 0.5.1
132
132
  type: :development
133
133
  prerelease: false
134
- version_requirements: *224027780
134
+ version_requirements: *17129720
135
135
  - !ruby/object:Gem::Dependency
136
136
  name: uglifier
137
- requirement: &224027200 !ruby/object:Gem::Requirement
137
+ requirement: &17128300 !ruby/object:Gem::Requirement
138
138
  none: false
139
139
  requirements:
140
140
  - - ! '>='
@@ -142,10 +142,10 @@ dependencies:
142
142
  version: '0'
143
143
  type: :development
144
144
  prerelease: false
145
- version_requirements: *224027200
145
+ version_requirements: *17128300
146
146
  - !ruby/object:Gem::Dependency
147
147
  name: therubyracer
148
- requirement: &224026480 !ruby/object:Gem::Requirement
148
+ requirement: &17126180 !ruby/object:Gem::Requirement
149
149
  none: false
150
150
  requirements:
151
151
  - - ! '>='
@@ -153,10 +153,10 @@ dependencies:
153
153
  version: '0'
154
154
  type: :development
155
155
  prerelease: false
156
- version_requirements: *224026480
156
+ version_requirements: *17126180
157
157
  - !ruby/object:Gem::Dependency
158
158
  name: ruby-prof
159
- requirement: &224025900 !ruby/object:Gem::Requirement
159
+ requirement: &17523620 !ruby/object:Gem::Requirement
160
160
  none: false
161
161
  requirements:
162
162
  - - ! '>='
@@ -164,7 +164,7 @@ dependencies:
164
164
  version: '0'
165
165
  type: :development
166
166
  prerelease: false
167
- version_requirements: *224025900
167
+ version_requirements: *17523620
168
168
  description:
169
169
  email:
170
170
  - umlaut-general@rubyforge.org
@@ -213,11 +213,13 @@ files:
213
213
  - app/assets/javascripts/umlaut_ui.js
214
214
  - app/assets/javascripts/umlaut.js
215
215
  - app/assets/javascripts/umlaut/update_html.js
216
+ - app/assets/javascripts/umlaut/#simple_visible_toggle.js#
216
217
  - app/assets/javascripts/umlaut/ajax_windows.js
217
218
  - app/assets/javascripts/umlaut/simple_visible_toggle.js
218
219
  - app/assets/javascripts/umlaut/ensure_window_size.js.erb
220
+ - app/assets/javascripts/umlaut/expand_contract_toggle.js
219
221
  - app/assets/javascripts/umlaut/search_autocomplete.js
220
- - app/assets/javascripts/umlaut/expand_contract_toggle.js.erb
222
+ - app/assets/javascripts/umlaut/#update_html.js#
221
223
  - app/assets/stylesheets/umlaut.css
222
224
  - app/models/sfx_url.rb
223
225
  - app/models/crossref_lookup.rb
@@ -321,6 +323,7 @@ files:
321
323
  - lib/opensearch_feed.rb
322
324
  - lib/holding.rb
323
325
  - lib/umlaut_configurable.rb
326
+ - lib/umlaut/#routes.rb#
324
327
  - lib/umlaut/routes.rb
325
328
  - lib/umlaut/version.rb
326
329
  - lib/umlaut/default_configuration.rb
@@ -333,6 +336,7 @@ files:
333
336
  - lib/service_adaptors/primo_service.rb
334
337
  - lib/service_adaptors/open_library.rb
335
338
  - lib/service_adaptors/elsevier_cover.rb
339
+ - lib/service_adaptors/#blacklight.rb#
336
340
  - lib/service_adaptors/ulrichs_link.rb
337
341
  - lib/service_adaptors/ulrichs_cover.rb
338
342
  - lib/service_adaptors/scopus.rb
@@ -481,7 +485,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
481
485
  version: '0'
482
486
  segments:
483
487
  - 0
484
- hash: -556718949677620916
488
+ hash: -986297622485492728
485
489
  required_rubygems_version: !ruby/object:Gem::Requirement
486
490
  none: false
487
491
  requirements: