Context-sensitive Requests in Primo

Bernardo Gomez - Emory University
Atlanta - Georgia USA

Context-sensitive request: Displaying link(s) based on user and item attributes

    Current request types:

  • Place a hold (via ALMA)

  • Book a visual item (via ALMA)

  • Book an archive item for viewing

  • Book an item from special collections for viewing

  • Place an item on reserves

  • Document delivery (scanning service)

  • Document delivery (hold a book)

Non-Alma requests appear as tabs in the tab row.

    Tab labels:

  • Book an archive item for viewing: Request from Rose Library
  • Book an item from special collections for viewing: Request from Rose Library
  • Place an item on reserves: Place on Reserves
  • Document delivery (scanning service): Document Delivery
  • Document delivery (hold a book): Document Delivery

* Requires primo sign-in

Generating the non-ALMA request links:

Primo view's footer contains javascript code

Javascript invokes Mehmet Celik's jQuery.PRIMO

jQuery.PRIMO provides PNX record, user.isLoggedIn() and ALMA's user_id

PNX indicates whether physical item belongs to Emory. If so, it contains ALMA's mms_id(s)

Javascript highlights


 <script type="text/javascript">
  jQuery(document).ready(function () {
    if (typeof(Storage) !== "undefined"){
      var isLoggedIn=jQuery.PRIMO.session.user.isLoggedIn();
      var record_count=jQuery.PRIMO.records.length;
      if (isLoggedIn ){
         bg_user_id=jQuery.PRIMO.session.user.id;
      }
      else{
         bg_user_id="0";
      }
      re_pattern_gatech=new RegExp(".*GALI_GIT_ALMA.*"),
      re_pattern_emo=new RegExp(".*01EMORY_ALMA.*"),
      var gatech_item=false;
      var emory_item=false;
      if (record_count == 1){
        pnx=jQuery.PRIMO.records[0].getPNX();
        parser=new DOMParser();
        xmlDoc = parser.parseFromString(pnx,"text/xml");
        x = xmlDoc.documentElement.childNodes;
        txt="";

        for (i = 0; i < x.length ;i++) {
           if (x[i].nodeName == "search"){
               search_node=x[i].childNodes;
               for (j=0; j< search_node.length;j++){
                  if (search_node[j].nodeName == "addsrcrecordid"){
                    record_id=search_node[j].childNodes[0].nodeValue;
                    if (merged_record_id == ""){
                        merged_record_id=record_id;
                    }
                    else{
                        merged_record_id=merged_record_id+","+record_id;
                    }

                  }
               }
           }
           if (x[i].nodeName == "control"){
               control_node=x[i].childNodes;
               for (j=0; j< control_node.length;j++){
                  if (control_node[j].nodeName == "sourceid"){
                    source_id=control_node[j].childNodes[0].nodeValue;
                    if (re_pattern_emo.test(source_id)){
                       emory_item=true;
                    }
                    if (re_pattern_gatech.test(source_id)){
                       gatech_item=true;
                    }
                  }
               }
           }
        }
  });

Webservice that returns applicable requests:

https://libapiproxyprod1.library.emory.edu/cgi-bin/primo_request?doc_id=&user_id=

        if (emory_item ){
             medusa_url="https://libapiproxyprod1.library.emory.edu/cgi-bin/primo_request?doc_id="+merged_record_id+"&user_id="+bg_user_id;
            jQuery.ajaxSetup({async:false});
            jQuery.get(medusa_url,function(xdata){
              parser=new DOMParser();
              xml_doc = parser.parseFromString(xdata,"text/xml");
              xnode=xml_doc.documentElement.childNodes;
          // more...
       }

Example: https://libapiproxyprod1.library.emory.edu/cgi-bin/primo_request?doc_id=990025556220302486&user_id=0036487

Web service response

   
    <result>
    <code>OK</code>
    <link>
    <request_link>
    <reserves>https://libapiproxyprod1.library.emory.edu/cgi-bin/process_reserves_primo_new?doc_id=990025556220302486&user_id=0036487</reserves>
    <docdelivery>https://libapiproxyprod1.library.emory.edu/cgi-bin/document_delivery_primo_new?doc_id=990025556220302486&user_id=0036487</docdelivery>
    </request_link>
    <worldcat_identity>http://worldcat.org/identities/lccn-n2005074653/</worldcat_identity>
   </link>
   <user_group>01</user_group>
   </result>
  

Javascript to append request tab

   
     if (reserves_url != ""){
        this_url="<a href="+reserves_url+" target=\"_blank\" id=\"reserves_bg\" title=\"Place this item on Reserves\">gt;Place on Reserves</a>gt;";
        $('#exlidResult0-TabsList').append('<li id="EULAresLink" class="EXLResultTab">gt;'+this_url+'</li>gt;');
     }
   
   

Handling Cross Origin Resource Sharing (CORS)

What is CORS? Browser received HTML page from Primo's domain but it is issuing an HTTP request to emory's domain.

What does the browser do? It issues request method "OPTIONS" to emory's API proxy.
   
   OPTIONS /cgi-bin/primo_request?doc_id=990031707790302486&user_id=0036487 HTTP/1.1
   Host: libapiproxyprod1.library.emory.edu
   Access-Control-Request-Method: GET
   Origin: http://discovere.emory.edu
   Access-Control-Request-Headers: exlrequesttype
   
   

API proxy's response:

   
  HTTP/1.1 200 OK
  Access-Control-Allow-Origin: http://discovere.emory.edu
  Access-Control-Allow-Methods: GET
  Access-Control-Allow-Headers: EXLREQUESTTYPE,authorization,x-from-exl-api-gateway
   
   

How does the webservice build the response?

   
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi 

def print_ok_cors(result,origin):
 """
  it prints an xml response with the appropriate headers
  to indicate a successful OPTIONS request.
 """
 print "Content-Type: text/xml; charset=utf-8"
 print "Access-Control-Allow-Origin: "+origin
 print "Access-Control-Allow-Methods: GET"
 print "Access-Control-Allow-Headers: EXLREQUESTTYPE,authorization,x-from-exl-api-gateway"
 print ""
 print result
 return

def main():
###
# configuration file has:
#preflight_hostnames=discovere.emory.edu;discoveretest.emory.edu;emory-primosb-alma.hosted.exlibrisgroup.com;primodbe.library.emory.edu;emory-primoprod.hosted.exlibrisgroup.com
###
   http_method=os.environ["REQUEST_METHOD"]
   if http_method == "OPTIONS":
       try:
           preflight_list=preflight_hostnames.split(";")
       except:
           preflight_list=[]

       origin_rule=re.compile("(http://|https://)(.*)")
       try:
          origin=os.environ["HTTP_ORIGIN"]
       except:
          print_error("NO HTTP_ORIGIN")
          return  1

       m=origin_rule.match(origin)
              if m:
       if m.group(2) in preflight_list:
             print_ok_cors("<result><code>OK</code></result>",origin)
       else:
             print_notok_cors("<result><code>ERROR</code></result>")
       return 0
   ##### 
   
   

How does the webservice determine the applicable (non-alma) requests?

It relies on request table

columns (context): user_group|library|location|material_type|archive|request_list title is an "archive" if MARC leader[11] is "p"

webservice relies on API utility to retrieve ALMA record

Observation: custom requests produce OpenUrls.

    Examples:
  • https://aeon.library.emory.edu/Logon/?Action=10&Form=30&ctx_ver=Z39.88-2004&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&rfr_id=info:sid/primo%3A010001338843&rft.genre=book&rft.btitle=The%20Women%27s%20Center%20Reid%20Lectureship,%20November%2011,%201975%20:%20papers%20by%20Alice%20W&rft.title=The%20Women%27s%20Center%20Reid%20Lectureship,%20November%2011,%201975%20:%20papers%20by%20Alice%20W&rft.au=Jordan,%20June&rft.date=[1976]&rft.place=New%20York%20:&rft.pub=Barnard%20College&rft.edition=&rft.isbn=&rft.callnumber=PS3573%20.A425%20W66%201976&rft.item_location=MARBL%20STACK&rft.barcode=010001338843&rft.doctype=RB&rft.lib=EMU
  • https://illiad.library.emory.edu/illiad/illiad.dll?Action=10&Form=30&ctx_ver=Z39.88-2004&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&rfr_id=info:sid/primo%3A070000157851&rft.genre=conference&rft.btitle=Quarterly%20journal%20of%20studies%20on%20alcohol.%20&rft.title=Quarterly%20journal%20of%20studies%20on%20alcohol.%20&rft.date=&rft.place=New%20Brunswick,%20N.J.%20etc.%20:&rft.pub=Journal%20of%20Studies%20on%20Alcohol,%20inc.%20%5Betc%5D&rft.edition=&rft.isbn=0033-5649&rft.callnumber=PERIODICAL%20VOL.%2015%201954&rft.item_location=LSC%20HSTORJ&rft.barcode=070000157851&rft.doctype=RB&rft.lib=LSCMAIN
  • Displaying custom request links in the new UI

    I chose "prmActionListAfter" component because it provides user information out of the box

    Disclaimer: new UI will go production in 2018

    Example: Rose Library archive item

    Highlights of angular code

           
    
      app.component('prmActionListAfter',{
        bindings: {parentCtrl: '<'},
        controller: 'prmActionListAfterController',
        template: `<div class="action-list-addon">
              <div ng-if=$ctrl.display_reserve_link><a ng-href="{{$ctrl.reserves_link}}" target="_blank">
              <div layout="column" layout-align="center center" class="layout-align-center-center layout-column">
              <prm-icon style="z-index:1; color: rgba(0, 0, 0, 0.57);" icon-type="svg" svg-icon-set="primo-actions" icon-definition="refworks"></prm-icon>
              <span class="action-list-addon-text">Place Item on Reserve</span>
              </div> 
              </a>
              </div>
              <div ng-if=$ctrl.display_docdelivery_link><a ng-href="{{$ctrl.docdelivery_link}}" target="_blank">
              <div layout="column" layout-align="center center" class="layout-align-center-center layout-column">
              <prm-icon style="z-index:1; color: rgba(0, 0, 0, 0.57);" icon-type="svg" svg-icon-set="primo-actions" icon-definition="refworks"></prm-icon>
              <span class="action-list-addon-text">Document Delivery</span>
              </div> 
              </a>
              </div>
    
              <div ng-if=$ctrl.display_marbl_link><a ng-href="{{$ctrl.marbl_link}}" target="_blank">
              <div layout="column" layout-align="center center" class="layout-align-center-center layout-column">
              <prm-icon style="z-index:1; color: rgba(0, 0, 0, 0.57);" icon-type="svg" svg-icon-set="primo-actions" icon-definition="refworks"></prm-icon>
              <span class="action-list-addon-text">Request from Rose Library(MARBL)</span>
              </div> 
              </a>
              </div>
    
              <div ng-if=$ctrl.display_finding_aids_link><a ng-href="{{$ctrl.finding_aids_link}}" target="_blank">
              <div layout="column" layout-align="center center" class="layout-align-center-center layout-column">
              <prm-icon style="z-index:1; color: rgba(0, 0, 0, 0.57);" icon-type="svg" svg-icon-set="primo-actions" icon-definition="refworks"></prm-icon>
              <span class="action-list-addon-text">Request from Rose Library(MARBL)</span>
              </div> 
              </a>
              </div>
              <div style="clear:both;"></div>`
    
      });
    
    
      app.controller('prmActionListAfterController', ['$http',function($http){
        var vm = this;
    
        var mms_id;
        var source_id;
        var emory_item=false;
        var gatech_item=false;
        var re_pattern_emo=/.*01EMORY_ALMA.*/;
        var re_pattern_gatech=/.*GALI_GIT_ALMA.*/;
        var id_list="";
        var mlen;
        var i;
        var j;
        var k;
        var rr;
        var medusa_url="";
        var parser;
        var xml_doc;
        var xnode;
        var bg_patron_status="XX";
        var links;
        var reqnode;
        var reserves_url="";
        var docdelivery_url="";
        var marbl_url="";
        var finding_aids_url="";
        var bg_user_id;
        var xdata="";
        var item_list;
        var item_count=0;
        var element_value="";
    
        source_id=vm.parentCtrl.item.pnx.control.sourceid;
    
        bg_user_id=vm.parentCtrl.primolyticsService.userSessionManagerService.jwtUtilService.getDecodedToken()['user'];
    
        if (bg_user_id.length > 8){
             if (bg_user_id.substr(0,10) == "anonymous-"){
                bg_user_id="0";
             }
        }
        if (re_pattern_emo.test(source_id)){
             emory_item=true;
        }
        if (re_pattern_gatech.test(source_id)){
            gatech_item=true;
        }
        if (emory_item){
    
    ###
         # pnx.search.addsrcrecordid provides list of mms_ids 
          mms_id=vm.parentCtrl.item.pnx.search.addsrcrecordid;
         # pnx.display.availlibrary indicates the present of ALMA physical items.
          item_list=vm.parentCtrl.item.pnx.display.availlibrary;
    ###
          if (item_list == null){
              item_count=0;
          }
          else{
              item_count=1;
          }
          mlen=mms_id.length;
          for (i=0; i < mlen;i++){
             id_list+=mms_id[i]+",";
          }
          if (id_list != ""){
             id_list=id_list.slice(0, -1); 
          }
          this.doc_id=id_list;
          this.display_it=true;
          if (item_count > 0){
                medusa_url="https://kleene.library.emory.edu/cgi-bin/primo_request?doc_id="+id_list+"&user_id="+bg_user_id;
    
                $http.get(medusa_url).then(function(response) {
                  xdata=response.data;
                  parser=new DOMParser();
                  xml_doc = parser.parseFromString(xdata,"text/xml");
                  xnode=xml_doc.documentElement.childNodes;
                  vm.reserves_link="";
                  vm.display_reserve_link=false;
                  vm.display_docdelivery_link=false;
                  for (k = 0; k < xnode.length ;k++) {
                    if (xnode[k].nodeName == "user_group"){
                        bg_patron_status=xnode[k].childNodes[0].nodeValue;
                    }
                    if (xnode[k].nodeName == "link"){
                        links=xnode[k].childNodes;
                        for (j=0; j< links.length; j++){
                            if (links[j].nodeName == "request_link"){
                               reqnode=links[j].childNodes;
                               if (reqnode.length > 0){
                                 for (rr = 0; rr< reqnode.length; rr++){
                                   if (reqnode[rr].nodeName == "reserves"){
                                      reserves_url=reqnode[rr].childNodes[0].nodeValue;
                                      vm.reserves_link=reserves_url;
                                      vm.display_reserve_link=true;
                                   }
                                   if (reqnode[rr].nodeName == "docdelivery"){
                                      docdelivery_url=reqnode[rr].childNodes[0].nodeValue;
                                      vm.docdelivery_link=docdelivery_url;
                                      vm.display_docdelivery_link=true;
                                   }
                                   if (reqnode[rr].nodeName == "marbl_finding_aids"){
                                      finding_aids_url=reqnode[rr].childNodes[0].nodeValue;
                                      vm.finding_aids_link=finding_aids_url;
                                      vm.display_finding_aids_link=true;
                                   }
                                   if (reqnode[rr].nodeName == "marbl_booking"){
                                      marbl_url=reqnode[rr].childNodes[0].nodeValue;
                                      vm.marbl_link=marbl_url;
                                      vm.display_marbl_link=true;
                                   }
                                 }
                               }
                            }
                        }
                    }
                  }
                });
          }
        }
    
    
           
          

    Drawback of current request mix:

    Request are dispersed in two areas: tabs and i-frame (Physical Resource) Screenshot

    A solution? Consolidated requests in external interface.

    Two tabs: "View Items" to list items, and "Get it" for menu of applicable requests.

    Example ("Get it" requires sign-in)

    Get-it Screenshot View-it screenshot

    Consolidated requests rely on extended request table.

    Request table contains columns for alma-hold(Y/N), alma-booking(Y/N),alma-hold pickup locations,alma-booking pickup locations Table columns (context): user_group|library|location|material_type|archive|emory_request_list|alma-hold(Y/N)|alma-booking(Y/N)|hold-pickup-libraries|booking-pickup-libraries

    Generating the consolidated request menu:

    Primo view's footer contains javascript code

    Javascript invokes Mehmet Celik's jQuery.PRIMO

    jQuery.PRIMO provides PNX record, user.isLoggedIn() and ALMA's user_id

    PNX indicates whether physical item belongs to Emory. If so, it contains ALMA's mms_id(s)

    Javascript highlights

    
     <script type="text/javascript">
      jQuery(document).ready(function () {
        if (typeof(Storage) !== "undefined"){
          var isLoggedIn=jQuery.PRIMO.session.user.isLoggedIn();
          var record_count=jQuery.PRIMO.records.length;
          if (isLoggedIn ){
             bg_user_id=jQuery.PRIMO.session.user.id;
          }
          else{
             bg_user_id="0";
          }
          re_pattern_gatech=new RegExp(".*GALI_GIT_ALMA.*"),
          re_pattern_emo=new RegExp(".*01EMORY_ALMA.*"),
          var gatech_item=false;
          var emory_item=false;
          if (record_count == 1){
            pnx=jQuery.PRIMO.records[0].getPNX();
            parser=new DOMParser();
            xmlDoc = parser.parseFromString(pnx,"text/xml");
            x = xmlDoc.documentElement.childNodes;
            txt="";
    
            for (i = 0; i < x.length ;i++) {
               if (x[i].nodeName == "search"){
                   search_node=x[i].childNodes;
                   for (j=0; j< search_node.length;j++){
                      if (search_node[j].nodeName == "addsrcrecordid"){
                        record_id=search_node[j].childNodes[0].nodeValue;
                        if (merged_record_id == ""){
                            merged_record_id=record_id;
                        }
                        else{
                            merged_record_id=merged_record_id+","+record_id;
                        }
    
                      }
                   }
               }
               if (x[i].nodeName == "control"){
                   control_node=x[i].childNodes;
                   for (j=0; j< control_node.length;j++){
                      if (control_node[j].nodeName == "sourceid"){
                        source_id=control_node[j].childNodes[0].nodeValue;
                        if (re_pattern_emo.test(source_id)){
                           emory_item=true;
                        }
                        if (re_pattern_gatech.test(source_id)){
                           gatech_item=true;
                        }
                      }
                   }
               }
            }
      });
    
    

    Webservice that returns applicable requests:

    https://kleene.library.emory.edu/cgi-bin/display_custom_request?doc_id=&user_id=
    
            if (emory_item ){
                 medusa_url="https://kleene.library.emory.edu/cgi-bin/display_custom_request?doc_id="+merged_record_id+"&user_id="+bg_user_id;
                 item_list_url="https://kleene.library.emory.edu/cgi-bin/alma_item_list?doc_id="+merged_record_id;
                jQuery.ajaxSetup({async:false});
                jQuery.get(medusa_url,function(xdata){
                  parser=new DOMParser();
                  xml_doc = parser.parseFromString(xdata,"text/xml");
                  xnode=xml_doc.documentElement.childNodes;
                  for (k = 0; k < xnode.length ;k++) {
                    if (xnode[k].nodeName == "user_group"){
                        bg_patron_status=xnode[k].childNodes[0].nodeValue;
                    }
                    if (xnode[k].nodeName == "request_link"){
                        request_url=xnode[k].childNodes[0].nodeValue;;
                    }
                  }
               },"text");
     //  placing the link in the "Get it" tab.
               if (request_url != ""){
                           this_url="<a href="+request_url+" target=\"_blank\" id=\"reserves_bg\" title=\"Request this title\">Get it</a>";
                           $('#exlidResult0-TabsList').append('<li id="EULAresLink" class="EXLResultTab">'+this_url+'</li>');
               }
            }
    
    

    Example: https://libapiproxyprod1.library.emory.edu/cgi-bin/display_custom_request?doc_id=990030289410302486,990031945630302486,990030842310302486&user_id=0036487

    Web service response

            
    <result>
      <code>OK</code>
      <request_link>https://kleene.library.emory.edu/cgi-bin/process_custom_request?doc_id=990030289410302486,990031945630302486,990030842310302486&user_id=0036487&req_type=alma_booking,hold,reserves</request_link>
      <user_group>01</user_group>
    </result>
    
            
           
    Example request menu Screenshots

    Note: webservice "display_custom_request" supports OPTIONS method to deal with CORS

    ALMA hold relies on ALMA API services.

             
             POST https://api-na.hosted.exlibrisgroup.com/almaws/v1/users/nnnn/requests?apikey=your-api-key&user_id_type=all_unique&item_pid=23249592970002466 
             <?xml version="1.0" encoding="UTF-8"?>
             <user_request>
             <request_type>HOLD</request_type>
             <pickup_location_type>LIBRARY</pickup_location_type>
             <pickup_location_library>MUSME</pickup_location_library>
             </user_request>
             
             

    ALMA booking relies on ALMA API services.

             
    POST https://api-na.hosted.exlibrisgroup.com/almaws/v1/users/nnnn/requests?item_pid=23319409310002486&apikey=your-api-key <?xml version="1.0" encoding="UTF-8"?>
    <user_request>
    <request_type>BOOKING</request_type>
    <pickup_location_type>LIBRARY</pickup_location_type>
    <pickup_location_library>MUSME</pickup_location_library>
    <booking_start_date>2017-07-13T09:00:00-04:00</booking_start_date>
    <booking_end_date>2017-07-13T14:00:00-04:00</booking_end_date>
    </user_request>
             
             

    Challenges to consolidated request menu:

    Challenge #1: Emory University runs a Fullfilment Network with another institution.

    Challenge #2: ALMA fulfillment's terms of use can't be exported in tabular form.

    Challenge #3: Inserting consolidated menu into Primo i-frame

    Fulfillment Network: there is no ALMA API to create users in another institution or to create requests in another institution (Georgia Tech).

    Georgia Tech item at Emory's Primo

    Georgia Tech item at Georgia Tech

    Fulfillment tool to get ALMA hold/booking context

    fulfillment tool

    Question

    to i-frame or not to i-frame?

    Thanks!

    Questions?