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,58 @@
|
|
1
|
+
class DispatchedService < ActiveRecord::Base
|
2
|
+
belongs_to :request
|
3
|
+
|
4
|
+
# Serialized hash of exception info.
|
5
|
+
serialize :exception_info
|
6
|
+
|
7
|
+
# Statuses for status column
|
8
|
+
# Still executing, has started
|
9
|
+
InProgress = 'in_progress'
|
10
|
+
# Queued up, not yet started. Generally used for background services.
|
11
|
+
Queued = 'queued'
|
12
|
+
# Complete, succesful. May or may not have produced responses, but
|
13
|
+
# completed succesfully either way.
|
14
|
+
Successful = 'successful'
|
15
|
+
# Failed, and do not advise trying again.
|
16
|
+
FailedFatal = 'failed_fatal' # Complete, failed,
|
17
|
+
# Failed, but it might be worth trying again.
|
18
|
+
FailedTemporary = 'failed_temporary'
|
19
|
+
|
20
|
+
|
21
|
+
def service=(service)
|
22
|
+
self.service_id = service.service_id
|
23
|
+
end
|
24
|
+
# instantiates a new service object that represents the service
|
25
|
+
# that dispatched.
|
26
|
+
def service
|
27
|
+
@service ||= ServiceStore.instantiate_service!( self.service_id, request )
|
28
|
+
end
|
29
|
+
|
30
|
+
# For old-time's sake, true can be used for Succesful
|
31
|
+
# and false can be used for FailedTemporary (that keeps
|
32
|
+
# previous semantics for false intact).
|
33
|
+
def status=(a_status)
|
34
|
+
a_status = FailedTemporary if a_status.kind_of?(FalseClass)
|
35
|
+
a_status = Successful if a_status.kind_of?(TrueClass)
|
36
|
+
|
37
|
+
# NO: @status = a_status
|
38
|
+
# Instead, this is how you 'override' an AR attribute:
|
39
|
+
write_attribute(:status, a_status)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Will silently refuse to over-write an existing stored exception.
|
43
|
+
def store_exception(a_exc)
|
44
|
+
return if a_exc.nil? || ! self.exception_info.nil?
|
45
|
+
# Just yaml'izing the exception doesn't keep the backtrace, which is
|
46
|
+
# what we wanted. Doh!
|
47
|
+
e_hash = Hash.new
|
48
|
+
e_hash[:class_name] = a_exc.class.name
|
49
|
+
e_hash[:message] = a_exc.message
|
50
|
+
e_hash[:backtrace] = a_exc.backtrace
|
51
|
+
|
52
|
+
write_attribute(:exception_info, e_hash)
|
53
|
+
end
|
54
|
+
|
55
|
+
def completed?
|
56
|
+
return (self.status != InProgress) && (self.status != Queued)
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# attribute context_obj_serialized has an XML OpenURL ContextObject sufficient to restore
|
2
|
+
# the original request and resolve the permalink. A link to a referent is
|
3
|
+
# also stored. But the referent may be purged, so self.referent may be null.
|
4
|
+
# The serialized contextobject will still be there.
|
5
|
+
class Permalink < ActiveRecord::Base
|
6
|
+
belongs_to :referent
|
7
|
+
|
8
|
+
# You should create Permalinks with this. Pass in a referent and referrer
|
9
|
+
#. Will save permalink to db
|
10
|
+
def self.new_with_values!(rft, rfr_id)
|
11
|
+
permalink = Permalink.new
|
12
|
+
|
13
|
+
permalink.referent = rft
|
14
|
+
permalink.orig_rfr_id = rfr_id
|
15
|
+
|
16
|
+
permalink.context_obj_serialized = permalink.referent.to_context_object.xml
|
17
|
+
|
18
|
+
permalink.save!
|
19
|
+
|
20
|
+
return permalink
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Takes the XML stored in self.context_obj_serialized, and turns it back
|
25
|
+
# into an OpenURL ContextObject
|
26
|
+
def restore_context_object
|
27
|
+
return OpenURL::ContextObject.new_from_xml(self.context_obj_serialized)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,473 @@
|
|
1
|
+
class Referent < ActiveRecord::Base
|
2
|
+
# for shortcut metadata manipulations
|
3
|
+
include MetadataHelper
|
4
|
+
|
5
|
+
# Shortcuts are really used as retrieval keys to 'shortcut' matching
|
6
|
+
# referent. They hold normalized value (use ReferentValue.normalize) or
|
7
|
+
# empty string. Never nil.
|
8
|
+
@@shortcut_attributes = [:atitle, :title, :issn, :isbn, :volume, :year]
|
9
|
+
has_many :requests
|
10
|
+
has_many :referent_values
|
11
|
+
has_many :permalinks
|
12
|
+
|
13
|
+
def before_validation_on_create
|
14
|
+
# shortcuts initialize to empty string, they should never be null.
|
15
|
+
@@shortcut_attributes.each do |key|
|
16
|
+
self[key] = "" if self[key].nil?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# When provided an OpenURL::ContextObject, it will return a Referent object
|
21
|
+
# (if one exists). At least that's the intent.
|
22
|
+
#This turns out to be a really
|
23
|
+
# tricky task, identifying when two citations that may not match exactly
|
24
|
+
# are the same citation. So this doesn't really work well--we err
|
25
|
+
# on the side of missing existing matches, better than finding
|
26
|
+
# a false match. So there are seldom matches found. A particular
|
27
|
+
# problem is that when the Referent is enhanced by a service,
|
28
|
+
# it will no longer match _itself_ as it came in! Oh well.
|
29
|
+
def self.find_by_context_object(co)
|
30
|
+
|
31
|
+
rft = co.referent
|
32
|
+
|
33
|
+
|
34
|
+
# Try to find for re-use by special indexed shortcut values. Create hash
|
35
|
+
# of shortcuts.
|
36
|
+
|
37
|
+
# Preload values as empty, even if they aren't found in our
|
38
|
+
# incoming referent--we want to find a match with them empty too, then!
|
39
|
+
shortcuts = {:atitle=>"", :title=>"", :issn=>"", :isbn=>"", :volume=>"", :year=>""}
|
40
|
+
|
41
|
+
# Special handling of title
|
42
|
+
incoming_title = rft.metadata['jtitle'] || rft.metadata['btitle'] || rft.metadata['title']
|
43
|
+
# DC OpenURL is an array, not a single value. Grr.
|
44
|
+
incoming_title = incoming_title[0] if incoming_title.kind_of?(Array)
|
45
|
+
shortcuts[:title] = ReferentValue.normalize(incoming_title) if incoming_title
|
46
|
+
# Special handling of date/year, since we use year instead of date for
|
47
|
+
# stored shortcut.
|
48
|
+
# I don't know why.
|
49
|
+
shortcuts[:year] = rft.metadata['date'] if rft.metadata['date']
|
50
|
+
|
51
|
+
# Other four.
|
52
|
+
[:atitle, :issn, :isbn, :volume].each do |att|
|
53
|
+
shortcuts[att] = ReferentValue.normalize( rft.metadata[att.to_s]) if rft.metadata[ att.to_s ]
|
54
|
+
end
|
55
|
+
# Don't look up by shortcuts if they're ALL blank. That doesn't do us well.
|
56
|
+
found_rft = nil
|
57
|
+
found_rft = Referent.find(:first, :conditions => shortcuts) if shortcuts.values.find {|v| ! v.empty?}
|
58
|
+
if ( found_rft && found_rft.metadata_intersects?( rft ) )
|
59
|
+
return found_rft
|
60
|
+
end
|
61
|
+
|
62
|
+
# found nothing?
|
63
|
+
return nil
|
64
|
+
end
|
65
|
+
|
66
|
+
# When provided an OpenURL::ContextObject, it will return a Referent object
|
67
|
+
# (creating one if doesn't already exist) . At least that's the idea.
|
68
|
+
# But see caveats at #find_by_context_object . Most of the time
|
69
|
+
# this ends up creating a new Referent.
|
70
|
+
# pass in referrer for source-specific referent munging.
|
71
|
+
def self.find_or_create_by_context_object(co)
|
72
|
+
# Okay, we need to do some pre-processing on weird context objects
|
73
|
+
# sent by, for example, firstSearch.
|
74
|
+
self.clean_up_context_object(co)
|
75
|
+
|
76
|
+
if rft = Referent.find_by_context_object(co)
|
77
|
+
return rft
|
78
|
+
else
|
79
|
+
rft = Referent.create_by_context_object(co)
|
80
|
+
return rft
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Does call save! on referent created.
|
85
|
+
# :permalink => false if you already have a permalink and don't
|
86
|
+
# need to create one. Caller should attach that permalink to this referent!
|
87
|
+
def self.create_by_context_object(co, options = {})
|
88
|
+
|
89
|
+
self.clean_up_context_object(co)
|
90
|
+
rft = Referent.new
|
91
|
+
|
92
|
+
# Wrap everything in a transaction for better efficiency, at least
|
93
|
+
# with MySQL, I think.
|
94
|
+
|
95
|
+
Referent.transaction do
|
96
|
+
|
97
|
+
rft.set_values_from_context_object(co)
|
98
|
+
|
99
|
+
unless ( options[:permalink] == false)
|
100
|
+
permalink = Permalink.new_with_values!(rft, co.referrer.identifier)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Add shortcuts.
|
104
|
+
rft.referent_values.each do | val |
|
105
|
+
rft.atitle = val.normalized_value if val.key_name == 'atitle' and val.metadata?
|
106
|
+
rft.title = val.normalized_value if val.key_name.match(/^[bj]?title$/) and val.metadata?
|
107
|
+
rft.issn = val.normalized_value if val.key_name == 'issn' and val.metadata?
|
108
|
+
rft.isbn = val.normalized_value if val.key_name == 'isbn' and val.metadata?
|
109
|
+
rft.volume = val.normalized_value if val.key_name == 'volume' and val.metadata?
|
110
|
+
rft.year = val.normalized_value if val.key_name == 'date' and val.metadata?
|
111
|
+
end
|
112
|
+
rft.save!
|
113
|
+
|
114
|
+
# Apply referent filters
|
115
|
+
rfr_id = ""
|
116
|
+
rfr_id = co.referrer.identifier if (co.referrer && ! co.referrer.identifier.blank?)
|
117
|
+
UmlautController.umlaut_config.lookup!("referent_filters", []).each do |regexp, filter|
|
118
|
+
if (regexp =~ rfr_id)
|
119
|
+
filter.filter(rft) if filter.respond_to?(:filter)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
return rft
|
124
|
+
end
|
125
|
+
|
126
|
+
# Okay, we need to do some pre-processing on weird context objects
|
127
|
+
# sent by, for example, firstSearch. Remove invalid identifiers.
|
128
|
+
# Also will adjust context objects according to configured
|
129
|
+
# umlaut refernet filters (see config.app_config.referent_filters in
|
130
|
+
# environment.rb )
|
131
|
+
# Mutator: Modifies ContextObject arg passed in.
|
132
|
+
def self.clean_up_context_object(co)
|
133
|
+
# First, remove any empty DOIs! or other empty identifiers?
|
134
|
+
# LOTS of sources send awful empty identifiers.
|
135
|
+
# That's not a valid identifier!
|
136
|
+
empty_ids = co.referent.identifiers.find_all { |i| i =~ Regexp.new('^[^:]+:[^/:]*(/|:)?$')}
|
137
|
+
empty_ids.each { |e| co.referent.delete_identifier( e )}
|
138
|
+
|
139
|
+
# Now look for ISSN identifiers that are on article_level. FirstSearch
|
140
|
+
# gives us ISSN identifiers incorrectly on article level cites.
|
141
|
+
issn_ids = co.referent.identifiers.find_all { |i| i =~ /^urn:ISSN/}
|
142
|
+
issn_ids.each do |issn_id|
|
143
|
+
# Long as we're at it, add an rft.issn if one's not there.
|
144
|
+
issn_data = issn_id.slice( (9..issn_id.length)) # actual ISSN without identifier prefix
|
145
|
+
co.referent.set_metadata(issn, issn_data) if co.referent.get_metadata('issn').blank? && ! issn_data.blank?
|
146
|
+
|
147
|
+
# And remove it as an identifier unless we know this is journal-level
|
148
|
+
# cite.
|
149
|
+
unless ( co.referent.get_metadata('genre') == 'journal' )
|
150
|
+
co.referent.delete_identifier( issn_id )
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Clean up OCLC numbers from old bad formats that may have snuck in to an info url incorrectly. # also delete preceding 0's
|
155
|
+
oclcnum_ids = co.referent.identifiers.find_all { |i| i =~ /^info:oclcnum/}
|
156
|
+
oclcnum_ids.each do |oclcnum_id|
|
157
|
+
# FIXME Does this regex need "ocn" as well?
|
158
|
+
if (oclcnum_id =~ /^info:oclcnum\/(ocm0*|ocn0*|\(OCoLC\)0*|ocl70*|0+)(.*)$/)
|
159
|
+
# Delete the original, take out just the actual oclcnum, not
|
160
|
+
# those old prefixes. or preceding 0s.
|
161
|
+
co.referent.delete_identifier( oclcnum_id )
|
162
|
+
co.referent.add_identifier("info:oclcnum/#{$2}")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
# Find or create a ReferentValue object hanging off this
|
173
|
+
# Referent, with given key name and value. key_name can
|
174
|
+
# be 'identifier', 'format', or any metadata key.
|
175
|
+
def ensure_value!(key_name, value)
|
176
|
+
normalized_value = ReferentValue.normalize(value)
|
177
|
+
|
178
|
+
rv = ReferentValue.find(:first,
|
179
|
+
:conditions => { :referent_id => self.id,
|
180
|
+
:key_name => key_name,
|
181
|
+
:normalized_value => normalized_value })
|
182
|
+
unless (rv)
|
183
|
+
rv = ReferentValue.new
|
184
|
+
rv.referent = self
|
185
|
+
|
186
|
+
rv.key_name = key_name
|
187
|
+
rv.value = value
|
188
|
+
rv.normalized_value = normalized_value
|
189
|
+
|
190
|
+
unless (key_name == "identifier" || key_name == "format")
|
191
|
+
rv.metadata = true
|
192
|
+
end
|
193
|
+
|
194
|
+
rv.save!
|
195
|
+
end
|
196
|
+
return rv
|
197
|
+
end
|
198
|
+
|
199
|
+
# Populate the referent_values table with a ropenurl contextobject object
|
200
|
+
def set_values_from_context_object(co)
|
201
|
+
|
202
|
+
rft = co.referent
|
203
|
+
|
204
|
+
|
205
|
+
# Multiple identifiers are possible!
|
206
|
+
rft.identifiers.each do |id_string|
|
207
|
+
ensure_value!('identifier', id_string)
|
208
|
+
end
|
209
|
+
if rft.format
|
210
|
+
ensure_value!('format', rft.format)
|
211
|
+
end
|
212
|
+
|
213
|
+
rft.metadata.each { | key, value |
|
214
|
+
next unless value
|
215
|
+
ensure_value!( key, value)
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
# pass in a Referent, or a ropenurl ContextObjectEntity that has a metadata
|
220
|
+
# method. Or really anything with a #metadata method returning openurl-style
|
221
|
+
# keys and values.
|
222
|
+
# Method returns true iff the keys in common to both metadata packages
|
223
|
+
# have equal (==) values.
|
224
|
+
def metadata_intersects?(arg)
|
225
|
+
|
226
|
+
# if it's empty, good enough.
|
227
|
+
return true unless arg
|
228
|
+
|
229
|
+
intersect_keys = self.metadata.keys & arg.metadata.keys
|
230
|
+
# Take out keys who's values are blank. If one is blank but not
|
231
|
+
# both, we can still consider that a match.
|
232
|
+
intersect_keys.delete_if{ |k| self.metadata[k].blank? || arg.metadata[k].blank? }
|
233
|
+
|
234
|
+
self_subset = self.metadata.reject{ |k, v| ! intersect_keys.include?(k) }
|
235
|
+
arg_subset = arg.metadata.reject{ |k, v| ! intersect_keys.include?(k) }
|
236
|
+
|
237
|
+
return self_subset == arg_subset
|
238
|
+
end
|
239
|
+
|
240
|
+
# Creates a hash of values from referrent_values, to assemble what was
|
241
|
+
# spread accross differnet db rows into one easy-lookup hash, for
|
242
|
+
# easy access. See also #to_citation for a different hash, specifically
|
243
|
+
# for use in View to print citation. And #to_context_object.
|
244
|
+
def metadata
|
245
|
+
metadata = {}
|
246
|
+
self.referent_values.each { | val |
|
247
|
+
metadata[val.key_name] = val.value if val.metadata? and not val.private_data?
|
248
|
+
}
|
249
|
+
return metadata
|
250
|
+
end
|
251
|
+
|
252
|
+
def private_data
|
253
|
+
self.referent_values
|
254
|
+
priv_data = {}
|
255
|
+
self.referent_values.each { | val |
|
256
|
+
priv_data[val.key_name] = val.value if val.private_data?
|
257
|
+
}
|
258
|
+
return priv_data
|
259
|
+
end
|
260
|
+
|
261
|
+
def identifiers
|
262
|
+
self.referent_values
|
263
|
+
identifiers = []
|
264
|
+
self.referent_values.each { | val |
|
265
|
+
if val.key_name == 'identifier'
|
266
|
+
identifiers << val.value
|
267
|
+
end
|
268
|
+
}
|
269
|
+
return identifiers
|
270
|
+
end
|
271
|
+
|
272
|
+
def add_identifier(id)
|
273
|
+
unless ( identifiers.find{|i| i == id} )
|
274
|
+
self.referent_values.create(:key_name => 'identifier', :value => id, :normalized_value => ReferentValue.normalize(id), :metadata => false, :private_data => false).save!
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def format
|
279
|
+
self.referent_values
|
280
|
+
self.referent_values.each { | val |
|
281
|
+
if val.key_name == 'format'
|
282
|
+
return val.value
|
283
|
+
end
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
# Some shortcuts for pulling out/manipulating specific especially
|
288
|
+
# useful data elements.
|
289
|
+
|
290
|
+
# finds and normalizes an LCCN. If multiple LCCNs are in the record,
|
291
|
+
# returns the first one. Returns a NORMALIZED lccn, but does NOT do
|
292
|
+
# validation. see:
|
293
|
+
# http://info-uri.info/registry/OAIHandler?verb=GetRecord&metadataPrefix=reg&identifier=info:lccn/
|
294
|
+
def lccn
|
295
|
+
return get_lccn(self)
|
296
|
+
end
|
297
|
+
|
298
|
+
# Gets an ISSN, makes sure it's a valid ISSN or else returns nil.
|
299
|
+
# So will return a valid ISSN (NOT empty string) or nil.
|
300
|
+
def issn
|
301
|
+
return get_issn(self)
|
302
|
+
end
|
303
|
+
|
304
|
+
def isbn
|
305
|
+
return get_isbn(self)
|
306
|
+
end
|
307
|
+
|
308
|
+
def oclcnum
|
309
|
+
return get_oclcnum(self)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Creates an OpenURL::ContextObject assembling all the data in this
|
313
|
+
# referrent.
|
314
|
+
def to_context_object
|
315
|
+
co = OpenURL::ContextObject.new
|
316
|
+
|
317
|
+
# Got to initialize the referent entity properly for our format.
|
318
|
+
# OpenURL sucks, this is confusing, yes.
|
319
|
+
fmt_uri = 'info:ofi/fmt:xml:xsd:' + self.format
|
320
|
+
co.referent = OpenURL::ContextObjectEntity.new_from_format( fmt_uri )
|
321
|
+
rft = co.referent
|
322
|
+
|
323
|
+
# Now set all the values.
|
324
|
+
self.referent_values.each do | val |
|
325
|
+
next if val.private_data?
|
326
|
+
if val.metadata?
|
327
|
+
rft.set_metadata(val.key_name, val.value)
|
328
|
+
next
|
329
|
+
end
|
330
|
+
rft.send('set_'+val.key_name, val.value) if rft.respond_to?('set_'+val.key_name)
|
331
|
+
end
|
332
|
+
return co
|
333
|
+
end
|
334
|
+
|
335
|
+
# Creates a hash for use in View code to display a citation
|
336
|
+
def to_citation
|
337
|
+
citation = {}
|
338
|
+
# call self.metadata once and use the array for efficiency, don't
|
339
|
+
# keep calling it. profiling shows it DOES make a difference.
|
340
|
+
my_metadata = self.metadata
|
341
|
+
|
342
|
+
|
343
|
+
if my_metadata['atitle'] && ! my_metadata['atitle'].blank?
|
344
|
+
citation[:title] = my_metadata['atitle']
|
345
|
+
citation[:title_label], citation[:subtitle_label] =
|
346
|
+
case my_metadata['genre']
|
347
|
+
when /article|journal|issue/ then ['Article Title', 'Journal Title']
|
348
|
+
when /bookitem|book/ then ['Chapter/Part Title', 'Book Title']
|
349
|
+
when /proceeding|conference/ then ['Proceeding Title', 'Conference Name']
|
350
|
+
when 'report' then ['Report Title','Report']
|
351
|
+
else
|
352
|
+
if self.format == 'book'
|
353
|
+
['Chapter/Part Title', 'Title']
|
354
|
+
elsif self.format == 'journal'
|
355
|
+
['Article Title', 'Journal Title']
|
356
|
+
else # default fall through, use much what SFX uses.
|
357
|
+
['Title', 'Source']
|
358
|
+
end
|
359
|
+
end
|
360
|
+
['title','btitle','jtitle'].each do | t_type |
|
361
|
+
if ! my_metadata[t_type].blank?
|
362
|
+
citation[:subtitle] = my_metadata[t_type]
|
363
|
+
citation[:container_title] = my_metadata[t_type]
|
364
|
+
break
|
365
|
+
end
|
366
|
+
end
|
367
|
+
else
|
368
|
+
citation[:title_label] = case my_metadata["genre"]
|
369
|
+
when /article|journal|issue/ then 'Journal Title'
|
370
|
+
when /bookitem|book/ then 'Book Title'
|
371
|
+
when /proceeding|conference/ then 'Conference Name'
|
372
|
+
when 'report' then 'Report Title'
|
373
|
+
else'Title'
|
374
|
+
end
|
375
|
+
['title','btitle','jtitle'].each do | t_type |
|
376
|
+
if ! my_metadata[t_type].blank?
|
377
|
+
citation[:title] = my_metadata[t_type]
|
378
|
+
break
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
# add publisher for books
|
383
|
+
if (my_metadata['genre'] == 'book')
|
384
|
+
citation[:pub] = my_metadata['pub'] unless my_metadata['pub'].blank?
|
385
|
+
end
|
386
|
+
|
387
|
+
citation[:issn] = issn if issn
|
388
|
+
citation[:isbn] = isbn if isbn
|
389
|
+
|
390
|
+
['volume','issue','date'].each do | key |
|
391
|
+
citation[key.to_sym] = my_metadata[key]
|
392
|
+
end
|
393
|
+
if ! my_metadata["au"].blank?
|
394
|
+
citation[:author] = my_metadata["au"]
|
395
|
+
elsif my_metadata["aulast"]
|
396
|
+
citation[:author] = my_metadata["aulast"]
|
397
|
+
if ! my_metadata["aufirst"].blank?
|
398
|
+
citation[:author] += ', '+my_metadata["aufirst"]
|
399
|
+
else
|
400
|
+
if ! my_metadata["auinit"].blank?
|
401
|
+
citation[:author] += ', '+my_metadata["auinit"]
|
402
|
+
else
|
403
|
+
if ! my_metadata["auinit1"].blank?
|
404
|
+
citation[:author] += ', '+my_metadata["auinit1"]
|
405
|
+
end
|
406
|
+
if ! my_metadata["auinitm"].blank?
|
407
|
+
citation[:author] += my_metadata["auinitm"]
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
if my_metadata['spage']
|
413
|
+
citation[:page] = my_metadata['spage']
|
414
|
+
citation[:page] += ' - ' + my_metadata['epage'] if ! my_metadata['epage'].blank?
|
415
|
+
end
|
416
|
+
citation[:identifiers] = []
|
417
|
+
self.identifiers.each do | id |
|
418
|
+
citation[:identifiers] << id unless (id.blank? || id.match(/^tag:/))
|
419
|
+
end
|
420
|
+
return citation
|
421
|
+
end
|
422
|
+
|
423
|
+
def type_of_thing
|
424
|
+
genre = self.metadata["genre"]
|
425
|
+
genre = nil if genre =~ /^unknown$/i
|
426
|
+
genre ||= "resource"
|
427
|
+
|
428
|
+
genre = "book section" if genre =~ /^bookitem$/i
|
429
|
+
|
430
|
+
return genre
|
431
|
+
end
|
432
|
+
|
433
|
+
def remove_value(key)
|
434
|
+
referent_values.find(:all, :conditions=> ['key_name =?', key]).each do |rv|
|
435
|
+
referent_values.delete(rv)
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
# options => { :overwrite => false } to only enhance if not already there
|
440
|
+
def enhance_referent(key, value, metadata=true, private_data=false, options = {})
|
441
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
442
|
+
return if value.nil?
|
443
|
+
|
444
|
+
matches = self.referent_values.to_a.find_all do |rv|
|
445
|
+
(rv.key_name == key) && (rv.metadata == metadata) && (rv.private_data == private_data)
|
446
|
+
end
|
447
|
+
|
448
|
+
matches.each do |rv|
|
449
|
+
unless (options[:overwrite] == false || rv.value == value)
|
450
|
+
rv.value = value
|
451
|
+
rv.save!
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
if (matches.length == 0)
|
456
|
+
val = self.referent_values.create(:key_name => key, :value => value, :normalized_value => ReferentValue.normalize(value), :metadata => metadata, :private_data => private_data)
|
457
|
+
val.save!
|
458
|
+
end
|
459
|
+
|
460
|
+
if key.match((/(^[ajb]?title$)|(^is[sb]n$)|(^volume$)|(^date$)/))
|
461
|
+
case key
|
462
|
+
when 'date' then self.year = ReferentValue.normalize(value)
|
463
|
+
when 'volume' then self.volume = ReferentValue.normalize(value)
|
464
|
+
when 'issn' then self.issn = ReferentValue.normalize(value)
|
465
|
+
when 'isbn' then self.isbn = ReferentValue.normalize(value)
|
466
|
+
when 'atitle' then self.atitle = ReferentValue.normalize(value)
|
467
|
+
else self.title = ReferentValue.normalize(value)
|
468
|
+
end
|
469
|
+
self.save!
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|