psu-libraries/psulib_blacklight

View on GitHub
app/javascript/availability/index.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * @file
 * Real Time Availability
 */

import React from 'react';
import ReactDOM from 'react-dom';
import locations from './libraries_locations.json';
import itemTypes from './item_types.json';
import reserveCirculationRules from './reserve_circulation_rules.json';
import Availability from './components/availability';
import Snippet from './components/snippet';

const availability = {
  // Load Sirsi locations
  allLocations: locations.locations,
  allLibraries: locations.libraries,
  illiadLocations: locations.request_via_ill,
  reservesScanLocations: locations.reserves_scan,
  allItemTypes: itemTypes.item_types,
  reserveCirculationRules: reserveCirculationRules.reserve_circulation_rules,
  movedLocations: [],
  nonHoldableLocations: locations.non_holdable,
  closedLibraries: locations.closed_libraries,

  sirsiUrl: '/availability/sirsi-data/?',
  sirsiItemUrl: '/availability/sirsi-item-data/?',
  mapScanUrl:
    'https://libraries.psu.edu/about/libraries/donald-w-hamer-center-maps-and-' +
    'geospatial-information/map-scanning-and-printing',

  /**
   * Load real time holdings and availability info from Sirsi Web Services
   */
  loadAvailability() {
    const titleIDs = [];
    const summaryHoldings = {};

    // Get the catkeys
    $('.availability').each(function () {
      const titleID = $(this).attr('data-keys');
      titleIDs.push(titleID);

      const summaryHoldingsData = $(this).attr('data-summary-holdings');
      if (summaryHoldingsData) {
        summaryHoldings[titleID] = JSON.parse(summaryHoldingsData);
      }
    });

    if (titleIDs.length > 0) {
      let allHoldings = [];
      let boundHoldings = [];
      const sirsiRequestParams = titleIDs
        .map((url) => `title_ids[]=${url}`)
        .join('&');

      $.ajax({
        url: availability.sirsiUrl + sirsiRequestParams,
      }).then(
        (response) => {
          $(response)
            .find('TitleInfo')
            .each(function () {
              const catkey = $(this).children('titleID').text();
              const totalCopiesAvailable = parseInt(
                $(this).find('totalCopiesAvailable').text(),
                10
              );
              const holdable = $(this).find('holdable').text();
              const numberOfBoundwithLinks = parseInt(
                $(this).find('numberOfBoundwithLinks').text(),
                10
              );

              const titleInfo = {
                jQueryObj: $(this),
                catkey,
                totalCopiesAvailable,
                holdable,
              };

              // Process for regular records
              allHoldings = availability.getAllHoldings(allHoldings, titleInfo);

              // Process for bound-with records
              if (numberOfBoundwithLinks > 0) {
                boundHoldings = availability.getBoundHoldings(
                  boundHoldings,
                  titleInfo
                );
              }

              // check to see if the item is only available online AND is in ON-ORDER status
              const isOnlineOnOrderOnly =
                availability.getIsOnlineOnOrderOnly(titleInfo);

              if (isOnlineOnOrderOnly) {
                $(`.availability[data-keys="${catkey}"`).data(
                  'isOnlineOnOrderOnly',
                  true
                );
              }
            });

          if (Object.keys(boundHoldings).length > 0) {
            // Get bound with parents and print availability data
            availability.processBoundParents(
              boundHoldings,
              allHoldings,
              summaryHoldings
            );
          } else {
            // Print availability data
            availability.availabilityDisplay(allHoldings, summaryHoldings);
          }
        },
        () => {
          availability.displayErrorMsg();
        }
      );
    }
  },

  getAllHoldings(allHoldings, titleInfo) {
    allHoldings[titleInfo.catkey] = [];

    titleInfo.jQueryObj.children('CallInfo').each(function () {
      const libraryID = $(this).children('libraryID').text();

      // Only for not online items (online items uses 856 urls for display)
      if (libraryID.toUpperCase() !== 'ONLINE') {
        const callNumber = $(this).children('callNumber').text();

        $(this)
          .children('ItemInfo')
          .each(function () {
            const currentLocationID = $(this)
              .children('currentLocationID')
              .text()
              .toUpperCase();
            const homeLocationID = $(this)
              .children('homeLocationID')
              .text()
              .toUpperCase();
            const itemID = $(this).children('itemID').text();
            const itemTypeID = $(this)
              .children('itemTypeID')
              .text()
              .toUpperCase();
            const reserveCollectionID = $(this)
              .children('reserveCollectionID')
              .text();
            const reserveCirculationRule = $(this)
              .children('reserveCirculationRule')
              .text();
            const dueDate = $(this).children('dueDate').text();
            const publicNote = $(this).children('publicNote').text();

            allHoldings[titleInfo.catkey].push({
              catkey: titleInfo.catkey,
              libraryID,
              locationID: currentLocationID,
              homeLocationID,
              itemID,
              callNumber,
              itemTypeID,
              totalCopiesAvailable: titleInfo.totalCopiesAvailable,
              holdable: titleInfo.holdable,
              reserveCollectionID,
              reserveCirculationRule,
              dueDate,
              publicNote,
            });
          });
      }
    });

    return allHoldings;
  },

  getBoundHoldings(boundHoldings, titleInfo) {
    boundHoldings[titleInfo.catkey] = [];

    titleInfo.jQueryObj.children('BoundwithLinkInfo').each(function () {
      const linkedAsParent = $(this).children('linkedAsParent').text();
      const linkedItemID = $(this).children('itemID').text();
      const callNumber = $(this).children('callNumber').text();

      $(this)
        .children('linkedTitle')
        .each(function () {
          const linkedCatkey = $(this).children('titleID').text();

          if (linkedAsParent === 'true' && titleInfo.catkey !== linkedCatkey) {
            boundHoldings[titleInfo.catkey][linkedItemID] = [];
            const linkedTitle = $(this).children('title').text();
            const author = $(this).children('author').text();
            const yearOfPublication = $(this)
              .children('yearOfPublication')
              .text();
            const boundinStatement = `${callNumber} bound in ${linkedTitle} ${author} ${yearOfPublication}`;

            boundHoldings[titleInfo.catkey][linkedItemID].push({
              catkey: titleInfo.catkey,
              linkedItemID,
              linkedCatkey,
              linkedTitle,
              author,
              yearOfPublication,
              callNumber: boundinStatement,
              totalCopiesAvailable: titleInfo.totalCopiesAvailable,
              holdable: titleInfo.holdable,
            });
          }
        });
    });

    if (Object.keys(boundHoldings[titleInfo.catkey]).length === 0) {
      delete boundHoldings[titleInfo.catkey];
    }

    return boundHoldings;
  },

  /**
   * Determines whether the item is available ONLY online and all copies are ON-ORDER
   */
  getIsOnlineOnOrderOnly(titleInfo) {
    let isOnlineOnOrderOnly = true;

    titleInfo.jQueryObj.children('CallInfo').each(function () {
      const libraryID = $(this).children('libraryID').text();

      if (libraryID.toUpperCase() !== 'ONLINE') {
        isOnlineOnOrderOnly = false;
        return false;
      }

      $(this)
        .children('ItemInfo')
        .each(function () {
          const currentLocationID = $(this)
            .children('currentLocationID')
            .text()
            .toUpperCase();

          if (currentLocationID !== 'ON-ORDER') {
            isOnlineOnOrderOnly = false;
            return false;
          }
        });
    });

    return isOnlineOnOrderOnly;
  },

  processBoundParents(boundHoldings, allHoldings, summaryHoldings) {
    const catkeys = Object.keys(boundHoldings);

    let itemIDs = [];
    $.each(catkeys, (i, catkey) => {
      itemIDs.push(Object.keys(boundHoldings[catkey]));
    });
    itemIDs = $.map(itemIDs, (value) => value);
    const sirsiBoundRequestParams = itemIDs
      .map((url) => `item_ids[]=${url}`)
      .join('&');

    $.ajax({
      url: availability.sirsiItemUrl + sirsiBoundRequestParams,
    }).then(
      (response) => {
        $(response)
          .find('TitleInfo')
          .each(function () {
            const parentCatkey = $(this).children('titleID').text();

            $(this)
              .children('CallInfo')
              .each(function () {
                const libraryID = $(this).children('libraryID').text();
                const callNumber = $(this).children('callNumber').text();

                $(this)
                  .children('ItemInfo')
                  .each(function () {
                    const currentLocationID = $(this)
                      .children('currentLocationID')
                      .text()
                      .toUpperCase();
                    const homeLocationID = $(this)
                      .children('homeLocationID')
                      .text()
                      .toUpperCase();
                    const itemID = $(this).children('itemID').text();
                    const itemTypeID = $(this)
                      .children('itemTypeID')
                      .text()
                      .toUpperCase();
                    const reserveCollectionID = $(this)
                      .children('reserveCollectionID')
                      .text();

                    $.each(catkeys, (i, catkey) => {
                      if (itemID in boundHoldings[catkey]) {
                        boundHoldings[catkey][itemID].forEach(
                          (boundHolding) => {
                            boundHolding.parentCatkey = parentCatkey;
                            boundHolding.parentCallNumber = callNumber;
                            boundHolding.itemTypeID = itemTypeID;
                            boundHolding.libraryID = libraryID;
                            boundHolding.locationID = currentLocationID;
                            boundHolding.homeLocationID = homeLocationID;
                            boundHolding.reserveCollectionID =
                              reserveCollectionID;

                            allHoldings[catkey].push(boundHolding);

                            // once processed remove to avoid duplicates
                            // when children of same parent are in the search results
                            delete boundHoldings[catkey][itemID];
                          }
                        );
                      }
                    });
                  });
              });
          });

        // Print availability data
        availability.availabilityDisplay(allHoldings, summaryHoldings);
      },
      () => {
        availability.displayErrorMsg();
      }
    );
  },

  availabilityDisplay(allHoldings, summaryHoldings) {
    $('.availability').each(function () {
      const availabilityHTML = $(this);
      const catkey = availabilityHTML.data('keys');
      const isOnlineOnOrderOnly = availabilityHTML.data('isOnlineOnOrderOnly');

      if (catkey in allHoldings) {
        const rawHoldings = allHoldings[catkey];
        const availabilityButton = availabilityHTML.find(
          '.availability-button'
        );
        const holdingsPlaceHolder = availabilityHTML.find(
          '.availability-holdings'
        );
        const snippetPlaceHolder = availabilityHTML.find(
          '.availability-snippet'
        );
        const holdButton = availabilityHTML.find('.hold-button');
        const noRecallsButton = availabilityHTML.find('.no-recalls-button');

        // If at least one physical copy, then display availability and holding info
        if (Object.keys(rawHoldings).length > 0) {
          availabilityButton.removeClass('invisible').addClass('visible');
          const holdings = availability.groupByLibrary(rawHoldings);
          const structuredHoldings =
            availability.availabilityDataStructurer(holdings);

          ReactDOM.render(
            React.createElement(Availability, {
              structuredHoldings,
              summaryHoldings: summaryHoldings ? summaryHoldings[catkey] : null,
            }),
            holdingsPlaceHolder[0]
          );

          if (snippetPlaceHolder && snippetPlaceHolder.length === 1) {
            ReactDOM.render(
              React.createElement(Snippet, { data: structuredHoldings }),
              snippetPlaceHolder[0]
            );
          }

          // If holdable and no-recalls then display the no-recalls button
          if (availability.showNoRecallsButton(rawHoldings)) {
            noRecallsButton.removeClass('d-none').addClass('d-md-inline');
          }

          // If holdable and not no-recalls then display the hold button
          if (availability.showHoldButton(rawHoldings)) {
            holdButton.removeClass('d-none').addClass('d-md-inline');
          }
        } else if (isOnlineOnOrderOnly) {
          // only have online copies, but they are in the process of being acquired by the library
          const beingAcquiredMsg = 'Being acquired by the library';

          // Document view
          holdingsPlaceHolder.html(`<h5>${beingAcquiredMsg}</h5>`);
          // Results view
          snippetPlaceHolder
            .parent('.row')
            .html(`<strong>${beingAcquiredMsg}</strong>`);
        } else {
          // Document view
          $('.metadata-availability').remove();
          // Results view
          $(this).parent('.blacklight-availability').remove();
        }
      } else {
        // Results view
        // When catkey not in the response, even the spinner
        // should not be displayed so remove the parent
        $(this).parent('.blacklight-availability').remove();
      }
    });

    // initialize tooltips
    $('i.fas.fa-info-circle[data-toggle="tooltip"]').tooltip();
  },

  availabilityDataStructurer(holdingMetadata) {
    const availabilityStructuredData = [];
    let holdingData = [];
    let pluralize = '';
    let library = '';
    const { allItemTypes } = availability;

    if (Object.keys(holdingMetadata).length > 0) {
      Object.keys(holdingMetadata).forEach((libraryID, index) => {
        library =
          libraryID in availability.allLibraries
            ? availability.allLibraries[libraryID]
            : '';
        pluralize = holdingMetadata[libraryID].length > 1 ? 'items' : 'item';

        // Supplement data with an itemType and remove callNumber conditionally
        holdingMetadata[libraryID].forEach((element) => {
          element.itemType =
            element.itemTypeID in allItemTypes
              ? allItemTypes[element.itemTypeID]
              : '';
        });

        holdingData = {
          summary: {
            libraryID,
            library,
            countAtLibrary: holdingMetadata[libraryID].length,
            pluralize,
          },
          holdings: holdingMetadata[libraryID],
        };

        availabilityStructuredData[index] = holdingData;
      });
    }

    return availabilityStructuredData;
  },

  // Group holding by library
  groupByLibrary(holdings) {
    return holdings.reduce((accumulator, object) => {
      const key = object.libraryID;

      if (!accumulator[key]) {
        accumulator[key] = [];
      }
      accumulator[key].push(object);

      return accumulator;
    }, {});
  },

  displayErrorMsg() {
    // Display the error message
    $('.availability').each(function () {
      $(this).addClass('availability-error alert alert-light');
      $(this).html(
        'Please check back shortly for item availability or ' +
          '<a href="https://libraries.psu.edu/ask">ask a librarian</a> for assistance.'
      );
    });
  },

  isMoved(location) {
    return availability.movedLocations.includes(location);
  },

  isReserves(holding) {
    return availability.reservesScanLocations.includes(holding.locationID);
  },

  isMicroform(holding) {
    return (
      ['UP-MICRO'].includes(holding.libraryID) &&
      holding.homeLocationID !== 'THESIS-NML' &&
      holding.itemTypeID === 'MICROFORM' &&
      !(holding.locationID in availability.illiadLocations)
    );
  },

  isIllLink(holding) {
    return (
      holding.locationID in availability.illiadLocations ||
      availability.isMicroform(holding)
    );
  },

  isArchivalThesis(holding) {
    return ['ARKTHESES', 'AH-X-TRANS'].includes(holding.locationID);
  },

  isArchivalMaterial(holding) {
    return (
      ['UP-SPECCOL'].includes(holding.libraryID) &&
      !availability.isMoved(holding.homeLocationID)
    );
  },

  showHoldButton(holdings) {
    return (
      holdings[0].holdable === 'true' &&
      !availability.allCourseReserves(holdings) &&
      !availability.noRecalls(holdings)
    );
  },

  showNoRecallsButton(holdings) {
    return holdings[0].holdable === 'true' && availability.noRecalls(holdings);
  },

  allCourseReserves(holdings) {
    for (const holding of holdings) {
      if (holding.reserveCollectionID.length === 0) {
        return false;
      }
    }

    return true;
  },

  noRecalls(holdings) {
    if (availability.allNonHoldable(holdings)) {
      return true;
    }

    return false;
  },

  isClosedLibrary(holding) {
    return availability.closedLibraries.includes(holding.libraryID);
  },

  allNonHoldable(holdings) {
    for (const holding of holdings) {
      if (
        !availability.isNonHoldableLocation(holding) &&
        !availability.isClosedLibrary(holding)
      ) {
        return false;
      }
    }

    return true;
  },

  isNonHoldableLocation(holding) {
    return availability.nonHoldableLocations.includes(holding.locationID);
  },
};

export default availability;