umlaut 3.0.0alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. data/LICENSE +7 -0
  2. data/README.md +49 -0
  3. data/Rakefile +37 -0
  4. data/app/assets/images/error.gif +0 -0
  5. data/app/assets/images/export_bg_bot.gif +0 -0
  6. data/app/assets/images/export_bg_mid.gif +0 -0
  7. data/app/assets/images/export_bg_top.gif +0 -0
  8. data/app/assets/images/famfamfam/book_open.png +0 -0
  9. data/app/assets/images/famfamfam/cross.png +0 -0
  10. data/app/assets/images/famfamfam/page_sound.gif +0 -0
  11. data/app/assets/images/famfamfam/page_text.gif +0 -0
  12. data/app/assets/images/famfamfam/page_up.gif +0 -0
  13. data/app/assets/images/famfamfam/page_white.png +0 -0
  14. data/app/assets/images/famfamfam/readme.html +1495 -0
  15. data/app/assets/images/famfamfam/tiny_cross.png +0 -0
  16. data/app/assets/images/frame_remove.gif +0 -0
  17. data/app/assets/images/ico_go.gif +0 -0
  18. data/app/assets/images/jhu_findit.gif +0 -0
  19. data/app/assets/images/list_closed.png +0 -0
  20. data/app/assets/images/list_open.png +0 -0
  21. data/app/assets/images/more_info.gif +0 -0
  22. data/app/assets/images/rails.png +0 -0
  23. data/app/assets/images/request.gif +0 -0
  24. data/app/assets/images/spinner.gif +0 -0
  25. data/app/assets/javascripts/umlaut/ajax_windows.js +35 -0
  26. data/app/assets/javascripts/umlaut/ensure_window_size.js.erb +34 -0
  27. data/app/assets/javascripts/umlaut/expand_contract_toggle.js +25 -0
  28. data/app/assets/javascripts/umlaut/search_autocomplete.js +46 -0
  29. data/app/assets/javascripts/umlaut/simple_visible_toggle.js +8 -0
  30. data/app/assets/javascripts/umlaut/update_html.js +152 -0
  31. data/app/assets/javascripts/umlaut.js +17 -0
  32. data/app/assets/stylesheets/umlaut.css +857 -0
  33. data/app/controllers/application_controller.rb +14 -0
  34. data/app/controllers/export_email_controller.rb +123 -0
  35. data/app/controllers/js_helper_controller.rb +10 -0
  36. data/app/controllers/link_router_controller.rb +87 -0
  37. data/app/controllers/open_search_controller.rb +9 -0
  38. data/app/controllers/resolve_controller.rb +288 -0
  39. data/app/controllers/resource_controller.rb +83 -0
  40. data/app/controllers/search_controller.rb +328 -0
  41. data/app/controllers/search_methods/sfx3.rb +148 -0
  42. data/app/controllers/search_methods/sfx4.rb +257 -0
  43. data/app/controllers/search_methods/sfx_api.rb +47 -0
  44. data/app/controllers/store_controller.rb +64 -0
  45. data/app/controllers/umlaut/controller_behavior.rb +20 -0
  46. data/app/controllers/umlaut/controller_logic.rb +96 -0
  47. data/app/controllers/umlaut/error_handling.rb +48 -0
  48. data/app/controllers/umlaut_controller.rb +112 -0
  49. data/app/helpers/application_helper.rb +4 -0
  50. data/app/helpers/emailer_helper.rb +43 -0
  51. data/app/helpers/export_email_helper.rb +34 -0
  52. data/app/helpers/open_search_helper.rb +7 -0
  53. data/app/helpers/resolve_helper.rb +225 -0
  54. data/app/helpers/search_helper.rb +50 -0
  55. data/app/helpers/umlaut/footer_helper.rb +64 -0
  56. data/app/helpers/umlaut/helper.rb +62 -0
  57. data/app/helpers/umlaut/html_head_helper.rb +37 -0
  58. data/app/helpers/umlaut/url_generation.rb +77 -0
  59. data/app/mailers/emailer.rb +48 -0
  60. data/app/models/clickthrough.rb +2 -0
  61. data/app/models/collection.rb +259 -0
  62. data/app/models/crossref_lookup.rb +2 -0
  63. data/app/models/dispatched_service.rb +58 -0
  64. data/app/models/permalink.rb +29 -0
  65. data/app/models/referent.rb +473 -0
  66. data/app/models/referent_value.rb +14 -0
  67. data/app/models/request.rb +449 -0
  68. data/app/models/service_response.rb +179 -0
  69. data/app/models/service_store.rb +59 -0
  70. data/app/models/service_type_value.rb +58 -0
  71. data/app/models/service_wave.rb +150 -0
  72. data/app/models/sfx_db/az_additional_title.rb +11 -0
  73. data/app/models/sfx_db/az_letter_group.rb +11 -0
  74. data/app/models/sfx_db/az_title.rb +38 -0
  75. data/app/models/sfx_db/az_title_v2.rb +34 -0
  76. data/app/models/sfx_db/isbn.rb +12 -0
  77. data/app/models/sfx_db/issn.rb +12 -0
  78. data/app/models/sfx_db/object.rb +35 -0
  79. data/app/models/sfx_db/object_portfolio.rb +6 -0
  80. data/app/models/sfx_db/publisher.rb +10 -0
  81. data/app/models/sfx_db/sfx_db_base.rb +54 -0
  82. data/app/models/sfx_db/target.rb +9 -0
  83. data/app/models/sfx_db/target_service.rb +10 -0
  84. data/app/models/sfx_db/title.rb +10 -0
  85. data/app/models/sfx_db.rb +10 -0
  86. data/app/models/sfx_url.rb +35 -0
  87. data/app/views/emailer/citation.text.erb +28 -0
  88. data/app/views/emailer/short_citation.text.erb +8 -0
  89. data/app/views/export_email/_email.html.erb +25 -0
  90. data/app/views/export_email/_send_email.html.erb +3 -0
  91. data/app/views/export_email/_send_txt.html.erb +3 -0
  92. data/app/views/export_email/_txt.html.erb +62 -0
  93. data/app/views/export_email/email.html.erb +3 -0
  94. data/app/views/export_email/send_email.html.erb +1 -0
  95. data/app/views/export_email/send_txt.html.erb +1 -0
  96. data/app/views/export_email/txt.html.erb +3 -0
  97. data/app/views/js_helper/loader.erb.js +13 -0
  98. data/app/views/layouts/umlaut.html.erb +52 -0
  99. data/app/views/open_search/index.html.erb +9 -0
  100. data/app/views/resolve/_api_in_progress.xml.erb +21 -0
  101. data/app/views/resolve/_background_progress.html.erb +51 -0
  102. data/app/views/resolve/_background_updater.html.erb +38 -0
  103. data/app/views/resolve/_citation.html.erb +87 -0
  104. data/app/views/resolve/_coins.html.erb +1 -0
  105. data/app/views/resolve/_compact_citation.html.erb +33 -0
  106. data/app/views/resolve/_cover_image.html.erb +35 -0
  107. data/app/views/resolve/_fulltext.html.erb +55 -0
  108. data/app/views/resolve/_help.html.erb +17 -0
  109. data/app/views/resolve/_holding.html.erb +91 -0
  110. data/app/views/resolve/_related_items.html.erb +35 -0
  111. data/app/views/resolve/_search_inside.html.erb +62 -0
  112. data/app/views/resolve/_section_display.html.erb +49 -0
  113. data/app/views/resolve/_service_errors.html.erb +29 -0
  114. data/app/views/resolve/_standard_response_item.html.erb +89 -0
  115. data/app/views/resolve/api.xml.builder +72 -0
  116. data/app/views/resolve/background_status.html.erb +26 -0
  117. data/app/views/resolve/index.html.erb +73 -0
  118. data/app/views/resolve/partial_html_sections.xml.erb +30 -0
  119. data/app/views/search/_a_to_z.html.erb +6 -0
  120. data/app/views/search/_citation.html.erb +94 -0
  121. data/app/views/search/_pager.html.erb +60 -0
  122. data/app/views/search/books.html.erb +103 -0
  123. data/app/views/search/journal_search.html.erb +90 -0
  124. data/app/views/search/journals.html.erb +167 -0
  125. data/app/views/search/opensearch_description.rxml +10 -0
  126. data/app/views/testing/index.html.erb +1 -0
  127. data/app/views/umlaut/README +5 -0
  128. data/app/views/umlaut/error.html.erb +45 -0
  129. data/db/migrate/01_umlaut_init.rb +113 -0
  130. data/db/orig_fixed_data/service_type_values.yml +120 -0
  131. data/db/seeds.rb +7 -0
  132. data/lib/CronTab.rb +192 -0
  133. data/lib/aws_product_sign.rb +146 -0
  134. data/lib/exlibris/aleph/patron.rb +64 -0
  135. data/lib/exlibris/aleph/record.rb +54 -0
  136. data/lib/exlibris/aleph/rest_api.rb +29 -0
  137. data/lib/exlibris/primo/holding.rb +192 -0
  138. data/lib/exlibris/primo/rsrc.rb +17 -0
  139. data/lib/exlibris/primo/searcher.rb +276 -0
  140. data/lib/exlibris/primo/source/aleph.rb +46 -0
  141. data/lib/exlibris/primo/source/distribution/nyu_aleph.rb +323 -0
  142. data/lib/exlibris/primo/toc.rb +17 -0
  143. data/lib/exlibris/primo_ws.rb +140 -0
  144. data/lib/generators/templates/umlaut_services.yml +237 -0
  145. data/lib/generators/umlaut/asset_hooks_generator.rb +44 -0
  146. data/lib/generators/umlaut/install_generator.rb +110 -0
  147. data/lib/hip3/bib.rb +291 -0
  148. data/lib/hip3/bib_searcher.rb +302 -0
  149. data/lib/hip3/custom_field_lookup.rb +44 -0
  150. data/lib/hip3/holding.rb +50 -0
  151. data/lib/hip3/item.rb +65 -0
  152. data/lib/hip3/receipt.rb +7 -0
  153. data/lib/hip3/serial_copy.rb +82 -0
  154. data/lib/holding.rb +32 -0
  155. data/lib/marc_helper.rb +254 -0
  156. data/lib/metadata_helper.rb +312 -0
  157. data/lib/opensearch_feed.rb +398 -0
  158. data/lib/opensearch_query.rb +98 -0
  159. data/lib/referent_filter.rb +16 -0
  160. data/lib/referent_filters/dissertation_catch.rb +45 -0
  161. data/lib/section_renderer.rb +503 -0
  162. data/lib/service.rb +336 -0
  163. data/lib/service_adaptors/ajax_export.rb +37 -0
  164. data/lib/service_adaptors/amazon.rb +412 -0
  165. data/lib/service_adaptors/blacklight.rb +327 -0
  166. data/lib/service_adaptors/book_finder.rb +40 -0
  167. data/lib/service_adaptors/bx.rb +51 -0
  168. data/lib/service_adaptors/cover_thing.rb +73 -0
  169. data/lib/service_adaptors/elsevier_cover.rb +57 -0
  170. data/lib/service_adaptors/email_export.rb +10 -0
  171. data/lib/service_adaptors/ezproxy.rb +171 -0
  172. data/lib/service_adaptors/google_book_search.rb +442 -0
  173. data/lib/service_adaptors/gpo.rb +124 -0
  174. data/lib/service_adaptors/hathi_trust.rb +308 -0
  175. data/lib/service_adaptors/hip3_service.rb +150 -0
  176. data/lib/service_adaptors/hip_holding_search.rb +237 -0
  177. data/lib/service_adaptors/internet_archive.rb +488 -0
  178. data/lib/service_adaptors/isbn_db.rb +86 -0
  179. data/lib/service_adaptors/isi.rb +258 -0
  180. data/lib/service_adaptors/jcr.rb +146 -0
  181. data/lib/service_adaptors/opac.rb +351 -0
  182. data/lib/service_adaptors/open_library.rb +316 -0
  183. data/lib/service_adaptors/open_library_cover.rb +73 -0
  184. data/lib/service_adaptors/primo_service.rb +392 -0
  185. data/lib/service_adaptors/primo_source.rb +78 -0
  186. data/lib/service_adaptors/pubmed.rb +133 -0
  187. data/lib/service_adaptors/request_to_fixture.rb +68 -0
  188. data/lib/service_adaptors/scopus.rb +295 -0
  189. data/lib/service_adaptors/sfx-new.rb +557 -0
  190. data/lib/service_adaptors/sfx.rb +566 -0
  191. data/lib/service_adaptors/sfx_backchannel_record.rb +69 -0
  192. data/lib/service_adaptors/txt_holding_export.rb +32 -0
  193. data/lib/service_adaptors/ulrichs_cover.rb +57 -0
  194. data/lib/service_adaptors/ulrichs_link.rb +47 -0
  195. data/lib/service_adaptors/worldcat.rb +116 -0
  196. data/lib/service_adaptors/worldcat_identities.rb +591 -0
  197. data/lib/tasks/umlaut.rake +134 -0
  198. data/lib/umlaut/default_configuration.rb +5 -0
  199. data/lib/umlaut/routes.rb +136 -0
  200. data/lib/umlaut/version.rb +3 -0
  201. data/lib/umlaut.rb +37 -0
  202. data/lib/umlaut_configurable.rb +343 -0
  203. data/lib/umlaut_http.rb +100 -0
  204. data/lib/xml_schema_helper.rb +109 -0
  205. data/test/dummy/Rakefile +7 -0
  206. data/test/dummy/app/assets/javascripts/application.js +13 -0
  207. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  208. data/test/dummy/app/controllers/application_controller.rb +3 -0
  209. data/test/dummy/app/controllers/umlaut_controller.rb +112 -0
  210. data/test/dummy/app/helpers/application_helper.rb +2 -0
  211. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  212. data/test/dummy/config/application.rb +45 -0
  213. data/test/dummy/config/boot.rb +10 -0
  214. data/test/dummy/config/database-jhu.yml +44 -0
  215. data/test/dummy/config/database.yml +25 -0
  216. data/test/dummy/config/environment.rb +5 -0
  217. data/test/dummy/config/environments/development.rb +34 -0
  218. data/test/dummy/config/environments/production.rb +60 -0
  219. data/test/dummy/config/environments/test.rb +39 -0
  220. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  221. data/test/dummy/config/initializers/inflections.rb +10 -0
  222. data/test/dummy/config/initializers/mime_types.rb +5 -0
  223. data/test/dummy/config/initializers/secret_token.rb +7 -0
  224. data/test/dummy/config/initializers/session_store.rb +8 -0
  225. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  226. data/test/dummy/config/locales/en.yml +5 -0
  227. data/test/dummy/config/routes.rb +61 -0
  228. data/test/dummy/config/umlaut_services.yml +237 -0
  229. data/test/dummy/config.ru +4 -0
  230. data/test/dummy/db/migrate/20111228211210_umlaut_init.rb +113 -0
  231. data/test/dummy/db/schema.rb +124 -0
  232. data/test/dummy/log/development.log +12981 -0
  233. data/test/dummy/log/production.log +0 -0
  234. data/test/dummy/public/404.html +26 -0
  235. data/test/dummy/public/422.html +26 -0
  236. data/test/dummy/public/500.html +26 -0
  237. data/test/dummy/public/favicon.ico +0 -0
  238. data/test/dummy/script/rails +6 -0
  239. data/test/dummy/tmp/cache/assets/C5F/340/sprockets%2F99692920160b7a279b86a80415b79db7 +0 -0
  240. data/test/dummy/tmp/cache/assets/C70/4D0/sprockets%2F034ad2036e623081bd352800786dfe80 +0 -0
  241. data/test/dummy/tmp/cache/assets/C73/920/sprockets%2Fd371318f22900492fd180f17c5e2a504 +9268 -0
  242. data/test/dummy/tmp/cache/assets/C80/980/sprockets%2Fc94807409c1523d43e18d25f35d93c41 +0 -0
  243. data/test/dummy/tmp/cache/assets/C8F/780/sprockets%2Fe47e28558116fb5f8038754e60d1961d +11769 -0
  244. data/test/dummy/tmp/cache/assets/CAA/EB0/sprockets%2F1d179210e8b76f1ea63c802688a015e4 +9271 -0
  245. data/test/dummy/tmp/cache/assets/CBB/9C0/sprockets%2F706f28923fb754cad04b9107c89986a1 +0 -0
  246. data/test/dummy/tmp/cache/assets/CBF/B60/sprockets%2F08ca89671549936265dcb673bf02e36f +0 -0
  247. data/test/dummy/tmp/cache/assets/CC9/9F0/sprockets%2F306166316e2cafd13c15e62b51a2339d +0 -0
  248. data/test/dummy/tmp/cache/assets/CF6/F20/sprockets%2F5b2ffa1103079dfd555197838f87a99f +0 -0
  249. data/test/dummy/tmp/cache/assets/CF7/2B0/sprockets%2F25a7c73655bd3598173b39d9f98bcd46 +862 -0
  250. data/test/dummy/tmp/cache/assets/CFE/080/sprockets%2F37fe9f4255baddbd549a659914929398 +0 -0
  251. data/test/dummy/tmp/cache/assets/D22/060/sprockets%2F9aec77b768e91a802d284271c58e2f7e +21357 -0
  252. data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  253. data/test/dummy/tmp/cache/assets/D33/6D0/sprockets%2F500129c57f1146e556ec3aacd6cd38c1 +0 -0
  254. data/test/dummy/tmp/cache/assets/D33/FD0/sprockets%2F2ba0b4e6334a77b923e5f770381bb2bf +0 -0
  255. data/test/dummy/tmp/cache/assets/D42/C20/sprockets%2Fbcf14e437b1582bf93b77670acf8e090 +21353 -0
  256. data/test/dummy/tmp/cache/assets/D50/A30/sprockets%2F7d8b294ac433db5d056538f8cf7c66b9 +0 -0
  257. data/test/dummy/tmp/cache/assets/D54/ED0/sprockets%2F71c9fa01091d432b131da3bb73faf3d4 +872 -0
  258. data/test/dummy/tmp/cache/assets/D65/590/sprockets%2Fc1bb92fc3406a126b7dd302edc96d629 +0 -0
  259. data/test/dummy/tmp/cache/assets/D71/6B0/sprockets%2Fde558b71b494cf09b1bf055c8dff0353 +0 -0
  260. data/test/dummy/tmp/cache/assets/D72/610/sprockets%2Fa8c708eeb30ef93de34d755d4f45d023 +859 -0
  261. data/test/dummy/tmp/cache/assets/D76/AD0/sprockets%2Fe2158cde93188cf5ab6457bc6d6602ec +0 -0
  262. data/test/dummy/tmp/cache/assets/D7A/E40/sprockets%2F9622ffcc499a57627cd1bb18fe31b8e4 +11772 -0
  263. data/test/dummy/tmp/cache/assets/D84/210/sprockets%2Fabd0103ccec2b428ac62c94e4c40b384 +0 -0
  264. data/test/dummy/tmp/cache/assets/D9B/770/sprockets%2F8aacf02eb7dbb0949704b28f27b87e0b +0 -0
  265. data/test/dummy/tmp/cache/assets/DA6/A80/sprockets%2F92e26d8e58d5bcc8b8f6c25d1b05b9c1 +0 -0
  266. data/test/dummy/tmp/cache/assets/DE8/790/sprockets%2Fd1333bde2b9aafcc712d11dd09ab35d8 +0 -0
  267. data/test/dummy/tmp/cache/assets/DF7/F30/sprockets%2F7bc16c4109b17fabe29f8ddbbf732d1c +374 -0
  268. data/test/dummy/tmp/cache/assets/E03/570/sprockets%2F493bdc0ac14cd4f57fdfe4253f992bde +0 -0
  269. data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  270. data/test/dummy/tmp/cache/assets/E0B/4B0/sprockets%2F7988df51a61c81ce6ede4a2d4c8cce4f +377 -0
  271. data/test/dummy/tmp/cache/assets/E5F/960/sprockets%2Fdc007b6cad5c7ef08e33ec28cfff0ef6 +0 -0
  272. data/test/fixtures/dispatched_services.yml +5 -0
  273. data/test/fixtures/permalinks.yml +5 -0
  274. data/test/fixtures/referent_values.yml +1734 -0
  275. data/test/fixtures/referents.yml +156 -0
  276. data/test/fixtures/requests.yml +284 -0
  277. data/test/fixtures/service_responses.yml +5 -0
  278. data/test/fixtures/sfx_urls.yml +4 -0
  279. data/test/performance/browsing_test.rb +9 -0
  280. data/test/test_helper.rb +10 -0
  281. data/test/umlaut_test.rb +7 -0
  282. data/test/unit/aleph_patron_test.rb +39 -0
  283. data/test/unit/aleph_record_benchmarks.rb +28 -0
  284. data/test/unit/aleph_record_test.rb +30 -0
  285. data/test/unit/aws_product_sign_test.rb +93 -0
  286. data/test/unit/collection_test.rb +76 -0
  287. data/test/unit/google_book_search_test.rb +101 -0
  288. data/test/unit/primo_searcher_test.rb +403 -0
  289. data/test/unit/primo_service_test.rb +939 -0
  290. data/test/unit/primo_ws_test.rb +131 -0
  291. data/test/unit/service_response_test.rb +9 -0
  292. data/test/unit/service_test.rb +33 -0
  293. metadata +580 -0
@@ -0,0 +1,78 @@
1
+ # == Overview
2
+ # PrimoSource is a PrimoService that converts primo_source service types into Primo source holdings.
3
+ # This mechanism allows linking to original data sources and expanded holdings information
4
+ # based on those sources and can be implemented per source.
5
+ # To create a Primo source holding, you first must create a local class representing the source in
6
+ # module Exlibris::Primo::Source::Local which extends Exlibris::Primo::Holding.
7
+ # Two methods are then available for overriding:
8
+ # :expand - expand holdings that may have been collapsed into a single availlibrary element
9
+ # in Primo based on information from the source
10
+ # default: [self]
11
+ # :dedup? - if this data source contain duplicate holdings that need to be deduped, set to true
12
+ # default: false
13
+ # The following properties can also be overridden in the initialize method
14
+ # :record_id, :source_id, :original_source_id, :source_record_id,
15
+ # :availlibrary, :institution_code, :institution, :library_code, :library,
16
+ # :status_code, :status, :id_one, :id_two, :origin, :display_type, :coverage, :notes,
17
+ # :url, :request_url, :match_reliability, :request_link_supports_ajax_call, :source_data
18
+ # PrimoSources are not for everyone as they require programming but they do allow further customization
19
+ # and functionality as necessary.
20
+ #
21
+ # == Further Documentation
22
+ # Exlibris::Primo::Holding provides further documentation related to creating local sources.
23
+ #
24
+ # ==Examples
25
+ # Two examples of customized sources are:
26
+ # * Exlibris::Primo::Source::Aleph
27
+ # * Exlibris::Primo::Source::Local::NYUAleph
28
+ class PrimoSource < PrimoService
29
+
30
+ # Overwrites PrimoService#new.
31
+ def initialize(config)
32
+ @service_types = ["holding"]
33
+ super(config)
34
+ end
35
+
36
+ # Overwrites PrimoService#handle.
37
+ def handle(request)
38
+ primo_sources = request.get_service_type('primo_source', {})
39
+ sources_seen = Array.new # for de-duplicating holdings from catalog.
40
+ primo_sources.each do |primo_source|
41
+ source = primo_source.view_data
42
+ # There are some cases where source records may need to be de-duplicated against existing records
43
+ # Check if we've already seen this record.
44
+ seen_sources_key = source.source_id.to_s + source.source_record_id.to_s
45
+ next if source.dedup? and sources_seen.include?(seen_sources_key)
46
+ # If we get this far, record that we've seen this holding.
47
+ sources_seen.push(seen_sources_key)
48
+ # There may be multiple holdings mapped to one availlibrary here,
49
+ # so we get the additional holdings and add them.
50
+ source.expand.each do |holding|
51
+ service_data = {}
52
+ @holding_attributes.each do |attr|
53
+ service_data[attr] = holding.method(attr).call
54
+ end
55
+ service_data.merge!({
56
+ :call_number => holding.call_number, :collection => holding.collection,
57
+ :collection_str => "#{holding.library} #{holding.collection}",
58
+ :coverage_str => holding.coverage.join("<br />"),
59
+ :coverage_str_array => holding.coverage,
60
+ # :expired determines whether we show the holding in this service
61
+ # Since this is fresh, the data has not yet expired.
62
+ :expired => false,
63
+ # :latest determines whether we show the holding in other services, e.g. txt and email.
64
+ # It persists for one more cycle than :expired so services that run after
65
+ # this one, but in the same resolution request have access to the latest holding data.
66
+ :latest => true
67
+ })
68
+ request.add_service_response(
69
+ service_data.merge(
70
+ :service=>self,
71
+ :service_type_value => "holding"
72
+ )
73
+ )
74
+ end
75
+ end
76
+ return request.dispatched(self, true)
77
+ end
78
+ end
@@ -0,0 +1,133 @@
1
+ # Looks up pmid from NLM api, and enhances referent with citation data.
2
+ #
3
+ # If you use SFX, you prob don't need/want this, as SFX does this already.
4
+ class Pubmed < Service
5
+ require 'uri'
6
+ require 'net/http'
7
+ require 'nokogiri'
8
+
9
+ include MetadataHelper
10
+
11
+ def initialize(config)
12
+ @display_name = "PubMed"
13
+ @url = 'http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi'
14
+ super(config)
15
+ end
16
+
17
+ def handle(request)
18
+ return request.dispatched(self, true) unless pmid = get_pmid(request.referent)
19
+
20
+ return request.dispatched(self, false) unless response = self.fetch_record(pmid)
21
+
22
+ self.enhance_referent(response, request)
23
+
24
+ return request.dispatched(self, true)
25
+ end
26
+
27
+ def service_types_generated
28
+ @service_types ||= [ServiceTypeValue["referent_enhance"]]
29
+ end
30
+
31
+
32
+ # Do the request. Takes the PMID as inputs
33
+ def fetch_record(pmid)
34
+ pmid_url = self.url + "?db=pubmed&retmode=xml&rettype=full&id="+pmid
35
+ begin
36
+ response = Net::HTTP.get_response(URI.parse(pmid_url))
37
+ rescue
38
+ return false
39
+ end
40
+ return false if response.body.match("<ERROR>Empty id list - nothing todo</ERROR>")
41
+ return response.body
42
+ end
43
+
44
+ # Pull everything useful out of the Pubmed record
45
+ def enhance_referent(body, request)
46
+ doc = Nokogiri::XML(body)
47
+ return unless cite = doc.at("PubmedArticleSet/PubmedArticle/MedlineCitation") # Nothing of interest here
48
+
49
+ return unless article = cite.at("Article") # No more useful metadata
50
+ if abstract = article.at("Abstract/AbstractText")
51
+ request.add_service_response(
52
+ :service=>self,
53
+ :display_text => "Abstract from #{@display_name}",
54
+ :content => abstract.inner_text,
55
+ :service_type_value => 'abstract') unless abstract.inner_text.blank?
56
+ end
57
+
58
+ if journal = article.at("Journal")
59
+ if issn = journal.at('ISSN')
60
+ if issn.attributes['issntype']=="Print"
61
+ request.referent.enhance_referent('issn', issn.inner_html)
62
+ else
63
+ request.referent.enhance_referent('eissn', issn.inner_html)
64
+ end
65
+ end
66
+ if jrnlissue = journal.at('JournalIssue')
67
+ if volume = jrnlissue.at('Volume')
68
+ request.referent.enhance_referent('volume', volume.inner_text)
69
+ end
70
+ if issue = jrnlissue.at('Issue')
71
+ request.referent.enhance_referent('issue', issue.inner_text)
72
+ end
73
+ if date = jrnlissue.at('PubDate')
74
+
75
+ request.referent.enhance_referent('date', openurl_date(date))
76
+
77
+ end
78
+ end
79
+
80
+ if jtitle = journal.at('Title')
81
+ request.referent.enhance_referent('jtitle', jtitle.inner_text)
82
+ end
83
+ if stitle = journal.at('ISOAbbreviation')
84
+ request.referent.enhance_referent('stitle', stitle.inner_text)
85
+ end
86
+
87
+ if atitle = article.at('ArticleTitle')
88
+ request.referent.enhance_referent('atitle', atitle.inner_text)
89
+ end
90
+
91
+ if pages = article.at('Pagination/MedlinePgn')
92
+ page_str = pages.inner_text
93
+ request.referent.enhance_referent('pages', page_str)
94
+ if spage = page_str.split("-")[0]
95
+ request.referent.enhance_referent('spage', spage.strip)
96
+ end
97
+ end
98
+
99
+ if author = article.at('AuthorList/Author')
100
+ if last_name = author.at('LastName')
101
+ request.referent.enhance_referent('aulast', last_name.inner_text)
102
+ end
103
+ if first_name = author.at('ForeName')
104
+ request.referent.enhance_referent('aufirst', first_name.inner_text)
105
+ end
106
+ if initials = author.at('Initials')
107
+ request.referent.enhance_referent('auinit', initials.inner_text)
108
+ end
109
+ end
110
+ end
111
+
112
+ end
113
+
114
+ # input a PubMed <PubDate> element, return
115
+ # a string usable as rft.date yyyymmdd
116
+ def openurl_date(date_xml)
117
+ date = ""
118
+
119
+ if y = date_xml.at("Year")
120
+ date << ("%04d" % y.inner_text.strip[0,4].to_i )
121
+ if m = date_xml.at("Month")
122
+ # Month name to number
123
+ date << ( "%02d" % DateTime.parse(m.inner_text.strip).month )
124
+ if d = date_xml.at("Day")
125
+ date << ("%02d" % d.inner_text.strip[0,2].to_i)
126
+ end
127
+ end
128
+ end
129
+
130
+ return date
131
+ end
132
+
133
+ end
@@ -0,0 +1,68 @@
1
+ # This service helps create fixture data for testing purposes.
2
+ # It really just writes out to Yaml the Request (attributes only), Referent,
3
+ # and ReferentValues.
4
+ # The Request, Referent and ReferentValues must be cut and pasted into the
5
+ # relevant fixture files to be used in testing.
6
+
7
+
8
+ class RequestToFixture < Service
9
+ required_config_params :file
10
+ attr_reader :file
11
+
12
+ def service_types_generated
13
+ end
14
+
15
+ def initialize(config)
16
+ super(config)
17
+ end
18
+
19
+ def handle(request)
20
+ final_string = ''
21
+ fh = File.open(@file, 'a')
22
+ dump_request(request, final_string)
23
+ dump_referent_values(request, final_string)
24
+
25
+ cleanup(final_string)
26
+ fh.puts final_string
27
+ fh.close
28
+ return request.dispatched(self, true)
29
+ end
30
+
31
+ def dump_request(request, string)
32
+ #YAML.dump(request, fh)
33
+ dump_proper(request, string, 'request')
34
+ put_cutline(string)
35
+ dump_proper(request.referent, string, 'referent')
36
+ put_cutline(string)
37
+ end
38
+
39
+ def dump_referent_values(request, string)
40
+ referent_values = request.referent.referent_values.sort_by{|rv| rv.id}
41
+ referent_values.each do |rv|
42
+ dump_proper(rv, string, 'referent_value')
43
+ end
44
+ put_cutline(string)
45
+ end
46
+
47
+ def dump_proper(data, string, type)
48
+ values = {}
49
+ data.attributes.each do |var, val|
50
+ values[var] = val
51
+ end
52
+ fixture = {}
53
+ fixture[type + '_' + data.id.to_s] = values
54
+ string << YAML.dump(fixture)
55
+
56
+ end
57
+
58
+ def put_cutline(string)
59
+ string << "\n-------------CUT HERE----------------\n"
60
+ end
61
+
62
+ # removes lines that only contain three dashes. These mess up our fixtures.
63
+ def cleanup(string)
64
+ string.gsub!(/^--- $/, "")
65
+ end
66
+
67
+
68
+ end
@@ -0,0 +1,295 @@
1
+ # Service adapter plug-in.
2
+ #
3
+ # PURPOSE: Includes "cited by", "similar articles" and "more by these authors"
4
+ # links from scopus. Also will throw in an abstract from Scopus if found.
5
+ #
6
+ # LIMTATIONS: You must be a Scopus customer for these links generated to work
7
+ # for your users at all! Off-campus users should be going through ezproxy, see
8
+ # the EZProxy plug-in.
9
+ # Must find a match in scopus, naturally. "cited by" will only
10
+ # be included if Scopus has non-0 "cited by" links. But there's no good way
11
+ # to precheck similar/more-by for this, so they are provided blind and may
12
+ # result in 0 hits. You can turn them off if you like, with @include_similar,
13
+ # and @include_more_by_authors.
14
+ #
15
+ # REGISTERING: This plug in actually has to use two seperate Scopus APIs.
16
+ # For the first, the scopus 'json' search api, you must regsiter and get an
17
+ # api key from scopus, which you can do here:
18
+ # http://searchapi.scopus.com
19
+ # Then include as @json_api_key in service config.
20
+ #
21
+ # For the second Scopus API, you theoretically need a Scopus "PartnerID" and
22
+ # corresponding "release number", in @partner_id and @scopus_release
23
+ # There's no real easy way to get one. Scopus says:
24
+ # "To obtain a partner ID or release number, contact your nearest regional
25
+ # Scopus office. A list of Scopus contacts is available at
26
+ # http://www.info.scopus.com/contactus/index.shtml"
27
+ # Bah! But fortunately, using the "partnerID" assigned to the Scopus Json
28
+ # API, 65, _seems_ to work, and is coded here as the default. You could try
29
+ # going with that. When you register a partnerID, you also get a 'salt key',
30
+ # which is currently not used by this code, but @link_salt_key is reserved
31
+ # for it in case added functionality does later.
32
+ #
33
+ class Scopus < Service
34
+ require 'open-uri'
35
+ require 'multi_json'
36
+ include MetadataHelper
37
+ include UmlautHttp
38
+
39
+ required_config_params :json_api_key
40
+
41
+ def service_types_generated
42
+ types = []
43
+ types.push( ServiceTypeValue[:abstract]) if @include_abstract
44
+ types.push( ServiceTypeValue[:cited_by] ) if @include_cited_by
45
+ types.push( ServiceTypeValue[:abstract] ) if @include_abstract
46
+ types.push( ServiceTypeValue[:similar] ) if @include_similar
47
+ types.push( ServiceTypeValue[@more_by_authors_type] ) if @include_more_by_authors
48
+
49
+ return types
50
+ end
51
+
52
+ def initialize(config)
53
+ #defaults
54
+ @display_name = "Scopus"
55
+ @registered_referer
56
+ @scopus_search_base = 'http://www.scopus.com/scsearchapi/search.url'
57
+
58
+ @include_abstract = true
59
+ @include_cited_by = true
60
+ @include_similar = true
61
+ @include_more_by_authors = true
62
+ @more_by_authors_type = "similar"
63
+
64
+ @inward_cited_by_url = "http://www.scopus.com/scopus/inward/citedby.url"
65
+ #@partner_id = "E5wmcMWC"
66
+ @partner_id = 65
67
+ @link_salt_key = nil
68
+ @scopus_release = "R6.0.0"
69
+
70
+ # Scopus offers two algorithms for finding similar items.
71
+ # This variable can be:
72
+ # "key" => keyword based similarity
73
+ # "ref" => reference based similiarity (cites similar refs?) Seems to offer 0 hits quite often, so we use keyword instead.
74
+ # "aut" => author. More docs by same authors. Incorporated as seperate link usually.
75
+ @more_like_this_type = "key"
76
+ @inward_more_like_url = "http://www.scopus.com/scopus/inward/mlt.url"
77
+
78
+ @credits = {
79
+ @display_name => "http://www.scopus.com/home.url"
80
+ }
81
+
82
+ super(config)
83
+ end
84
+
85
+ def handle(request)
86
+ scopus_search = scopus_search(request)
87
+
88
+ # we can't make a good query, nevermind.
89
+ return request.dispatched(self, true) if scopus_search.blank?
90
+
91
+
92
+ # The default fields returned dont' include the eid (Scopus unique id) that we need, so we'll supply our own exhaustive list of &fields=
93
+ url =
94
+ "#{@scopus_search_base}?devId=#{@json_api_key}&search=#{scopus_search}&callback=findit_callback&fields=title,doctype,citedbycount,inwardurl,sourcetitle,issn,vol,issue,page,pubdate,eid,scp,doi,firstAuth,authlist,affiliations,abstract";
95
+
96
+ # Make the call.
97
+ headers = {}
98
+ headers["Referer"] = @registered_referer if @registered_referer
99
+
100
+ response = open(url, headers).read
101
+
102
+ # Okay, Scopus insists on using a jsonp technique to embed the json array in
103
+ # a procedure call. We don't want that, take the actual content out of it's
104
+ # jsonp wrapper.
105
+ response =~ /^\w*findit_callback\((.*)\);?$/
106
+ response = $1;
107
+
108
+ # Take the first hit from scopus's results, hope they relevancy ranked it
109
+ # well. For DOI/pmid search, there should ordinarly be only one hit!
110
+ results = MultiJson.decode(response)
111
+
112
+ if ( results["ERROR"])
113
+ Rails.logger.error("Error from Scopus API: #{results["ERROR"].inspect} openurl: ?#{request.referent.to_context_object.kev}")
114
+ return request.dispatched(self, false)
115
+ end
116
+
117
+ # For reasons not clear to me, the JSON data structures vary.
118
+ first_hit = nil
119
+ if ( results["PartOK"])
120
+ first_hit = results["PartOK"]["Results"][0]
121
+ elsif ( results["OK"] )
122
+ first_hit = results["OK"]["Results"][0]
123
+ else
124
+ # error.
125
+ end
126
+
127
+ if ( first_hit )
128
+
129
+ if (@include_cited_by && first_hit["citedbycount"].to_i > 0)
130
+ add_cited_by_response(first_hit, request)
131
+ end
132
+
133
+ if (@include_abstract && first_hit["abstract"])
134
+ add_abstract(first_hit, request)
135
+ end
136
+
137
+ if (@include_similar)
138
+ url = more_like_this_url(first_hit)
139
+ # Pre-checking for actual hits not currently working, disabled.
140
+ if (true || ( hits = check_for_hits(url) ) > 0 )
141
+ request.add_service_response(
142
+ :service=>self,
143
+ :display_text => "#{hits} #{ServiceTypeValue[:similar].display_name_pluralize.downcase.capitalize}",
144
+ :url => url,
145
+ :service_type_value => :similar)
146
+ end
147
+ end
148
+
149
+ if ( @include_more_by_authors)
150
+ url = more_like_this_url(first_hit, :type => "aut")
151
+ # Pre-checking for actual hits not currently working, disabled.
152
+ if (true || ( hits = check_for_hits(url) ) > 0 )
153
+ request.add_service_response(
154
+ :service=>self,
155
+ :display_text => "#{hits} More from these authors",
156
+ :url => url,
157
+ :service_type_value => :similar)
158
+ end
159
+ end
160
+
161
+ end
162
+
163
+ return request.dispatched(self, true)
164
+ end
165
+
166
+ # Comes up with a scopus advanced search query intended to find the exact
167
+ # known item identified by this citation.
168
+ #
169
+ # Will try to use DOI or PMID if available. Otherwise
170
+ # will use issn/year/vol/iss/start page if available.
171
+ # In some cases may resort to author/title.
172
+ def scopus_search(request)
173
+
174
+ if (doi = get_doi(request.referent))
175
+ return CGI.escape( "DOI(#{phrase(doi)})" )
176
+ elsif (pmid = get_pmid(request.referent))
177
+ return CGI.escape( "PMID(#{phrase(pmid)})" )
178
+ elsif (isbn = get_isbn(request.referent))
179
+ # I don't think scopus has a lot of ISBN-holding citations, but
180
+ # it allows search so we might as well try.
181
+ return CGI.escape( "ISBN(#{phrase(isbn)})" )
182
+ else
183
+ # Okay, we're going to try to do it on issn/vol/issue/page.
184
+ # If we don't have issn, we'll reluctantly use journal title
185
+ # (damn you google scholar).
186
+ metadata = request.referent.metadata
187
+ issn = request.referent.issn
188
+ if ( (issn || ! metadata['jtitle'].blank? ) &&
189
+ ! metadata['volume'].blank? &&
190
+ ! metadata['issue'].blank? &&
191
+ ! metadata['spage'].blank? )
192
+ query = "VOLUME(#{phrase(metadata['volume'])}) AND ISSUE(#{phrase(metadata['issue'])}) AND PAGEFIRST(#{phrase(metadata['spage'])}) "
193
+ if ( issn )
194
+ query += " AND (ISSN(#{phrase(issn)}) OR EISSN(#{phrase(issn)}))"
195
+ else
196
+ query += " AND EXACTSRCTITLE(#{phrase(metadata['jtitle'])})"
197
+ end
198
+ return CGI.escape(query)
199
+ end
200
+
201
+ end
202
+ end
203
+
204
+ # backslash escapes any double quotes, and embeds string in scopus
205
+ # phrase search double quotes. Does NOT uri-escape.
206
+ def phrase(str)
207
+ '"' + str.gsub('"', '\\"') + '"'
208
+ end
209
+
210
+ # Input is a ruby hash that came from the scopus JSON, representing
211
+ # a single hit. We're going to add this as a result.
212
+ def add_cited_by_response(result, request)
213
+ # While scopus provides an "inwardurl" in the results, this just takes
214
+ # us to the record detail page. We actually want to go RIGHT to the
215
+ # list of cited-by items. So we create our own, based on Scopus's
216
+ # reversed engineered predictable URLs.
217
+
218
+ count = result["citedbycount"]
219
+ label = ServiceTypeValue[:cited_by].display_name_pluralize.downcase.capitalize
220
+ if count && count == 1
221
+ label = ServiceTypeValue[:cited_by].display_name.downcase.capitalize
222
+ end
223
+ cited_by_url = cited_by_url( result )
224
+
225
+ request.add_service_response(:service=>self,
226
+ :display_text => "#{count} #{label}",
227
+ :count=> count,
228
+ :url => cited_by_url,
229
+ :service_type_value => :cited_by)
230
+
231
+ end
232
+
233
+ def add_abstract(first_hit, request)
234
+
235
+ return if first_hit["abstract"].blank?
236
+
237
+ request.add_service_response(
238
+ :service=>self,
239
+ :display_text => "Abstract from #{@display_name}",
240
+ :content => first_hit["abstract"],
241
+ :url => detail_url(first_hit),
242
+ :service_type_value => :abstract)
243
+ end
244
+
245
+ def detail_url(hash)
246
+ url = hash["inwardurl"]
247
+ # for some reason ampersand's in query string have wound up double escaped
248
+ # and need to be fixed.
249
+ url = url.gsub(/\&amp\;/, '&')
250
+
251
+ return url
252
+ end
253
+
254
+ def cited_by_url(result)
255
+ eid = CGI.escape(result["eid"])
256
+ #return "#{@scopus_cited_by_base}?eid=#{eid}&src=s&origin=recordpage"
257
+ # Use the new scopus direct link format!
258
+ return "#{@inward_cited_by_url}?partnerID=#{@partner_id}&rel=#{@scopus_release}&eid=#{eid}"
259
+ return
260
+ end
261
+
262
+ def more_like_this_url(result, options = {})
263
+ options[:type] ||= @more_like_this_type
264
+
265
+ eid = CGI.escape(result["eid"])
266
+ return "#{@inward_more_like_url}?partnerID=#{@partner_id}&rel=#{@scopus_release}&eid=#{eid}&mltType=#{options[:type]}"
267
+ end
268
+
269
+ # NOT currently working. Scopus doesn't make this easy.
270
+ # Takes a scopus direct url for which we're not sure if there will be results
271
+ # or not, and requests it and html screen-scrapes to get hit count. (We
272
+ # can conveniently find this just in the html <title> at least).
273
+ # Works for cited_by and more_like_this searches at present.
274
+ # May break if Scopus changes their html title!
275
+ def check_for_hits(url)
276
+
277
+ response = http_fetch(url).body
278
+
279
+ response_html = Nokogiri::HTML(response)
280
+
281
+ title = response_xml.at('title').inner_text
282
+ # title is "X documents" (or 'Documents') if there are hits.
283
+ # It's annoyingly "Search Error" if there are either 0 hits, or
284
+ # if there was an actual error. So we can't easily log actual
285
+ # errors, sorry.
286
+ title.downcase =~ /^\s*(\d+)?\s+document/
287
+ if ( hits = $1)
288
+ return hits.to_i
289
+ else
290
+ return 0
291
+ end
292
+ end
293
+
294
+
295
+ end