├── config.yml ├── docs ├── plugin_bibs.png ├── plugin_index.png ├── plugin_items.png ├── plugin_menu.png └── plugin_holdings.png ├── frontend ├── plugin_init.rb ├── assets │ ├── alma_integrations.js │ └── alma_integrations.css ├── views │ └── alma_integrations │ │ ├── index.html.erb │ │ ├── _add_bibs_form.html.erb │ │ ├── search.html.erb │ │ ├── _resource_linker.html.erb │ │ ├── _form.html.erb │ │ ├── _add_holdings_form.html.erb │ │ ├── _search_bibs.html.erb │ │ ├── _items_pagination.html.erb │ │ ├── _search_holdings.html.erb │ │ └── _search_items.html.erb ├── routes.rb ├── locales │ └── en.yml ├── models │ ├── alma_requester.rb │ ├── record_builder.rb │ └── alma_integrator.rb └── controllers │ └── alma_integrations_controller.rb └── README.md /config.yml: -------------------------------------------------------------------------------- 1 | repository_menu_controller: alma_integrations 2 | no_automatic_routes: true 3 | -------------------------------------------------------------------------------- /docs/plugin_bibs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duspeccoll/alma_integrations/HEAD/docs/plugin_bibs.png -------------------------------------------------------------------------------- /docs/plugin_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duspeccoll/alma_integrations/HEAD/docs/plugin_index.png -------------------------------------------------------------------------------- /docs/plugin_items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duspeccoll/alma_integrations/HEAD/docs/plugin_items.png -------------------------------------------------------------------------------- /docs/plugin_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duspeccoll/alma_integrations/HEAD/docs/plugin_menu.png -------------------------------------------------------------------------------- /docs/plugin_holdings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duspeccoll/alma_integrations/HEAD/docs/plugin_holdings.png -------------------------------------------------------------------------------- /frontend/plugin_init.rb: -------------------------------------------------------------------------------- 1 | ArchivesSpace::Application.extend_aspace_routes(File.join(File.dirname(__FILE__), "routes.rb")) 2 | -------------------------------------------------------------------------------- /frontend/assets/alma_integrations.js: -------------------------------------------------------------------------------- 1 | function AlmaIntegrations($alma_integrations_form) { 2 | this.$alma_integrations_form = $alma_integrations_form; 3 | this.setup_form(); 4 | } 5 | 6 | AlmaIntegrations.prototype.setup_form = function() { 7 | var self = this; 8 | $(document).trigger("loadedrecordsubforms.aspace", this.$alma_integrations_form); 9 | }; 10 | 11 | $(document).ready(function() { 12 | var almaIntegrations = new AlmaIntegrations($("#alma_integrations_form")); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= setup_context :title => I18n.t("plugins.alma_integrations.title") %> 2 | 3 |
4 |
5 |
6 | <%= render_aspace_partial :partial => "shared/flash_messages" %> 7 |

<%= I18n.t("plugins.alma_integrations.title") %>

8 |
9 |
10 | 11 | <%= render_aspace_partial :partial => "form" %> 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/routes.rb: -------------------------------------------------------------------------------- 1 | ArchivesSpace::Application.routes.draw do 2 | [AppConfig[:frontend_proxy_prefix], AppConfig[:frontend_prefix]].uniq.each do |prefix| 3 | scope prefix do 4 | match('/plugins/alma_integrations' => 'alma_integrations#index', :via => [:get]) 5 | match('/plugins/alma_integrations/search' => 'alma_integrations#search', :via => [:post]) 6 | match('/plugins/alma_integrations/add_bibs' => 'alma_integrations#add_bibs', :via => [:post]) 7 | match('/plugins/alma_integrations/add_holdings' => 'alma_integrations#add_holdings', :via => [:post]) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /frontend/assets/alma_integrations.css: -------------------------------------------------------------------------------- 1 | .pagination > li > button { 2 | position: relative; 3 | float: left; 4 | padding: 6px 12px; 5 | line-height: 1.42857143; 6 | text-decoration: none; 7 | color: #337ab7; 8 | background-color: #fff; 9 | border: 1px solid #ddd; 10 | margin-left: -1px; 11 | } 12 | 13 | .pagination > li > button:hover, 14 | .pagination > li > button:focus { 15 | color: darken(#337ab7, 15%); 16 | background-color: #eee; 17 | border-color: #ddd; 18 | } 19 | 20 | .pagination-sm > li > button { 21 | padding: 5px 10px; 22 | font-size: 12px; 23 | } 24 | 25 | .search-success { 26 | color: #155724; 27 | } 28 | 29 | .search-error { 30 | color: #721c24; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_add_bibs_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag({:controller => :alma_integrations, :action => :add_bibs}, {:class => "form-horizontal", :id => "post_bibs_form", :method => :post}) do |form| %> 2 | 3 | 4 | <% unless results['mms'].nil? %> 5 | 6 | <% end %> 7 |
8 |
9 | <%= submit_tag I18n.t("plugins.alma_integrations.actions.push"), :class => "btn btn-primary pull-right" %> 10 | <%= link_to "Edit Resource", {:controller => :resolver, :action => :resolve_edit, :uri => @results['ref']}, :class => "btn btn-cancel btn-default" %> 11 |
12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /frontend/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | plugins: 3 | alma_integrations: 4 | label: Alma Integrations 5 | title: Alma Integrations 6 | labels: 7 | holdings_id: Holdings ID 8 | location_code: Location Code 9 | location_name: Location Name 10 | actions: 11 | add: Add 12 | create: Create 13 | push: Push to Alma 14 | search: Search 15 | submit: Submit 16 | record_types: 17 | bibs: BIBs 18 | holdings: Holdings 19 | items: Items 20 | errors: 21 | no_marc: "No MARC record found in ArchivesSpace, search terminated" 22 | no_mms: "No MMS ID provided for this Resource." 23 | holdings: 24 | # populate these according to your preferred labels for holdings codes 25 | hscol: Hampden Center Special Collections 26 | pscol: Special Collections AAC Closed Stacks 27 | psmap: Special Collections Map Cases 28 | psfol: Special Collections Folios 29 | psovz: Special Collections Oversize 30 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/search.html.erb: -------------------------------------------------------------------------------- 1 | <%= setup_context :title => "#{I18n.t("plugins.alma_integrations.record_types.#{@results['record_type']}")} for #{@results['id']} #{@results['title']}", :trail => [[I18n.t("plugins.alma_integrations.title"), {:controller => :alma_integrations, :action => :index}]] %> 2 | 3 | <% results = @results['results'] %> 4 | 5 |
6 |
7 |
8 | <%= render_aspace_partial :partial => "shared/flash_messages" %> 9 |

<%= I18n.t("plugins.alma_integrations.title") %>

10 |
11 |
12 | 13 |
14 |
15 | <%= render_aspace_partial :partial => "search_#{@results['record_type']}", :locals => {:results => results} %> 16 |
17 |
18 | 19 | <%= render_aspace_partial :partial => "form" %> 20 |
21 | 22 | 23 | "/> 24 | -------------------------------------------------------------------------------- /frontend/models/alma_requester.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | 3 | # convenience class for making API requests 4 | # 5 | # despite the name, it is also used for making GET requests of the ArchivesSpace API 6 | 7 | class AlmaRequester 8 | 9 | def get(uri, opts = {}) 10 | response = Net::HTTP.start(uri.host, uri.port, opts) do |http| 11 | req = Net::HTTP::Get.new(uri) 12 | req['X-ArchivesSpace-Session'] = Thread.current[:backend_session] if uri.to_s.start_with?("#{AppConfig[:backend_url]}") 13 | http.request(req) 14 | end 15 | 16 | response 17 | end 18 | 19 | def post(uri, data, opts = {}) 20 | response = Net::HTTP.start(uri.host, uri.port, opts) do |http| 21 | req = Net::HTTP::Post.new(uri) 22 | req.body = data 23 | req.content_type = 'application/xml' 24 | http.request(req) 25 | end 26 | 27 | response 28 | end 29 | 30 | def put(uri, data, opts = {}) 31 | response = Net::HTTP.start(uri.host, uri.port, opts) do |http| 32 | req = Net::HTTP::Put.new(uri) 33 | req.body = data 34 | req.content_type = 'application/xml' 35 | http.request(req) 36 | end 37 | 38 | response 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_resource_linker.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | if params['resource'].blank? 3 | selected_json = "{}" 4 | else 5 | selected_json = params['resource']['_resolved'] 6 | end 7 | 8 | %> 9 | 10 |
11 | " 18 | data-selected="<%= selected_json %>" 19 | data-multiplicity="one" 20 | data-types='<%= ['resource'].to_json %>' 21 | /> 22 |
23 | 24 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Search Alma

3 |
4 | <%= form_tag({:controller => :alma_integrations, :action => :search}, {:class => "form-horizontal", :id => "alma_integrations_form"}) do |f| %> 5 |
6 | 7 |
8 | <%= render_aspace_partial :partial => "resource_linker" %> 9 |
10 |
11 | 12 |
13 | 14 |
15 | 20 |
21 |
22 | 23 |
24 |
25 | <%= submit_tag I18n.t("plugins.alma_integrations.actions.search"), :class => "btn btn-primary pull-right" %> 26 |
27 |
28 | <% end %> 29 |
30 |
31 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_add_holdings_form.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | existing_codes = results['holdings'].map{|h| h['code']} 3 | %> 4 | 5 |
6 |

Add New Holdings

7 |
8 | <%= form_tag({:controller => :alma_integrations, :action => :add_holdings}, {:class => "form-horizontal", :id => "add_holdings_form", :method => :post}) do |f| %> 9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 | 23 |
24 |
25 |
26 |
27 |
28 | <%= submit_tag I18n.t("plugins.alma_integrations.actions.add"), :class => "btn btn-primary pull-right" %> 29 |
30 |
31 |
32 | <% end %> 33 |
34 |
35 | -------------------------------------------------------------------------------- /frontend/models/record_builder.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | 3 | class RecordBuilder 4 | 5 | def build_bib(record, mms) 6 | marc = Nokogiri::XML(record) 7 | 8 | # Nokogiri won't put 'standalone' in the header so you have to do it yourself 9 | header = Nokogiri::XML('') 10 | 11 | doc = Nokogiri::XML::Builder.with(header){ |xml| xml.bib }.to_xml 12 | 13 | data = Nokogiri::XML(doc) 14 | if mms 15 | mms_id = Nokogiri::XML::Node.new('mms_id', data) 16 | mms_id.content = mms 17 | data.root.add_child(mms_id) 18 | end 19 | data.root.add_child(marc.at_css('record')) 20 | 21 | data.to_xml 22 | end 23 | 24 | def build_holding(code, id) 25 | controlfield_string = Time.now.strftime("%y%m%d") 26 | controlfield_string += "2u^^^^8^^^4001uueng0000000" 27 | # populate 852$b from alma_holdings config 28 | building = AppConfig[:alma_holdings].select{|a| a[1] == code}.first[0] 29 | 30 | # Nokogiri won't put 'standalone' in the header so you have to do it yourself 31 | doc = Nokogiri::XML('') 32 | 33 | builder = Nokogiri::XML::Builder.with(doc) do |xml| 34 | xml.holding { 35 | xml.record { 36 | xml.leader "^^^^^nx^^a22^^^^^1n^4500" 37 | xml.controlfield(:tag => '008') { xml.text controlfield_string } 38 | xml.datafield(:ind1 => '0', :tag => '852') { 39 | xml.subfield(:code => 'b') { xml.text building } 40 | xml.subfield(:code => 'c') { xml.text code } 41 | xml.subfield(:code => 'h') { xml.text "MS #{id}" } 42 | } 43 | } 44 | } 45 | end 46 | 47 | builder.to_xml 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_search_bibs.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= I18n.t("plugins.alma_integrations.record_types.#{@results['record_type']}") %> for <%= @results['id'] %> <%= @results['title'] %>

3 |
4 |
5 |
6 |
7 | <% if results['aspace'].has_key?('error') %> 8 |
9 | Not found in ArchivesSpace: <%= results['aspace']['error'] %> 10 |
11 | <% else %> 12 |
13 | Found in ArchivesSpace: <%= results['aspace']['success'] %> 14 |
15 | <% end %> 16 | <% if results['alma'].has_key?('error') %> 17 |
18 | Not found in Alma: <%= results['alma']['error'] %> 19 |
20 | <% else %> 21 |
22 | Found in Alma: <%= results['alma']['success'] %> 23 |
24 | <% end %> 25 |
26 |
27 |
28 | 29 |

MARC Record

30 |
31 |
32 |
33 |
<%= results['marc'] %>
34 |
35 |
36 |
37 | 38 | <%= render_aspace_partial :partial => "add_bibs_form", :locals => {:results => results} %> 39 |
40 |
41 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_items_pagination.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | page_limit = 10 3 | 4 | first_page = [results['page'] - page_limit / 2, 1].max 5 | last_page = [first_page + page_limit, results['last_page']].min 6 | 7 | page_range = (first_page..last_page) 8 | %> 9 | 10 | <% if results %> 11 | <%= form_tag({:controller => :alma_integrations, :action => :search}) do |f| %> 12 | 13 | 14 |
15 | 44 |
45 | <% end %> 46 | <% end %> 47 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_search_holdings.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= I18n.t("plugins.alma_integrations.record_types.#{@results['record_type']}") %> for <%= @results['id'] %> <%= @results['title'] %>

3 |
4 |
5 |
6 |
7 | <% if @results['mms'].nil? %> 8 |

No MMS ID is provided for this Resource, so no Holdings may be retrieved from or added to Alma. Holdings must be associated with an Alma BIB record.

9 | <% else %> 10 | <% if results['holdings'].nil? or results['holdings'].empty? %> 11 |

No holdings were found.

12 | <% else %> 13 |

Displaying <%= results['count'] %> record(s):

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% results['holdings'].each do |holding| %> 24 | 25 | 26 | 27 | 28 | 29 | <% end %> 30 | 31 |
<%= I18n.t("plugins.alma_integrations.labels.holdings_id") %><%= I18n.t("plugins.alma_integrations.labels.location_code") %><%= I18n.t("plugins.alma_integrations.labels.location_name") %>
<%= holding['id'] %><%= holding['code'] %><%= holding['name'] %>
32 | <% end %> 33 | <% end %> 34 |
35 |
36 |
37 |
38 |
39 | 40 | <%= render_aspace_partial :partial => "add_holdings_form", :locals => {:results => results} %> 41 | -------------------------------------------------------------------------------- /frontend/views/alma_integrations/_search_items.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= I18n.t("plugins.alma_integrations.record_types.#{@results['record_type']}") %> for <%= @results['id'] %> <%= @results['title'] %>

3 |
4 |
5 |
6 |
7 | <% if results['items'].nil? || results['items'].empty? %> 8 |

No items found in Alma for this collection.

9 | <% else %> 10 |

Displaying Items <%= results['offset'] + 1 %> - <%= [results['offset'] + 10, results['count']].min %> of <%= results['count'] %>:

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% results['items'].each do |item| %> 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | <% end %> 39 | 40 |
Item PIDBarcodeDescriptionLocationAlma ProfileArchivesSpace ProfileTop Container URI
<%= item['pid'] %><%= item['barcode'] %><%= item['description'] %><%= item['location'] %><%= item['alma_profile'] %><%= item['as_profile'] unless item['as_profile'].blank? %> 33 | <% unless item['top_container'].blank? %> 34 | <%= item['top_container'] %> 35 | <% end %> 36 |
41 | 42 | <%= render_aspace_partial :partial => "items_pagination", :locals => {:results => results} %> 43 | <% end %> 44 |
45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /frontend/controllers/alma_integrations_controller.rb: -------------------------------------------------------------------------------- 1 | require 'advanced_query_builder' 2 | 3 | class AlmaIntegrationsController < ApplicationController 4 | 5 | set_access_control "view_repository" => [:index, :search, :add_bibs, :add_holdings] 6 | 7 | def index 8 | end 9 | 10 | def search 11 | if params['resource'].nil? && params['ref'].nil? 12 | flash[:error] = "Error: No resource selected" 13 | redirect_to :action => :index 14 | end 15 | 16 | params['ref'] = params['resource']['ref'] if params['ref'].nil? 17 | @results = do_search(params) 18 | @holdings = AppConfig[:alma_holdings] if params['record_type'] == 'holdings' 19 | end 20 | 21 | def add_bibs 22 | post_bibs(params) 23 | end 24 | 25 | # debugging adding holdings 26 | def add_holdings 27 | post_holdings(params) 28 | end 29 | 30 | private 31 | 32 | def integrator 33 | AlmaIntegrator.new(AppConfig[:alma_api_url], AppConfig[:alma_apikey]) 34 | end 35 | 36 | # set this to your user-defined MMS ID field 37 | def mms_field 38 | 'string_2' 39 | end 40 | 41 | def do_search(params) 42 | ref = params['ref'] 43 | page = params['page'].nil? ? 1 : params['page'].to_i 44 | json = JSONModel::HTTP::get_json(ref) 45 | 46 | results = { 47 | 'title' => json['title'], 48 | 'id' => json['id_0'], 49 | 'ref' => ref, 50 | 'record_type' => params['record_type'], 51 | } 52 | 53 | if json['user_defined'].nil? or json['user_defined'][mms_field].nil? 54 | results['mms'] = nil 55 | else 56 | results['mms'] = json['user_defined'][mms_field] 57 | end 58 | 59 | results['results'] = case params['record_type'] 60 | when "bibs" 61 | integrator.search_bibs(ref, results['mms']) 62 | when "holdings" 63 | integrator.search_holdings(results['mms']) 64 | when "items" 65 | integrator.search_items(results['mms'], page) 66 | end 67 | 68 | results 69 | end 70 | 71 | def update_resource(ref, mms) 72 | obj = JSONModel::HTTP.get_json(ref) 73 | if obj['user_defined'].nil? 74 | obj['user_defined'] = { mms_field => mms } 75 | else 76 | obj['user_defined'][mms_field] = mms 77 | end 78 | 79 | uri = URI("#{JSONModel::HTTP.backend_url}#{ref}") 80 | response = JSONModel::HTTP.post_json(uri, obj.to_json) 81 | end 82 | 83 | def post_bibs(params) 84 | data = RecordBuilder.new.build_bib(params['marc'], params['mms']) 85 | response = integrator.post_bib(params['mms'], data) 86 | 87 | if response.is_a?(Net::HTTPSuccess) 88 | if params['mms'].nil? 89 | doc = Nokogiri::XML(response.body) 90 | mms = doc.at_css('mms_id').text 91 | flash[:success] = "BIB created. MMS ID: #{mms}" 92 | update_resource(params['ref'], mms) 93 | else 94 | flash[:success] = "BIB updated. MMS ID: #{params['mms']}" 95 | end 96 | else 97 | flash[:error] = "Error: #{response.body}" 98 | end 99 | 100 | redirect_to :action => :index 101 | end 102 | 103 | def post_holdings(params) 104 | data = RecordBuilder.new.build_holding(params['code'], params['id']) 105 | response = integrator.post_holding(params['mms'], data) 106 | 107 | if response.is_a?(Net::HTTPSuccess) 108 | doc = Nokogiri::XML(response.body) 109 | flash[:success] = "Holdings created. Holding ID: #{doc.at_css('holding_id').text}" 110 | else 111 | flash[:error] = "Error: #{response.body}" 112 | end 113 | 114 | redirect_to :action => :index 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /frontend/models/alma_integrator.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'nokogiri' 3 | require 'advanced_query_builder' 4 | 5 | class AlmaIntegrator 6 | 7 | def initialize(baseurl, key) 8 | @baseurl = baseurl 9 | @key = key 10 | end 11 | 12 | def get_archivesspace_bib(ref) 13 | aspace = {} 14 | uri = URI("#{JSONModel::HTTP.backend_url}#{ref.gsub(/(\d+)$/,'marc21/\1.xml')}") 15 | response = AlmaRequester.new.get(uri) 16 | if response.is_a?(Net::HTTPSuccess) 17 | xml = Nokogiri::XML(response.body,&:noblanks) 18 | aspace['content'] = xml.at_css('record') 19 | else 20 | aspace['error'] = JSON.parse(response.body)['error'] 21 | end 22 | 23 | return aspace 24 | end 25 | 26 | def get_alma_bib(mms) 27 | alma = {} 28 | 29 | if mms.nil? 30 | alma['error'] = I18n.t("plugins.alma_integrations.errors.no_mms") 31 | else 32 | uri = URI("#{@baseurl}/#{mms}") 33 | uri.query = URI.encode_www_form({:apikey => @key}) 34 | response = AlmaRequester.new.get(uri, :use_ssl => true) 35 | xml = Nokogiri::XML(response.body,&:noblanks) 36 | if response.is_a?(Net::HTTPSuccess) 37 | alma['content'] = xml.at_css('record') 38 | else 39 | alma['error'] = "[#{xml.at_css('errorCode').text}] #{xml.at_css('errorMessage').text}" 40 | end 41 | end 42 | 43 | alma 44 | end 45 | 46 | def sync_bibs(aspace, alma) 47 | aspace_008 = aspace['content'].at_css('controlfield[@tag="008"]') 48 | alma_008 = alma['content'].at_css('controlfield[@tag="008"]') 49 | 50 | if aspace_008.text[0,6] != alma_008.text[0,6] 51 | controlfield_string = alma_008.text[0,6] 52 | controlfield_string += aspace_008.text[6..-1] 53 | aspace['content'].at_css('controlfield[@tag="008"]').content = controlfield_string 54 | end 55 | 56 | return aspace['content'].to_xml(indent: 2) 57 | end 58 | 59 | def search_bibs(ref, mms) 60 | results = {'mms' => mms} 61 | 62 | # first, try to get the ArchivesSpace MARC record 63 | # next, try to get the Alma MARC record 64 | # last, compare them to see if any changes need to be made before overlay 65 | # (e.g. bringing over Alma's 008/0-5 for date on file) 66 | 67 | aspace = get_archivesspace_bib(ref) 68 | if aspace.has_key?('error') 69 | results['aspace'] = {'error' => aspace['error']} 70 | results['alma'] = {'error' => I18n.t("plugins.alma_integrations.errors.no_marc")} 71 | else 72 | results['aspace'] = {'success' => ref} 73 | alma = get_alma_bib(mms) 74 | if alma.has_key?('error') 75 | results['alma'] = {'error' => alma['error']} 76 | results['marc'] = aspace['content'].to_xml(indent: 2) 77 | else 78 | results['alma'] = {'success' => mms} 79 | results['marc'] = sync_bibs(aspace, alma) 80 | end 81 | end 82 | 83 | results 84 | end 85 | 86 | def search_holdings(mms) 87 | results = { 'holdings' => [] } 88 | 89 | return if mms.nil? 90 | 91 | uri = URI("#{@baseurl}/#{mms}/holdings") 92 | uri.query = URI.encode_www_form({:apikey => @key, :format => 'json'}) 93 | response = AlmaRequester.new.get(uri, :use_ssl => true) 94 | 95 | if response.is_a?(Net::HTTPSuccess) 96 | obj = JSON.parse(response.body) 97 | results['count'] = obj['total_record_count'] 98 | if results['count'] > 0 99 | holdings = obj['holding'] 100 | holdings.each do |holding| 101 | h = { 102 | 'id' => holding['holding_id'], 103 | 'code' => holding['location']['value'], 104 | 'name' => holding['location']['desc'] 105 | } 106 | 107 | results['holdings'].push(h) 108 | end 109 | end 110 | end 111 | 112 | results 113 | end 114 | 115 | def get_aspace_item_data(barcode) 116 | item_data = {} 117 | 118 | aq = AdvancedQueryBuilder.new 119 | aq.and('barcode_u_sstr', barcode) 120 | url = "#{JSONModel(:top_container).uri_for("")}/search" 121 | obj = JSONModel::HTTP::get_json(url, {'filter' => aq.build.to_json}) 122 | 123 | item = obj['response']['docs'].first 124 | return item_data if item.nil? 125 | 126 | unless item['container_profile_display_string_u_sstr'].nil? 127 | item_data['profile'] = item['container_profile_display_string_u_sstr'].first 128 | .partition('[') 129 | .first 130 | .rstrip 131 | end 132 | item_data['top_container'] = item['uri'] unless item['uri'].nil? 133 | 134 | return item_data 135 | end 136 | 137 | def search_items(mms,page) 138 | results = { 'page' => page, 'offset' => (page - 1) * 10, 'items' => [] } 139 | 140 | uri = URI("#{@baseurl}/#{mms}/holdings/ALL/items") 141 | uri.query = URI.encode_www_form({:apikey => @key, :format => 'json', :offset => results['offset']}) 142 | response = AlmaRequester.new.get(uri, :use_ssl => true) 143 | 144 | if response.is_a?(Net::HTTPSuccess) 145 | obj = JSON.parse(response.body) 146 | results['count'] = obj['total_record_count'] 147 | results['last_page'] = obj['total_record_count'].round(-1) / 10 148 | if results['count'] > 0 149 | items = obj['item'] 150 | items.each do |item| 151 | item_data = item['item_data'] 152 | as_item_data = get_aspace_item_data(item_data['barcode']) 153 | i = { 154 | 'pid' => item_data['pid'], 155 | 'barcode' => item_data['barcode'], 156 | 'description' => item_data['description'], 157 | 'location' => item_data['location']['value'], 158 | 'alma_profile' => item_data['internal_note_2'], 159 | 'as_profile' => as_item_data['profile'], 160 | 'top_container' => as_item_data['top_container'] 161 | } 162 | 163 | results['items'].push(i) 164 | end 165 | end 166 | end 167 | 168 | results 169 | end 170 | 171 | def post_bib(mms, data) 172 | if mms.nil? 173 | uri = URI(@baseurl) 174 | uri.query = URI.encode_www_form({:apikey => @key}) 175 | response = AlmaRequester.new.post(uri, data, :use_ssl => true) 176 | else 177 | uri = URI("#{@baseurl}/#{mms}") 178 | uri.query = URI.encode_www_form({:apikey => @key}) 179 | response = AlmaRequester.new.put(uri, data, :use_ssl => true) 180 | end 181 | 182 | response 183 | end 184 | 185 | def post_holding(mms, data) 186 | uri = URI("#{@baseurl}/#{mms}/holdings") 187 | uri.query = URI.encode_www_form({:apikey => @key}) 188 | response = AlmaRequester.new.post(uri, data, :use_ssl => true) 189 | 190 | response 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alma/ArchivesSpace Integrations 2 | 3 | This plugin provides integrations between the ArchivesSpace archival collection management system and the Alma library management system from Ex Libris. It is built on the [Top Container](https://github.com/hudmol/container_management) functionality that Hudson Molonglo developed for ArchivesSpace. Based on the Resource record provided by a user, the integrations will perform the following API calls: 4 | 5 | * Check for a BIB with the Resource's MMS ID 6 | * Check for holdings associated with the BIB identified by that MMS ID 7 | 8 | Additionally, the integrations allow a user to add new holdings, create a new BIB record if no MMS ID is present or the provided MMS ID does not match any BIB record in Alma, or sync changes to a Resource in ArchivesSpace with the BIB record uniquely identified by the Resource's MMS ID. 9 | 10 | # Prerequisites 11 | 12 | ## For your Resource record 13 | 14 | You will need to have a data element in your ArchivesSpace Resources assigned to the MMS IDs for their Alma bibliographic records, so that the API calls have an identifier against which to check. The University of Denver records MMS IDs in User Defined String 2; for now, this plugin assumes you also do this. Future development will allow this field to be configured on a per-instance basis. 15 | 16 | ## For your config.rb file 17 | 18 | You will need to add three configuration settings to your config.rb file for these integrations to work: 19 | 20 | * **AppConfig[:alma_api_url]** represents the URL you use to access the Alma API. These are region-specific; find yours [here](https://developers.exlibrisgroup.com/alma/apis#calling). Note that since the plugin only uses the `/bibs` API, you will need to include "/bibs" at the end of the API URL string. 21 | * **AppConfig[:alma_apikey]** is the specific API key you use to access the Alma APIs. You may need to consult with your library IT department to access an API key to use for this plugin. If you would like to test API calls against the Alma sandbox, you may request a personal API key through the Alma Developer Network; instructions for this may be found [here](https://developers.exlibrisgroup.com/alma/apis#logging). 22 | * **AppConfig[:alma_holdings]** is an array of the building and location codes in place at your institution. Each item in the array is itself an array, consisting of a building code (`852$b`) and a location code (`852$c`). These are added to the 852 field of the holdings records that the plugin creates. 23 | 24 | # Using the integrations 25 | 26 | The integrations may be accessed via the repository menu: 27 | 28 | ![Access the plugin by clicking on the repository menu dropdown. Hover over "Plugins," then select "Alma Integrations."](docs/plugin_menu.png) 29 | 30 | The plugin consists of a search form with two fields: the Resource, a linker where the user is prompted to search for a collection whose metadata in Alma they wish to view, and the Record Type, where the user is prompted to select the type of metadata with which they wish to work (either BIBs, Holdings, or Items). Once the resource and record type are selected, clicking the “Submit” button will initiate an Alma search. 31 | 32 | ![The Alma Integrations plugin index. Two fields are required: the Resource to be searched, and the Alma record type whose data the user would like to see.](docs/plugin_index.png) 33 | 34 | Depending on the record type selected, the plugin will query the Alma API for either BIB, Holding, or Item metadata using the MMS ID provided in the linked Resource record. If no MMS ID is provided, an alert to that effect. will appear in the search results view. 35 | 36 | ## Resource BIBs 37 | 38 | If the user selects BIBs as their record type, the plugin will display a side-by-side view of the MARC record generated by the ArchivesSpace API and the MARC record, if any, that is present in Alma with the MMS ID attached to the ArchivesSpace record. If there is no MMS ID present, the plugin will display a message to that effect in the Alma view. 39 | 40 | ![The BIBs view in the Alma Integrations plugin. Displays side-by-side MARC representations of the linked Resource, one generated by the ArchivesSpace API and one as it is recorded in Alma.](docs/plugin_bibs.png) 41 | 42 | If the user wishes to push changes from ArchivesSpace to Alma, e.g. if Resource-level metadata was added or updated in ArchivesSpace, clicking the “Push to Alma” button will overwrite the existing Alma MARC record with the MARC record generated by the ArchivesSpace API. If no MARC record is present in Alma for a Resource, clicking this button will create a new MARC record in Alma, then add that record’s MMS ID to the linked ArchivesSpace Resource. (Note that Alma's default is to suppress new records created via the API; for now you will need to use the Metadata Editor to un-suppress the record manually if you would like it published to Primo.) 43 | 44 | Currently there is no way to pull changes made in Alma back into ArchivesSpace. 45 | 46 | ## Resource Holdings 47 | 48 | If the user selects Holdings as their record type, the plugin searches for all holdings records attached to the BIB with the MMS ID provided by the user in the search form. It cross-checks the results against the list of location codes provided in the `alma_holdings_codes` setting of the ArchivesSpace instance's `config.rb` file, and returns a list of the holdings found via the API, including the record ID, location code, and location name for each. 49 | 50 | To add new holdings, the user may select the desired holdings location from the drop-down list found in the Add New Holdings sub-record form. This list contains the location codes set in the `alma_holdings_codes` configuration setting which were not found in the holdings search. Upon selecting a location and clicking the “Add” button, ArchivesSpace will attempt to post the new holdings to Alma, then return to the plugin index page. If successful, the new Holdings ID will be returned; if not, the plugin will return the error message returned by the Alma API. 51 | 52 | ![Holdings results from the Alma Integrations plugin](docs/plugin_holdings.png) 53 | 54 | ## Resource Items 55 | 56 | Currently a search for the Item record type returns a list of items attached to the BIB record with the MMS ID found on the linked Resource record. The plugin displays fields that are used by Special Collections and Archives at DU for inventory control and container management. There is no way to synchronize item-level metadata between ArchivesSpace and Alma in any way at this time. 57 | 58 | ![Item results from the Alma Integrations plugin](docs/plugin_items.png) 59 | 60 | # Future Development 61 | 62 | Requests for new features and bug fixes can be filed as [Issues](https://github.com/duspeccoll/alma_integrations/issues). 63 | 64 | # In Conclusion 65 | 66 | Feel free to kick the tires on this against your own ArchivesSpace/Alma environment and let me know how it works. Questions, comments, and/or pull requests welcome! E-mail: kmc35 [at] psu.edu. 67 | --------------------------------------------------------------------------------