umlaut 3.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. data/README.md +1 -1
  2. data/app/assets/stylesheets/umlaut/_resolve.scss +1 -1
  3. data/app/controllers/feedback_controller.rb +46 -0
  4. data/app/controllers/resource_controller.rb +19 -1
  5. data/app/controllers/search_methods/sfx4.rb +15 -0
  6. data/app/controllers/umlaut_controller.rb +6 -0
  7. data/app/helpers/emailer_helper.rb +17 -3
  8. data/app/mailers/feedback_mailer.rb +25 -0
  9. data/app/mixin_logic/metadata_helper.rb +37 -0
  10. data/app/models/referent.rb +22 -19
  11. data/app/models/request.rb +3 -0
  12. data/app/referent_filters/dissertation_catch.rb +1 -1
  13. data/app/service_adaptors/all_books_dot_com.rb +17 -0
  14. data/app/service_adaptors/illiad.rb +161 -0
  15. data/app/service_adaptors/isbn_db.rb +6 -1
  16. data/app/service_adaptors/isbn_link.rb +57 -0
  17. data/app/service_adaptors/scopus.rb +4 -0
  18. data/app/service_adaptors/scopus2.rb +330 -0
  19. data/app/views/feedback/_resolve_section.html.erb +16 -0
  20. data/app/views/feedback/new.html.erb +33 -0
  21. data/app/views/feedback_mailer/feedback.text.erb +23 -0
  22. data/app/views/layouts/umlaut.html.erb +2 -0
  23. data/app/views/umlaut/_alerts.html.erb +14 -0
  24. data/lib/generators/templates/umlaut_services.yml +8 -8
  25. data/lib/umlaut/routes.rb +8 -1
  26. data/lib/umlaut/version.rb +1 -1
  27. data/test/dummy/tmp/cache/assets/BFF/760/sprockets%2Fe00969069e468419c393709f042b4527 +0 -0
  28. data/test/dummy/tmp/cache/assets/C9D/060/sprockets%2F5c8956a1666824a1d214531abd22e2a2 +0 -0
  29. data/test/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  30. data/test/dummy/tmp/cache/assets/D15/FC0/sprockets%2F8cbf3a8b7acb7fc27a42168846226385 +0 -0
  31. data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  32. data/test/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  33. data/test/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  34. data/test/dummy/tmp/cache/assets/D98/990/sprockets%2F710ede3e7f5ebab14e9772fa88c00c02 +0 -0
  35. data/test/dummy/tmp/cache/assets/DAD/BA0/sprockets%2F193f81f7e4eae26eaaa7d909c0c8e956 +0 -0
  36. data/test/dummy/tmp/cache/assets/DCB/620/sprockets%2F2332a294ceeab3ab9b5ee643989dc0eb +0 -0
  37. data/test/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  38. data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  39. data/test/functional/feedback_controller_test.rb +59 -0
  40. data/test/test_helper.rb +13 -0
  41. data/test/unit/feedback_mailer_test.rb +57 -0
  42. data/test/unit/illiad_test.rb +146 -0
  43. data/test/unit/metadata_helper_test.rb +49 -0
  44. data/test/unit/referent_to_citation_test.rb +45 -0
  45. data/test/unit/scopus2_test.rb +147 -0
  46. data/test/vcr_cassettes/scopus/live_test_with_no_hits.yml +52 -0
  47. data/test/vcr_cassettes/scopus/live_test_with_result.yml +62 -0
  48. data/test/vcr_cassettes/scopus/live_trigger_scopus_error.yml +50 -0
  49. metadata +34 -120
  50. data/test/dummy/tmp/cache/sass/b43409235ed55124ccf6a0235156711682d0fe98/umlaut.css.scssc +0 -0
  51. data/test/dummy/tmp/cache/sass/d2b87393a9fcb33d01e765e1c09b76db7f14f647/bootstrap-responsive.scssc +0 -0
  52. data/test/dummy/tmp/cache/sass/d2b87393a9fcb33d01e765e1c09b76db7f14f647/bootstrap.scssc +0 -0
  53. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_accordion.scssc +0 -0
  54. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_alerts.scssc +0 -0
  55. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_breadcrumbs.scssc +0 -0
  56. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_button-groups.scssc +0 -0
  57. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_buttons.scssc +0 -0
  58. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_carousel.scssc +0 -0
  59. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_close.scssc +0 -0
  60. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_code.scssc +0 -0
  61. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_component-animations.scssc +0 -0
  62. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_dropdowns.scssc +0 -0
  63. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_forms.scssc +0 -0
  64. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_grid.scssc +0 -0
  65. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_hero-unit.scssc +0 -0
  66. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_labels-badges.scssc +0 -0
  67. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_layouts.scssc +0 -0
  68. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_media.scssc +0 -0
  69. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_mixins.scssc +0 -0
  70. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_modals.scssc +0 -0
  71. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_navbar.scssc +0 -0
  72. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_navs.scssc +0 -0
  73. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_pager.scssc +0 -0
  74. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_pagination.scssc +0 -0
  75. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_popovers.scssc +0 -0
  76. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_progress-bars.scssc +0 -0
  77. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_reset.scssc +0 -0
  78. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_responsive-1200px-min.scssc +0 -0
  79. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_responsive-767px-max.scssc +0 -0
  80. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_responsive-768px-979px.scssc +0 -0
  81. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_responsive-navbar.scssc +0 -0
  82. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_responsive-utilities.scssc +0 -0
  83. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_scaffolding.scssc +0 -0
  84. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_sprites.scssc +0 -0
  85. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_tables.scssc +0 -0
  86. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_thumbnails.scssc +0 -0
  87. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_tooltip.scssc +0 -0
  88. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_type.scssc +0 -0
  89. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_utilities.scssc +0 -0
  90. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_variables.scssc +0 -0
  91. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/_wells.scssc +0 -0
  92. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/bootstrap.scssc +0 -0
  93. data/test/dummy/tmp/cache/sass/db21ae7b0d8da2224e8e1d588985ee0507dc926a/responsive.scssc +0 -0
  94. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_admin.scssc +0 -0
  95. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_az.scssc +0 -0
  96. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_forms.scssc +0 -0
  97. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_icons.scssc +0 -0
  98. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_layout.scssc +0 -0
  99. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_misc.scssc +0 -0
  100. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_mixins.scssc +0 -0
  101. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_modal.scssc +0 -0
  102. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_resolve.scssc +0 -0
  103. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_results.scssc +0 -0
  104. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_search.scssc +0 -0
  105. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_spinner.scssc +0 -0
  106. data/test/dummy/tmp/cache/sass/fc140da5c32e19b5c3f84a23bb2383ebc83fcc20/_variables.scssc +0 -0
@@ -0,0 +1,330 @@
1
+ # Service adapter plug-in.
2
+ #
3
+ #
4
+ # PURPOSE: Includes "cited by", "similar articles" and "more by these authors"
5
+ # links from scopus.
6
+ #
7
+ # LIMTATIONS: You must be a Scopus customer for these links generated to work
8
+ # for your users at all! Off-campus users should be probably going through ezproxy, see
9
+ # the EZProxy plug-in.
10
+ # Must find a match in scopus, naturally. "cited by" will only
11
+ # be included if Scopus has non-0 "cited by" links. But there's no good way
12
+ # to precheck similar/more-by for this, so they are provided blind and may
13
+ # result in 0 hits. You can turn them off if you like, with @include_similar,
14
+ # and @include_more_by_authors.
15
+ # Abstracts are not used because it seems to violate Scopus terms of service
16
+ # to use them.
17
+ #
18
+ # REGISTERING: Register for a Scopus API key at:
19
+ # http://www.developers.elsevier.com/action/devprojects?pageOrigin=cmsPage&zone=topNavBar
20
+ # Look for "Register a new site" button at the bottom right of the page.
21
+ #
22
+ # For the second Scopus API, you theoretically need a Scopus "PartnerID" and
23
+ # corresponding "release number", in @partner_id and @scopus_release
24
+ # There's no real easy way to get one. Scopus says:
25
+ # "To obtain a partner ID or release number, contact your nearest regional
26
+ # Scopus office. A list of Scopus contacts is available at
27
+ # http://www.info.scopus.com/contactus/index.shtml"
28
+ # Bah! But fortunately, using the "partnerID" assigned to the Scopus Json
29
+ # API, 65, _seems_ to work, and is coded here as the default. You could try
30
+ # going with that. When you register a partnerID, you also get a 'salt key',
31
+ # which is currently not used by this code, but @link_salt_key is reserved
32
+ # for it in case added functionality does later.
33
+ #
34
+ # SCOPUS USEFUL URLS:
35
+ #
36
+ # api key register: http://www.developers.elsevier.com/action/devprojects?pageOrigin=cmsPage&zone=topNavBar
37
+ #
38
+ # 'content policies' terms of use: http://www.developers.elsevier.com/cms/content-apis
39
+ #
40
+ # API overview docs: http://www.developers.elsevier.com/cms/content-apis
41
+ #
42
+ # Various other api docs? Confused myself as to organization here.
43
+ #
44
+ # * http://www.developers.elsevier.com/devcms/content-api-search-request
45
+ # * http://www.developers.elsevier.com/devcms/content/search-fields-overview
46
+ # * http://api.elsevier.com/content/search/#d0n14606
47
+ #
48
+ # Some API recommendations for federated search: http://www.developers.elsevier.com/cms/restful-api-federated-search
49
+ #
50
+ class Scopus2 < Service
51
+ require 'umlaut_http'
52
+ require 'nokogiri'
53
+
54
+ include ActionView::Helpers::SanitizeHelper
55
+
56
+ include MetadataHelper
57
+ include UmlautHttp
58
+
59
+ required_config_params :api_key
60
+
61
+ attr_accessor :scopus_search_base
62
+
63
+ def service_types_generated
64
+ types = []
65
+ types.push( ServiceTypeValue[:cited_by] ) if @include_cited_by
66
+ types.push( ServiceTypeValue[:abstract] ) if @include_abstract
67
+ types.push( ServiceTypeValue[:similar] ) if @include_similar
68
+ types.push( ServiceTypeValue[@more_by_authors_type] ) if @include_more_by_authors
69
+
70
+ return types
71
+ end
72
+
73
+ def initialize(config)
74
+ #defaults
75
+ @display_name = "Scopus"
76
+ @registered_referer
77
+ @scopus_search_base = 'http://api.elsevier.com/content/search/index:SCOPUS'
78
+
79
+ @include_cited_by = true
80
+ @include_similar = true
81
+ @include_more_by_authors = true
82
+ @more_by_authors_type = "similar"
83
+
84
+ @inward_cited_by_url = "http://www.scopus.com/scopus/inward/citedby.url"
85
+ #@partner_id = "E5wmcMWC"
86
+ @partner_id = 65
87
+ @link_salt_key = nil
88
+ @scopus_release = "R6.0.0"
89
+
90
+ # Scopus offers two algorithms for finding similar items.
91
+ # This variable can be:
92
+ # "key" => keyword based similarity
93
+ # "ref" => reference based similiarity (cites similar refs?) Seems to offer 0 hits quite often, so we use keyword instead.
94
+ # "aut" => author. More docs by same authors. Incorporated as seperate link usually.
95
+ @more_like_this_type = "key"
96
+ @inward_more_like_url = "http://www.scopus.com/scopus/inward/mlt.url"
97
+
98
+ @credits = {
99
+ @display_name => "http://www.scopus.com/home.url"
100
+ }
101
+
102
+ super(config)
103
+ end
104
+
105
+ def xml_namespaces
106
+ @xml_namespaces ||=
107
+ { "atom" => "http://www.w3.org/2005/Atom",
108
+ "dc" => "http://purl.org/dc/elements/1.1/",
109
+ "opensearch" => "http://a9.com/-/spec/opensearch/1.1/",
110
+ "prism" => "http://prismstandard.org/namespaces/basic/2.0/"
111
+ }
112
+ end
113
+
114
+ def handle(request)
115
+ scopus_query = scopus_query(request)
116
+
117
+ # we can't make a good query, nevermind.
118
+ return request.dispatched(self, true) if scopus_query.blank?
119
+
120
+ url = scopus_url(scopus_query)
121
+
122
+
123
+ # Make the call.
124
+ headers = {"Accept" => "application/xml"}
125
+ headers["Referer"] = @registered_referer if @registered_referer
126
+
127
+ response = http_fetch(url, :headers => headers, :raise_on_http_error_code => false)
128
+
129
+ unless response.kind_of? Net::HTTPSuccess
130
+ # error, sometimes we have info in XML <service-error>
131
+ xml = begin
132
+ Nokogiri::XML(response.body)
133
+ rescue Exception
134
+ nil
135
+ end
136
+
137
+ code, message = nil, nil
138
+ if xml && error = xml.at_xpath("./service-error")
139
+ code = error.at_xpath("./status/statusCode")
140
+ message = error.at_xpath("./status/statusText")
141
+ end
142
+ e = StandardError.new("Scopus returned error: #{code}: #{message}: scopus query: #{url}")
143
+ return request.dispatched(self, DispatchedService::FailedFatal, e)
144
+ end
145
+
146
+ xml = Nokogiri::XML(response.body)
147
+
148
+ # Take the first hit from scopus's results, hope they relevancy ranked it
149
+ # well. For DOI/pmid search, there should ordinarly be only one hit!
150
+ first_hit = xml.at_xpath("//atom:entry[1]", xml_namespaces)
151
+
152
+ # Weirdly, a zero-hit result has one <atom:entry> containing an
153
+ # <atom:error> (Sic). Could other kinds of errors be reported that
154
+ # way too? Maybe. Better check just in case, ugh.
155
+ if first_hit && (error = first_hit.at_xpath("./atom:error", xml_namespaces))
156
+ scopus_message = error.text
157
+
158
+ if scopus_message == "Result set was empty"
159
+ # Just zero hits, no big deal, but nothing to do.
160
+ return request.dispatched(self, true)
161
+ else
162
+ # real error, log it.
163
+ e = StandardError.new("Scopus returned error: #{error.text}: scopus query: #{url}")
164
+ return request.dispatched(self, DispatchedService::FailedFatal, e)
165
+ end
166
+ end
167
+
168
+ if first_hit
169
+ if first_hit && (error = first_hit.at_xpath("./atom:error", xml_namespaces))
170
+ e = StandardError.new("Scopus returned error: #{error.text}")
171
+ return request.dispatched(self, DispatchedService::FailedFatal, e)
172
+ end
173
+
174
+ if (@include_cited_by)
175
+ try_add_cited_by_response(first_hit, request)
176
+ end
177
+
178
+ if (@include_similar)
179
+ url = more_like_this_url(first_hit)
180
+ # Pre-checking for actual hits not currently working, disabled.
181
+ if (true || ( hits = check_for_hits(url) ) > 0 )
182
+ request.add_service_response(
183
+ :service=>self,
184
+ :display_text => "#{hits} #{ServiceTypeValue[:similar].display_name_pluralize.downcase.capitalize}",
185
+ :url => url,
186
+ :service_type_value => :similar)
187
+ end
188
+ end
189
+
190
+ if ( @include_more_by_authors)
191
+ url = more_like_this_url(first_hit, :type => "aut")
192
+ # Pre-checking for actual hits not currently working, disabled.
193
+ if (true || ( hits = check_for_hits(url) ) > 0 )
194
+ request.add_service_response(
195
+ :service=>self,
196
+ :display_text => "#{hits} More from these authors",
197
+ :url => url,
198
+ :service_type_value => :similar)
199
+ end
200
+ end
201
+
202
+ end
203
+
204
+ return request.dispatched(self, true)
205
+ end
206
+
207
+
208
+ # Returns a scopus advanced search query intended to find the exact
209
+ # known item identified by this citation.
210
+ #
211
+ # NOT uri-escaped yet, make sure to uri-escape before putting it in a uri
212
+ # param!
213
+ #
214
+ # Will try to use DOI or PMID if available. Otherwise
215
+ # will use issn/year/vol/iss/start page if available.
216
+ # In some cases may resort to author/title.
217
+ def scopus_query(request)
218
+
219
+ if (doi = get_doi(request.referent))
220
+ return "DOI(#{phrase(doi)})"
221
+ elsif (pmid = get_pmid(request.referent))
222
+ return "PMID(#{phrase(pmid)})"
223
+ elsif (isbn = get_isbn(request.referent))
224
+ # I don't think scopus has a lot of ISBN-holding citations, but
225
+ # it allows search so we might as well try.
226
+ return "ISBN(#{phrase(isbn)})"
227
+ else
228
+ # Okay, we're going to try to do it on issn/vol/issue/page.
229
+ # If we don't have issn, we'll reluctantly use journal title
230
+ # (damn you google scholar).
231
+ metadata = request.referent.metadata
232
+ issn = request.referent.issn
233
+ if ( (issn || ! metadata['jtitle'].blank? ) &&
234
+ ! metadata['volume'].blank? &&
235
+ ! metadata['issue'].blank? &&
236
+ ! metadata['spage'].blank? )
237
+ query = "VOLUME(#{phrase(metadata['volume'])}) AND ISSUE(#{phrase(metadata['issue'])}) AND PAGEFIRST(#{phrase(metadata['spage'])}) "
238
+ if ( issn )
239
+ query += " AND (ISSN(#{phrase(issn)}) OR EISSN(#{phrase(issn)}))"
240
+ else
241
+ query += " AND EXACTSRCTITLE(#{phrase(metadata['jtitle'])})"
242
+ end
243
+ return query
244
+ end
245
+ end
246
+ return nil
247
+ end
248
+
249
+ def scopus_url(query)
250
+ "#{@scopus_search_base}?apiKey=#{CGI.escape @api_key}&query=#{CGI.escape query}"
251
+ end
252
+
253
+ # backslash escapes any double quotes, and embeds string in scopus
254
+ # phrase search double quotes. Does NOT uri-escape.
255
+ def phrase(str)
256
+ '"' + str.gsub('"', '\\"') + '"'
257
+ end
258
+
259
+ # Input is a ruby hash that came from the scopus JSON, representing
260
+ # a single hit. We're going to add this as a result.
261
+ def try_add_cited_by_response(result, request)
262
+ # While scopus provides an "inwardurl" in the results, this just takes
263
+ # us to the record detail page. We actually want to go RIGHT to the
264
+ # list of cited-by items. So we create our own, based on Scopus's
265
+ # reversed engineered predictable URLs.
266
+
267
+ count_str = result.at_xpath("atom:citedby-count/text()", xml_namespaces).to_s
268
+ count_i = count_str.to_i
269
+
270
+ return if count_i < 1
271
+
272
+ label = ServiceTypeValue[:cited_by].display_name_pluralize.downcase.capitalize
273
+ if count_i == 1
274
+ label = ServiceTypeValue[:cited_by].display_name.downcase.capitalize
275
+ end
276
+ cited_by_url = cited_by_url( result )
277
+
278
+ request.add_service_response(:service=>self,
279
+ :display_text => "#{count_str} #{label}",
280
+ :count=> count_i,
281
+ :url => cited_by_url,
282
+ :service_type_value => :cited_by)
283
+ end
284
+
285
+ def eid_from_hit(result)
286
+ result.at_xpath("atom:eid/text()", xml_namespaces).to_s
287
+ end
288
+
289
+ def cited_by_url(result)
290
+ eid = CGI.escape( eid_from_hit(result) )
291
+ #return "#{@scopus_cited_by_base}?eid=#{eid}&src=s&origin=recordpage"
292
+ # Use the new scopus direct link format!
293
+ return "#{@inward_cited_by_url}?partnerID=#{@partner_id}&rel=#{@scopus_release}&eid=#{eid}"
294
+ return
295
+ end
296
+
297
+ def more_like_this_url(result, options = {})
298
+ options[:type] ||= @more_like_this_type
299
+ eid = CGI.escape eid_from_hit(result)
300
+
301
+ return "#{@inward_more_like_url}?partnerID=#{@partner_id}&rel=#{@scopus_release}&eid=#{eid}&mltType=#{options[:type]}"
302
+ end
303
+
304
+ # NOT currently working. Scopus doesn't make this easy.
305
+ # Takes a scopus direct url for which we're not sure if there will be results
306
+ # or not, and requests it and html screen-scrapes to get hit count. (We
307
+ # can conveniently find this just in the html <title> at least).
308
+ # Works for cited_by and more_like_this searches at present.
309
+ # May break if Scopus changes their html title!
310
+ def check_for_hits(url)
311
+
312
+ response = http_fetch(url).body
313
+
314
+ response_html = Nokogiri::HTML(response)
315
+
316
+ title = response_xml.at('title').inner_text
317
+ # title is "X documents" (or 'Documents') if there are hits.
318
+ # It's annoyingly "Search Error" if there are either 0 hits, or
319
+ # if there was an actual error. So we can't easily log actual
320
+ # errors, sorry.
321
+ title.downcase =~ /^\s*(\d+)?\s+document/
322
+ if ( hits = $1)
323
+ return hits.to_i
324
+ else
325
+ return 0
326
+ end
327
+ end
328
+
329
+
330
+ end
@@ -0,0 +1,16 @@
1
+ <div class="help-text-icon">?</div>
2
+ <%= render 'section_heading', :presenter => renderer, :force_render => true %>
3
+
4
+ <ul class="response_list umlaut-help-list">
5
+ <% umlaut_config.feedback.contacts.each_pair do |key, config| %>
6
+ <li>
7
+ <% if config[:link_out] %>
8
+ <%# simply link out to somewhere outside of umlaut %>
9
+ <%= link_to config[:label], config[:link_out] %>
10
+ <% else %>
11
+ <%# link to our feedback form %>
12
+ <%= link_to config[:label], feedback_path(umlaut_request.id, :contact_id => key) %>
13
+ <% end %>
14
+ </li>
15
+ <% end %>
16
+ </ul>
@@ -0,0 +1,33 @@
1
+ <h1>Contact Us <small>Send us questions, problems, or comments</small></h1>
2
+
3
+ <%= form_tag(feedback_path(params[:request_id], :contact_id => params[:contact_id]) , :method => :post, :class => "feedback-form form-horizontal") do %>
4
+
5
+ <div class="control-group">
6
+ <label class="control-label" for="name">Your Name</label>
7
+ <div class="controls">
8
+ <input type="text" id="name" name="name">
9
+ </div>
10
+ </div>
11
+
12
+ <div class="control-group">
13
+ <label class="control-label" for="email">Your Email</label>
14
+ <div class="controls">
15
+ <input type="text" id="email" name="email">
16
+ </div>
17
+ </div>
18
+
19
+ <div class="control-group">
20
+ <label class="control-label" for="feedback">Question or Comment</label>
21
+ <div class="controls">
22
+ <textarea id="feedback" name="feedback" class="input-xxlarge" rows=6></textarea>
23
+ </div>
24
+ </div>
25
+
26
+
27
+ <div class="control-group">
28
+ <div class="controls">
29
+ <button type="submit" class="btn">Send</button>
30
+ </div>
31
+ </div>
32
+
33
+ <% end %>
@@ -0,0 +1,23 @@
1
+ ** User **
2
+
3
+ <%= @name %> <<%= @email %>>
4
+
5
+ ** User Comments **
6
+
7
+ <%= @feedback %>
8
+
9
+ ** Citation **
10
+
11
+ <%- if @umlaut_request -%>
12
+ <%= brief_citation(@umlaut_request, :include_labels => true) %>
13
+ <%= citation_identifiers(@umlaut_request) %>
14
+ <%- if @umlaut_request.referent.permalinks.present? -%>
15
+ Permalink: <%= url_for(:controller=>"store",
16
+ :id=> @umlaut_request.referent.permalinks.first.id,
17
+ :only_path => false, :host => @host) %>
18
+
19
+ Original Source: <%= @umlaut_request.referrer_id %>
20
+ <%- end -%>
21
+ <%- else -%>
22
+ No citation supplied.
23
+ <%- end -%>
@@ -14,6 +14,8 @@
14
14
  <%= render :partial => "umlaut/header" %>
15
15
 
16
16
  <div class="umlaut-main-container-fluid container-fluid <%= "umlaut-#{params[:controller]}-container" %>">
17
+ <%= render :partial => "umlaut/alerts" %>
18
+
17
19
  <%= yield %>
18
20
  </div>
19
21
 
@@ -0,0 +1,14 @@
1
+ <%-
2
+ text, css_class = nil, nil
3
+ [:alert_success, :alert_info, :alert_error].each do |key|
4
+ if flash[key]
5
+ text = flash[key]
6
+ css_class = key.to_s.parameterize
7
+ end
8
+ end
9
+ -%>
10
+ <% if text %>
11
+ <div class="alert <%= css_class %>">
12
+ <%= text %>
13
+ </div>
14
+ <% end %>
@@ -29,7 +29,14 @@ default:
29
29
  disabled: true
30
30
  display_name: Find It
31
31
  base_url: YOUR_SFX_BASE_URL
32
- priority: 3
32
+ priority: 3
33
+
34
+ # blind link to price comparison site AllBooks.com.
35
+ # just a blind link with no API pre-check, will be near
36
+ # instantaneous to calculate.
37
+ AllBooksDotCom:
38
+ type: AllBooksDotCom
39
+ priority: 3
33
40
 
34
41
 
35
42
  # First half of Amazon, run in foreground, get metadata and cover images.
@@ -134,13 +141,6 @@ default:
134
141
  type: UlrichsLink
135
142
  priority: d
136
143
 
137
- IsbnDb:
138
- display_name: isbndb.com
139
- disabled: true
140
- type: IsbnDb
141
- priority: d
142
- access_key: YOUR_ISBNDB_ACCESS_KEY
143
-
144
144
  EmailExport:
145
145
  type: EmailExport
146
146
  priority: d
data/lib/umlaut/routes.rb CHANGED
@@ -31,7 +31,7 @@ module Umlaut
31
31
  end
32
32
 
33
33
  def default_route_sets
34
- [:root, :permalinks, :a_z, :resolve, :open_search, :link_router, :export_email, :resources, :search, :javascript]
34
+ [:root, :permalinks, :a_z, :resolve, :open_search, :link_router, :export_email, :resources, :search, :javascript, :feedback]
35
35
  end
36
36
 
37
37
  module RouteSets
@@ -151,6 +151,13 @@ module Umlaut
151
151
  match 'images/spinner.gif' => redirect("/assets/spinner.gif")
152
152
  end
153
153
  end
154
+
155
+ def feedback
156
+ add_routes do |options|
157
+ get 'feedback(/:request_id)', :to => "feedback#new", :as => "feedback"
158
+ post 'feedback(/:request_id)' => 'feedback#create'
159
+ end
160
+ end
154
161
 
155
162
  def admin
156
163
  add_routes do |options|
@@ -1,3 +1,3 @@
1
1
  module Umlaut
2
- VERSION = "3.2.0"
2
+ VERSION = "3.3.0"
3
3
  end
@@ -0,0 +1,59 @@
1
+ require 'test_helper'
2
+
3
+ class FeedbackControllerTest < ActionController::TestCase
4
+ setup do
5
+ @umlaut_request = fake_umlaut_request("/resolve?sid=google&auinit=S&aulast=Madsbad&atitle=Mechanisms+of+changes+in+glucose+metabolism+and+bodyweight+after+bariatric+surgery&id=doi:10.1016/S2213-8587(13)70218-3&title=The+Lancet+Diabetes+%26+Endocrinology&volume=2&issue=2&date=2014&spage=152&issn=2213-8587")
6
+ @umlaut_request.save!
7
+
8
+ FeedbackController.umlaut_config.feedback ||= Confstruct::Configuration.new
9
+ FeedbackController.umlaut_config.feedback.contacts ||= Confstruct::Configuration.new
10
+ FeedbackController.umlaut_config.feedback.contacts[:example] = Confstruct::Configuration.new(
11
+ :email_address => "librarian@example.org",
12
+ :label => "Librarian"
13
+ )
14
+ end
15
+
16
+ test "display feedback form" do
17
+ get :new, :contact_id => "example"
18
+ assert_response :success
19
+ assert_template "feedback/new"
20
+ end
21
+
22
+ test "send feedback" do
23
+ assert_difference 'ActionMailer::Base.deliveries.size', +1 do
24
+ post :create, :contact_id => 'example', :name => "Joe Example", :email => "joe@example.org", :feedback => "Some feedback", :request_id => @umlaut_request.id
25
+ end
26
+ email = ActionMailer::Base.deliveries.last
27
+
28
+ assert_equal ["librarian@example.org"], email.to
29
+ assert_equal ["joe@example.org"], email.reply_to
30
+ assert_include email.body, "Some feedback"
31
+
32
+ assert_present flash[:alert_success]
33
+
34
+ assert_redirected_to(:controller => "resolve", :action => "index", :'umlaut.request_id' => @umlaut_request.id)
35
+ end
36
+
37
+ test "#create raises error on missing contact_id email" do
38
+ assert_raise(FeedbackController::NoFeedbackEmailFoundException) do
39
+ post :create, :name => "Joe Example", :email => "joe@example.org", :feedback => "Some feedback", :request_id => @umlaut_request.id
40
+ end
41
+
42
+ assert_raise(FeedbackController::NoFeedbackEmailFoundException) do
43
+ post :create, :contact_id => "NO_SUCH_ID", :name => "Joe Example", :email => "joe@example.org", :feedback => "Some feedback", :request_id => @umlaut_request.id
44
+ end
45
+ end
46
+
47
+ test "#new raises error on missing contact_id email" do
48
+ assert_raise(FeedbackController::NoFeedbackEmailFoundException) do
49
+ get :new
50
+ end
51
+
52
+ assert_raise(FeedbackController::NoFeedbackEmailFoundException) do
53
+ get :new, :contact_id => "NO_SUCH_ID"
54
+ end
55
+ end
56
+
57
+
58
+
59
+ end
data/test/test_helper.rb CHANGED
@@ -95,3 +95,16 @@ end
95
95
  def assert_length(size, list)
96
96
  assert_equal size, list.length, "Expected size of #{size} for #{list}"
97
97
  end
98
+
99
+
100
+ # Methods you can use to make a mocked up Rails Request and corersponding Umlaut Request
101
+ # Pass in a URL, absolute or partial, eg "/resolve?isbn=X"
102
+ def fake_rails_request(umlaut_url)
103
+ # hard to figure out how to mock a request, this seems to work
104
+ ActionController::TestRequest.new(Rack::MockRequest.env_for(umlaut_url))
105
+ end
106
+
107
+ def fake_umlaut_request(umlaut_url)
108
+ rails_request = fake_rails_request(umlaut_url)
109
+ Request.find_or_create(rails_request.params, {}, rails_request)
110
+ end
@@ -0,0 +1,57 @@
1
+
2
+
3
+ require 'test_helper'
4
+
5
+ class FeedbackMailerTest < ActionMailer::TestCase
6
+ setup do
7
+ ActionMailer::Base.deliveries.clear
8
+
9
+ @host = "umlaut.example.org"
10
+ @to_email = "librarian@example.org"
11
+ end
12
+
13
+ test "feedback" do
14
+ umlaut_request = fake_umlaut_request("/resolve?sid=google&auinit=S&aulast=Madsbad&atitle=Mechanisms+of+changes+in+glucose+metabolism+and+bodyweight+after+bariatric+surgery&id=doi:10.1016/S2213-8587(13)70218-3&title=The+Lancet+Diabetes+%26+Endocrinology&volume=2&issue=2&date=2014&spage=152&issn=2213-8587")
15
+
16
+ feedback_name = "Joe Example"
17
+ feedback_email = "joe@example.org"
18
+ feedback_text = "This is my feedback, yes it is"
19
+
20
+ email = FeedbackMailer.feedback(@host, @to_email, :umlaut_request => umlaut_request, :name => feedback_name, :email => feedback_email, :feedback => feedback_text).deliver
21
+
22
+ assert ActionMailer::Base.deliveries.present?
23
+ assert_equal [UmlautController.umlaut_config.from_email_addr], email.from
24
+ assert_equal [feedback_email], email.reply_to
25
+ assert_equal [@to_email], email.to
26
+ assert_equal "#{UmlautController.umlaut_config.app_name} Feedback: #{feedback_name}", email.subject
27
+
28
+ assert_includes email.body, feedback_text
29
+
30
+ assert_includes email.body, "#{feedback_name} <#{feedback_email}>"
31
+ # citation
32
+ assert_includes email.body, "Mechanisms of changes in glucose metabolism and bodyweight"
33
+ assert_includes email.body, "Madsbad"
34
+ assert_includes email.body, "The Lancet Diabetes & Endocrinology"
35
+ assert_includes email.body, "Published: 2014 Vol: 2 Iss: 2 p. 152"
36
+ assert_includes email.body, "ISSN: 22138587"
37
+ assert_includes email.body, "10.1016/S2213-8587(13)70218-3" # doi
38
+
39
+ assert_includes email.body, "Original Source: #{umlaut_request.referrer_id}"
40
+
41
+ # Permalink properly generated and included
42
+ assert_present umlaut_request.referent.permalinks
43
+ assert_includes email.body, "Permalink: http"
44
+ end
45
+
46
+ test "feedback with no umlaut_request" do
47
+ feedback_name = "Joe Example"
48
+ feedback_email = "joe@example.org"
49
+ feedback_text = "This is my feedback, yes it is"
50
+
51
+ email = FeedbackMailer.feedback(@host, @to_email, :name => feedback_name, :email => feedback_email, :feedback => feedback_text).deliver
52
+ # just no raise is good enough for this test for now, mostly
53
+
54
+ assert_includes email.body, "No citation supplied"
55
+ end
56
+
57
+ end