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.
- data/LICENSE +7 -0
- data/README.md +49 -0
- data/Rakefile +37 -0
- data/app/assets/images/error.gif +0 -0
- data/app/assets/images/export_bg_bot.gif +0 -0
- data/app/assets/images/export_bg_mid.gif +0 -0
- data/app/assets/images/export_bg_top.gif +0 -0
- data/app/assets/images/famfamfam/book_open.png +0 -0
- data/app/assets/images/famfamfam/cross.png +0 -0
- data/app/assets/images/famfamfam/page_sound.gif +0 -0
- data/app/assets/images/famfamfam/page_text.gif +0 -0
- data/app/assets/images/famfamfam/page_up.gif +0 -0
- data/app/assets/images/famfamfam/page_white.png +0 -0
- data/app/assets/images/famfamfam/readme.html +1495 -0
- data/app/assets/images/famfamfam/tiny_cross.png +0 -0
- data/app/assets/images/frame_remove.gif +0 -0
- data/app/assets/images/ico_go.gif +0 -0
- data/app/assets/images/jhu_findit.gif +0 -0
- data/app/assets/images/list_closed.png +0 -0
- data/app/assets/images/list_open.png +0 -0
- data/app/assets/images/more_info.gif +0 -0
- data/app/assets/images/rails.png +0 -0
- data/app/assets/images/request.gif +0 -0
- data/app/assets/images/spinner.gif +0 -0
- data/app/assets/javascripts/umlaut/ajax_windows.js +35 -0
- data/app/assets/javascripts/umlaut/ensure_window_size.js.erb +34 -0
- data/app/assets/javascripts/umlaut/expand_contract_toggle.js +25 -0
- data/app/assets/javascripts/umlaut/search_autocomplete.js +46 -0
- data/app/assets/javascripts/umlaut/simple_visible_toggle.js +8 -0
- data/app/assets/javascripts/umlaut/update_html.js +152 -0
- data/app/assets/javascripts/umlaut.js +17 -0
- data/app/assets/stylesheets/umlaut.css +857 -0
- data/app/controllers/application_controller.rb +14 -0
- data/app/controllers/export_email_controller.rb +123 -0
- data/app/controllers/js_helper_controller.rb +10 -0
- data/app/controllers/link_router_controller.rb +87 -0
- data/app/controllers/open_search_controller.rb +9 -0
- data/app/controllers/resolve_controller.rb +288 -0
- data/app/controllers/resource_controller.rb +83 -0
- data/app/controllers/search_controller.rb +328 -0
- data/app/controllers/search_methods/sfx3.rb +148 -0
- data/app/controllers/search_methods/sfx4.rb +257 -0
- data/app/controllers/search_methods/sfx_api.rb +47 -0
- data/app/controllers/store_controller.rb +64 -0
- data/app/controllers/umlaut/controller_behavior.rb +20 -0
- data/app/controllers/umlaut/controller_logic.rb +96 -0
- data/app/controllers/umlaut/error_handling.rb +48 -0
- data/app/controllers/umlaut_controller.rb +112 -0
- data/app/helpers/application_helper.rb +4 -0
- data/app/helpers/emailer_helper.rb +43 -0
- data/app/helpers/export_email_helper.rb +34 -0
- data/app/helpers/open_search_helper.rb +7 -0
- data/app/helpers/resolve_helper.rb +225 -0
- data/app/helpers/search_helper.rb +50 -0
- data/app/helpers/umlaut/footer_helper.rb +64 -0
- data/app/helpers/umlaut/helper.rb +62 -0
- data/app/helpers/umlaut/html_head_helper.rb +37 -0
- data/app/helpers/umlaut/url_generation.rb +77 -0
- data/app/mailers/emailer.rb +48 -0
- data/app/models/clickthrough.rb +2 -0
- data/app/models/collection.rb +259 -0
- data/app/models/crossref_lookup.rb +2 -0
- data/app/models/dispatched_service.rb +58 -0
- data/app/models/permalink.rb +29 -0
- data/app/models/referent.rb +473 -0
- data/app/models/referent_value.rb +14 -0
- data/app/models/request.rb +449 -0
- data/app/models/service_response.rb +179 -0
- data/app/models/service_store.rb +59 -0
- data/app/models/service_type_value.rb +58 -0
- data/app/models/service_wave.rb +150 -0
- data/app/models/sfx_db/az_additional_title.rb +11 -0
- data/app/models/sfx_db/az_letter_group.rb +11 -0
- data/app/models/sfx_db/az_title.rb +38 -0
- data/app/models/sfx_db/az_title_v2.rb +34 -0
- data/app/models/sfx_db/isbn.rb +12 -0
- data/app/models/sfx_db/issn.rb +12 -0
- data/app/models/sfx_db/object.rb +35 -0
- data/app/models/sfx_db/object_portfolio.rb +6 -0
- data/app/models/sfx_db/publisher.rb +10 -0
- data/app/models/sfx_db/sfx_db_base.rb +54 -0
- data/app/models/sfx_db/target.rb +9 -0
- data/app/models/sfx_db/target_service.rb +10 -0
- data/app/models/sfx_db/title.rb +10 -0
- data/app/models/sfx_db.rb +10 -0
- data/app/models/sfx_url.rb +35 -0
- data/app/views/emailer/citation.text.erb +28 -0
- data/app/views/emailer/short_citation.text.erb +8 -0
- data/app/views/export_email/_email.html.erb +25 -0
- data/app/views/export_email/_send_email.html.erb +3 -0
- data/app/views/export_email/_send_txt.html.erb +3 -0
- data/app/views/export_email/_txt.html.erb +62 -0
- data/app/views/export_email/email.html.erb +3 -0
- data/app/views/export_email/send_email.html.erb +1 -0
- data/app/views/export_email/send_txt.html.erb +1 -0
- data/app/views/export_email/txt.html.erb +3 -0
- data/app/views/js_helper/loader.erb.js +13 -0
- data/app/views/layouts/umlaut.html.erb +52 -0
- data/app/views/open_search/index.html.erb +9 -0
- data/app/views/resolve/_api_in_progress.xml.erb +21 -0
- data/app/views/resolve/_background_progress.html.erb +51 -0
- data/app/views/resolve/_background_updater.html.erb +38 -0
- data/app/views/resolve/_citation.html.erb +87 -0
- data/app/views/resolve/_coins.html.erb +1 -0
- data/app/views/resolve/_compact_citation.html.erb +33 -0
- data/app/views/resolve/_cover_image.html.erb +35 -0
- data/app/views/resolve/_fulltext.html.erb +55 -0
- data/app/views/resolve/_help.html.erb +17 -0
- data/app/views/resolve/_holding.html.erb +91 -0
- data/app/views/resolve/_related_items.html.erb +35 -0
- data/app/views/resolve/_search_inside.html.erb +62 -0
- data/app/views/resolve/_section_display.html.erb +49 -0
- data/app/views/resolve/_service_errors.html.erb +29 -0
- data/app/views/resolve/_standard_response_item.html.erb +89 -0
- data/app/views/resolve/api.xml.builder +72 -0
- data/app/views/resolve/background_status.html.erb +26 -0
- data/app/views/resolve/index.html.erb +73 -0
- data/app/views/resolve/partial_html_sections.xml.erb +30 -0
- data/app/views/search/_a_to_z.html.erb +6 -0
- data/app/views/search/_citation.html.erb +94 -0
- data/app/views/search/_pager.html.erb +60 -0
- data/app/views/search/books.html.erb +103 -0
- data/app/views/search/journal_search.html.erb +90 -0
- data/app/views/search/journals.html.erb +167 -0
- data/app/views/search/opensearch_description.rxml +10 -0
- data/app/views/testing/index.html.erb +1 -0
- data/app/views/umlaut/README +5 -0
- data/app/views/umlaut/error.html.erb +45 -0
- data/db/migrate/01_umlaut_init.rb +113 -0
- data/db/orig_fixed_data/service_type_values.yml +120 -0
- data/db/seeds.rb +7 -0
- data/lib/CronTab.rb +192 -0
- data/lib/aws_product_sign.rb +146 -0
- data/lib/exlibris/aleph/patron.rb +64 -0
- data/lib/exlibris/aleph/record.rb +54 -0
- data/lib/exlibris/aleph/rest_api.rb +29 -0
- data/lib/exlibris/primo/holding.rb +192 -0
- data/lib/exlibris/primo/rsrc.rb +17 -0
- data/lib/exlibris/primo/searcher.rb +276 -0
- data/lib/exlibris/primo/source/aleph.rb +46 -0
- data/lib/exlibris/primo/source/distribution/nyu_aleph.rb +323 -0
- data/lib/exlibris/primo/toc.rb +17 -0
- data/lib/exlibris/primo_ws.rb +140 -0
- data/lib/generators/templates/umlaut_services.yml +237 -0
- data/lib/generators/umlaut/asset_hooks_generator.rb +44 -0
- data/lib/generators/umlaut/install_generator.rb +110 -0
- data/lib/hip3/bib.rb +291 -0
- data/lib/hip3/bib_searcher.rb +302 -0
- data/lib/hip3/custom_field_lookup.rb +44 -0
- data/lib/hip3/holding.rb +50 -0
- data/lib/hip3/item.rb +65 -0
- data/lib/hip3/receipt.rb +7 -0
- data/lib/hip3/serial_copy.rb +82 -0
- data/lib/holding.rb +32 -0
- data/lib/marc_helper.rb +254 -0
- data/lib/metadata_helper.rb +312 -0
- data/lib/opensearch_feed.rb +398 -0
- data/lib/opensearch_query.rb +98 -0
- data/lib/referent_filter.rb +16 -0
- data/lib/referent_filters/dissertation_catch.rb +45 -0
- data/lib/section_renderer.rb +503 -0
- data/lib/service.rb +336 -0
- data/lib/service_adaptors/ajax_export.rb +37 -0
- data/lib/service_adaptors/amazon.rb +412 -0
- data/lib/service_adaptors/blacklight.rb +327 -0
- data/lib/service_adaptors/book_finder.rb +40 -0
- data/lib/service_adaptors/bx.rb +51 -0
- data/lib/service_adaptors/cover_thing.rb +73 -0
- data/lib/service_adaptors/elsevier_cover.rb +57 -0
- data/lib/service_adaptors/email_export.rb +10 -0
- data/lib/service_adaptors/ezproxy.rb +171 -0
- data/lib/service_adaptors/google_book_search.rb +442 -0
- data/lib/service_adaptors/gpo.rb +124 -0
- data/lib/service_adaptors/hathi_trust.rb +308 -0
- data/lib/service_adaptors/hip3_service.rb +150 -0
- data/lib/service_adaptors/hip_holding_search.rb +237 -0
- data/lib/service_adaptors/internet_archive.rb +488 -0
- data/lib/service_adaptors/isbn_db.rb +86 -0
- data/lib/service_adaptors/isi.rb +258 -0
- data/lib/service_adaptors/jcr.rb +146 -0
- data/lib/service_adaptors/opac.rb +351 -0
- data/lib/service_adaptors/open_library.rb +316 -0
- data/lib/service_adaptors/open_library_cover.rb +73 -0
- data/lib/service_adaptors/primo_service.rb +392 -0
- data/lib/service_adaptors/primo_source.rb +78 -0
- data/lib/service_adaptors/pubmed.rb +133 -0
- data/lib/service_adaptors/request_to_fixture.rb +68 -0
- data/lib/service_adaptors/scopus.rb +295 -0
- data/lib/service_adaptors/sfx-new.rb +557 -0
- data/lib/service_adaptors/sfx.rb +566 -0
- data/lib/service_adaptors/sfx_backchannel_record.rb +69 -0
- data/lib/service_adaptors/txt_holding_export.rb +32 -0
- data/lib/service_adaptors/ulrichs_cover.rb +57 -0
- data/lib/service_adaptors/ulrichs_link.rb +47 -0
- data/lib/service_adaptors/worldcat.rb +116 -0
- data/lib/service_adaptors/worldcat_identities.rb +591 -0
- data/lib/tasks/umlaut.rake +134 -0
- data/lib/umlaut/default_configuration.rb +5 -0
- data/lib/umlaut/routes.rb +136 -0
- data/lib/umlaut/version.rb +3 -0
- data/lib/umlaut.rb +37 -0
- data/lib/umlaut_configurable.rb +343 -0
- data/lib/umlaut_http.rb +100 -0
- data/lib/xml_schema_helper.rb +109 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/umlaut_controller.rb +112 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database-jhu.yml +44 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +34 -0
- data/test/dummy/config/environments/production.rb +60 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +61 -0
- data/test/dummy/config/umlaut_services.yml +237 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/migrate/20111228211210_umlaut_init.rb +113 -0
- data/test/dummy/db/schema.rb +124 -0
- data/test/dummy/log/development.log +12981 -0
- data/test/dummy/log/production.log +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dummy/tmp/cache/assets/C5F/340/sprockets%2F99692920160b7a279b86a80415b79db7 +0 -0
- data/test/dummy/tmp/cache/assets/C70/4D0/sprockets%2F034ad2036e623081bd352800786dfe80 +0 -0
- data/test/dummy/tmp/cache/assets/C73/920/sprockets%2Fd371318f22900492fd180f17c5e2a504 +9268 -0
- data/test/dummy/tmp/cache/assets/C80/980/sprockets%2Fc94807409c1523d43e18d25f35d93c41 +0 -0
- data/test/dummy/tmp/cache/assets/C8F/780/sprockets%2Fe47e28558116fb5f8038754e60d1961d +11769 -0
- data/test/dummy/tmp/cache/assets/CAA/EB0/sprockets%2F1d179210e8b76f1ea63c802688a015e4 +9271 -0
- data/test/dummy/tmp/cache/assets/CBB/9C0/sprockets%2F706f28923fb754cad04b9107c89986a1 +0 -0
- data/test/dummy/tmp/cache/assets/CBF/B60/sprockets%2F08ca89671549936265dcb673bf02e36f +0 -0
- data/test/dummy/tmp/cache/assets/CC9/9F0/sprockets%2F306166316e2cafd13c15e62b51a2339d +0 -0
- data/test/dummy/tmp/cache/assets/CF6/F20/sprockets%2F5b2ffa1103079dfd555197838f87a99f +0 -0
- data/test/dummy/tmp/cache/assets/CF7/2B0/sprockets%2F25a7c73655bd3598173b39d9f98bcd46 +862 -0
- data/test/dummy/tmp/cache/assets/CFE/080/sprockets%2F37fe9f4255baddbd549a659914929398 +0 -0
- data/test/dummy/tmp/cache/assets/D22/060/sprockets%2F9aec77b768e91a802d284271c58e2f7e +21357 -0
- data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/dummy/tmp/cache/assets/D33/6D0/sprockets%2F500129c57f1146e556ec3aacd6cd38c1 +0 -0
- data/test/dummy/tmp/cache/assets/D33/FD0/sprockets%2F2ba0b4e6334a77b923e5f770381bb2bf +0 -0
- data/test/dummy/tmp/cache/assets/D42/C20/sprockets%2Fbcf14e437b1582bf93b77670acf8e090 +21353 -0
- data/test/dummy/tmp/cache/assets/D50/A30/sprockets%2F7d8b294ac433db5d056538f8cf7c66b9 +0 -0
- data/test/dummy/tmp/cache/assets/D54/ED0/sprockets%2F71c9fa01091d432b131da3bb73faf3d4 +872 -0
- data/test/dummy/tmp/cache/assets/D65/590/sprockets%2Fc1bb92fc3406a126b7dd302edc96d629 +0 -0
- data/test/dummy/tmp/cache/assets/D71/6B0/sprockets%2Fde558b71b494cf09b1bf055c8dff0353 +0 -0
- data/test/dummy/tmp/cache/assets/D72/610/sprockets%2Fa8c708eeb30ef93de34d755d4f45d023 +859 -0
- data/test/dummy/tmp/cache/assets/D76/AD0/sprockets%2Fe2158cde93188cf5ab6457bc6d6602ec +0 -0
- data/test/dummy/tmp/cache/assets/D7A/E40/sprockets%2F9622ffcc499a57627cd1bb18fe31b8e4 +11772 -0
- data/test/dummy/tmp/cache/assets/D84/210/sprockets%2Fabd0103ccec2b428ac62c94e4c40b384 +0 -0
- data/test/dummy/tmp/cache/assets/D9B/770/sprockets%2F8aacf02eb7dbb0949704b28f27b87e0b +0 -0
- data/test/dummy/tmp/cache/assets/DA6/A80/sprockets%2F92e26d8e58d5bcc8b8f6c25d1b05b9c1 +0 -0
- data/test/dummy/tmp/cache/assets/DE8/790/sprockets%2Fd1333bde2b9aafcc712d11dd09ab35d8 +0 -0
- data/test/dummy/tmp/cache/assets/DF7/F30/sprockets%2F7bc16c4109b17fabe29f8ddbbf732d1c +374 -0
- data/test/dummy/tmp/cache/assets/E03/570/sprockets%2F493bdc0ac14cd4f57fdfe4253f992bde +0 -0
- data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/dummy/tmp/cache/assets/E0B/4B0/sprockets%2F7988df51a61c81ce6ede4a2d4c8cce4f +377 -0
- data/test/dummy/tmp/cache/assets/E5F/960/sprockets%2Fdc007b6cad5c7ef08e33ec28cfff0ef6 +0 -0
- data/test/fixtures/dispatched_services.yml +5 -0
- data/test/fixtures/permalinks.yml +5 -0
- data/test/fixtures/referent_values.yml +1734 -0
- data/test/fixtures/referents.yml +156 -0
- data/test/fixtures/requests.yml +284 -0
- data/test/fixtures/service_responses.yml +5 -0
- data/test/fixtures/sfx_urls.yml +4 -0
- data/test/performance/browsing_test.rb +9 -0
- data/test/test_helper.rb +10 -0
- data/test/umlaut_test.rb +7 -0
- data/test/unit/aleph_patron_test.rb +39 -0
- data/test/unit/aleph_record_benchmarks.rb +28 -0
- data/test/unit/aleph_record_test.rb +30 -0
- data/test/unit/aws_product_sign_test.rb +93 -0
- data/test/unit/collection_test.rb +76 -0
- data/test/unit/google_book_search_test.rb +101 -0
- data/test/unit/primo_searcher_test.rb +403 -0
- data/test/unit/primo_service_test.rb +939 -0
- data/test/unit/primo_ws_test.rb +131 -0
- data/test/unit/service_response_test.rb +9 -0
- data/test/unit/service_test.rb +33 -0
- 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(/\&\;/, '&')
|
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
|