median 0.0.1
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/.gitignore +7 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +118 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +5 -0
- data/Rakefile +38 -0
- data/lib/median.rb +33 -0
- data/lib/median/aleph.rb +18 -0
- data/lib/median/aleph/authentication.rb +64 -0
- data/lib/median/aleph/cash.rb +35 -0
- data/lib/median/aleph/hold.rb +86 -0
- data/lib/median/aleph/item.rb +34 -0
- data/lib/median/aleph/loan.rb +59 -0
- data/lib/median/aleph/patron.rb +110 -0
- data/lib/median/aleph/search.rb +11 -0
- data/lib/median/aleph/utils.rb +82 -0
- data/lib/median/primo.rb +9 -0
- data/lib/median/primo/facet.rb +27 -0
- data/lib/median/primo/record.rb +115 -0
- data/lib/median/primo/search.rb +32 -0
- data/lib/median/primo/search_request.rb +218 -0
- data/lib/median/primo/search_result.rb +64 -0
- data/lib/median/version.rb +3 -0
- data/lib/median/xml_support.rb +91 -0
- data/lib/tasks/median_tasks.rake +4 -0
- data/median.gemspec +24 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/mailers/.gitkeep +0 -0
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +56 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -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 +58 -0
- data/test/dummy/lib/assets/.gitkeep +0 -0
- data/test/dummy/log/.gitkeep +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/median_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- metadata +192 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
class Median::Aleph::Item
|
2
|
+
include Median::XmlSupport
|
3
|
+
|
4
|
+
field :sub_library_code, :xpath => './z30-sub-library-code'
|
5
|
+
field :sub_library_string, :xpath => './z30/z30-sub-library'
|
6
|
+
field :collection, :xpath => './z30/z30-collection'
|
7
|
+
field :signature, :xpath => './z30/z30-call-no'
|
8
|
+
field :barcode, :xpath => './z30/z30-barcode'
|
9
|
+
field :status_code, :xpath => './z30-item-status-code'
|
10
|
+
field :status_string, :xpath => './z30/z30-item-status'
|
11
|
+
field :material, :xpath => './z30/z30-material'
|
12
|
+
field :loan_date, :xpath => './status', :process => :format_date
|
13
|
+
field :no_of_holds, :xpath => './queue', :process => :get_no_of_holds
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def format_date(value)
|
18
|
+
begin
|
19
|
+
Date.strptime(value, '%d/%m/%y')
|
20
|
+
rescue
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_no_of_holds(value)
|
26
|
+
begin
|
27
|
+
matches = value.match(/([0-9]+).+([0-9]+)/)
|
28
|
+
matches[1].to_i
|
29
|
+
rescue
|
30
|
+
0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Median::Aleph
|
2
|
+
module Loan
|
3
|
+
|
4
|
+
def get_loans(patron_id, options = {})
|
5
|
+
options = options.reverse_merge(history: false)
|
6
|
+
loans = []
|
7
|
+
|
8
|
+
# load loans
|
9
|
+
url = "#{Median.config.aleph_rest_service_base_url}/patron/#{patron_id}/circulationActions/loans"
|
10
|
+
xml = options[:history] ?
|
11
|
+
get_url(url, view: 'brief', type: 'history', no_loans: 250) :
|
12
|
+
get_url(url, view: 'brief')
|
13
|
+
|
14
|
+
# parse result
|
15
|
+
xml.xpath('//loan').each do |loan_xml|
|
16
|
+
loan = OpenStruct.new
|
17
|
+
loan.id = loan_xml.attribute('href').try(:content).try(:split, '/').try(:last)
|
18
|
+
loan.can_renew = (loan_xml.attribute('renew').try(:content).try(:upcase) == 'Y') ? true : false
|
19
|
+
loan.author = loan_xml.at_xpath('z13-author').try(:content)
|
20
|
+
loan.title = loan_xml.at_xpath('z13-title').try(:content)
|
21
|
+
loan.year = loan_xml.at_xpath('z13-year').try(:content)
|
22
|
+
return_date = loan_xml.at_xpath('z36h-returned-date').try(:content)
|
23
|
+
due_date = loan_xml.at_xpath('z36-due-date').try(:content)
|
24
|
+
loan.returned = return_date.present? ? Date.strptime(return_date, '%Y%m%d') : nil
|
25
|
+
loan.due = due_date.present? ? Date.strptime(due_date, '%Y%m%d') : nil
|
26
|
+
loans << loan
|
27
|
+
end
|
28
|
+
|
29
|
+
# sort by due date ascending
|
30
|
+
loans.sort{|x,y| x.due <=> y.due }
|
31
|
+
|
32
|
+
# return
|
33
|
+
loans
|
34
|
+
end
|
35
|
+
|
36
|
+
def renew_loan(patron_id, loan_id)
|
37
|
+
xml = post_url("#{Median.config.aleph_rest_service_base_url}/patron/#{patron_id}/circulationActions/loans/#{loan_id}")
|
38
|
+
|
39
|
+
result_code = xml.at_xpath('//reply-code').try(:content).to_i
|
40
|
+
if result_code == 0 # no error
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
def renew_loans(patron_id)
|
48
|
+
xml = post_url("#{Median.config.aleph_rest_service_base_url}/patron/#{patron_id}/circulationActions/loans")
|
49
|
+
|
50
|
+
result_code = xml.at_xpath('//reply-code').try(:content).to_i
|
51
|
+
if result_code == 0 # no error
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Median::Aleph
|
2
|
+
module Patron
|
3
|
+
|
4
|
+
def change_patron_email(patron_id, email)
|
5
|
+
data01 = change_patron_email!(patron_id, email, "01")
|
6
|
+
data02 = change_patron_email!(patron_id, email, "02")
|
7
|
+
|
8
|
+
if bor_update_successfull?(data02)
|
9
|
+
return true
|
10
|
+
else
|
11
|
+
return false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def change_patron_password(patron_id, current_password, new_password)
|
17
|
+
# first (re)authenticate
|
18
|
+
if AlephApi.authenticate(patron_id, current_password)
|
19
|
+
# after successfull authentication, write the new password
|
20
|
+
data = post_url(Median.config.aleph_x_service_base_url,
|
21
|
+
op: 'update_bor',
|
22
|
+
update_flag: 'Y',
|
23
|
+
library: Median.config.aleph_user_library,
|
24
|
+
xml_full_req: <<-XML
|
25
|
+
<?xml version="1.0"?>
|
26
|
+
<p-file-20>
|
27
|
+
<patron-record>
|
28
|
+
<z303>
|
29
|
+
<match-id-type>01</match-id-type>
|
30
|
+
<match-id>#{patron_id}</match-id>
|
31
|
+
<record-action>X</record-action>
|
32
|
+
</z303>
|
33
|
+
<z308>
|
34
|
+
<record-action>U</record-action>
|
35
|
+
<z308-key-type>01</z308-key-type>
|
36
|
+
<z308-key-data>#{patron_id}</z308-key-data>
|
37
|
+
<z308-verification>#{new_password}</z308-verification>
|
38
|
+
</z308>
|
39
|
+
</patron-record>
|
40
|
+
</p-file-20>
|
41
|
+
XML
|
42
|
+
)
|
43
|
+
|
44
|
+
if bor_update_successfull?(data)
|
45
|
+
return true
|
46
|
+
else
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
else
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def change_patron_email!(patron_id, email, type)
|
57
|
+
data = post_url(Median.config.aleph_x_service_base_url,
|
58
|
+
op: 'update_bor',
|
59
|
+
update_flag: 'Y',
|
60
|
+
library: Median.config.aleph_user_library,
|
61
|
+
xml_full_req: <<-XML
|
62
|
+
<?xml version="1.0"?>
|
63
|
+
<p-file-20>
|
64
|
+
<patron-record>
|
65
|
+
<z303>
|
66
|
+
<match-id-type>01</match-id-type>
|
67
|
+
<match-id>#{patron_id}</match-id>
|
68
|
+
<record-action>X</record-action>
|
69
|
+
</z303>
|
70
|
+
<z304>
|
71
|
+
<record-action>U</record-action>
|
72
|
+
<z304-id>#{patron_id}</z304-id>
|
73
|
+
<z304-address-type>#{type}</z304-address-type>
|
74
|
+
<z304-sequence>01</z304-sequence>
|
75
|
+
<z304-email-address>#{email}</z304-email-address>
|
76
|
+
</z304>
|
77
|
+
</patron-record>
|
78
|
+
</p-file-20>
|
79
|
+
XML
|
80
|
+
)
|
81
|
+
|
82
|
+
return data
|
83
|
+
end
|
84
|
+
|
85
|
+
# Checks if the call was successfull. We face a problem here. X-Service Api
|
86
|
+
# was created by morons, so we need a dirty hack to distinguish between
|
87
|
+
# a successfull result and an error, because the message is always given
|
88
|
+
# inside an error-tag. E.g.
|
89
|
+
#
|
90
|
+
# An error:
|
91
|
+
# <update-bor>
|
92
|
+
# <patron-id>PB29096</patron-id>
|
93
|
+
# <error>Cannot update / delete z304. No match found for ID: PB29096.</error>
|
94
|
+
# ...
|
95
|
+
# </update-bor>
|
96
|
+
#
|
97
|
+
# A success:
|
98
|
+
# <update-bor>
|
99
|
+
# <patron-id>PA06003114</patron-id>
|
100
|
+
# <error>Succeeded to REWRITE table z308. cur-id PA06003114.</error>
|
101
|
+
# ...
|
102
|
+
# </update-bor>
|
103
|
+
#
|
104
|
+
def bor_update_successfull?(data)
|
105
|
+
return (data.at_xpath('/update-bor/patron-id') and
|
106
|
+
data.at_xpath('/update-bor/error').try(:content).try(:match, /Succ/i))
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Median::Aleph
|
2
|
+
module Search
|
3
|
+
|
4
|
+
def find_items(record_id, patron_id = nil)
|
5
|
+
url = "#{Median.config.aleph_rest_service_base_url}/record/#{record_id}/items"
|
6
|
+
xml = get_url(url, view: 'full')
|
7
|
+
xml.xpath('//items/item').collect { |xml| Item.new(xml) }
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Median::Aleph
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
def get_url(url, data = {})
|
5
|
+
begin
|
6
|
+
uri = get_uri(url)
|
7
|
+
uri.query = URI.encode_www_form(data)
|
8
|
+
http = get_http_connection(uri)
|
9
|
+
|
10
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
11
|
+
request.set_form_data(data)
|
12
|
+
|
13
|
+
response = http.request(request)
|
14
|
+
Nokogiri::XML(response.body)
|
15
|
+
rescue Exception => e
|
16
|
+
raise Median::ConnectionError.new(e)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def post_url(url, data = {})
|
21
|
+
begin
|
22
|
+
uri = get_uri(url)
|
23
|
+
http = get_http_connection(uri)
|
24
|
+
|
25
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
26
|
+
request.set_form_data(data)
|
27
|
+
|
28
|
+
response = http.request(request)
|
29
|
+
Nokogiri::XML(response.body)
|
30
|
+
rescue Exception => e
|
31
|
+
raise Median::ConnectionError.new(e)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_url(url, data = {})
|
36
|
+
begin
|
37
|
+
uri = get_uri(url)
|
38
|
+
uri.query = URI.encode_www_form(data)
|
39
|
+
http = get_http_connection(uri)
|
40
|
+
|
41
|
+
request = Net::HTTP::Delete.new(uri.request_uri)
|
42
|
+
request.set_form_data(data)
|
43
|
+
|
44
|
+
response = http.request(request)
|
45
|
+
Nokogiri::XML(response.body)
|
46
|
+
rescue Exception => e
|
47
|
+
raise Median::ConnectionError.new(e)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def put_url(url, data = {})
|
52
|
+
begin
|
53
|
+
uri = get_uri(url)
|
54
|
+
uri.query = URI.encode_www_form(data)
|
55
|
+
http = get_http_connection(uri)
|
56
|
+
|
57
|
+
request = Net::HTTP::Put.new(uri.request_uri)
|
58
|
+
request.set_form_data(data)
|
59
|
+
|
60
|
+
response = http.request(request)
|
61
|
+
Nokogiri::XML(response.body)
|
62
|
+
rescue Exception => e
|
63
|
+
raise Median::ConnectionError.new(e)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def get_uri(url, params = {})
|
70
|
+
url = "#{url}?#{params.collect{"#{k.to_s}=#{v.to_s}"}.join('&')}" if params
|
71
|
+
URI.parse(URI.escape(url))
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_http_connection(uri)
|
75
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
76
|
+
http.use_ssl = (uri.scheme == 'https')
|
77
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
78
|
+
return http
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
data/lib/median/primo.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class Median::Primo::Facet
|
2
|
+
include Median::XmlSupport
|
3
|
+
|
4
|
+
field :name, :xpath => '.', :attribute => 'NAME'
|
5
|
+
|
6
|
+
def initialize(xml)
|
7
|
+
super(xml)
|
8
|
+
@values = xml.xpath(".//FACET_VALUES").collect{ |xml| FacetValue.new(xml) }
|
9
|
+
|
10
|
+
if name == 'creationdate'
|
11
|
+
@values.sort!{ |x, y| y.key <=> x.key }
|
12
|
+
else
|
13
|
+
@values.sort!{ |x, y| y.count <=> x.count }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :values
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
class FacetValue
|
22
|
+
include Median::XmlSupport
|
23
|
+
|
24
|
+
field :key, :xpath => '.', :attribute => 'KEY'
|
25
|
+
field :count, :xpath => '.', :attribute => 'VALUE', :process => proc{ |v| v.to_i }
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
class Median::Primo::Record
|
2
|
+
include Median::XmlSupport
|
3
|
+
|
4
|
+
field :rank, xpath: '.', :attribute => 'RANK', process: proc{|v| v.to_f}
|
5
|
+
field :id, xpath: './/record/search/recordid'
|
6
|
+
field :ils_id, xpath: './/record/control/ilsapiid'
|
7
|
+
field :source_ids, xpath: './/record/control/sourceid', multiple: true
|
8
|
+
field :type, xpath: './/record/display/type'
|
9
|
+
field :title, xpath: './/record/display/title'
|
10
|
+
field :authors, xpath: './/record/display/creator', multiple: true, autosplit: ';'
|
11
|
+
field :contributors, xpath: './/record/display/contributor', multiple: true, autosplit: ';'
|
12
|
+
field :publisher, xpath: './/record/display/publisher'
|
13
|
+
field :ispartof, xpath: './/record/display/ispartof'
|
14
|
+
field :creationdate, xpath: './/record/display/creationdate'
|
15
|
+
field :edition, xpath: './/record/display/edition'
|
16
|
+
field :versions, xpath: './/record/display/version', process: proc{|v| v.to_i}, default: 1
|
17
|
+
field :subjects, xpath: './/record/search/subject', multiple: true
|
18
|
+
field :item_format, xpath: './/record/display/format'
|
19
|
+
field :languages, xpath: './/record/display/language', multiple: true, autosplit: ';'
|
20
|
+
field :identifier, xpath: './/record/display/identifier'
|
21
|
+
field :relations, xpath: './/record/display/relation', multiple: true
|
22
|
+
field :notations, xpath: './/record/search/lsr15', multiple: true, autosplit: ';'
|
23
|
+
field :superorder, xpath: './/record/search/lsr01'
|
24
|
+
field :suborder, xpath: './/record/search/lsr02'
|
25
|
+
field :frbrgroupid, xpath: './/record/facets/frbrgroupid'
|
26
|
+
field :description, xpath: './/record/display/description'
|
27
|
+
field :delivery_category, xpath: './/record/delivery/delcategory', process: proc{|v| v.downcase}
|
28
|
+
field :fulltext_available, xpath: './/record/delivery/fulltext', process: proc{|v| v.downcase != 'no_fulltext'}, default: true
|
29
|
+
|
30
|
+
attr_reader :links
|
31
|
+
|
32
|
+
def initialize(xml)
|
33
|
+
super(xml)
|
34
|
+
@links = Links.new(xml)
|
35
|
+
end
|
36
|
+
|
37
|
+
def dedub?
|
38
|
+
self.id.present? ? self.id.downcase.start_with?('dedupmrg') : false
|
39
|
+
end
|
40
|
+
|
41
|
+
def local_resource?
|
42
|
+
!online_resource?
|
43
|
+
end
|
44
|
+
|
45
|
+
def online_resource?
|
46
|
+
online_resource_categories = ['online_resource', 'online resource', 'remote search resource', 'sfx resource']
|
47
|
+
online_resource_categories.include?(self.delivery_category)
|
48
|
+
end
|
49
|
+
|
50
|
+
def versions?
|
51
|
+
versions > 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def series?
|
55
|
+
type.try(:downcase) == 'serie' or delivery_category == 'structural metadata'
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
class Links
|
61
|
+
include Median::XmlSupport
|
62
|
+
|
63
|
+
field :openurl, xpath: './LINKS/openurl', process: :force_sfx_menu_language
|
64
|
+
field :backlink, xpath: './LINKS/backlink'
|
65
|
+
field :thumbnail, xpath: './LINKS/thumbnail'
|
66
|
+
field :resource_links, xpath: './LINKS/linktorsrc', multiple: true
|
67
|
+
|
68
|
+
def filtered_resource_links
|
69
|
+
filter_ezb_links(self.resource_links)
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_resource_link
|
73
|
+
filtered_resource_links.first
|
74
|
+
end
|
75
|
+
|
76
|
+
def resource_link?
|
77
|
+
self.resource_links.present?
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def filter_ezb_links(links)
|
84
|
+
ezb_link = get_ezb_link(links)
|
85
|
+
ezb_link.present? ? [ezb_link] : links
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_ezb_link(links)
|
89
|
+
links.each do |link|
|
90
|
+
return link if link.include? 'uni-regensburg.de/ezeit'
|
91
|
+
end
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def force_sfx_menu_language(openurl)
|
96
|
+
openurl.gsub(/req\.language=.+&/, '') # remove the language setting to force thedefault
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# GetIt links are broken in the SOAP API. The links always point to the first result.
|
102
|
+
#
|
103
|
+
#class GetItLinks < Array
|
104
|
+
# def initialize(xml)
|
105
|
+
# super()
|
106
|
+
# nodes = xml.xpath('./GETIT')
|
107
|
+
# nodes.each do |node|
|
108
|
+
# #puts node.inspect
|
109
|
+
# #self << {getit1: node.attribute('GetIt1').try(:value), getit2: node.attribute('GetIt2').try(:value)}
|
110
|
+
# #puts self
|
111
|
+
# end
|
112
|
+
# end
|
113
|
+
#end
|
114
|
+
|
115
|
+
end
|