From 3df2d36ba2dde5422a66f56b06ee9f1eb30b52c1 Mon Sep 17 00:00:00 2001 From: Ere Maijala <ere.maijala@helsinki.fi> Date: Tue, 18 Feb 2014 07:51:16 +0200 Subject: [PATCH] Implemented support for ILL requests in VuFind core code and VoyagerRestful and Demo drivers. --- config/vufind/Demo.ini | 3 + config/vufind/VoyagerRestful.ini | 33 +- config/vufind/config.ini | 5 + languages/en-gb.ini | 6 + languages/en.ini | 30 + languages/fi.ini | 30 + languages/sv.ini | 30 + module/VuFind/config/module.config.php | 4 +- .../src/VuFind/Controller/AbstractBase.php | 15 +- .../src/VuFind/Controller/AjaxController.php | 70 ++ .../Controller/MyResearchController.php | 56 ++ .../src/VuFind/Controller/Plugin/Factory.php | 14 + .../VuFind/Controller/Plugin/ILLRequests.php | 195 +++++ .../VuFind/Controller/RecordController.php | 122 +++ module/VuFind/src/VuFind/ILS/Connection.php | 90 ++ module/VuFind/src/VuFind/ILS/Driver/Demo.php | 282 +++++- .../VuFind/src/VuFind/ILS/Driver/Voyager.php | 2 +- .../src/VuFind/ILS/Driver/VoyagerRestful.php | 825 +++++++++++++++++- module/VuFind/src/VuFind/ILS/Logic/Holds.php | 236 ++--- themes/blueprint/css/styles.css | 31 +- themes/blueprint/js/ill.js | 29 + themes/blueprint/js/record.js | 12 +- .../templates/RecordTab/holdingsils.phtml | 4 + .../templates/myresearch/illrequests.phtml | 168 ++++ .../blueprint/templates/myresearch/menu.phtml | 3 + .../templates/record/illrequest.phtml | 111 +++ themes/bootstrap/js/ill.js | 29 + themes/bootstrap/js/record.js | 16 +- .../templates/RecordTab/holdingsils.phtml | 4 + .../templates/myresearch/illrequests.phtml | 168 ++++ .../bootstrap/templates/myresearch/menu.phtml | 3 + .../templates/record/illrequest.phtml | 126 +++ 32 files changed, 2594 insertions(+), 158 deletions(-) create mode 100644 module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php create mode 100644 themes/blueprint/js/ill.js create mode 100644 themes/blueprint/templates/myresearch/illrequests.phtml create mode 100644 themes/blueprint/templates/record/illrequest.phtml create mode 100644 themes/bootstrap/js/ill.js create mode 100644 themes/bootstrap/templates/myresearch/illrequests.phtml create mode 100644 themes/bootstrap/templates/record/illrequest.phtml diff --git a/config/vufind/Demo.ini b/config/vufind/Demo.ini index 8ce6f422063..e3f4727c3a7 100644 --- a/config/vufind/Demo.ini +++ b/config/vufind/Demo.ini @@ -6,3 +6,6 @@ idsInMyResearch = true ; Whether to support storage retrieval requests storageRetrievalRequests = true + +; Whether to support ILL requests +ILLRequests = true diff --git a/config/vufind/VoyagerRestful.ini b/config/vufind/VoyagerRestful.ini index 53486e13f85..390d6c02d1e 100644 --- a/config/vufind/VoyagerRestful.ini +++ b/config/vufind/VoyagerRestful.ini @@ -134,4 +134,35 @@ holdCheckLimit = 15 ; to display. If you set this to false, all items will have renewal checkboxes and ; we will only check validity if a user attempts a renewal -- faster, but less ; accurate. -checkUpFront = true \ No newline at end of file +checkUpFront = true + +; This section controls UB (Universal Borrowing, ILL in VuFind) behavior. To enable, +; uncomment (at minimum) the HMACKeys and extraFields settings below. See also +; section UBRequestSources for mapping between patron ID's and UB libraries. +[ILLRequests] + +; HMACKeys - A list of form element names that will be analyzed for consistency +; during form processing. Most users should not need to change this setting. +HMACKeys = item_id:holdings_id + +; extraFields - A colon-separated list used to display extra visible fields in the +; request form. Supported values are "pickUpLibrary", +; "pickUpLibraryLocation", "requiredByDate" and "comments" (although comments are +; not properly stored for UB requests at least in version 8.1) +extraFields = pickUpLibrary:pickUpLibraryLocation:requiredByDate + +; defaultRequiredDate - A colon-separated list used to set the default "not required +; after" date for holds in the format days:months:years +; e.g. 0:1:0 will set a "not required after" date of 1 month from the current date +defaultRequiredDate = 0:1:0 + +; Optional help texts that can be displayed on the ILL form +helpText = "ILL Help text for all languages." +helpText[en-gb] = "ILL Help text for English language." + +; This section lists the valid patron id prefixes for UB (ILL in VuFind) requests, +; and maps them to the their Voyager UB library IDs. Any patron of another library +; with a prefix listed here may attempt a UB request in this system. +[ILLRequestSources] +;devdb = "1@DEVDB20011102161616" +;otherdb = "1@OTHERDB20011030191919" diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 4f8076d50ed..3e534bc3c95 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -151,6 +151,11 @@ cancel_holds_enabled = false ; default is false cancel_storage_retrieval_requests_enabled = false +; Determines if ILL requests can be cancelled or not. +; Options are true or false. +; default is false +cancel_ill_requests_enabled = false + ; Determines if item can be renewed or not. Options are true or false. ; default is false renewals_enabled = false diff --git a/languages/en-gb.ini b/languages/en-gb.ini index 305c7531a07..41313f97aa3 100644 --- a/languages/en-gb.ini +++ b/languages/en-gb.ini @@ -35,6 +35,12 @@ hold_cancel_success = "Your request was successfully cancelled" hold_cancel_success_items = "request(s) were successfully cancelled" hold_error_fail = "Your request failed. Please contact the issue desk for further assistance" hold_place_fail_missing = "Your request failed. Some data was missing. Please contact the issue desk for further assistance" +ill_request_available = "Available for Collection" +ill_request_cancel_fail = "Your request was not cancelled. Please contact the issue desk for further assistance" +ill_request_error_fail = "Your request failed. Please contact the issue desk for further assistance" +ill_request_error_technical = "Your request failed due to a system error. Please contact the issue desk for further assistance" +ill_request_place_fail_missing = "Your request failed. Some data was missing. Please contact the issue desk for further assistance" +ill_request_profile_html = "For storage retrieval request information, please establish your <a href="%%url%%">Library Catalogue Profile</a>." Item removed from favorites = "Item removed from favourites" Library Catalog Password = "Library Catalogue Password" Library Catalog Profile = "Library Catalogue Profile" diff --git a/languages/en.ini b/languages/en.ini index d6e1ef60f86..b41d29b82e3 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -182,6 +182,8 @@ confirm_dialog_no = Cancel confirm_dialog_yes = Confirm confirm_hold_cancel_all_text = "Do you wish to cancel all your current holds?" confirm_hold_cancel_selected_text = "Do you wish to cancel your selected holds?" +confirm_ill_request_cancel_all_text = "Do you wish to cancel all your current interlibrary loan requests?" +confirm_ill_request_cancel_selected_text = "Do you wish to cancel your selected interlibrary loan requests?" confirm_storage_retrieval_request_cancel_all_text = "Do you wish to cancel all your current storage retrieval requests?" confirm_storage_retrieval_request_cancel_selected_text = "Do you wish to cancel your selected storage retrieval requests?" Contents = Contents @@ -378,6 +380,33 @@ hold_success = "Your request was successful" Home = Home home_browse = "Browse by" Identifier = "Identifier" +ill_request_available = "Available for Pickup" +ill_request_canceled = "Canceled" +ill_request_cancel = "Cancel Interlibrary Loan Request" +ill_request_cancel_all = "Cancel All Interlibrary Loan Requests" +ill_request_cancel_fail = "Your request was not canceled. Please contact the circulation desk for further assistance" +ill_request_cancel_selected = "Cancel Selected Interlibrary Loan Requests" +ill_request_cancel_success = "Your request was successfully canceled" +ill_request_cancel_success_items = "request(s) were successfully canceled" +ill_request_check_text = "Check Interlibrary Loan Request" +ill_request_comments = "Comments" +ill_request_date_invalid = "Please enter a valid date" +ill_request_date_past = "Please enter a date in the future" +ill_request_empty_selection = "No interlibrary loan requests were selected" +ill_request_error_blocked = "You do not have sufficient privileges to place an interlibrary loan request on this item" +ill_request_error_fail = "Your request failed. Please contact the circulation desk for further assistance" +ill_request_error_technical = "Your request failed due to a system error. Please contact the circulation desk for further assistance" +ill_request_error_unknown_patron_source = "Patron library not identified in interlibrary loan request." +ill_request_invalid_pickup = "An invalid pickup location was entered. Please try again" +ill_request_pick_up_library = "Pick Up Library" +ill_request_pick_up_location = "Pick Up Location" +ill_request_place_fail_missing = "Your request failed. Some data was missing. Please contact the circulation desk for further assistance" +ill_request_place_success = "Your request was successful" +ill_request_place_text = "Place an Interlibrary Loan Request" +ill_request_processed = "Processed" +ill_request_profile_html = "For interlibrary loan request information, please establish your <a href="%%url%%">Library Catalog Profile</a>." +ill_request_submit_text = "Place Request" +Interlibrary Loan Requests = "Interlibrary Loan Requests" Illustrated = Illustrated ils_offline_holdings_message = "Holdings and item availability information is currently unavailable. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:" ils_offline_home_message = "Your account details and live item information will be unavailable during this time. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:" @@ -826,6 +855,7 @@ Year of Publication = "Year of Publication" Yesterday = Yesterday You do not have any fines = "You do not have any fines" You do not have any holds or recalls placed = "You do not have any holds or recalls placed" +You do not have any interlibrary loan requests placed = "You do not have any interlibrary loan requests placed" You do not have any items checked out = "You do not have any items checked out" You do not have any saved resources = "You do not have any saved resources. Perform a search and use the Add to Favorites button to save items." You do not have any storage retrieval requests placed = "You do not have any storage retrieval requests placed" diff --git a/languages/fi.ini b/languages/fi.ini index a9e2079c8fd..92bb8928407 100644 --- a/languages/fi.ini +++ b/languages/fi.ini @@ -179,6 +179,8 @@ confirm_dialog_no = Peruuta confirm_dialog_yes = Vahvista confirm_hold_cancel_all_text = "Haluatko perua kaikki varaukset?" confirm_hold_cancel_selected_text = "Haluatko perua valitsemasi varaukset?" +confirm_ill_request_cancel_all_text = "Haluatko perua kaikki kaukolainatilaukset?" +confirm_ill_request_cancel_selected_text = "Haluatko perua valitsemasi kaukolainatilaukset?" confirm_storage_retrieval_request_cancel_all_text = "Haluatko perua kaikki tilaukset?" confirm_storage_retrieval_request_cancel_selected_text = "Haluatko perua valitsemasi tilaukset?" Contents = "Sisältö" @@ -375,6 +377,33 @@ hold_success = "Varauspyyntö onnistui" Home = "Koti" home_browse = "Selaus:" Identifier = "Tunniste" +ill_request_available = "Noudettavissa" +ill_request_canceled = "Peruttu" +ill_request_cancel = "Peru tilaus" +ill_request_cancel_all = "Peru kaikki tilaukset" +ill_request_cancel_fail = "Tilaustasi ei peruttu. Ota yhteyttä asiakaspalveluun." +ill_request_cancel_selected = "Peru valitut tilaukset" +ill_request_cancel_success = "Tilaus peruttu" +ill_request_cancel_success_items = "tilaus(ta) peruttu" +ill_request_check_text = "Tarkista kaukolainatilaus" +ill_request_comments = "Lisätiedot" +ill_request_date_invalid = "Syötä kelvollinen päivämäärä" +ill_request_date_past = "Syötä tätä päivää myöhempi päivämäärä" +ill_request_empty_selection = "Yhtään tilausta ei valittu" +ill_request_error_blocked = "Tilaaminen ei ole mahdollista, koska olet lainauskiellossa tai sinulla on jo vastaava tilaus." +ill_request_error_fail = "Tilaus epäonnistui. Ota yhteyttä kirjaston asiakaspalveluun." +ill_request_error_technical = "Tilaaminen epäonnistui järjestelmävirheen vuoksi. Ota yhteyttä kirjaston asiakaspalveluun." +ill_request_error_unknown_patron_source = "Asiakkaan kirjastoa ei tunnistettu kaukolainauksessa." +ill_request_invalid_pickup = "Valittu noutopaikka on virheellinen. Yritä uudestaan." +ill_request_pick_up_library = "Noutokirjasto" +ill_request_pick_up_location = "Noutopaikka" +ill_request_place_fail_missing = "Tilaus epäonnistui puuttuvien tietojen vuoksi. Ota yhteyttä kirjaston asiakaspalveluun." +ill_request_place_success = "Kaukolainatilaus onnistui" +ill_request_place_text = "Tee kaukolainatilaus" +ill_request_processed = "Käsitelty" +ill_request_profile_html = "Kirjaudu <a href="%%url%%">kirjastokortilla</a> nähdäksesi kaukolainatilaukset." +ill_request_submit_text = "Tee kaukolainatilaus" +Interlibrary Loan Requests = "Kaukolainatilaukset" Illustrated = "Kuvitus" ils_offline_holdings_message = "Saatavuustiedot eivät ole juuri nyt käytettävissä. Pahoittelemme tästä aiheutunutta vaivaa. Voitte ottaa yhteyttä:" ils_offline_home_message = "Tilitietosi ja ajantasaiset saatavuustiedot ovat poissa käytöstä tämän ajan. Pahoittelemme tästä aiheutunutta vaivaa. Voitte ottaa yhteyttä:" @@ -827,6 +856,7 @@ Year of Publication = "Julkaisuvuosi" Yesterday = "Eilisestä" You do not have any fines = "Ei maksamattomia maksuja" You do not have any holds or recalls placed = "Ei voimassaolevia varauksia" +You do not have any interlibrary loan requests placed = "Ei voimassaolevia kaukolainatilauksia" You do not have any items checked out = "Ei lainoja" You do not have any saved resources = "Ei tallennettuja tietueita" You do not have any storage retrieval requests placed = "Ei voimassaolevia varastotilauksia" diff --git a/languages/sv.ini b/languages/sv.ini index 8d6ae0c9679..f14e6c812e1 100644 --- a/languages/sv.ini +++ b/languages/sv.ini @@ -179,6 +179,8 @@ confirm_dialog_no = Nej confirm_dialog_yes = Ja confirm_hold_cancel_all_text = "Är du säker att du vill återkalla alla reserveringar?" confirm_hold_cancel_selected_text = "Är du säker att du vill återkalla utvalda reserveringar?" +confirm_ill_request_cancel_all_text = "Är du säker du vill återkalla alla fjärrlånbeställningar?" +confirm_ill_request_cancel_selected_text = "Är du säker du vill återkalla utvalda fjärrlånbeställningar?" confirm_storage_retrieval_request_cancel_all_text = "Är du säker du vill återkalla alla beställningar?" confirm_storage_retrieval_request_cancel_selected_text = "Är du säker du vill återkalla utvalda beställningar?" Contents = "Innehåll" @@ -375,6 +377,33 @@ hold_success = "Material har reserverats." Home = "Hem" home_browse = "Bläddring:" Identifier = "Identifier" +ill_request_available = "Inväntar avhämtning" +ill_request_canceled = "Återkallad" +ill_request_cancel = "Återkalla beställningen" +ill_request_cancel_all = "Återkalla alla beställningar" +ill_request_cancel_fail = "Beställningen kunde inte återkallas. Vänd dig till kundtjänst." +ill_request_cancel_selected = "Återkalla utvalda beställningar" +ill_request_cancel_success = "Beställningen har återkallats" +ill_request_cancel_success_items = "beställning(ar) har återkallats" +ill_request_check_text = "Kolla fjärrlånbeställning" +ill_request_comments = "Tilläggsuppgifter" +ill_request_date_invalid = "Mata i ett giltigt datum" +ill_request_date_past = "Datumet måste vara i framtiden" +ill_request_empty_selection = "Inga utvalda beställningar" +ill_request_error_blocked = "Det är inte möjligt att placera en beställning eftersom du är i låneförbud eller redan har en likvärdig beställning." +ill_request_error_fail = "Fjärrlånbeställning misslyckades. Vänd dig till bibliotekets kundtjänst." +ill_request_error_technical = "Beställningen misslyckades. Vänd dig till bibliotekets kundtjänst." +ill_request_error_unknown_patron_source = "Kundens bibliotek identifieras inte i begäran om fjärrlån." +ill_request_invalid_pickup = "Avhämtningsplats duger inte. Kolla och försök igen." +ill_request_pick_up_library = "Avhämtningsbibliotek" +ill_request_pick_up_location = "Avhämtningsplats" +ill_request_place_fail_missing = "Beställning misslyckades p.g.a. saknande uppgifter. Vänd dig till bibliotekets kundtjänst." +ill_request_place_success = "Material har beställts." +ill_request_place_text = "Fjärrlånbeställning" +ill_request_processed = "Behandlad" +ill_request_profile_html = ""Logga in med din <a href="%%url%%">bibliotekskort</a> för att se fjärrlånbeställningar." +ill_request_submit_text = "Beställ" +Interlibrary Loan Requests = "Fjärrlånbeställningar" Illustrated = "Illustrerad" ils_offline_holdings_message = "Holdings and item availability information is currently unavailable. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:" ils_offline_home_message = "Your account details and live item information will be unavailable during this time. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:" @@ -828,6 +857,7 @@ Year of Publication = "Publiceringsår" Yesterday = "Från igår" You do not have any fines = "Du har inga obetalda avgifter" You do not have any holds or recalls placed = "Du har inga reserveringar i kraft" +You do not have any interlibrary loan requests placed = "Du har inga fjärrlånbeställningar i kraft" You do not have any items checked out = "Du har inga lån" You do not have any saved resources = "Du har inga sparade resurser" You do not have any storage retrieval requests placed = "Du har inga lagerbeställningar i kraft" diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index 5d8290a919a..f79eb975e7c 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -102,6 +102,7 @@ $config = array( 'factories' => array( 'holds' => array('VuFind\Controller\Plugin\Factory', 'getHolds'), 'storageRetrievalRequests' => array('VuFind\Controller\Plugin\Factory', 'getStorageRetrievalRequests'), + 'ILLRequests' => array('VuFind\Controller\Plugin\Factory', 'getILLRequests'), 'reserves' => array('VuFind\Controller\Plugin\Factory', 'getReserves'), ), 'invokables' => array( @@ -537,7 +538,7 @@ $recordRoutes = array( $nonTabRecordActions = array( 'AddComment', 'DeleteComment', 'AddTag', 'Save', 'Email', 'SMS', 'Cite', 'Export', 'RDF', 'Hold', 'BlockedHold', 'Home', 'StorageRetrievalRequest', - 'BlockedStorageRetrievalRequest' + 'BlockedStorageRetrievalRequest', 'ILLRequest', 'BlockedILLRequest' ); // Define list-related routes -- route name => MyResearch action @@ -563,6 +564,7 @@ $staticRoutes = array( 'MyResearch/Favorites', 'MyResearch/Fines', 'MyResearch/Holds', 'MyResearch/Home', 'MyResearch/Logout', 'MyResearch/Profile', 'MyResearch/SaveSearch', 'MyResearch/StorageRetrievalRequests', + 'MyResearch/ILLRequests', 'QRCode/Show', 'QRCode/Unavailable', 'OAI/Server', 'Pazpar2/Home', 'Pazpar2/Search', 'Records/Home', 'Search/Advanced', 'Search/Email', 'Search/History', 'Search/Home', diff --git a/module/VuFind/src/VuFind/Controller/AbstractBase.php b/module/VuFind/src/VuFind/Controller/AbstractBase.php index 968468d1c08..c91f2538556 100644 --- a/module/VuFind/src/VuFind/Controller/AbstractBase.php +++ b/module/VuFind/src/VuFind/Controller/AbstractBase.php @@ -374,15 +374,24 @@ class AbstractBase extends AbstractActionController /** * Translate a string if a translator is available. * - * @param string $msg Message to translate + * @param string $msg Message to translate + * @param string $default Default value to use if no translation is found (null + * for no default). * * @return string */ - public function translate($msg) + public function translate($msg, $default = null) { - return $this->getServiceLocator()->has('VuFind\Translator') + $translated = $this->getServiceLocator()->has('VuFind\Translator') ? $this->getServiceLocator()->get('VuFind\Translator')->translate($msg) : $msg; + + // Did the translation fail to change anything? If so, use default: + if (!is_null($default) && $translated == $msg) { + $translated = $default; + } + + return $translated; } /** diff --git a/module/VuFind/src/VuFind/Controller/AjaxController.php b/module/VuFind/src/VuFind/Controller/AjaxController.php index 015dc5c2259..36f13b10917 100644 --- a/module/VuFind/src/VuFind/Controller/AjaxController.php +++ b/module/VuFind/src/VuFind/Controller/AjaxController.php @@ -1087,6 +1087,19 @@ class AjaxController extends AbstractBase $patron = $this->getAuthManager()->storedCatalogLogin(); if ($patron) { switch ($requestType) { + case 'ILLRequest': + $results = $catalog->checkILLRequestIsValid( + $id, $data, $patron + ); + + $msg = $results + ? $this->translate( + 'ill_request_place_text' + ) + : $this->translate( + 'ill_request_error_blocked' + ); + break; case 'StorageRetrievalRequest': $results = $catalog->checkStorageRetrievalRequestIsValid( $id, $data, $patron @@ -1348,6 +1361,63 @@ class AjaxController extends AbstractBase return $this->output($html, self::STATUS_OK); } + /** + * Get pick up locations for a library + * + * @return \Zend\Http\Response + */ + protected function getLibraryPickupLocationsAjax() + { + $this->writeSession(); // avoid session write timing bug + $id = $this->params()->fromQuery('id'); + $pickupLib = $this->params()->fromQuery('pickupLib'); + if (!empty($id) && !empty($pickupLib)) { + // check if user is logged in + $user = $this->getUser(); + if (!$user) { + return $this->output( + array( + 'status' => false, + 'msg' => $this->translate('You must be logged in first') + ), + self::STATUS_NEED_AUTH + ); + } + + try { + $catalog = $this->getILS(); + $patron = $this->getAuthManager()->storedCatalogLogin(); + if ($patron) { + $params = array( + 'id' => $id, + 'pickupLib' => $pickupLib, + 'patron' => $patron + ); + $results = $catalog->getILLPickupLocations( + $id, $pickupLib, $patron + ); + foreach ($results as &$result) { + if (isset($result['name'])) { + $result['name'] = $this->translate( + 'location_' . $result['name'], + $result['name'] + ); + } + } + return $this->output( + array('locations' => $results), self::STATUS_OK + ); + } + } catch (\Exception $e) { + // Do nothing -- just fail through to the error message below. + } + } + + return $this->output( + $this->translate('An error has occurred'), self::STATUS_ERROR + ); + } + /** * Convenience method for accessing results * diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php index d747fecffa8..5690f10d47a 100644 --- a/module/VuFind/src/VuFind/Controller/MyResearchController.php +++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php @@ -930,6 +930,62 @@ class MyResearchController extends AbstractBase return $view; } + /** + * Send list of ill requests to view + * + * @return mixed + */ + public function illRequestsAction() + { + // Stop now if the user does not have valid catalog credentials available: + if (!is_array($patron = $this->catalogLogin())) { + return $patron; + } + + // Connect to the ILS: + $catalog = $this->getILS(); + + // Process cancel requests if necessary: + $cancelStatus = $catalog->checkFunction('cancelILLRequests'); + $view = $this->createViewModel(); + $view->cancelResults = $cancelStatus + ? $this->ILLRequests()->cancelILLRequests( + $catalog, $patron + ) + : array(); + // If we need to confirm + if (!is_array($view->cancelResults)) { + return $view->cancelResults; + } + + // By default, assume we will not need to display a cancel form: + $view->cancelForm = false; + + // Get request details: + $result = $catalog->getMyILLRequests($patron); + $recordList = array(); + $this->ILLRequests()->resetValidation(); + foreach ($result as $current) { + // Add cancel details if appropriate: + $current = $this->ILLRequests()->addCancelDetails( + $catalog, $current, $cancelStatus, $patron + ); + if ($cancelStatus + && $cancelStatus['function'] != "getCancelILLRequestLink" + && isset($current['cancel_details']) + ) { + // Enable cancel form if necessary: + $view->cancelForm = true; + } + + // Build record driver: + $recordList[] = $this->getDriverForILSRecord($current); + } + + $view->recordList = $recordList; + return $view; + } + /** * Send list of checked out books to view * diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php index aec0729b626..68a1fa8750d 100644 --- a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php +++ b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php @@ -65,6 +65,20 @@ class Factory ); } + /** + * Construct the ILLRequests plugin. + * + * @param ServiceManager $sm Service manager. + * + * @return ILLRequests + */ + public static function getILLRequests(ServiceManager $sm) + { + return new ILLRequests( + $sm->getServiceLocator()->get('VuFind\HMAC') + ); + } + /** * Construct the Reserves plugin. * diff --git a/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php b/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php new file mode 100644 index 00000000000..2754027fb89 --- /dev/null +++ b/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php @@ -0,0 +1,195 @@ +<?php +/** + * VuFind Action Helper - ILL Requests Support Methods + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * Copyright (C) The National Library of Finland 2014. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Controller_Plugins + * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://www.vufind.org Main Page + */ +namespace VuFind\Controller\Plugin; +use Zend\Mvc\Controller\Plugin\AbstractPlugin, Zend\Session\Container; + +/** + * Zend action helper to perform ILL request related actions + * + * @category VuFind2 + * @package Controller_Plugins + * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://www.vufind.org Main Page + */ +class ILLRequests extends Holds +{ + /** + * Grab the Container object for storing helper-specific session + * data. + * + * @return Container + */ + protected function getSession() + { + if (!isset($this->session)) { + $this->session = new Container('ILLRequests_Helper'); + } + return $this->session; + } + + /** + * Update ILS details with cancellation-specific information, if appropriate. + * + * @param \VuFind\ILS\Connection $catalog ILS connection object + * @param array $ilsDetails Details from ILS driver's + * getMyILLRequests() method + * @param array $cancelStatus Cancellation settings from ILS + * @param array $patron ILS patron + * driver's checkFunction() method + * + * @return array $ilsDetails with cancellation info added + */ + public function addCancelDetails($catalog, $ilsDetails, $cancelStatus, $patron) + { + // Generate form details for cancelling requests if enabled + if ($cancelStatus) { + if ($cancelStatus['function'] == 'getCancelILLRequestsLink' + ) { + // Build OPAC URL + $ilsDetails['cancel_link'] + = $catalog->getCancelILLRequestLink( + $ilsDetails, + $patron + ); + } else { + // Form Details + $ilsDetails['cancel_details'] + = $catalog->getCancelILLRequestDetails( + $ilsDetails, + $patron + ); + $this->rememberValidId( + $ilsDetails['cancel_details'] + ); + } + } + + return $ilsDetails; + } + + /** + * Process cancel request. + * + * @param \VuFind\ILS\Connection $catalog ILS connection object + * @param array $patron Current logged in patron + * + * @return array The result of the cancellation, an + * associative array keyed by item ID (empty if no cancellations performed) + */ + public function cancelILLRequests($catalog, $patron) + { + // Retrieve the flashMessenger helper: + $flashMsg = $this->getController()->flashMessenger(); + $params = $this->getController()->params(); + + // Pick IDs to cancel based on which button was pressed: + $all = $params->fromPost('cancelAll'); + $selected = $params->fromPost('cancelSelected'); + if (!empty($all)) { + $details = $params->fromPost('cancelAllIDS'); + } else if (!empty($selected)) { + $details = $params->fromPost('cancelSelectedIDS'); + } else { + // No button pushed -- no action needed + return array(); + } + + if (!empty($details)) { + // Confirm? + if ($params->fromPost('confirm') === "0") { + $url = $this->getController()->url() + ->fromRoute('myresearch-illrequests'); + if ($params->fromPost('cancelAll') !== null) { + return $this->getController()->confirm( + 'ill_request_cancel_all', + $url, + $url, + 'confirm_ill_request_cancel_all_text', + array( + 'cancelAll' => 1, + 'cancelAllIDS' => $params->fromPost('cancelAllIDS') + ) + ); + } else { + return $this->getController()->confirm( + 'ill_request_cancel_selected', + $url, + $url, + 'confirm_ill_request_cancel_selected_text', + array( + 'cancelSelected' => 1, + 'cancelSelectedIDS' => + $params->fromPost('cancelSelectedIDS') + ) + ); + } + } + + foreach ($details as $info) { + // If the user input contains a value not found in the session + // whitelist, something has been tampered with -- abort the process. + if (!in_array($info, $this->getSession()->validIds)) { + $flashMsg->setNamespace('error') + ->addMessage('error_inconsistent_parameters'); + return array(); + } + } + + // Add Patron Data to Submitted Data + $cancelResults = $catalog->cancelILLRequests( + array('details' => $details, 'patron' => $patron) + ); + if ($cancelResults == false) { + $flashMsg->setNamespace('error')->addMessage( + 'ill_request_cancel_fail' + ); + } else { + if ($cancelResults['count'] > 0) { + // TODO : add a mechanism for inserting tokens into translated + // messages so we can avoid a double translation here. + $msg = $this->getController()->translate( + 'ill_request_cancel_success_items' + ); + $flashMsg->setNamespace('info')->addMessage( + $cancelResults['count'] . ' ' . $msg + ); + } + return $cancelResults; + } + } else { + $flashMsg->setNamespace('error')->addMessage( + 'ill_request_empty_selection' + ); + } + return array(); + } +} diff --git a/module/VuFind/src/VuFind/Controller/RecordController.php b/module/VuFind/src/VuFind/Controller/RecordController.php index e2c428db40b..4fcf9229151 100644 --- a/module/VuFind/src/VuFind/Controller/RecordController.php +++ b/module/VuFind/src/VuFind/Controller/RecordController.php @@ -77,6 +77,18 @@ class RecordController extends AbstractRecord return $this->redirectToRecord('#top'); } + /** + * Action for dealing with blocked ILL requests. + * + * @return mixed + */ + public function blockedILLRequestAction() + { + $this->flashMessenger()->setNamespace('error') + ->addMessage('ill_request_error_blocked'); + return $this->redirectToRecord('#top'); + } + /** * Action for dealing with holds. * @@ -289,6 +301,116 @@ class RecordController extends AbstractRecord ); } + /** + * Action for dealing with ILL requests. + * + * @return mixed + */ + public function illRequestAction() + { + $driver = $this->loadRecord(); + + // If we're not supposed to be here, give up now! + $catalog = $this->getILS(); + $checkRequests = $catalog->checkFunction( + 'ILLRequests', + $driver->getUniqueID() + ); + if (!$checkRequests) { + return $this->forwardTo('Record', 'Home'); + } + + // Stop now if the user does not have valid catalog credentials available: + if (!is_array($patron = $this->catalogLogin())) { + return $patron; + } + + // Do we have valid information? + // Sets $this->logonURL and $this->gatheredDetails + $gatheredDetails = $this->ILLRequests()->validateRequest( + $checkRequests['HMACKeys'] + ); + if (!$gatheredDetails) { + return $this->redirectToRecord(); + } + + // Block invalid requests: + if (!$catalog->checkILLRequestIsValid( + $driver->getUniqueID(), $gatheredDetails, $patron + )) { + return $this->blockedILLRequestAction(); + } + + // Send various values to the view so we can build the form: + + $extraFields = isset($checkRequests['extraFields']) + ? explode(":", $checkRequests['extraFields']) : array(); + + // Process form submissions if necessary: + if (!is_null($this->params()->fromPost('placeILLRequest'))) { + // If we made it this far, we're ready to place the hold; + // if successful, we will redirect and can stop here. + + // Add Patron Data to Submitted Data + $details = $gatheredDetails + array('patron' => $patron); + + // Attempt to place the hold: + $function = (string)$checkRequests['function']; + $results = $catalog->$function($details); + + // Success: Go to Display Storage Retrieval Requests + if (isset($results['success']) && $results['success'] == true) { + $this->flashMessenger()->setNamespace('info') + ->addMessage('ill_request_place_success'); + if ($this->inLightbox()) { + return false; + } + return $this->redirect()->toRoute( + 'myresearch-illrequests' + ); + } else { + // Failure: use flash messenger to display messages, stay on + // the current form. + if (isset($results['status'])) { + $this->flashMessenger()->setNamespace('error') + ->addMessage($results['status']); + } + if (isset($results['sysMessage'])) { + $this->flashMessenger()->setNamespace('error') + ->addMessage($results['sysMessage']); + } + } + } + + // Find and format the default required date: + $defaultRequired = $this->ILLRequests() + ->getDefaultRequiredDate($checkRequests); + $defaultRequired = $this->getServiceLocator()->get('VuFind\DateConverter') + ->convertToDisplayDate("U", $defaultRequired); + + // Get pickup libraries + $pickupLibraries = $catalog->getILLPickUpLibraries( + $driver->getUniqueID(), $patron, $gatheredDetails + ); + + // Get pickup locations. Note that these are independent of pickup library, + // and library specific locations must be retrieved when a library is + // selected. + $pickupLocations = $catalog->getPickUpLocations($patron, $gatheredDetails); + + return $this->createViewModel( + array( + 'gatheredDetails' => $gatheredDetails, + 'pickupLibraries' => $pickupLibraries, + 'pickupLocations' => $pickupLocations, + 'homeLibrary' => $this->getUser()->home_library, + 'extraFields' => $extraFields, + 'defaultRequiredDate' => $defaultRequired, + 'helpText' => $checkRequests['helpText'] + ) + ); + } + /** * Is the result scroller active? * diff --git a/module/VuFind/src/VuFind/ILS/Connection.php b/module/VuFind/src/VuFind/ILS/Connection.php index b327cbaaf96..9ea0d0cae86 100644 --- a/module/VuFind/src/VuFind/ILS/Connection.php +++ b/module/VuFind/src/VuFind/ILS/Connection.php @@ -416,6 +416,72 @@ class Connection implements TranslatorAwareInterface return $response; } + /** + * Check ILL Request + * + * A support method for checkFunction(). This is responsible for checking + * the driver configuration to determine if the system supports storage + * retrieval requests. + * + * @param string $functionConfig The ILL request configuration values + * + * @return mixed On success, an associative array with specific function keys + * and values either for placing requests via a form; on failure, false. + */ + protected function checkMethodILLRequests($functionConfig) + { + $response = false; + + if ($this->checkCapability('placeILLRequest') + && isset($functionConfig['HMACKeys']) + ) { + $response = array('function' => 'placeILLRequest'); + $response['HMACKeys'] = explode(':', $functionConfig['HMACKeys']); + if (isset($functionConfig['extraFields'])) { + $response['extraFields'] = $functionConfig['extraFields']; + } + if (isset($functionConfig['helpText'])) { + $response['helpText'] = $this->getHelpText( + $functionConfig['helpText'] + ); + } + } + return $response; + } + + /** + * Check Cancel ILL Requests + * + * A support method for checkFunction(). This is responsible for checking + * the driver configuration to determine if the system supports Cancelling + * ILL Requests. + * + * @param string $functionConfig The Cancel function configuration values + * + * @return mixed On success, an associative array with specific function keys + * and values either for cancelling requests via a form or a URL; + * on failure, false. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function checkMethodcancelILLRequests($functionConfig) + { + $response = false; + + if (isset($this->config->cancel_ill_requests_enabled) + && $this->config->cancel_ill_requests_enabled + ) { + if ($this->checkCapability('cancelILLRequests')) { + $response = array('function' => 'cancelILLRequests'); + } elseif ($this->checkCapability('getCancelILLRequestLink') + ) { + $response = array( + 'function' => 'getCancelILLRequestLink' + ); + } + } + return $response; + } + /** * Get proper help text from the function config * @@ -484,6 +550,30 @@ class Connection implements TranslatorAwareInterface return false; } + /** + * Check ILL Request is Valid + * + * This is responsible for checking if an ILL request is valid + * + * @param string $id A Bibliographic ID + * @param array $data Collected Holds Data + * @param array $patron Patron related data + * + * @return mixed The result of the checkILLRequestIsValid + * function if it exists, false if it does not + */ + public function checkILLRequestIsValid($id, $data, $patron) + { + if ($this->checkCapability('checkILLRequestIsValid')) { + return $this->getDriver()->checkILLRequestIsValid( + $id, $data, $patron + ); + } + // If the driver has no checkILLRequestIsValid method, we + // will assume that the request is not valid + return false; + } + /** * Get Holds Mode * diff --git a/module/VuFind/src/VuFind/ILS/Driver/Demo.php b/module/VuFind/src/VuFind/ILS/Driver/Demo.php index 26b0f196909..3f6f4397a3f 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Demo.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Demo.php @@ -84,6 +84,13 @@ class Demo extends AbstractBase */ protected $storageRetrievalRequests = true; + /** + * Should we support ILLRequests? + * + * @var bool + */ + protected $ILLRequests = true; + /** * Date converter object * @@ -122,6 +129,10 @@ class Demo extends AbstractBase $this->storageRetrievalRequests = $this->config['Catalog']['storageRetrievalRequests']; } + if (isset($this->config['Catalog']['ILLRequests'])) { + $this->ILLRequests = $this->config['Catalog']['ILLRequests']; + } + // Establish a namespace in the session for persisting fake data (to save // on Solr hits): $this->session = new SessionContainer('DemoDriver'); @@ -234,15 +245,20 @@ class Demo extends AbstractBase 'addLink' => $patron ? rand()%10 == 0 ? 'block' : true : false, 'storageRetrievalRequest' => 'auto', 'addStorageRetrievalRequestLink' => $patron + ? rand()%10 == 0 ? 'block' : 'check' + : false, + 'ILLRequest' => 'auto', + 'addILLRequestLink' => $patron ? rand()%10 == 0 ? 'block' : 'check' : false ); } /** - * Generate a list of holds or storage retrieval requests. + * Generate a list of holds, storage retrieval requests or ILL requests. * - * @param string $requestType Request type (Holds or StorageRetrievalRequests) + * @param string $requestType Request type (Holds, StorageRetrievalRequests or + * ILLRequests) * * @return ArrayObject List of requests */ @@ -624,6 +640,24 @@ class Demo extends AbstractBase return $this->session->storageRetrievalRequests; } + /** + * Get Patron ILL Requests + * + * This is responsible for retrieving all ILL requests by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return mixed Array of the patron's ILL requests + */ + public function getMyILLRequests($patron) + { + if (!isset($this->session->ILLRequests)) { + $this->session->ILLRequests + = $this->createRequestList('ILLRequests'); + } + return $this->session->ILLRequests; + } + /** * Get Patron Transactions * @@ -1258,6 +1292,239 @@ class Demo extends AbstractBase return array('success' => true); } + /** + * Check if ILL request available + * + * This is responsible for determining if an item is requestable + * + * @param string $id The Bib ID + * @param array $data An Array of item data + * @param patron $patron An array of patron data + * + * @return string True if request is valid, false if not + */ + public function checkILLRequestIsValid($id, $data, $patron) + { + if (!$this->ILLRequests || rand() % 10 == 0) { + return false; + } + return true; + } + + /** + * Place ILL Request + * + * Attempts to place an ILL request on a particular item and returns + * an array with result details + * + * @param array $details An array of item and patron data + * + * @return mixed An array of data on the request including + * whether or not it was successful and a system message (if available) + */ + public function placeILLRequest($details) + { + if (!$this->ILLRequests) { + return array( + "success" => false, + "sysMessage" => 'ILL requests are disabled.' + ); + } + // Simulate failure: + if (rand() % 2) { + return array( + "success" => false, + "sysMessage" => + 'Demonstrating failure; keep trying and ' . + 'it will work eventually.' + ); + } + + if (!isset($this->session->ILLRequests)) { + $this->session->ILLRequests = new ArrayObject(); + } + $lastRequest = count($this->session->ILLRequests) - 1; + $nextId = $lastRequest >= 0 + ? $this->session->ILLRequests[$lastRequest]['item_id'] + 1 + : 0; + + // Figure out appropriate expiration date: + if (!isset($holdDetails['requiredBy']) + || empty($holdDetails['requiredBy']) + ) { + $expire = strtotime("now + 30 days"); + } else { + try { + $expire = $this->dateConverter->convertFromDisplayDate( + "U", $details['requiredBy'] + ); + } catch (DateException $e) { + // Hold Date is invalid + return array( + 'success' => false, + 'sysMessage' => 'ill_request_date_invalid' + ); + } + } + if ($expire <= time()) { + return array( + 'success' => false, + 'sysMessage' => 'ill_request_date_past' + ); + } + + $this->session->ILLRequests->append( + array( + "id" => $details['id'], + "location" => $details['pickUpLocation'], + "expire" => date("j-M-y", $expire), + "create" => date("j-M-y"), + "processed" => rand()%3 == 0 ? date("j-M-y", $expire) : '', + "reqnum" => sprintf("%06d", $nextId), + "item_id" => $nextId + ) + ); + + return array('success' => true); + } + + /** + * Get ILL Pickup Libraries + * + * This is responsible for getting information on the possible pickup libraries + * + * @param string $id Record ID + * @param array $patron Patron + * + * @return bool|array False if request not allowed, or an array of associative + * arrays with libraries. + */ + public function getILLPickupLibraries($id, $patron) + { + if (!$this->ILLRequests) { + return false; + } + + $details = array( + array( + 'id' => 1, + 'name' => 'Main Library', + 'isDefault' => true + ), + array( + 'id' => 2, + 'name' => 'Branch Library', + 'isDefault' => false + ) + ); + + return $details; + } + + /** + * Get ILL Pickup Locations + * + * This is responsible for getting a list of possible pickup locations for a + * library + * + * @param string $id Record ID + * @param string $pickupLib Pickup library ID + * @param array $patron Patron + * + * @return boo|array False if request not allowed, or an array of + * locations. + */ + public function getILLPickupLocations($id, $pickupLib, $patron) + { + switch ($pickupLib) { + case 1: + return array( + array( + 'id' => 1, + 'name' => 'Circulation Desk', + 'isDefault' => true + ), + array( + 'id' => 2, + 'name' => 'Reference Desk', + 'isDefault' => false + ) + ); + case 2: + return array( + array( + 'id' => 3, + 'name' => 'Main Desk', + 'isDefault' => false + ), + array( + 'id' => 4, + 'name' => 'Library Bus', + 'isDefault' => true + ) + ); + } + return array(); + } + + /** + * Cancel ILL Request + * + * Attempts to Cancel an ILL request on a particular item. The + * data in $cancelDetails['details'] is determined by + * getCancelILLRequestDetails(). + * + * @param array $cancelDetails An array of item and patron data + * + * @return array An array of data on each request including + * whether or not it was successful and a system message (if available) + */ + public function cancelILLRequests($cancelDetails) + { + // Rewrite the items in the session, removing those the user wants to + // cancel. + $newRequests = new ArrayObject(); + $retVal = array('count' => 0, 'items' => array()); + foreach ($this->session->ILLRequests as $current) { + if (!in_array($current['reqnum'], $cancelDetails['details'])) { + $newRequests->append($current); + } else { + // 50% chance of cancel failure for testing purposes + if (rand() % 2) { + $retVal['count']++; + $retVal['items'][$current['item_id']] = array( + 'success' => true, + 'status' => 'ill_request_cancel_success' + ); + } else { + $newRequests->append($current); + $retVal['items'][$current['item_id']] = array( + 'success' => false, + 'status' => 'ill_request_cancel_fail', + 'sysMessage' => + 'Demonstrating failure; keep trying and ' . + 'it will work eventually.' + ); + } + } + } + + $this->session->ILLRequests = $newRequests; + return $retVal; + } + + /** + * Get Cancel ILL Request Details + * + * @param array $details An array of item data + * + * @return string Data for use in a form field + */ + public function getCancelILLRequestDetails($details) + { + return $details['reqnum']; + } + /** * Public Function which specifies renew, hold and cancel settings. * @@ -1283,6 +1550,17 @@ class Demo extends AbstractBase . ' with some <span style="color: red">styling</span>.' ); } + if ($function == 'ILLRequests' && $this->ILLRequests) { + return array( + 'enabled' => true, + 'HMACKeys' => 'number', + 'extraFields' => + 'comments:pickUpLibrary:pickUpLibraryLocation:requiredByDate', + 'defaultRequiredDate' => '0:1:0', + 'helpText' => 'This is an ILL request help text' + . ' with some <span style="color: red">styling</span>.' + ); + } return array(); } } diff --git a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php index bcec19174af..13979e17882 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php @@ -1625,7 +1625,7 @@ class Voyager extends AbstractBase 'status' => utf8_encode($sqlRow['STATUS_DESC']), 'statusDate' => $statusDate, 'location' => $this->getLocationName($sqlRow['PICKUP_LOCATION_ID']), - 'created' => $createDate, + 'create' => $createDate, 'processed' => $processedDate, 'expire' => $expireDate, 'reply' => utf8_encode($sqlRow['REPLY_NOTE']), diff --git a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php index 73e312a5d3a..b3c203329b7 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php +++ b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php @@ -5,6 +5,7 @@ * PHP version 5 * * Copyright (C) Villanova University 2007. + * Copyright (C) The National Library of Finland 2014. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -24,13 +25,15 @@ * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> * @author Demian Katz <demian.katz@villanova.edu> * @author Luke O'Sullivan <l.osullivan@swansea.ac.uk> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki */ namespace VuFind\ILS\Driver; use PDO, PDOException, VuFind\Exception\Date as DateException, - VuFind\Exception\ILS as ILSException; - + VuFind\Exception\ILS as ILSException, + Zend\Session\Container as SessionContainer; + /** * Voyager Restful ILS Driver * @@ -39,6 +42,7 @@ use PDO, PDOException, VuFind\Exception\Date as DateException, * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> * @author Demian Katz <demian.katz@villanova.edu> * @author Luke O'Sullivan <l.osullivan@swansea.ac.uk> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki */ @@ -136,6 +140,13 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte */ protected $titleHoldsMode; + /** + * Container for storing cached ILS data. + * + * @var SessionContainer + */ + protected $session; + /** * Constructor * @@ -149,6 +160,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte parent::__construct($dateConverter); $this->holdsMode = $holdsMode; $this->titleHoldsMode = $titleHoldsMode; + } /** @@ -196,6 +208,9 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte $this->checkRenewalsUpFront = isset($this->config['Renewals']['checkUpFront']) ? $this->config['Renewals']['checkUpFront'] : true; + + // Establish a namespace in the session for persisting cached data + $this->session = new SessionContainer('VoyagerRestful_' . $this->dbName); } /** @@ -216,6 +231,47 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte return $functionConfig; } + /** + * Helper function for fetching cached data. + * Data is cached for up to 30 seconds so that it would be faster to process + * e.g. requests where multiple calls to the backend are made. + * + * @param string $id Cache entry id + * + * @return mixed|null Cached entry or null if not cached or expired + */ + protected function getCachedData($id) + { + if (isset($this->session->cache[$id])) { + $item = $this->session->cache[$id]; + if (time() - $item['time'] > 30) { + return $item['entry']; + } + } + return null; + } + + /** + * Helper function for storing cached data. + * Data is cached for up to 30 seconds so that it would be faster to process + * e.g. requests where multiple calls to the backend are made. + * + * @param string $id Cache entry id + * @param mixed $entry Entry to be cached + * + * @return void + */ + protected function putCachedData($id, $entry) + { + if (!isset($this->session->cache)) { + $this->session->cache = array(); + } + $this->session->cache[$id] = array( + 'time' => time(), + 'entry' => $entry + ); + } + /** * Support method for VuFind Hold Logic. Take an array of status strings * and determines whether or not an item is holdable based on the @@ -293,6 +349,20 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte return true; } + /** + * Support method for VuFind ILL Logic. Take a holdings row array + * and determine whether or not an ILL (UB) request is allowed. + * + * @param array $holdingsRow The holdings row to analyze. + * + * @return bool Whether an item is holdable + * @access protected + */ + protected function isILLRequestAllowed($holdingsRow) + { + return true; + } + /** * Protected support method for getHolding. * @@ -339,8 +409,11 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte $is_borrowable = isset($row['_fullRow']['ITEM_TYPE_ID']) ? $this->isBorrowable($row['_fullRow']['ITEM_TYPE_ID']) : false; $is_holdable = $this->isHoldable($row['_fullRow']['STATUS_ARRAY']); - $isStorageRetrievalRequestAllowed - = $this->isStorageRetrievalRequestAllowed($row); + $isStorageRetrievalRequestAllowed + = isset($this->config['StorageRetrievalRequests']) + && $this->isStorageRetrievalRequestAllowed($row); + $isILLRequestAllowed = isset($this->config['ILLRequests']) + && $this->isILLRequestAllowed($row); // If the item cannot be borrowed or if the item is not holdable, // set is_holdable to false if (!$is_borrowable || !$is_holdable) { @@ -393,13 +466,22 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte } } + $ILLRequest = ''; + $addILLRequestLink = false; + if ($patron && $isILLRequestAllowed) { + $ILLRequest = 'auto'; + $addILLRequestLink = 'check'; + } + $holding[$i] += array( 'is_holdable' => $is_holdable, 'holdtype' => $holdType, 'addLink' => $addLink, 'level' => "copy", 'storageRetrievalRequest' => $storageRetrieval, - 'addStorageRetrievalRequestLink' => $addStorageRetrievalLink + 'addStorageRetrievalRequestLink' => $addStorageRetrievalLink, + 'ILLRequest' => $ILLRequest, + 'addILLRequestLink' => $addILLRequestLink ); unset($holding[$i]['_fullRow']); } @@ -445,6 +527,9 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte */ public function checkStorageRetrievalRequestIsValid($id, $data, $patron) { + if (!isset($this->config['StorageRetrievalRequests'])) { + return false; + } if ($this->checkAccountBlocks($patron['id'])) { return 'block'; } @@ -461,7 +546,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte } return true; } - + /** * Determine Renewability * @@ -660,6 +745,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte } // Add Params + $queryString = array(); foreach ($params as $key => $param) { $queryString[] = urlencode($key). "=" . urlencode($param); } @@ -711,6 +797,18 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte return $simpleXML; } + /** + * Encode a string for XML + * + * @param string $string String to be encoded + * + * @return string Encoded string + */ + protected function encodeXML($string) + { + return htmlspecialchars($string, ENT_COMPAT, "UTF-8"); + } + /** * Build Basic XML * @@ -729,7 +827,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte foreach ($nodes as $nodeName => $nodeValue) { $xmlString .= "<" . $nodeName . ">"; - $xmlString .= htmlspecialchars($nodeValue, ENT_COMPAT, "UTF-8"); + $xmlString .= $this->encodeXML($nodeValue); // Split out any attributes $nodeName = strtok($nodeName, ' '); $xmlString .= "</" . $nodeName . ">"; @@ -758,6 +856,12 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte */ protected function checkAccountBlocks($patronId) { + $cacheId = "blocks_$patronId"; + $data = $this->getCachedData($cacheId); + if (!is_null($data)) { + return $data; + } + $blockReason = false; // Build Hierarchy @@ -772,15 +876,15 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte "view" => "full" ); - $blocks = $this->makeRequest($hierarchy, $params); + $blockReason = array(); + $blocks = $this->makeRequest($hierarchy, $params); if ($blocks) { $node = "reply-text"; $reply = (string)$blocks->$node; // Valid Response if ($reply == "ok" && isset($blocks->blocks)) { - $blockReason = array(); foreach ($blocks->blocks->institution->borrowingBlock as $borrowBlock ) { @@ -788,7 +892,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte } } } - + $this->putCachedData($cacheId, $blockReason); return $blockReason; } @@ -1133,6 +1237,91 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte ); } + /** + * Get Patron Remote Holds + * + * This is responsible for retrieving all remote holds by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @throws DateException + * @throws ILSException + * @return array Array of the patron's holds on success. + */ + protected function getRemoteHolds($patron) + { + // Build Hierarchy + $hierarchy = array( + 'patron' => $patron['id'], + 'circulationActions' => 'requests', + 'holds' => false + ); + + // Add Required Params + $params = array( + "patron_homedb" => $this->ws_patronHomeUbId, + "view" => "full" + ); + + $results = $this->makeRequest($hierarchy, $params); + + if ($results === false) { + throw new ILSException('System error fetching remote holds'); + } + + $replyCode = (string)$results->{'reply-code'}; + if ($replyCode != 0 && $replyCode != 8) { + throw new ILSException('System error fetching remote holds'); + } + $holds = array(); + if (isset($results->holds->institution)) { + foreach ($results->holds->institution as $institution) { + // Only take remote holds + if ($institution == 'LOCAL') { + continue; + } + + foreach ($institution->hold as $hold) { + $item = $hold->requestItem; + + $holds[] = array( + 'id' => '', + 'type' => (string)$item->holdType, + 'location' => (string)$item->pickupLocation, + 'expire' => (string)$item->expiredDate + ? $this->dateFormat->convertToDisplayDate( + 'Y-m-d', (string)$item->expiredDate + ) + : '', + // Looks like expired date shows creation date for + // UB requests, but who knows + 'create' => (string)$item->expiredDate + ? $this->dateFormat->convertToDisplayDate( + 'Y-m-d', (string)$item->expiredDate + ) + : '', + 'position' => (string)$item->queuePosition, + 'available' => (string)$item->status == '2', + 'reqnum' => (string)$item->holdRecallId, + 'item_id' => (string)$item->itemId, + 'volume' => '', + 'publication_year' => '', + 'title' => (string)$item->itemTitle, + 'institution_id' => (string)$institution->attributes()->id, + 'institution_name' => (string)$item->dbName, + 'institution_dbkey' => (string)$item->dbKey, + 'in_transit' => (substr((string)$item->statusText, 0, 13) + == 'In transit to') + ? substr((string)$item->statusText, 14) + : '' + ); + } + } + } + + return $holds; + } + /** * Place Hold * @@ -1330,8 +1519,23 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte */ public function getMyStorageRetrievalRequests($patron) { - $requests = parent::getMyStorageRetrievalRequests($patron); - + $requests = array_merge( + parent::getMyStorageRetrievalRequests($patron), + $this->getRemoteCallSlips($patron) + ); + return $requests; + } + + /** + * Get Patron Remote Storage Retrieval Requests (Call Slips). Gets remote + * callslips via the API. + * + * @param array $patron The patron array from patronLogin + * + * @return mixed Array of the patron's storage retrieval requests. + */ + protected function getRemoteCallSlips($patron) + { // Build Hierarchy $hierarchy = array( 'patron' => $patron['id'], @@ -1351,6 +1555,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte if ($replyCode != 0 && $replyCode != 8) { throw new Exception('System error fetching call slips'); } + $requests = array(); if (isset($results->callslips->institution)) { foreach ($results->callslips->institution as $institution) { if ((string)$institution->attributes()->id == 'LOCAL') { @@ -1370,7 +1575,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte : '', // Looks like expired date shows creation date for // call slip requests, but who knows - 'created' => (string)$item->expiredDate + 'create' => (string)$item->expiredDate ? $this->dateFormat->convertToDisplayDate( 'Y-m-d', (string)$item->expiredDate ) @@ -1594,4 +1799,598 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte . '|' . $details['reqnum']; return $details; } + + /** + * A helper function that retrieves UB request details for ILL and caches them + * for a short while for faster access. + * + * @param string $id BIB id + * @param array $patron Patron + * + * @return boolean|array False if UB request is not available or an array + * of details on success + */ + protected function getUBRequestDetails($id, $patron) + { + $requestId = "ub_{$id}_" . $patron['id']; + $data = $this->getCachedData($requestId); + if (!empty($data)) { + return $data; + } + + if (strstr($patron['id'], '.') === false) { + $this->debug( + "getUBRequestDetails: no prefix in patron id '{$patron['id']}'" + ); + $this->putCachedData($requestId, false); + return false; + } + list($source, $patronId) = explode('.', $patron['id'], 2); + if (!isset($this->config['ILLRequestSources'][$source])) { + $this->debug("getUBRequestDetails: source '$source' unknown"); + $this->putCachedData($requestId, false); + return false; + } + + list($catSource, $catUsername) = explode('.', $patron['cat_username'], 2); + $patronId = $this->encodeXML($patronId); + $patronHomeUbId = $this->encodeXML( + $this->config['ILLRequestSources'][$source] + ); + $lastname = $this->encodeXML($patron['lastname']); + $barcode = $this->encodeXML($catUsername); + $bibId = $this->encodeXML($id); + $bibDbName = $this->encodeXML($this->config['Catalog']['database']); + $localUbId = $this->encodeXML($this->ws_patronHomeUbId); + + // Call PatronRequestsService first to check that UB is an available request + // type. Additionally, this seems to be mandatory, as PatronRequestService + // may fail otherwise. + $xml = <<<EOT +<?xml version="1.0" encoding="UTF-8"?> +<ser:serviceParameters +xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters"> + <ser:parameters> + <ser:parameter key="bibId"> + <ser:value>$bibId</ser:value> + </ser:parameter> + <ser:parameter key="bibDbCode"> + <ser:value>LOCAL</ser:value> + </ser:parameter> + </ser:parameters> + <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$patronHomeUbId" + patronId="$patronId"> + <ser:authFactor type="B">$barcode</ser:authFactor> + </ser:patronIdentifier> +</ser:serviceParameters> +EOT; + + $response = $this->makeRequest( + array('PatronRequestsService' => false), array(), 'POST', $xml + ); + + if ($response === false) { + $this->session->UBDetails[$requestId] = array( + 'time' => time(), + 'data' => false + ); + $this->putCachedData($requestId, false); + return false; + } + // Process + $response->registerXPathNamespace( + 'ser', 'http://www.endinfosys.com/Voyager/serviceParameters' + ); + $response->registerXPathNamespace( + 'req', 'http://www.endinfosys.com/Voyager/requests' + ); + foreach ($response->xpath('//ser:message') as $message) { + // Any message means a problem, right? + $this->putCachedData($requestId, false); + return false; + } + $requestCount = count( + $response->xpath("//req:requestIdentifier[@requestCode='UB']") + ); + if ($requestCount == 0) { + // UB request not available + $this->putCachedData($requestId, false); + return false; + } + + $xml = <<<EOT +<?xml version="1.0" encoding="UTF-8"?> +<ser:serviceParameters +xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters"> + <ser:parameters> + <ser:parameter key="bibId"> + <ser:value>$bibId</ser:value> + </ser:parameter> + <ser:parameter key="bibDbCode"> + <ser:value>LOCAL</ser:value> + </ser:parameter> + <ser:parameter key="bibDbName"> + <ser:value>$bibDbName</ser:value> + </ser:parameter> + <ser:parameter key="requestCode"> + <ser:value>UB</ser:value> + </ser:parameter> + <ser:parameter key="requestSiteId"> + <ser:value>$localUbId</ser:value> + </ser:parameter> + </ser:parameters> + <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$patronHomeUbId" + patronId="$patronId"> + <ser:authFactor type="B">$barcode</ser:authFactor> + </ser:patronIdentifier> +</ser:serviceParameters> +EOT; + + $response = $this->makeRequest( + array('PatronRequestService' => false), array(), 'POST', $xml + ); + + if ($response === false) { + $this->putCachedData($requestId, false); + return false; + } + // Process + $response->registerXPathNamespace( + 'ser', 'http://www.endinfosys.com/Voyager/serviceParameters' + ); + $response->registerXPathNamespace( + 'req', 'http://www.endinfosys.com/Voyager/requests' + ); + foreach ($response->xpath('//ser:message') as $message) { + // Any message means a problem, right? + $this->putCachedData($requestId, false); + return false; + } + $items = array(); + $libraries = array(); + $locations = array(); + $requiredByDate = ''; + foreach ($response->xpath('//req:field') as $field) { + switch ($field->attributes()->labelKey) { + case 'selectItem': + foreach ($field->xpath('./req:select/req:option') as $option) { + $items[] = array( + 'id' => (string)$option->attributes()->id, + 'name' => (string)$option + ); + } + break; + case 'pickupLib': + foreach ($field->xpath('./req:select/req:option') as $option) { + $libraries[] = array( + 'id' => (string)$option->attributes()->id, + 'name' => (string)$option, + 'isDefault' => $option->attributes()->isDefault == 'Y' + ); + } + break; + case 'pickUpAt': + foreach ($field->xpath('./req:select/req:option') as $option) { + $locations[] = array( + 'id' => (string)$option->attributes()->id, + 'name' => (string)$option, + 'isDefault' => $option->attributes()->isDefault == 'Y' + ); + } + break; + case 'notNeededAfter': + $node = current($field->xpath('./req:text')); + $requiredByDate = $this->dateFormat->convertToDisplayDate( + "Y-m-d H:i", (string)$node + ); + break; + } + } + $results = array( + 'items' => $items, + 'libraries' => $libraries, + 'locations' => $locations, + 'requiredBy' => $requiredByDate + ); + $this->putCachedData($requestId, $results); + return $results; + } + + /** + * checkILLRequestIsValid + * + * This is responsible for determining if an item is requestable + * + * @param string $id The Bib ID + * @param array $data An Array of item data + * @param patron $patron An array of patron data + * + * @return string True if request is valid, false if not + */ + public function checkILLRequestIsValid($id, $data, $patron) + { + if (!isset($this->config['ILLRequests'])) { + $this->debug('ILL Requests not configured'); + return false; + } + + $level = isset($data['level']) ? $data['level'] : "copy"; + $itemID = ($level != 'title' && isset($data['item_id'])) + ? $data['item_id'] + : false; + + if ($level == 'copy' && $itemID === false) { + $this->debug('Item ID missing'); + return false; + } + + $results = $this->getUBRequestDetails($id, $patron); + if ($results === false) { + $this->debug('getUBRequestDetails returned false'); + return false; + } + if ($level == 'copy') { + $found = false; + foreach ($results['items'] as $item) { + if ($item['id'] == "$itemID.$id") { + $found = true; + break; + } + } + if (!$found) { + $this->debug('Item not requestable'); + return false; + } + } + + return true; + } + + /** + * Get ILL (UB) Pickup Libraries + * + * This is responsible for getting information on the possible pickup libraries + * + * @param string $id Record ID + * @param array $patron Patron + * + * @return bool|array False if request not allowed, or an array of associative + * arrays with libraries. + */ + public function getILLPickupLibraries($id, $patron) + { + if (!isset($this->config['ILLRequests'])) { + return false; + } + + $results = $this->getUBRequestDetails($id, $patron); + if ($results === false) { + $this->debug('getUBRequestDetails returned false'); + return false; + } + + return $results['libraries']; + } + + /** + * Get ILL (UB) Pickup Locations + * + * This is responsible for getting a list of possible pickup locations for a + * library + * + * @param string $id Record ID + * @param string $pickupLib Pickup library ID + * @param array $patron Patron + * + * @return bool|array False if request not allowed, or an array of + * locations. + */ + public function getILLPickupLocations($id, $pickupLib, $patron) + { + if (!isset($this->config['ILLRequests'])) { + return false; + } + + list($source, $patronId) = explode('.', $patron['id'], 2); + if (!isset($this->config['ILLRequestSources'][$source])) { + return $this->holdError('ill_request_unknown_patron_source'); + } + + list($catSource, $catUsername) = explode('.', $patron['cat_username'], 2); + $patronId = $this->encodeXML($patronId); + $patronHomeUbId = $this->encodeXML( + $this->config['ILLRequestSources'][$source] + ); + $lastname = $this->encodeXML($patron['lastname']); + $barcode = $this->encodeXML($catUsername); + $localUbId = $this->encodeXML($this->ws_patronHomeUbId); + $pickupLib = $this->encodeXML($pickupLib); + + $xml = <<<EOT +<?xml version="1.0" encoding="UTF-8"?> +<ser:serviceParameters +xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters"> + <ser:parameters> + <ser:parameter key="pickupLibId"> + <ser:value>$pickupLib</ser:value> + </ser:parameter> + </ser:parameters> + <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$patronHomeUbId" + patronId="$patronId"> + <ser:authFactor type="B">$barcode</ser:authFactor> + </ser:patronIdentifier> +</ser:serviceParameters> +EOT; + + $response = $this->makeRequest( + array('UBPickupLibService' => false), array(), 'POST', $xml + ); + + if ($response === false) { + return new PEAR_Error('ill_request_error_technical'); + } + // Process + $response->registerXPathNamespace( + 'ser', 'http://www.endinfosys.com/Voyager/serviceParameters' + ); + $response->registerXPathNamespace( + 'req', 'http://www.endinfosys.com/Voyager/requests' + ); + foreach ($response->xpath('//ser:message') as $message) { + // Any message means a problem, right? + return new PEAR_Error('ill_request_error_technical'); + } + $locations = array(); + foreach ($response->xpath('//req:location') as $location) { + $locations[] = array( + 'id' => (string)$location->attributes()->id, + 'name' => (string)$location, + 'isDefault' => $location->attributes()->isDefault == 'Y' + ); + } + return $locations; + } + + /** + * Place ILL (UB) Request + * + * Attempts to place an UB request on a particular item and returns + * an array with result details or a PEAR error on failure of support classes + * + * @param array $details An array of item and patron data + * + * @return mixed An array of data on the request including + * whether or not it was successful and a system message (if available) + * @access public + */ + public function placeILLRequest($details) + { + $patron = $details['patron']; + list($source, $patronId) = explode('.', $patron['id'], 2); + if (!isset($this->config['ILLRequestSources'][$source])) { + return $this->holdError('ill_request_error_unknown_patron_source'); + } + + list($catSource, $catUsername) = explode('.', $patron['cat_username'], 2); + $patronId = htmlspecialchars($patronId, ENT_COMPAT, 'UTF-8'); + $patronHomeUbId = $this->encodeXML( + $this->config['ILLRequestSources'][$source] + ); + $lastname = $this->encodeXML($patron['lastname']); + $ubId = $this->encodeXML($patronHomeUbId); + $barcode = $this->encodeXML($catUsername); + $pickupLocation = $this->encodeXML($details['pickUpLibraryLocation']); + $pickupLibrary = $this->encodeXML($details['pickUpLibrary']); + $itemId = $this->encodeXML($details['item_id'] . '.' . $details['id']); + $comment = $this->encodeXML( + isset($details['comment']) ? $details['comment'] : '' + ); + $bibId = $this->encodeXML($details['id']); + $bibDbName = $this->encodeXML($this->config['Catalog']['database']); + $localUbId = $this->encodeXML($this->ws_patronHomeUbId); + + // Convert last interest date from Display Format to Voyager required format + try { + $lastInterestDate = $this->dateFormat->convertFromDisplayDate( + "Y-m-d", $details['requiredBy'] + ); + } catch (DateException $e) { + // Date is invalid + return $this->holdError("ill_request_date_invalid"); + } + + // Attempt Request + $xml = <<<EOT +<?xml version="1.0" encoding="UTF-8"?> +<ser:serviceParameters +xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters"> + <ser:parameters> + <ser:parameter key="bibId"> + <ser:value>$bibId</ser:value> + </ser:parameter> + <ser:parameter key="bibDbCode"> + <ser:value>LOCAL</ser:value> + </ser:parameter> + <ser:parameter key="bibDbName"> + <ser:value>$bibDbName</ser:value> + </ser:parameter> + <ser:parameter key="Select_Library"> + <ser:value>$localUbId</ser:value> + </ser:parameter> + <ser:parameter key="requestCode"> + <ser:value>UB</ser:value> + </ser:parameter> + <ser:parameter key="requestSiteId"> + <ser:value>$localUbId</ser:value> + </ser:parameter> + <ser:parameter key="itemId"> + <ser:value>$itemId</ser:value> + </ser:parameter> + <ser:parameter key="Select_Pickup_Lib"> + <ser:value>$pickupLibrary</ser:value> + </ser:parameter> + <ser:parameter key="PICK"> + <ser:value>$pickupLocation</ser:value> + </ser:parameter> + <ser:parameter key="REQNNA"> + <ser:value>$lastInterestDate</ser:value> + </ser:parameter> + <ser:parameter key="REQCOMMENTS"> + <ser:value>$comment</ser:value> + </ser:parameter> + </ser:parameters> + <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$ubId" + patronId="$patronId"> + <ser:authFactor type="B">$barcode</ser:authFactor> + </ser:patronIdentifier> +</ser:serviceParameters> +EOT; + + $response = $this->makeRequest( + array('SendPatronRequestService' => false), array(), 'POST', $xml + ); + + if ($response === false) { + return $this->holdError('ill_request_error_technical'); + } + // Process + $response->registerXPathNamespace( + 'ser', 'http://www.endinfosys.com/Voyager/serviceParameters' + ); + $response->registerXPathNamespace( + 'req', 'http://www.endinfosys.com/Voyager/requests' + ); + foreach ($response->xpath('//ser:message') as $message) { + if ($message->attributes()->type == 'success') { + return array( + 'success' => true, + 'status' => 'ill_request_success' + ); + } + if ($message->attributes()->type == 'system') { + return $this->holdError('ill_request_error_technical'); + } + } + + return $this->holdError('ill_request_error_blocked'); + } + + /** + * Get Patron ILL Requests + * + * This is responsible for retrieving all UB requests by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return mixed Array of the patron's holds on success, PEAR_Error + * otherwise. + * @access public + */ + public function getMyILLRequests($patron) + { + return array_merge( + $this->getRemoteHolds($patron), + $this->getRemoteCallSlips($patron) + ); + + return $holds; + } + + /** + * Cancel ILL (UB) Requests + * + * Attempts to Cancel an UB request on a particular item. The + * data in $cancelDetails['details'] is determined by + * getCancelILLRequestDetails(). + * + * @param array $cancelDetails An array of item and patron data + * + * @return array An array of data on each request including + * whether or not it was successful and a system message (if available) + * @access public + */ + public function cancelILLRequests($cancelDetails) + { + $details = $cancelDetails['details']; + $patron = $cancelDetails['patron']; + $count = 0; + $response = array(); + + foreach ($details as $cancelDetails) { + list($dbKey, $itemId, $type, $cancelCode) = explode("|", $cancelDetails); + + // Create Rest API Cancel Key + $cancelID = ($dbKey ? $dbKey : $this->ws_dbKey) . "|" . $cancelCode; + + // Build Hierarchy + $hierarchy = array( + "patron" => $patron['id'], + "circulationActions" => 'requests' + ); + // An UB request is + if ($type == 'C') { + $hierarchy['callslips'] = $cancelID; + } else { + $hierarchy['holds'] = $cancelID; + } + + // Add Required Params + $params = array( + "patron_homedb" => $this->ws_patronHomeUbId, + "view" => "full" + ); + + // Get Data + $cancel = $this->makeRequest($hierarchy, $params, "DELETE"); + + if ($cancel) { + + // Process Cancel + $cancel = $cancel->children(); + $node = "reply-text"; + $reply = (string)$cancel->$node; + $count = ($reply == "ok") ? $count+1 : $count; + + $response[$itemId] = array( + 'success' => ($reply == "ok") ? true : false, + 'status' => ($reply == "ok") + ? "ill_request_cancel_success" : "ill_request_cancel_fail", + 'sysMessage' => ($reply == "ok") ? false : $reply, + ); + + } else { + $response[$itemId] = array( + 'success' => false, + 'status' => "ill_request_cancel_fail" + ); + } + } + $result = array('count' => $count, 'items' => $response); + return $result; + } + + /** + * Get Cancel ILL (UB) Request Details + * + * In Voyager an UB request is either a call slip (pending delivery) or a hold + * (pending checkout). In order to cancel an UB request, Voyager requires the + * patron details, an item ID, request type and a recall ID. This function + * returns the information as a string separated by pipes, which is then + * submitted as form data and extracted by the CancelILLRequests function. + * + * @param array $details An array of item data + * + * @return string Data for use in a form field + * @access public + */ + public function getCancelILLRequestDetails($details) + { + $details = (isset($details['institution_dbkey']) + ? $details['institution_dbkey'] + : '') + . '|' . $details['item_id'] + . '|' . $details['type'] + . '|' . $details['reqnum']; + return $details; + } } diff --git a/module/VuFind/src/VuFind/ILS/Logic/Holds.php b/module/VuFind/src/VuFind/ILS/Logic/Holds.php index 949e0e4fd67..ef9805753c8 100644 --- a/module/VuFind/src/VuFind/ILS/Logic/Holds.php +++ b/module/VuFind/src/VuFind/ILS/Logic/Holds.php @@ -178,6 +178,9 @@ class Holds } else { $holdings = $this->generateHoldings($result, $mode); } + + $holdings = $this->processStorageRetrievalRequests($holdings, $id); + $holdings = $this->processILLRequests($holdings, $id); } return $this->formatHoldings($holdings); } @@ -219,11 +222,6 @@ class Holds // Are holds allowed? $checkHolds = $this->catalog->checkFunction("Holds", $id); - // Are storage retrieval requests allowed? - $checkStorageRetrievalRequests = $this->catalog->checkFunction( - "StorageRetrievalRequests" - ); - foreach ($result as $copy) { $show = !in_array($copy['location'], $this->hideHoldings); if ($show) { @@ -234,38 +232,15 @@ class Holds // instead of the hold form: $copy['link'] = $copy['addLink'] === 'block' ? $this->getBlockedDetails($copy) - : $this->getHoldDetails( - $copy, $checkHolds['HMACKeys'] + : $this->getRequestDetails( + $copy, $checkHolds['HMACKeys'], 'Hold' ); // If we are unsure whether hold options are available, // set a flag so we can check later via AJAX: $copy['check'] = $copy['addLink'] == 'check'; } } - - if ($checkStorageRetrievalRequests) { - // Is this copy requestable - if (isset($copy['addStorageRetrievalRequestLink']) - && $copy['addStorageRetrievalRequestLink'] - ) { - // If the request is blocked, link to an error page - // instead of the hold form: - $copy['storageRetrievalRequestLink'] - = $copy['addStorageRetrievalRequestLink'] === 'block' - ? $this->getBlockedStorageRetrievalRequestDetails( - $copy - ) - : $this->getStorageRetrievalRequestDetails( - $copy, - $checkStorageRetrievalRequests['HMACKeys'] - ); - // If we are unsure whether request options are - // available, set a flag so we can check later via AJAX: - $copy['checkStorageRetrievalRequest'] - = $copy['addStorageRetrievalRequestLink'] - === 'check'; - } - } + $holdings[$copy['location']][] = $copy; } } @@ -305,11 +280,6 @@ class Holds // Are holds allowed? $checkHolds = $this->catalog->checkFunction("Holds"); - // Are storage retrieval requests allowed? - $checkStorageRetrievalRequests = $this->catalog->checkFunction( - "StorageRetrievalRequests" - ); - if ($checkHolds && is_array($holdings)) { // Generate Links // Loop through each holding @@ -354,89 +324,137 @@ class Holds } else { /* Build non-opac link */ $holdings[$location_key][$copy_key]['link'] - = $this->getHoldDetails( - $copy, $checkHolds['HMACKeys'] + = $this->getRequestDetails( + $copy, $checkHolds['HMACKeys'], 'Hold' ); } } } } } - - if ($checkStorageRequests && is_array($holdings)) { - // Generate Links - // Loop through each holding - foreach ($holdings as $location_key => $location) { - foreach ($location as $copy_key => $copy) { - if (isset($copy['addStorageRetrievalRequestLink']) - && $copy['addStorageRetrievalRequestLink'] - && $copy['addStorageRetrievalRequestLink'] !== 'block' - ) { - $copy['storageRetrievalRequestLink'] - = $this->getStorageRetrievalRequestDetails( - $copy, - $checkStorageRetrievalRequests['HMACKeys'] - ); - // If we are unsure whether storage retrieval - // request is available, set a flag so we can check - // later via AJAX: - $copy['checkStorageRetrievalRequest'] - = $copy['addStorageRetrievalRequestLink'] === - 'check'; - } - } - } - } } return $holdings; } /** - * Get Hold Form - * - * Supplies holdLogic with the form details required to place a hold + * Process storage retrieval request information in holdings and set the links + * accordingly. + * + * @param array $holdings Holdings * - * @param array $holdDetails An array of item data - * @param array $HMACKeys An array of keys to hash + * @return array Modified holdings + */ + protected function processStorageRetrievalRequests($holdings) + { + if (!is_array($holdings)) { + return $holdings; + } + + // Are storage retrieval requests allowed? + $requestConfig = $this->catalog->checkFunction( + 'StorageRetrievalRequests' + ); + + if (!$requestConfig) { + return $holdings; + } + + // Generate Links + // Loop through each holding + foreach ($holdings as &$location) { + foreach ($location as &$copy) { + // Is this copy requestable + if (isset($copy['addStorageRetrievalRequestLink']) + && $copy['addStorageRetrievalRequestLink'] + ) { + // If the request is blocked, link to an error page + // instead of the form: + if ($copy['addStorageRetrievalRequestLink'] === 'block') { + $copy['storageRetrievalRequestLink'] + = $this->getBlockedStorageRetrievalRequestDetails($copy); + } else { + $copy['storageRetrievalRequestLink'] + = $this->getRequestDetails( + $copy, + $requestConfig['HMACKeys'], + 'StorageRetrievalRequest' + ); + } + // If we are unsure whether request options are + // available, set a flag so we can check later via AJAX: + $copy['checkStorageRetrievalRequest'] + = $copy['addStorageRetrievalRequestLink'] === 'check'; + } + } + } + return $holdings; + } + + /** + * Process ILL request information in holdings and set the links accordingly. + * + * @param array $holdings Holdings * - * @return array Details for generating URL + * @return array Modified holdings */ - protected function getHoldDetails($holdDetails, $HMACKeys) + protected function processILLRequests($holdings) { - // Generate HMAC - $HMACkey = $this->hmac->generate($HMACKeys, $holdDetails); - - // Add Params - foreach ($holdDetails as $key => $param) { - $needle = in_array($key, $HMACKeys); - if ($needle) { - $queryString[] = $key. "=" .urlencode($param); - } + if (!is_array($holdings)) { + return $holdings; } - - // Add HMAC - $queryString[] = "hashKey=" . urlencode($HMACkey); - $queryString = implode('&', $queryString); - - // Build Params - return array( - 'action' => 'Hold', 'record' => $holdDetails['id'], - 'query' => $queryString, 'anchor' => "#tabnav" + + // Are storage retrieval requests allowed? + $requestConfig = $this->catalog->checkFunction( + 'ILLRequests' ); + + if (!$requestConfig) { + return $holdings; + } + + // Generate Links + // Loop through each holding + foreach ($holdings as &$location) { + foreach ($location as &$copy) { + // Is this copy requestable + if (isset($copy['addILLRequestLink']) + && $copy['addILLRequestLink'] + ) { + // If the request is blocked, link to an error page + // instead of the form: + if ($copy['addILLRequestLink'] === 'block') { + $copy['ILLRequestLink'] + = $this->getBlockedILLRequestDetails($copy); + } else { + $copy['ILLRequestLink'] + = $this->getRequestDetails( + $copy, + $requestConfig['HMACKeys'], + 'ILLRequest' + ); + } + // If we are unsure whether request options are + // available, set a flag so we can check later via AJAX: + $copy['checkILLRequest'] + = $copy['addILLRequestLink'] === 'check'; + } + } + } + return $holdings; } - + /** - * Get Storage Retrieval Request Form + * Get Hold Form * - * Supplies holdLogic with the form details required to place a storage - * retrieval request + * Supplies holdLogic with the form details required to place a request * - * @param array $details An array of item data - * @param array $HMACKeys An array of keys to hash + * @param array $details An array of item data + * @param array $HMACKeys An array of keys to hash + * @param string $action The action for which the details are built * - * @return array Details for generating URL + * @return array Details for generating URL */ - protected function getStorageRetrievalRequestDetails($details, $HMACKeys) + protected function getRequestDetails($details, $HMACKeys, $action) { // Generate HMAC $HMACkey = $this->hmac->generate($HMACKeys, $details); @@ -445,7 +463,7 @@ class Holds foreach ($details as $key => $param) { $needle = in_array($key, $HMACKeys); if ($needle) { - $queryString[] = $key . "=" . urlencode($param); + $queryString[] = $key. "=" .urlencode($param); } } @@ -455,13 +473,11 @@ class Holds // Build Params return array( - 'action' => 'StorageRetrievalRequest', - 'record' => $details['id'], - 'query' => $queryString, - 'anchor' => "#tabnav" + 'action' => $action, 'record' => $details['id'], + 'query' => $queryString, 'anchor' => "#tabnav" ); } - + /** * Returns a URL to display a "blocked hold" message. * @@ -493,6 +509,22 @@ class Holds ); } + /** + * Returns a URL to display a "blocked ILL request" message. + * + * @param array $details An array of item data + * + * @return array Details for generating URL + */ + protected function getBlockedILLRequestDetails($details) + { + // Build Params + return array( + 'action' => 'BlockedILLRequest', + 'record' => $details['id'] + ); + } + /** * Get an array of suppressed location names. * diff --git a/themes/blueprint/css/styles.css b/themes/blueprint/css/styles.css index d4ba5565e89..c1d15a3ba3d 100644 --- a/themes/blueprint/css/styles.css +++ b/themes/blueprint/css/styles.css @@ -91,7 +91,7 @@ div.dialogLoading { height:100%; } -.ajax_availability, .ajax_hold_availability, .ajax_storage_retrieval_request_availability { +.ajax_availability, .ajax_hold_availability, .ajax_storage_retrieval_request_availability, .ajax_ill_request_availability, .ajax_ill_request_loading { background: url(../images/ajax_loading.gif) no-repeat left top; padding:0 .5em .5em 20px; } @@ -905,42 +905,21 @@ h3.list { padding:.5em .5em .5em 20px; margin-right:1em; } -.holdPlace { +.holdPlace, .storageRetrievalRequestPlace, .ILLRequestPlace { background-image:url(../images/fugue/holdPlace.png); background-repeat:no-repeat; background-position: left; padding:.5em .5em .5em 20px; margin-right:1em; } -.holdCancel { +.holdCancel, .storageRetrievalRequestCancel, .ILLRequestCancel { background-image:url(../images/fugue/holdCancel.png); background-repeat:no-repeat; background-position: left; padding:.5em .5em .5em 20px; margin-right:1em; } -.holdCancelAll { - background-image:url(../images/fugue/holdCancelAll.png); - background-repeat:no-repeat; - background-position: left; - padding:.5em .5em .5em 20px; - margin-right:1em; -} -.storageRetrievalRequestPlace { - background-image:url(../images/fugue/holdPlace.png); - background-repeat:no-repeat; - background-position: left; - padding:.5em .5em .5em 20px; - margin-right:1em; -} -.storageRetrievalRequestCancel { - background-image:url(../images/fugue/holdCancel.png); - background-repeat:no-repeat; - background-position: left; - padding:.5em .5em .5em 20px; - margin-right:1em; -} -.storageRetrievalRequestCancelAll { +.holdCancelAll, .storageRetrievalRequestCancelAll, .ILLRequestCancelAll { background-image:url(../images/fugue/holdCancelAll.png); background-repeat:no-repeat; background-position: left; @@ -961,7 +940,7 @@ h3.list { padding:.5em .5em .5em 20px; margin-right:1em; } -.checkRequest { +.checkRequest, .checkStorageRetrievalRequest, .checkILLRequest { background-image:url(../images/fugue/checkRequest.png); background-repeat:no-repeat; padding-left:18px; diff --git a/themes/blueprint/js/ill.js b/themes/blueprint/js/ill.js new file mode 100644 index 00000000000..fe02ebc41b4 --- /dev/null +++ b/themes/blueprint/js/ill.js @@ -0,0 +1,29 @@ +function setUpILLRequestForm(recordId) { + $("#ILLRequestForm #pickupLibrary").change(function() { + $("#ILLRequestForm #pickupLibraryLocation option").remove(); + $("#ILLRequestForm #pickupLibraryLocationLabel").addClass("ajax_ill_request_loading"); + var url = path + '/AJAX/JSON?' + $.param({method:'getLibraryPickupLocations', id: recordId, pickupLib: $("#ILLRequestForm #pickupLibrary").val() }); + $.ajax({ + dataType: 'json', + cache: false, + url: url, + success: function(response) { + if (response.status == 'OK') { + $.each(response.data.locations, function() { + var option = $("<option></option>").attr("value", this.id).text(this.name); + if (this.isDefault) { + option.attr("selected", "selected"); + } + $("#ILLRequestForm #pickupLibraryLocation").append(option); + }); + } + $("#ILLRequestForm #pickupLibraryLocationLabel").removeClass("ajax_ill_request_loading"); + }, + fail: function() { + $("#ILLRequestForm #pickupLibraryLocationLabel").removeClass("ajax_ill_request_loading"); + } + }); + + }); + $("#ILLRequestForm #pickupLibrary").change(); +} diff --git a/themes/blueprint/js/record.js b/themes/blueprint/js/record.js index e9be8396065..f4a971ecfe6 100644 --- a/themes/blueprint/js/record.js +++ b/themes/blueprint/js/record.js @@ -45,9 +45,6 @@ function setUpCheckRequest() { 'checkRequest ajax_hold_availability', 'holdBlocked'); } }); -} - -function setUpCheckStorageRetrievalRequest() { $('.checkStorageRetrievalRequest').each(function(i) { if($(this).hasClass('checkStorageRetrievalRequest')) { $(this).addClass('ajax_storage_retrieval_request_availability'); @@ -56,6 +53,14 @@ function setUpCheckStorageRetrievalRequest() { 'storageRetrievalRequestBlocked'); } }); + $('.checkILLRequest').each(function(i) { + if($(this).hasClass('checkILLRequest')) { + $(this).addClass('ajax_ill_request_availability'); + var isValid = checkRequestIsValid(this, this.href, 'ILLRequest', + 'checkILLRequest ajax_ill_request_availability', + 'ILLRequestBlocked'); + } + }); } function deleteRecordComment(element, recordId, recordSource, commentId) { @@ -179,5 +184,4 @@ $(document).ready(function(){ }); setUpCheckRequest(); - setUpCheckStorageRetrievalRequest(); }); \ No newline at end of file diff --git a/themes/blueprint/templates/RecordTab/holdingsils.phtml b/themes/blueprint/templates/RecordTab/holdingsils.phtml index b15b3169731..8d9292efe7c 100644 --- a/themes/blueprint/templates/RecordTab/holdingsils.phtml +++ b/themes/blueprint/templates/RecordTab/holdingsils.phtml @@ -81,6 +81,7 @@ <? foreach ($holding['items'] as $row): ?> <? $check = (isset($row['check']) && $row['check']); ?> <? $checkStorageRetrievalRequest = (isset($row['checkStorageRetrievalRequest']) && $row['checkStorageRetrievalRequest']); ?> + <? $checkILLRequest = (isset($row['checkILLRequest']) && $row['checkILLRequest']); ?> <? if (isset($row['barcode']) && $row['barcode'] != ""): ?> <tr vocab="http://schema.org/" typeof="Offer"> <th><?=$this->transEsc("Copy")?> <?=$this->escapeHtml($row['number'])?></th> @@ -103,6 +104,9 @@ <? if (isset($row['storageRetrievalRequestLink']) && $row['storageRetrievalRequestLink']): ?> <a class="storageRetrievalRequestPlace<?=$checkStorageRetrievalRequest ? ' checkStorageRetrievalRequest' : ''?>" href="<?=$this->recordLink()->getRequestUrl($row['storageRetrievalRequestLink'])?>"><span><?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?></span></a> <? endif; ?> + <? if (isset($row['ILLRequestLink']) && $row['ILLRequestLink']): ?> + <a class="ILLRequestPlace<?=$checkILLRequest ? ' checkILLRequest' : ''?>" href="<?=$this->recordLink()->getRequestUrl($row['ILLRequestLink'])?>"><span><?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?></span></a> + <? endif; ?> </div> <? else: ?> <? /* Begin Unavailable Items (Recalls) */ ?> diff --git a/themes/blueprint/templates/myresearch/illrequests.phtml b/themes/blueprint/templates/myresearch/illrequests.phtml new file mode 100644 index 00000000000..77a4df18f3e --- /dev/null +++ b/themes/blueprint/templates/myresearch/illrequests.phtml @@ -0,0 +1,168 @@ +<? + // Set up page title: + $this->headTitle($this->translate('ILL Requests')); + + // Set up breadcrumbs: + $this->layout()->breadcrumbs = '<a href="' . $this->url('myresearch-home') . '">' + . $this->transEsc('Your Account') . '</a>' . '<span>></span><em>' + . $this->transEsc('ILL Requests') . '</em>'; +?> +<div class="<?=$this->layoutClass('mainbody')?>"> + <h3><?=$this->transEsc('ILL Requests') ?></h3> + + <?=$this->flashmessages()?> + + <? if (!empty($this->recordList)): ?> + <? if ($this->cancelForm): ?> + <form name="cancelForm" action="" method="post" id="cancelILLRequest"> + <input type="hidden" id="cancelConfirm" name="confirm" value="0"/> + <div class="toolbar"> + <ul> + <li><input type="submit" class="button ILLRequestCancel" name="cancelSelected" value="<?=$this->transEsc("ill_request_cancel_selected") ?>"/></li> + <li><input type="submit" class="button ILLRequestCancelAll" name="cancelAll" value="<?=$this->transEsc("ill_request_cancel_all") ?>"/></li> + </ul> + </div> + <div class="clearer"></div> + <? endif; ?> + + <ul class="recordSet"> + <? $iteration = 0; ?> + <? foreach ($this->recordList as $resource): ?> + <? $iteration++; ?> + <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?> + <li class="result<? if (($iteration % 2) == 0): ?> alt<? endif; ?>"> + <? if ($this->cancelForm && isset($ilsDetails['cancel_details'])): ?> + <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $resource->getUniqueId()); ?> + <label for="checkbox_<?=$safeId?>" class="offscreen"><?=$this->transEsc("Select this record")?></label> + <input type="hidden" name="cancelAllIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" /> + <input type="checkbox" name="cancelSelectedIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" class="checkbox" style="margin-left:0;" id="checkbox_<?=$safeId?>" /> + <? endif; ?> + <div id="record<?=$this->escapeHtml($resource->getUniqueId()) ?>"> + <div class="span-2"> + <? if ($summThumb = $this->record($resource)->getThumbnail()): ?> + <img src="<?=$this->escapeHtml($summThumb)?>" class="summcover" alt="<?=$this->transEsc('Cover Image')?>"/> + <? else: ?> + <img src="<?=$this->url('cover-unavailable')?>" class="summcover" alt="<?=$this->transEsc('No Cover Image')?>"/> + <? endif; ?> + </div> + <div class="span-10"> + <? + // If this is a non-missing Solr record, we should display a link: + if (is_a($resource, 'VuFind\\RecordDriver\\SolrDefault') && !is_a($resource, 'VuFind\\RecordDriver\\Missing')) { + $title = $resource->getTitle(); + $title = empty($title) ? $this->transEsc('Title not available') : $this->escapeHtml($title); + echo '<a href="' . $this->recordLink()->getUrl($resource) . + '" class="title">' . $title . '</a>'; + } else if (isset($ilsDetails['title']) && !empty($ilsDetails['title'])){ + // If the record is not available in Solr, perhaps the ILS driver sent us a title we can show... + echo $this->escapeHtml($ilsDetails['title']); + } else { + // Last resort -- indicate that no title could be found. + echo $this->transEsc('Title not available'); + } + ?><br/> + <? $listAuthor = $resource->getPrimaryAuthor(); if (!empty($listAuthor)): ?> + <?=$this->transEsc('by')?>: + <a href="<?=$this->record($resource)->getLink('author', $listAuthor)?>"><?=$this->escapeHtml($listAuthor)?></a><br/> + <? endif; ?> + <? /* TODO: tags + {if $resource.tags} + <?=$this->transEsc('Your Tags')?>: + {foreach from=$resource.tags item=tag name=tagLoop} + <a href="{$url}/Search/Results?tag={$tag->tag|escape:"url"}">{$tag->tag|escape}</a>{if !$smarty.foreach.tagLoop.last},{/if} + {/foreach} + <br/> + {/if} + */ ?> + <? /* TODO: notes + {if $resource.notes} + <?=$this->transEsc('Notes')?>: {$resource.notes|escape}<br/> + {/if} + */ ?> + + <? $formats = $resource->getFormats(); if (count($formats) > 0): ?> + <?=$this->record($resource)->getFormatList()?> + <br/> + <? endif; ?> + <? if (isset($ilsDetails['volume']) && !empty($ilsDetails['volume'])): ?> + <strong><?=$this->transEsc('Volume')?>:</strong> <?=$this->escapeHtml($ilsDetails['volume'])?> + <br /> + <? endif; ?> + + <? if (isset($ilsDetails['publication_year']) && !empty($ilsDetails['publication_year'])): ?> + <strong><?=$this->transEsc('Year of Publication')?>:</strong> <?=$this->escapeHtml($ilsDetails['publication_year'])?> + <br /> + <? endif; ?> + + <? if (isset($ilsDetails['institution_name']) && !empty($ilsDetails['institution_name'])): ?> + <strong><?=$this->transEsc('institution_' . $ilsDetails['institution_name'], array(), $ilsDetails['institution_name']) ?></strong> + <br /> + <? endif; ?> + + <? /* Depending on the ILS driver, the "location" value may be a string or an ID; figure out the best + value to display... */ ?> + <? $pickupDisplay = ''; ?> + <? $pickupTranslate = false; ?> + <? if (isset($ilsDetails['location'])): ?> + <? if ($this->pickup): ?> + <? foreach ($this->pickup as $library): ?> + <? if ($library['locationID'] == $ilsDetails['location']): ?> + <? $pickupDisplay = $library['locationDisplay']; ?> + <? $pickupTranslate = true; ?> + <? endif; ?> + <? endforeach; ?> + <? endif; ?> + <? if (empty($pickupDisplay)): ?> + <? $pickupDisplay = $ilsDetails['location']; ?> + <? endif; ?> + <? endif; ?> + <? if (!empty($pickupDisplay)): ?> + <strong><?=$this->transEsc('pick_up_location') ?>:</strong> + <?=$pickupTranslate ? $this->transEsc($pickupDisplay) : $this->escapeHtml($pickupDisplay)?> + <br /> + <? endif; ?> + + <strong><?=$this->transEsc('Created') ?>:</strong> <?=$this->escapeHtml($ilsDetails['create']) ?> + <? if (!empty($ilsDetails['expire'])): ?> + | <strong><?=$this->transEsc('Expires') ?>:</strong> <?=$this->escapeHtml($ilsDetails['expire']) ?> + <? endif; ?> + <br /> + + <? if (isset($this->cancelResults['items'])): ?> + <? foreach ($this->cancelResults['items'] as $itemId=>$cancelResult): ?> + <? if ($itemId == $ilsDetails['item_id'] && $cancelResult['success'] == false): ?> + <div class="error"><?=$this->transEsc($cancelResult['status']) ?><? if ($cancelResult['sysMessage']) echo ' : ' . $this->transEsc($cancelResult['sysMessage']); ?></div> + <? endif; ?> + <? endforeach; ?> + <? endif; ?> + + <? if (isset($ilsDetails['processed']) && $ilsDetails['processed']): ?> + <div class="info"><?=$this->transEsc("ill_request_processed") . (is_string($ilsDetails['processed']) ? ': ' . $ilsDetails['processed'] : '') ?></div> + <? endif; ?> + <? if (isset($ilsDetails['available']) && $ilsDetails['available']): ?> + <div class="info"><?=$this->transEsc("ill_request_available") ?></div> + <? endif; ?> + <? if (isset($ilsDetails['canceled']) && $ilsDetails['canceled']): ?> + <div class="info"><?=$this->transEsc("ill_request_canceled") . (is_string($ilsDetails['canceled']) ? ': ' . $ilsDetails['canceled'] : '') ?></div> + <? endif; ?> + <? if (isset($ilsDetails['cancel_link'])): ?> + <p><a href="<?=$this->escapeHtml($ilsDetails['cancel_link']) ?>"><?=$this->transEsc("ill_request_cancel") ?></a></p> + <? endif; ?> + + </div> + <div class="clear"></div> + </div> + </li> + <? endforeach; ?> + </ul> + <? if ($this->cancelForm): ?></form><? endif; ?> + <? else: ?> + <?=$this->transEsc('You do not have any interlibrary loan requests placed') ?>. + <? endif; ?> +</div> + +<div class="<?=$this->layoutClass('sidebar')?>"> + <?=$this->context($this)->renderInContext("myresearch/menu.phtml", array('active' => 'ILLRequests'))?> +</div> + +<div class="clear"></div> diff --git a/themes/blueprint/templates/myresearch/menu.phtml b/themes/blueprint/templates/myresearch/menu.phtml index 29f4b52f945..d9e49280740 100644 --- a/themes/blueprint/templates/myresearch/menu.phtml +++ b/themes/blueprint/templates/myresearch/menu.phtml @@ -8,6 +8,9 @@ <? if ($this->ils()->checkFunction('StorageRetrievalRequests')): ?> <li<?=$this->active == 'storageRetrievalRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-storageretrievalrequests')?>"><?=$this->transEsc('Storage Retrieval Requests')?></a></li> <? endif; ?> + <? if ($this->ils()->checkFunction('ILLRequests')): ?> + <li<?=$this->active == 'ILLRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-illrequests')?>"><?=$this->transEsc('Interlibrary Loan Requests')?></a></li> + <? endif; ?> <li<?=$this->active == 'fines' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-fines')?>"><?=$this->transEsc('Fines')?></a></li> <li<?=$this->active == 'profile' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-profile')?>"><?=$this->transEsc('Profile')?></a></li> <? endif; ?> diff --git a/themes/blueprint/templates/record/illrequest.phtml b/themes/blueprint/templates/record/illrequest.phtml new file mode 100644 index 00000000000..4cd30fb0409 --- /dev/null +++ b/themes/blueprint/templates/record/illrequest.phtml @@ -0,0 +1,111 @@ +<? + // Set up ill script: + $this->headScript()->appendFile("ill.js"); + + // Set page title. + $this->headTitle($this->translate('ill_request_place_text') . ': ' . $this->driver->getBreadcrumb()); + + // Set up breadcrumbs: + $this->layout()->breadcrumbs = $this->getLastSearchLink($this->transEsc('Search'), '', '<span>></span>') . + $this->recordLink()->getBreadcrumb($this->driver) . '<span>></span><em>' . $this->transEsc('ill_request_place_text') . '</em>'; +?> +<h2><?=$this->transEsc('ill_request_place_text')?></h2> +<? if ($this->helpText): ?> +<p class="helptext"><?=$this->helpText?></p> +<? endif; ?> + +<?=$this->flashmessages()?> +<div id="ILLRequestForm" class="ILLRequestForm"> + + <form action="" method="post"> + + <? if (in_array("itemId", $this->extraFields)): ?> + <div> + <strong><?=$this->transEsc('ill_request_item')?>:</strong><br/> + <select name="gatheredDetails[itemId]"> + <? foreach ($this->items as $item): ?> + <option value="<?=$this->escapeHtml($item['id'])?>"<?=($this->gatheredDetails['itemId'] == $item['id']) ? ' selected="selected"' : ''?>><?=$this->escapeHtml($item['name'])?></option> + <? endforeach; ?> + </select> + </div> + <? endif; ?> + + <? if (in_array("pickUpLibrary", $this->extraFields)): ?> + <div> + <? if (count($this->pickupLibraries) > 1): ?> + <? + if (isset($this->gatheredDetails['pickUpLibrary']) && $this->gatheredDetails['pickUpLibrary'] !== "") { + $selected = $this->gatheredDetails['pickUpLibrary']; + } else { + $selected = false; + } + ?> + <strong><?=$this->transEsc("ill_request_pick_up_library")?>:</strong><br/> + <select id="pickupLibrary" name="gatheredDetails[pickUpLibrary]"> + <? foreach ($this->pickupLibraries as $lib): ?> + <option value="<?=$this->escapeHtml($lib['id'])?>"<?=(($selected === false && isset($lib['isDefault']) && $lib['isDefault']) || $selected === $lib['id']) ? ' selected="selected"' : ''?>> + <?=$this->transEsc('library_' . $lib['name'], null, $lib['name'])?> + </option> + <? endforeach; ?> + </select> + <? endif; ?> + </div> + <? endif; ?> + + <? if (in_array("pickUpLibraryLocation", $this->extraFields)): ?> + <div> + <span id="pickupLibraryLocationLabel"><strong><?=$this->transEsc("ill_request_pick_up_location")?>:</strong></span><br/> + <select id="pickupLibraryLocation" name="gatheredDetails[pickUpLibraryLocation]"> + </select> + </div> + <? endif; ?> + + <? if (in_array("pickUpLocation", $this->extraFields)): ?> + <div> + <? if (count($this->pickupLocations) > 1): ?> + <? + if (isset($this->gatheredDetails['pickUpLocation']) && $this->gatheredDetails['pickUpLocation'] !== "") { + $selected = $this->gatheredDetails['pickUpLocation']; + } elseif (isset($this->homeLibrary) && $this->homeLibrary !== "") { + $selected = $this->homeLibrary; + } else { + $selected = false; + } + ?> + <strong><?=$this->transEsc("pick_up_location")?>:</strong><br/> + <select id="pickupLocation" name="gatheredDetails[pickUpLocation]"> + <? foreach ($this->pickupLocations as $loc): ?> + <option value="<?=$this->escapeHtml($loc['id'])?>"<?=(($selected === false && isset($loc['isDefault']) && $loc['isDefault']) || $selected === $loc['id']) ? ' selected="selected"' : ''?>> + <?=$this->escapeHtml($loc['name'])?> + </option> + <? endforeach; ?> + </select> + <? endif; ?> + </div> + <? endif; ?> + + <? if (in_array("requiredByDate", $this->extraFields)): ?> + <div> + <strong><?=$this->transEsc("hold_required_by")?>: </strong> + <div id="requiredByHolder"><input id="requiredByDate" type="text" name="gatheredDetails[requiredBy]" value="<?=(isset($this->gatheredDetails['requiredBy']) && !empty($this->gatheredDetails['requiredBy'])) ? $this->escapeHtml($this->gatheredDetails['requiredBy']) : $this->escapeHtml($this->defaultRequiredDate)?>" size="8" /> <strong>(<?=$this->dateTime()->getDisplayDateFormat()?>)</strong></div> + </div> + <? endif; ?> + + <? if (in_array("comments", $this->extraFields)): ?> + <div> + <strong><?=$this->transEsc("Comments")?>:</strong><br/> + <textarea rows="3" cols="20" name="gatheredDetails[comment]"><?=isset($this->gatheredDetails['comment']) ? $this->escapeHtml($this->gatheredDetails['comment']) : ''?></textarea> + </div> + <? endif; ?> + + <input type="submit" name="placeILLRequest" value="<?=$this->transEsc('ill_request_submit_text')?>"/> + + </form> + +</div> + +<script type="text/javascript"> +$(document).ready(function(){ + setUpILLRequestForm('<?=$this->escapeHtml($this->driver->getUniqueId()) ?>'); +}); +</script> diff --git a/themes/bootstrap/js/ill.js b/themes/bootstrap/js/ill.js new file mode 100644 index 00000000000..923707ddc3d --- /dev/null +++ b/themes/bootstrap/js/ill.js @@ -0,0 +1,29 @@ +function setUpILLRequestForm(recordId) { + $("#ILLRequestForm #pickupLibrary").change(function() { + $("#ILLRequestForm #pickupLibraryLocation option").remove(); + $("#ILLRequestForm #pickupLibraryLocationLabel i").addClass("icon-spinner icon-spin"); + var url = path + '/AJAX/JSON?' + $.param({method:'getLibraryPickupLocations', id: recordId, pickupLib: $("#ILLRequestForm #pickupLibrary").val() }); + $.ajax({ + dataType: 'json', + cache: false, + url: url, + success: function(response) { + if (response.status == 'OK') { + $.each(response.data.locations, function() { + var option = $("<option></option>").attr("value", this.id).text(this.name); + if (this.isDefault) { + option.attr("selected", "selected"); + } + $("#ILLRequestForm #pickupLibraryLocation").append(option); + }); + } + $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("icon-spinner icon-spin"); + }, + fail: function() { + $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("icon-spinner icon-spin"); + } + }); + + }); + $("#ILLRequestForm #pickupLibrary").change(); +} diff --git a/themes/bootstrap/js/record.js b/themes/bootstrap/js/record.js index 57336dc2af5..8feb077e2b9 100644 --- a/themes/bootstrap/js/record.js +++ b/themes/bootstrap/js/record.js @@ -39,19 +39,25 @@ function checkRequestIsValid(element, requestURL, requestType, blockedClass) { function setUpCheckRequest() { $('.checkRequest').each(function(i) { - if($(this).hasClass('checkRequest')) { + if ($(this).hasClass('checkRequest')) { var isValid = checkRequestIsValid(this, this.href, 'Hold', 'holdBlocked'); } }); -} - -function setUpCheckStorageRetrievalRequest() { $('.checkStorageRetrievalRequest').each(function(i) { - if($(this).hasClass('checkStorageRetrievalRequest')) { + if ($(this).hasClass('checkStorageRetrievalRequest')) { var isValid = checkRequestIsValid(this, this.href, 'StorageRetrievalRequest', 'StorageRetrievalRequestBlocked'); } }); + $('.checkILLRequest').each(function(i) { + if ($(this).hasClass('checkILLRequest')) { + var isValid = checkRequestIsValid(this, this.href, 'ILLRequest', + 'ILLRequestBlocked'); + } + }); +} + +function setUpCheckStorageRetrievalRequest() { } function deleteRecordComment(element, recordId, recordSource, commentId) { diff --git a/themes/bootstrap/templates/RecordTab/holdingsils.phtml b/themes/bootstrap/templates/RecordTab/holdingsils.phtml index 83fddf6957d..62f69dc4252 100644 --- a/themes/bootstrap/templates/RecordTab/holdingsils.phtml +++ b/themes/bootstrap/templates/RecordTab/holdingsils.phtml @@ -81,6 +81,7 @@ <? foreach ($holding['items'] as $row): ?> <? $check = (isset($row['check']) && $row['check']); ?> <? $checkStorageRetrievalRequest = (isset($row['checkStorageRetrievalRequest']) && $row['checkStorageRetrievalRequest']); ?> + <? $checkILLRequest = (isset($row['checkILLRequest']) && $row['checkILLRequest']); ?> <? if (isset($row['barcode']) && $row['barcode'] != ""): ?> <tr vocab="http://schema.org/" typeof="Offer"> <th><?=$this->transEsc("Copy")?> <?=$this->escapeHtml($row['number'])?></th> @@ -101,6 +102,9 @@ <? if (isset($row['storageRetrievalRequestLink']) && $row['storageRetrievalRequestLink']): ?> <a class="<?=$checkStorageRetrievalRequest ? 'checkStorageRetrievalRequest ' : ''?>inlineblock modal-link placeStorageRetrievalRequest" href="<?=$this->recordLink()->getRequestUrl($row['storageRetrievalRequestLink'])?>" title="<?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?>"><i class="icon-flag"></i> <?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?></a> <? endif; ?> + <? if (isset($row['ILLRequestLink']) && $row['ILLRequestLink']): ?> + <a class="<?=$checkILLRequest ? 'checkILLRequest ' : ''?>inlineblock modal-link placeILLRequest" href="<?=$this->recordLink()->getRequestUrl($row['ILLRequestLink'])?>" title="<?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?>"><i class="icon-flag"></i> <?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?></a> + <? endif; ?> <? else: ?> <? /* Begin Unavailable Items (Recalls) */ ?> <span class="text-error"><?=$this->transEsc($row['status'])?><link property="availability" href="http://schema.org/OutOfStock" /></span> diff --git a/themes/bootstrap/templates/myresearch/illrequests.phtml b/themes/bootstrap/templates/myresearch/illrequests.phtml new file mode 100644 index 00000000000..4ee652f20d8 --- /dev/null +++ b/themes/bootstrap/templates/myresearch/illrequests.phtml @@ -0,0 +1,168 @@ +<? + // Set up page title: + $this->headTitle($this->translate('Interlibrary Loan Requests')); + + // Set up breadcrumbs: + $this->layout()->breadcrumbs = '<li><a href="' . $this->url('myresearch-home') . '">' . $this->transEsc('Your Account') . '</a> <span class="divider">></span></li>' + . '<li class="active">' . $this->transEsc('Interlibrary Loan Requests') . '</li>'; +?> + +<div class="<?=$this->layoutClass('mainbody')?>"> + <h2><?=$this->transEsc('Interlibrary Loan Requests') ?></h2> + + <?=$this->flashmessages()?> + + <? if (!empty($this->recordList)): ?> + <? if ($this->cancelForm): ?> + <form name="cancelForm" class="inline" action="" method="post" id="cancelILLRequest"> + <input type="hidden" id="submitType" name="cancelSelected" value="1"/> + <input type="hidden" id="cancelConfirm" name="confirm" value="0"/> + <div class="btn-group"> + <input id="cancelSelected" name="cancelSelected" type="submit" value="<?=$this->transEsc("ill_request_cancel_selected") ?>" class="btn dropdown-toggle" data-toggle="dropdown"/> + <ul class="dropdown-menu"> + <li class="disabled"><a><?=$this->transEsc("confirm_ill_request_cancel_selected_text") ?></a></li> + <li><a href="#" onClick="$('#cancelConfirm').val(1);$('#submitType').attr('name','cancelSelected');$(this).parents('form').submit(); return false;"><?=$this->transEsc('confirm_dialog_yes') ?></a></li> + <li><a href="#" onClick="return false;"><?=$this->transEsc('confirm_dialog_no')?></a></li> + </ul> + </div> + <div class="btn-group"> + <input id="cancelAll" name="cancelAll" type="submit" value="<?=$this->transEsc("ill_request_cancel_all") ?>" class="btn dropdown-toggle" data-toggle="dropdown"/> + <ul class="dropdown-menu"> + <li class="disabled"><a><?=$this->transEsc("confirm_ill_request_cancel_all_text") ?></a></li> + <li><a href="#" onClick="$('#cancelConfirm').val(1);$('#submitType').attr('name','cancelAll');$(this).parents('form').submit(); return false;"><?=$this->transEsc('confirm_dialog_yes') ?></a></li> + <li><a href="#" onClick="return false;"><?=$this->transEsc('confirm_dialog_no')?></a></li> + </ul> + </div> + <? endif; ?> + + <? $iteration = 0; ?> + <? foreach ($this->recordList as $resource): ?> + <hr/> + <? $iteration++; ?> + <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?> + <div id="record<?=$this->escapeHtml($resource->getUniqueId()) ?>" class="row-fluid"> + <? if ($this->cancelForm && isset($ilsDetails['cancel_details'])): ?> + <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $resource->getUniqueId()); ?> + <input type="hidden" name="cancelAllIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" /> + <div class="pull-left"> + <input type="checkbox" name="cancelSelectedIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" id="checkbox_<?=$safeId?>" /> + </div> + <? endif; ?> + <div class="span2 text-center"> + <? if ($summThumb = $this->record($resource)->getThumbnail()): ?> + <img src="<?=$this->escapeHtml($summThumb)?>" class="summcover" alt="<?=$this->transEsc('Cover Image')?>"/> + <? else: ?> + <img src="<?=$this->url('cover-unavailable')?>" class="summcover" alt="<?=$this->transEsc('No Cover Image')?>"/> + <? endif; ?> + </div> + <div class="span9"> + <? + // If this is a non-missing Solr record, we should display a link: + if (is_a($resource, 'VuFind\\RecordDriver\\SolrDefault') && !is_a($resource, 'VuFind\\RecordDriver\\Missing')) { + $title = $resource->getTitle(); + $title = empty($title) ? $this->transEsc('Title not available') : $this->escapeHtml($title); + echo '<a href="' . $this->recordLink()->getUrl($resource) + . '" class="title">' . $title . '</a>'; + } else if (isset($ilsDetails['title']) && !empty($ilsDetails['title'])){ + // If the record is not available in Solr, perhaps the ILS driver sent us a title we can show... + echo $this->escapeHtml($ilsDetails['title']); + } else { + // Last resort -- indicate that no title could be found. + echo $this->transEsc('Title not available'); + } + ?><br/> + <? $listAuthor = $resource->getPrimaryAuthor(); if (!empty($listAuthor)): ?> + <?=$this->transEsc('by')?>: + <a href="<?=$this->record($resource)->getLink('author', $listAuthor)?>"><?=$this->escapeHtml($listAuthor)?></a><br/> + <? endif; ?> + <? /* TODO: tags + {if $resource.tags} + <?=$this->transEsc('Your Tags')?>: + {foreach from=$resource.tags item=tag name=tagLoop} + <a href="{$url}/Search/Results?tag={$tag->tag|escape:"url"}">{$tag->tag|escape}</a>{if !$smarty.foreach.tagLoop.last},{/if} + {/foreach} + <br/> + {/if} + */ ?> + <? /* TODO: notes + {if $resource.notes} + <?=$this->transEsc('Notes')?>: {$resource.notes|escape}<br/> + {/if} + */ ?> + + <? $formats = $resource->getFormats(); if (count($formats) > 0): ?> + <?=str_replace('class="', 'class="label label-info ', $this->record($resource)->getFormatList())?> + <br/> + <? endif; ?> + <? if (isset($ilsDetails['volume']) && !empty($ilsDetails['volume'])): ?> + <strong><?=$this->transEsc('Volume')?>:</strong> <?=$this->escapeHtml($ilsDetails['volume'])?> + <br /> + <? endif; ?> + + <? if (isset($ilsDetails['publication_year']) && !empty($ilsDetails['publication_year'])): ?> + <strong><?=$this->transEsc('Year of Publication')?>:</strong> <?=$this->escapeHtml($ilsDetails['publication_year'])?> + <br /> + <? endif; ?> + + <? /* Depending on the ILS driver, the "location" value may be a string or an ID; figure out the best + value to display... */ ?> + <? $pickupDisplay = ''; ?> + <? $pickupTranslate = false; ?> + <? if (isset($ilsDetails['location'])): ?> + <? if ($this->pickup): ?> + <? foreach ($this->pickup as $library): ?> + <? if ($library['locationID'] == $ilsDetails['location']): ?> + <? $pickupDisplay = $library['locationDisplay']; ?> + <? $pickupTranslate = true; ?> + <? endif; ?> + <? endforeach; ?> + <? endif; ?> + <? if (empty($pickupDisplay)): ?> + <? $pickupDisplay = $ilsDetails['location']; ?> + <? endif; ?> + <? endif; ?> + <? if (!empty($pickupDisplay)): ?> + <strong><?=$this->transEsc('pick_up_location') ?>:</strong> + <?=$pickupTranslate ? $this->transEsc($pickupDisplay) : $this->escapeHtml($pickupDisplay)?> + <br /> + <? endif; ?> + + <strong><?=$this->transEsc('Created') ?>:</strong> <?=$this->escapeHtml($ilsDetails['create']) ?> + <? if (!empty($ilsDetails['expire'])): ?> + | <strong><?=$this->transEsc('Expires') ?>:</strong> <?=$this->escapeHtml($ilsDetails['expire']) ?> + <? endif; ?> + <br /> + + <? if (isset($this->cancelResults['items'])): ?> + <? foreach ($this->cancelResults['items'] as $itemId=>$cancelResult): ?> + <? if ($itemId == $ilsDetails['item_id'] && $cancelResult['success'] == false): ?> + <div class="alert alert-error"><?=$this->transEsc($cancelResult['status']) ?><? if ($cancelResult['sysMessage']) echo ' : ' . $this->transEsc($cancelResult['sysMessage']); ?></div> + <? endif; ?> + <? endforeach; ?> + <? endif; ?> + + <? if (isset($ilsDetails['processed']) && $ilsDetails['processed']): ?> + <div class="text-success"><?=$this->transEsc("ill_request_processed") . (is_string($ilsDetails['processed']) ? ': ' . $ilsDetails['processed'] : '') ?></div> + <? endif; ?> + <? if (isset($ilsDetails['available']) && $ilsDetails['available']): ?> + <div class="text-success"><?=$this->transEsc("ill_request_available") ?></div> + <? endif; ?> + <? if (isset($ilsDetails['canceled']) && $ilsDetails['canceled']): ?> + <div class="text-success"><?=$this->transEsc("ill_request_canceled") . (is_string($ilsDetails['canceled']) ? ': ' . $ilsDetails['canceled'] : '') ?></div> + <? endif; ?> + <? if (isset($ilsDetails['cancel_link'])): ?> + <p><a href="<?=$this->escapeHtml($ilsDetails['cancel_link']) ?>"><?=$this->transEsc("ill_request_cancel") ?></a></p> + <? endif; ?> + + </div> + </div> + <? endforeach; ?> + <? if ($this->cancelForm): ?></form><? endif; ?> + <? else: ?> + <?=$this->transEsc('You do not have any interlibrary loan requests placed') ?>. + <? endif; ?> +</div> + +<div class="<?=$this->layoutClass('sidebar')?>"> + <?=$this->context($this)->renderInContext("myresearch/menu.phtml", array('active' => 'ILLRequests'))?> +</div> diff --git a/themes/bootstrap/templates/myresearch/menu.phtml b/themes/bootstrap/templates/myresearch/menu.phtml index 47a2e8b2fca..48971b020c0 100644 --- a/themes/bootstrap/templates/myresearch/menu.phtml +++ b/themes/bootstrap/templates/myresearch/menu.phtml @@ -7,6 +7,9 @@ <? if ($this->ils()->checkFunction('StorageRetrievalRequests')): ?> <li<?=$this->active == 'storageRetrievalRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-storageretrievalrequests')?>"><?=$this->transEsc('Storage Retrieval Requests')?> <i class="icon-archive pull-right"></i></a></li> <? endif; ?> + <? if ($this->ils()->checkFunction('ILLRequests')): ?> + <li<?=$this->active == 'ILLRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-illrequests')?>"><?=$this->transEsc('Interlibrary Loan Requests')?> <i class="icon-exchange pull-right"></i></a></li> + <? endif; ?> <li<?=$this->active == 'fines' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-fines')?>"><?=$this->transEsc('Fines')?> <i class="icon-usd pull-right"></i></a></li> <li<?=$this->active == 'profile' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-profile')?>"><?=$this->transEsc('Profile')?> <i class="icon-user pull-right"></i></a></li> <? endif; ?> diff --git a/themes/bootstrap/templates/record/illrequest.phtml b/themes/bootstrap/templates/record/illrequest.phtml new file mode 100644 index 00000000000..24e34aefc03 --- /dev/null +++ b/themes/bootstrap/templates/record/illrequest.phtml @@ -0,0 +1,126 @@ +<? + // Set up ill script: + $this->headScript()->appendFile("ill.js"); + + // Set page title. + $this->headTitle($this->translate('ill_request_place_text') . ': ' . $this->driver->getBreadcrumb()); + + // Set up breadcrumbs: + $this->layout()->breadcrumbs = '<li>' . $this->getLastSearchLink($this->transEsc('Search'), '', '<span class="divider">></span> </li>') + . '<li>' . $this->recordLink()->getBreadcrumb($this->driver) . '<span class="divider">></span> </li>' + . '<li class="active">' . $this->transEsc('ill_request_place_text') . '</li>'; +?> +<p class="lead"><?=$this->transEsc('ill_request_place_text')?></p> +<?=$this->flashmessages()?> +<div id="ILLRequestForm" class="storage-retrieval-request-form"> + <form action="" class="form-horizontal" method="post"> + + <? if (in_array("itemId", $this->extraFields)): ?> + <div class="control-group"> + <label class="control-label"><?=$this->transEsc('ill_request_item')?>:</label> + <div class="controls"> + <select id="itemId" name="gatheredDetails[itemId]"> + <? foreach ($this->items as $item): ?> + <option value="<?=$this->escapeHtml($item['id'])?>"<?=($this->gatheredDetails['itemId'] == $item['id']) ? ' selected="selected"' : ''?>> + <?=$this->escapeHtml($item['name'])?> + </option> + <? endforeach; ?> + </select> + </div> + </div> + <? endif; ?> + + <? if (in_array("pickUpLibrary", $this->extraFields)): ?> + <div class="control-group"> + <? if (count($this->pickupLibraries) > 1): ?> + <? + if (isset($this->gatheredDetails['pickUpLibrary']) && $this->gatheredDetails['pickUpLibrary'] !== "") { + $selected = $this->gatheredDetails['pickUpLibrary']; + } else { + $selected = false; + } + ?> + <label class="control-label"><?=$this->transEsc("ill_request_pick_up_library")?>:</label> + <div class="controls"> + <select id="pickupLibrary" name="gatheredDetails[pickUpLibrary]"> + <? foreach ($this->pickupLibraries as $lib): ?> + <option value="<?=$this->escapeHtml($lib['id'])?>"<?=(($selected === false && isset($lib['isDefault']) && $lib['isDefault']) || $selected === $lib['id']) ? ' selected="selected"' : ''?>> + <?=$this->transEsc('library_' . $lib['name'], null, $lib['name'])?> + </option> + <? endforeach; ?> + </select> + </div> + <? endif; ?> + </div> + <? endif; ?> + + <? if (in_array("pickUpLibraryLocation", $this->extraFields)): ?> + <div class="control-group"> + <label id="pickupLibraryLocationLabel" class="control-label"><i></i> <?=$this->transEsc("ill_request_pick_up_location")?>:</label> + <div class="controls"> + <select id="pickupLibraryLocation" name="gatheredDetails[pickUpLibraryLocation]"> + </select> + </div> + </div> + <? endif; ?> + + <? if (in_array("pickUpLocation", $this->extraFields)): ?> + <? if (count($this->pickup) > 1): ?> + <div class="control-group"> + <? + if (isset($this->gatheredDetails['pickUpLocation']) && $this->gatheredDetails['pickUpLocation'] !== "") { + $selected = $this->gatheredDetails['pickUpLocation']; + } elseif (isset($this->homeLibrary) && $this->homeLibrary !== "") { + $selected = $this->homeLibrary; + } else { + $selected = $this->defaultPickup; + } + ?> + <label class="control-label"><?=$this->transEsc("pick_up_location")?>:</label> + <div class="controls"> + <select name="gatheredDetails[pickUpLocation]"> + <? foreach ($this->pickup as $lib): ?> + <option value="<?=$this->escapeHtml($lib['locationID'])?>"<?=($selected == $lib['locationID']) ? ' selected="selected"' : ''?>> + <?=$this->escapeHtml($lib['locationDisplay'])?> + </option> + <? endforeach; ?> + </select> + </div> + </div> + <? else: ?> + <input type="hidden" name="gatheredDetails[pickUpLocation]" value="<?=$this->escapeHtml($this->defaultPickup)?>" /> + <? endif; ?> + <? endif; ?> + + <? if (in_array("requiredByDate", $this->extraFields)): ?> + <div class="control-group"> + <label class="control-label"><?=$this->transEsc("hold_required_by")?>:</label> + <div class="controls"> + <input id="requiredByDate" type="text" name="gatheredDetails[requiredBy]" value="<?=(isset($this->gatheredDetails['requiredBy']) && !empty($this->gatheredDetails['requiredBy'])) ? $this->escapeHtml($this->gatheredDetails['requiredBy']) : $this->escapeHtml($this->defaultRequiredDate)?>" size="8" /> + (<?=$this->dateTime()->getDisplayDateFormat()?>) + </div> + </div> + <? endif; ?> + + <? if (in_array("comments", $this->extraFields)): ?> + <div class="control-group"> + <label class="control-label"><?=$this->transEsc("Comments")?>:</label> + <div class="controls"> + <textarea rows="3" cols="20" name="gatheredDetails[comment]"><?=isset($this->gatheredDetails['comment']) ? $this->escapeHtml($this->gatheredDetails['comment']) : ''?></textarea> + </div> + </div> + <? endif; ?> + + <div class="control-group"> + <div class="controls"> + <input class="btn btn-primary" type="submit" name="placeILLRequest" value="<?=$this->transEsc('ill_request_submit_text')?>"/> + </div> + </div> + </form> +</div> + +<script type="text/javascript"> +$(document).ready(function(){ + setUpILLRequestForm('<?=$this->escapeHtml($this->driver->getUniqueId()) ?>'); +}); +</script> -- GitLab