From 6a3625db66774f025011dd6024442f3f6e64f5f0 Mon Sep 17 00:00:00 2001 From: Ere Maijala <ere.maijala@helsinki.fi> Date: Thu, 20 Mar 2014 15:37:46 +0200 Subject: [PATCH] Implemented support for request groups with holds in core code, Demo driver and Voyager drivers. --- config/vufind/VoyagerRestful.ini | 39 +- languages/en.ini | 3 + languages/fi.ini | 3 + languages/sv.ini | 5 +- .../src/VuFind/Controller/AjaxController.php | 57 ++ .../src/VuFind/Controller/Plugin/Holds.php | 44 +- .../VuFind/Controller/RecordController.php | 47 +- module/VuFind/src/VuFind/ILS/Driver/Demo.php | 148 +++-- .../VuFind/src/VuFind/ILS/Driver/Voyager.php | 15 +- .../src/VuFind/ILS/Driver/VoyagerRestful.php | 529 +++++++++++++++--- themes/blueprint/css/styles.css | 2 +- themes/blueprint/js/hold.js | 41 ++ .../templates/myresearch/holds.phtml | 5 + themes/blueprint/templates/record/hold.phtml | 61 +- .../templates/myresearch/holds.phtml | 5 + themes/bootstrap/js/hold.js | 41 ++ .../templates/myresearch/holds.phtml | 5 + themes/bootstrap/templates/record/hold.phtml | 88 ++- themes/jquerymobile/css/styles.css | 9 +- themes/jquerymobile/js/hold.js | 44 ++ .../templates/myresearch/holds.phtml | 7 + .../jquerymobile/templates/record/hold.phtml | 61 +- 22 files changed, 1078 insertions(+), 181 deletions(-) create mode 100644 themes/blueprint/js/hold.js create mode 100644 themes/bootstrap/js/hold.js create mode 100644 themes/jquerymobile/js/hold.js diff --git a/config/vufind/VoyagerRestful.ini b/config/vufind/VoyagerRestful.ini index a8264087fa3..d4c232ec7bf 100644 --- a/config/vufind/VoyagerRestful.ini +++ b/config/vufind/VoyagerRestful.ini @@ -91,8 +91,8 @@ HMACKeys = item_id:holdtype:level defaultRequiredDate = 0:1:0 ; extraHoldFields - A colon-separated list used to display extra visible fields in the -; place holds form. Supported values are "comments", "requiredByDate" and -; "pickUpLocation" +; place holds form. Supported values are "comments", "requiredByDate", +; "pickUpLocation" and "requestGroup" extraHoldFields = comments:requiredByDate:pickUpLocation ; A Pick Up Location Code used to pre-select the pick up location drop down list and @@ -108,6 +108,41 @@ defaultPickUpLocation = "" ; link. Use "0" to check all items via ajax. Default is 15. holdCheckLimit = 15 +; A request group ID used to pre-select the request group drop down list and +; provide a default option if others are not available. Must be one of the following: +; 1) false (default) to indicate that the user always has to choose the group +; 2) empty string to indicate that the first value is default +; 3) correspond with one of the request group IDs returned by getRequestGroups(). +; This setting is only effective if requestGroup is specified in extraHoldFields. +;defaultRequestGroup = "" + +; By default the request group list is sorted alphabetically. This setting can be +; used to manually set the order by entering request group IDs as a colon-separated list. +; This setting is only effective if requestGroup is specified in extraHoldFields. +;requestGroupOrder = 33 + +; By default the available pickup locations don't have to belong to the selected request group. +; Uncomment this setting to limit pickup locations to the request group. +; This setting is only effective if requestGroup is specified in extraHoldFields. +;pickupLocationsInRequestGroup = true + +; By default a title hold can be placed even when there are no items. Uncomment this +; to prevent holds if no items exist. If request groups are enabled, item existence is +; checked only in the selected request group. +;checkItemsExist = true + +; By default a title hold can be placed even when there are items available. Uncomment this +; to prevent holds if items are available. If request groups are enabled, availability is +; checked only in the selected request group. +;checkItemsAvailable = true + +; A colon-separated list of request group ids for which the available item check is disabled. +; If a listed request group is selected, the availability check is not made regardless of the +; setting above. +; This setting is only effective if requestGroup is specified in extraHoldFields. +disableAvailabilityCheckForRequestGroups = "15:19:21:32" + + ; This section controls call slip behavior. To enable, uncomment (at minimum) the ; HMACKeys and extraFields settings below. [StorageRetrievalRequests] diff --git a/languages/en.ini b/languages/en.ini index b1e96b926f1..1db85d8f67c 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -371,12 +371,14 @@ hold_empty_selection = "No holds were selected" hold_error_blocked = "You do not have sufficient privileges to place a hold on this item" hold_error_fail = "Your request failed. Please contact the circulation desk for further assistance" hold_invalid_pickup = "An invalid pick up location was entered. Please try again" +hold_invalid_request_group = "An invalid library was entered. Please try again" hold_login = "for hold and recall information" hold_place = "Place Request" hold_place_fail_missing = "Your request failed. Some data was missing. Please contact the circulation desk for further assistance" hold_place_success = "Your request was successful" hold_profile_html = "For hold and recall information, please establish your <a href="%%url%%">Library Catalog Profile</a>." hold_queue_position = "Queue Position" +hold_request_group = "Library" hold_required_by = "No longer required after" hold_success = "Your request was successful" Home = Home @@ -695,6 +697,7 @@ Select your carrier = "Select your carrier" Selected = "Selected" select_page = "Select Page" select_pickup_location = "Select Pick Up Location" +select_request_group = "Select Library" Send = Send Send us your feedback! = "Send us your feedback!" Sensor Image = "Sensor Image" diff --git a/languages/fi.ini b/languages/fi.ini index 1bfc223f3f1..e2961a42eb2 100644 --- a/languages/fi.ini +++ b/languages/fi.ini @@ -368,12 +368,14 @@ hold_empty_selection = "Yhtään varausta ei valittu" hold_error_blocked = "Varaaminen ei ole mahdollista, koska olet lainauskiellossa." hold_error_fail = "Pyyntö epäonnistui. Ota yhteyttä kirjaston asiakaspalveluun." hold_invalid_pickup = "Valittu noutopaikka on virheellinen. Yritä uudestaan." +hold_invalid_request_group = "Valittu kirjasto on virheellinen. Yritä uudestaan." hold_login = "tehdäksesi varauspyynnön" hold_place = "Tee varaus" hold_place_fail_missing = "Pyyntö epäonnistui puuttuvien tietojen vuoksi. Ota yhteyttä kirjaston asiakaspalveluun." hold_place_success = "Varauspyyntö onnistui" hold_profile_html = "Kirjaudu <a href="%%url%%">kirjastokortilla</a> nähdäksesi varaukset." hold_queue_position = "Sijainti jonossa" +hold_request_group = "Kirjasto" hold_required_by = "Viimeinen voimassaolopäivä" hold_success = "Varauspyyntö onnistui" Home = "Koti" @@ -697,6 +699,7 @@ Select your carrier = "Valitse operaattori" Selected = "Valittu" select_page = "Valitse sivu" select_pickup_location = "Valitse noutopaikka" +select_request_group = "Valitse kirjasto" Send = "Lähetä" Send us your feedback! = "Lähetä meille palautetta" Sensor Image = "Kaukokartoituskuva" diff --git a/languages/sv.ini b/languages/sv.ini index 532ad8c09c3..b8df60b0d03 100644 --- a/languages/sv.ini +++ b/languages/sv.ini @@ -368,12 +368,14 @@ hold_empty_selection = "Inga utvalda reserveringar" hold_error_blocked = "Det är inte möjligt att placera en reservering eftersom du är i låneförbud." hold_error_fail = "Reservering misslyckades. Vänd dig till bibliotekets kundtjänst." hold_invalid_pickup = "Avhämtningsplats duger inte. Kolla och försök igen." -hold_login = "för att reservera material" +hold_invalid_request_group = "Biblioteket duger inte. Kolla och försök igen." +hold_login = "för att reservera material" hold_place = "reservera" hold_place_fail_missing = "Reservering misslyckades p.g.a. saknande uppgifter. Vänd dig till bibliotekets kundtjänst." hold_place_success = "Material har reserverats." hold_profile_html = "Logga in med din <a href="%%url%%">bibliotekskort</a> för att se reserveringar." hold_queue_position = "Läget i kön" +hold_request_group = "Bibliotek" hold_required_by = "Sista dagen i kraft" hold_success = "Material har reserverats." Home = "Hem" @@ -697,6 +699,7 @@ Select your carrier = "Välj teleoperatör" Selected = "Vald" select_page = "Välj sida" select_pickup_location = "Välj avhämtningsplats" +select_request_group = "Välj bibliotek" Send = "Skicka" Send us your feedback! = "Skicka os din feedback" Sensor Image = "Fjärranalysbild" diff --git a/module/VuFind/src/VuFind/Controller/AjaxController.php b/module/VuFind/src/VuFind/Controller/AjaxController.php index ee6648e06d5..0091a6858ad 100644 --- a/module/VuFind/src/VuFind/Controller/AjaxController.php +++ b/module/VuFind/src/VuFind/Controller/AjaxController.php @@ -1427,6 +1427,63 @@ class AjaxController extends AbstractBase ); } + /** + * Get pick up locations for a request group + * + * @return \Zend\Http\Response + */ + protected function getRequestGroupPickupLocationsAjax() + { + $this->writeSession(); // avoid session write timing bug + $id = $this->params()->fromQuery('id'); + $requestGroupId = $this->params()->fromQuery('requestGroupId'); + if (!empty($id) && !empty($requestGroupId)) { + // 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) { + $details = array( + 'id' => $id, + 'requestGroupId' => $requestGroupId + ); + $results = $catalog->getPickupLocations( + $patron, $details + ); + foreach ($results as &$result) { + if (isset($result['locationDisplay'])) { + $result['locationDisplay'] = $this->translate( + 'location_' . $result['locationDisplay'], + array(), + $result['locationDisplay'] + ); + } + } + 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/Plugin/Holds.php b/module/VuFind/src/VuFind/Controller/Plugin/Holds.php index 3dfccde71b9..dfe3a1afda8 100644 --- a/module/VuFind/src/VuFind/Controller/Plugin/Holds.php +++ b/module/VuFind/src/VuFind/Controller/Plugin/Holds.php @@ -191,7 +191,7 @@ class Holds extends AbstractPlugin ); } } - + 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. @@ -308,6 +308,48 @@ class Holds extends AbstractPlugin return false; } + /** + * Check if the user-provided request group is valid. + * + * @param string $requestGroupId Id of user-specified request group + * @param array $extraHoldFields Hold form fields enabled by + * configuration/driver + * @param array $requestGroups Request group list from driver + * + * @return bool + */ + public function validateRequestGroupInput( + $requestGroupId, $extraHoldFields, $requestGroups + ) { + // Not having to care for pickUpLocation is equivalent to having a valid one. + if (!in_array('requestGroup', $extraHoldFields)) { + return true; + } + + // Check the valid pickup locations for a match against user input: + return $this->validateRequestGroup($requestGroupId, $requestGroups); + } + + /** + * Check if the provided request group is valid. + * + * @param string $requestGroupId Id of the request group to check + * @param array $requestGroups Request group list from driver + * + * @return bool + */ + public function validateRequestGroup($requestGroupId, $requestGroups) + { + foreach ($requestGroups as $group) { + if ($requestGroupId == $group['id']) { + return true; + } + } + + // If we got this far, something is wrong! + return false; + } + /** * Getting a default required date based on hold settings. * diff --git a/module/VuFind/src/VuFind/Controller/RecordController.php b/module/VuFind/src/VuFind/Controller/RecordController.php index 2ec1fef4766..f2535b3f083 100644 --- a/module/VuFind/src/VuFind/Controller/RecordController.php +++ b/module/VuFind/src/VuFind/Controller/RecordController.php @@ -97,7 +97,7 @@ class RecordController extends AbstractRecord public function holdAction() { $driver = $this->loadRecord(); - + // If we're not supposed to be here, give up now! $catalog = $this->getILS(); $checkHolds = $catalog->checkFunction("Holds", $driver->getUniqueID()); @@ -126,18 +126,28 @@ class RecordController extends AbstractRecord // Send various values to the view so we can build the form: $pickup = $catalog->getPickUpLocations($patron, $gatheredDetails); + $requestGroups = $catalog->checkCapability('getRequestGroups') + ? $catalog->getRequestGroups($driver->getUniqueID(), $patron) + : array(); $extraHoldFields = isset($checkHolds['extraHoldFields']) ? explode(":", $checkHolds['extraHoldFields']) : array(); // Process form submissions if necessary: if (!is_null($this->params()->fromPost('placeHold'))) { - // If the form contained a pickup location, make sure that - // the value has not been tampered with: - if (!$this->holds()->validatePickUpInput( + // If the form contained a pickup location or request group, make sure + // they are valid: + if (!$this->holds()->validateRequestGroupInput( + $gatheredDetails['requestGroupId'], + $extraHoldFields, + $requestGroups) + ) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('hold_invalid_request_group'); + } elseif (!$this->holds()->validatePickUpInput( $gatheredDetails['pickUpLocation'], $extraHoldFields, $pickup )) { $this->flashMessenger()->setNamespace('error') - ->addMessage('error_inconsistent_parameters'); + ->addMessage('hold_invalid_pickup'); } else { // If we made it this far, we're ready to place the hold; // if successful, we will redirect and can stop here. @@ -184,6 +194,18 @@ class RecordController extends AbstractRecord } catch (\Exception $e) { $defaultPickup = false; } + try { + $defaultRequestGroup = empty($requestGroups) + ? false + : $catalog->getDefaultRequestGroup($patron, $gatheredDetails); + } catch (\Exception $e) { + $defaultRequestGroup = false; + } + + $requestGroupNeeded = in_array('requestGroup', $extraHoldFields) + && !empty($requestGroups) + && (empty($gatheredDetails['level']) + || $gatheredDetails['level'] != 'copy'); return $this->createViewModel( array( @@ -192,7 +214,10 @@ class RecordController extends AbstractRecord 'defaultPickup' => $defaultPickup, 'homeLibrary' => $this->getUser()->home_library, 'extraHoldFields' => $extraHoldFields, - 'defaultRequiredDate' => $defaultRequired + 'defaultRequiredDate' => $defaultRequired, + 'requestGroups' => $requestGroups, + 'defaultRequestGroup' => $defaultRequestGroup, + 'requestGroupNeeded' => $requestGroupNeeded ) ); } @@ -311,11 +336,11 @@ class RecordController extends AbstractRecord public function illRequestAction() { $driver = $this->loadRecord(); - + // If we're not supposed to be here, give up now! $catalog = $this->getILS(); $checkRequests = $catalog->checkFunction( - 'ILLRequests', + 'ILLRequests', $driver->getUniqueID() ); if (!$checkRequests) { @@ -344,7 +369,7 @@ class RecordController extends AbstractRecord } // Send various values to the view so we can build the form: - + $extraFields = isset($checkRequests['extraFields']) ? explode(":", $checkRequests['extraFields']) : array(); @@ -396,10 +421,10 @@ class RecordController extends AbstractRecord ); // Get pickup locations. Note that these are independent of pickup library, - // and library specific locations must be retrieved when a library is + // and library specific locations must be retrieved when a library is // selected. $pickupLocations = $catalog->getPickUpLocations($patron, $gatheredDetails); - + return $this->createViewModel( array( 'gatheredDetails' => $gatheredDetails, diff --git a/module/VuFind/src/VuFind/ILS/Driver/Demo.php b/module/VuFind/src/VuFind/ILS/Driver/Demo.php index d93db3aa477..17960d69253 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Demo.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Demo.php @@ -90,7 +90,7 @@ class Demo extends AbstractBase * @var bool */ protected $ILLRequests = true; - + /** * Date converter object * @@ -132,7 +132,7 @@ class Demo extends AbstractBase 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'); @@ -256,10 +256,10 @@ class Demo extends AbstractBase /** * Generate a list of holds, storage retrieval requests or ILL requests. - * + * * @param string $requestType Request type (Holds, StorageRetrievalRequests or * ILLRequests) - * + * * @return ArrayObject List of requests */ protected function createRequestList($requestType) @@ -272,6 +272,8 @@ class Demo extends AbstractBase // loop. $this->prepSolr(); + $requestGroups = $this->getRequestGroups(null, null); + $list = new ArrayObject(); for ($i = 0; $i < $items; $i++) { $location = $this->getFakeLoc(false); @@ -283,7 +285,7 @@ class Demo extends AbstractBase "item_id" => $i, "reqnum" => $i ); - + if ($i == 2 || rand()%5 == 1) { // Mimic an ILL request $currentItem["id"] = "ill_request_$i"; @@ -291,7 +293,7 @@ class Demo extends AbstractBase $currentItem['institution_id'] = 'ill_institution'; $currentItem['institution_name'] = 'ILL Library'; $currentItem['institution_dbkey'] = 'ill_institution'; - } else { + } else { if ($this->idsInMyResearch) { $currentItem['id'] = $this->getRandomBibId(); } else { @@ -306,31 +308,33 @@ class Demo extends AbstractBase } else { $currentItem['available'] = true; } + $pos = rand(0, count($requestGroups) - 1); + $currentItem['requestGroup'] = $requestGroups[$pos]['name']; } else { $status = rand()%5; $currentItem['available'] = $status == 1; $currentItem['canceled'] = $status == 2; $currentItem['processed'] = ($status == 1 || rand(1, 3) == 3) - ? date("j-M-y") + ? date("j-M-y") : ''; if ($requestType == 'ILLRequests') { $transit = rand()%2; - if (!$currentItem['available'] + if (!$currentItem['available'] && !$currentItem['canceled'] && $transit == 1 ) { - $currentItem['in_transit'] = $location; + $currentItem['in_transit'] = $location; } else { $currentItem['in_transit'] = false; } } } - + $list->append($currentItem); } - return $list; + return $list; } - + /** * Get Status * @@ -664,7 +668,7 @@ class Demo extends AbstractBase } return $this->session->storageRetrievalRequests; } - + /** * Get Patron ILL Requests * @@ -683,7 +687,7 @@ class Demo extends AbstractBase } return $this->session->ILLRequests; } - + /** * Get Patron Transactions * @@ -730,7 +734,7 @@ class Demo extends AbstractBase // Renewal limit $renewLimit = $renew + rand()%3; - + // Pending requests : 0,0,0,0,0,1,2,3,4,5 $req = rand()%10 - 5; if ($req < 0) { @@ -738,7 +742,7 @@ class Demo extends AbstractBase } if ($i == 2 || rand()%5 == 1) { - // Mimic an ILL loan + // Mimic an ILL loan $transList[] = array( 'duedate' => $due_date, 'dueStatus' => $dueStatus, @@ -850,6 +854,55 @@ class Demo extends AbstractBase return $locations[0]['locationID']; } + /** + * Get Default Request Group + * + * Returns the default request group + * + * @param array $patron Patron information returned by the patronLogin + * method. + * @param array $holdDetails Optional array, only passed in when getting a list + * in the context of placing a hold; contains most of the same values passed to + * placeHold, minus the patron data. May be used to limit the request group options + * or may be ignored. + * + * @return false|string The default request group for the patron. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getDefaultRequestGroup($patron = false, $holdDetails = null) + { + if (rand(0, 1) == 1) { + return false; + } + $requestGroups = $this->getRequestGroups(0, 0); + return $requestGroups[0]['id']; + } + + /** + * Get request groups + * + * @param integer $bibId BIB ID + * @param array $patron Patron information returned by the patronLogin + * method. + * + * @return array False if request groups not in use or an array of + * associative arrays with id and name keys + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRequestGroups($bibId = null, $patron = null) + { + return array( + array( + 'id' => 1, + 'name' => 'Main Library' + ), + array( + 'id' => 2, + 'name' => 'Branch Library' + ) + ); + } + /** * Get Funds * @@ -1182,7 +1235,7 @@ class Demo extends AbstractBase { return $checkOutDetails['item_id']; } - + /** * Check if hold or recall available * @@ -1202,7 +1255,7 @@ class Demo extends AbstractBase } return true; } - + /** * Place Hold * @@ -1256,6 +1309,13 @@ class Demo extends AbstractBase ); } + $requestGroup = ''; + foreach ($this->getRequestGroups(null, null) as $group) { + if ($group['id'] == $holdDetails['requestGroupId']) { + $requestGroup = $group['name']; + break; + } + } $this->session->holds->append( array( "id" => $holdDetails['id'], @@ -1265,7 +1325,8 @@ class Demo extends AbstractBase "reqnum" => sprintf("%06d", $nextId), "item_id" => $nextId, "volume" => '', - "processed" => '' + "processed" => '', + "requestGroup" => $requestGroup ) ); @@ -1291,7 +1352,7 @@ class Demo extends AbstractBase } return true; } - + /** * Place a Storage Retrieval Request * @@ -1326,9 +1387,9 @@ class Demo extends AbstractBase } $lastRequest = count($this->session->storageRetrievalRequests) - 1; $nextId = $lastRequest >= 0 - ? $this->session->storageRetrievalRequests[$lastRequest]['item_id'] + 1 + ? $this->session->storageRetrievalRequests[$lastRequest]['item_id'] + 1 : 0; - + // Figure out appropriate expiration date: if (!isset($details['requiredBy']) || empty($details['requiredBy']) @@ -1353,7 +1414,7 @@ class Demo extends AbstractBase 'sysMessage' => 'storage_retrieval_request_date_past' ); } - + $this->session->storageRetrievalRequests->append( array( "id" => $details['id'], @@ -1388,7 +1449,7 @@ class Demo extends AbstractBase } return true; } - + /** * Place ILL Request * @@ -1423,9 +1484,9 @@ class Demo extends AbstractBase } $lastRequest = count($this->session->ILLRequests) - 1; $nextId = $lastRequest >= 0 - ? $this->session->ILLRequests[$lastRequest]['item_id'] + 1 + ? $this->session->ILLRequests[$lastRequest]['item_id'] + 1 : 0; - + // Figure out appropriate expiration date: if (!isset($details['requiredBy']) || empty($details['requiredBy']) @@ -1450,7 +1511,7 @@ class Demo extends AbstractBase 'sysMessage' => 'ill_request_date_past' ); } - + // Verify pickup library and location $pickupLocation = ''; $pickupLocations = $this->getILLPickupLocations( @@ -1470,7 +1531,7 @@ class Demo extends AbstractBase 'sysMessage' => 'ill_request_place_fail_missing' ); } - + $this->session->ILLRequests->append( array( "id" => $details['id'], @@ -1485,7 +1546,7 @@ class Demo extends AbstractBase return array('success' => true); } - + /** * Get ILL Pickup Libraries * @@ -1494,7 +1555,7 @@ class Demo extends AbstractBase * @param string $id Record ID * @param array $patron Patron * - * @return bool|array False if request not allowed, or an array of associative + * @return bool|array False if request not allowed, or an array of associative * arrays with libraries. * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -1503,34 +1564,34 @@ class Demo extends AbstractBase 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 + * + * 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 + * @return boo|array False if request not allowed, or an array of * locations. * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -1543,7 +1604,7 @@ class Demo extends AbstractBase 'id' => 1, 'name' => 'Circulation Desk', 'isDefault' => true - ), + ), array( 'id' => 2, 'name' => 'Reference Desk', @@ -1564,7 +1625,7 @@ class Demo extends AbstractBase ) ); } - return array(); + return array(); } /** @@ -1624,7 +1685,7 @@ class Demo extends AbstractBase { return $details['reqnum']; } - + /** * Public Function which specifies renew, hold and cancel settings. * @@ -1637,7 +1698,8 @@ class Demo extends AbstractBase if ($function == 'Holds') { return array( 'HMACKeys' => 'id', - 'extraHoldFields' => 'comments:pickUpLocation:requiredByDate', + 'extraHoldFields' => + 'comments:requestGroup:pickUpLocation:requiredByDate', 'defaultRequiredDate' => 'driver:0:2:0', ); } @@ -1655,7 +1717,7 @@ class Demo extends AbstractBase return array( 'enabled' => true, 'HMACKeys' => 'number', - 'extraFields' => + 'extraFields' => 'comments:pickUpLibrary:pickUpLibraryLocation:requiredByDate', 'defaultRequiredDate' => '0:1:0', 'helpText' => 'This is an ILL request help text' diff --git a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php index 31ce5176c32..97649bf5d78 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php @@ -1155,7 +1155,7 @@ class Voyager extends AbstractBase } else { $sql .= "lower(PATRON.{$login_field}) = :login"; } - + try { $bindLogin = strtolower(utf8_decode($login)); $bindBarcode = strtolower(utf8_decode($barcode)); @@ -1480,7 +1480,8 @@ class Voyager extends AbstractBase "MFHD_ITEM.ITEM_ENUM", "MFHD_ITEM.YEAR", "BIB_TEXT.TITLE_BRIEF", - "BIB_TEXT.TITLE" + "BIB_TEXT.TITLE", + "REQUEST_GROUP.GROUP_NAME as REQUEST_GROUP_NAME" ); // From @@ -1488,7 +1489,9 @@ class Voyager extends AbstractBase $this->dbName.".HOLD_RECALL", $this->dbName.".HOLD_RECALL_ITEMS", $this->dbName.".MFHD_ITEM", - $this->dbName.".BIB_TEXT" + $this->dbName.".BIB_TEXT", + $this->dbName.".VOYAGER_DATABASES", + $this->dbName.".REQUEST_GROUP" ); // Where @@ -1498,7 +1501,10 @@ class Voyager extends AbstractBase "HOLD_RECALL_ITEMS.ITEM_ID = MFHD_ITEM.ITEM_ID(+)", "(HOLD_RECALL_ITEMS.HOLD_RECALL_STATUS IS NULL OR " . "HOLD_RECALL_ITEMS.HOLD_RECALL_STATUS < 3)", - "BIB_TEXT.BIB_ID = HOLD_RECALL.BIB_ID" + "BIB_TEXT.BIB_ID = HOLD_RECALL.BIB_ID", + "(HOLD_RECALL.HOLDING_DB_ID IS NULL OR (HOLD_RECALL.HOLDING_DB_ID = " . + "VOYAGER_DATABASES.DB_ID AND VOYAGER_DATABASES.DB_CODE = 'LOCAL'))", + "HOLD_RECALL.REQUEST_GROUP_ID = REQUEST_GROUP.GROUP_ID(+)" ); // Bind @@ -1546,6 +1552,7 @@ class Voyager extends AbstractBase 'id' => $sqlRow['BIB_ID'], 'type' => $sqlRow['HOLD_RECALL_TYPE'], 'location' => $sqlRow['PICKUP_LOCATION'], + 'requestGroup' => $sqlRow['REQUEST_GROUP_NAME'], 'expire' => $expireDate, 'create' => $createDate, 'position' => $sqlRow['QUEUE_POSITION'], diff --git a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php index 791a24c8e84..cdbf7f8b935 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php +++ b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php @@ -147,6 +147,42 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte */ protected $cookies = false; + /** + * Whether request groups are enabled + * + * @var bool + */ + protected $requestGroupsEnabled; + + /** + * Default request group + * + * @var bool|string + */ + protected $defaultRequestGroup; + + /** + * Whether pickup location must belong to the request group + * + * @var bool + */ + protected $pickupLocationsInRequestGroup; + + /** + * Whether to check that items exist when placing a hold or recall request + * + * @var bool + */ + protected $checkItemsExist; + + /** + * Whether to check that items are not available when placing a hold or recall + * request + * + * @var bool + */ + protected $checkItemsNotAvailable; + /** * Constructor * @@ -210,6 +246,30 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte = isset($this->config['CallSlips']['callSlipCheckLimit']) ? $this->config['CallSlips']['callSlipCheckLimit'] : "15"; + $this->requestGroupsEnabled + = isset($this->config['Holds']['extraHoldFields']) + && in_array( + 'requestGroup', + explode(':', $this->config['Holds']['extraHoldFields']) + ); + $this->defaultRequestGroup + = isset($this->config['Holds']['defaultRequestGroup']) + ? $this->config['Holds']['defaultRequestGroup'] : false; + if ($this->defaultRequestGroup === 'user-selected') { + $this->defaultRequestGroup = false; + } + $this->pickupLocationsInRequestGroup + = isset($this->config['Holds']['pickupLocationsInRequestGroup']) + ? $this->config['Holds']['pickupLocationsInRequestGroup'] : false; + + $this->checkItemsExist + = isset($this->config['Holds']['checkItemsExist']) + ? $this->config['Holds']['checkItemsExist'] : false; + $this->checkItemsNotAvailable + = isset($this->config['Holds']['checkItemsNotAvailable']) + ? $this->config['Holds']['checkItemsNotAvailable'] : false; + + // Establish a namespace in the session for persisting cached data $this->session = new SessionContainer('VoyagerRestful_' . $this->dbName); } @@ -229,6 +289,19 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte } else { $functionConfig = false; } + + // Make sure request group selection is displayed if request groups are + // enabled + if ($function == 'Holds' && $this->requestGroupsEnabled + && strpos($functionConfig['extraHoldFields'], 'requestGroup') === false + ) { + if (!empty($functionConfig['extraHoldFields'])) { + $functionConfig['extraHoldFields'] .= ':requestGroup'; + } else { + $functionConfig['extraHoldFields'] = 'requestGroup'; + } + } + return $functionConfig; } @@ -513,6 +586,14 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte return false; } } + + if ('title' == $level && $this->requestGroupsEnabled) { + // Verify that there are valid request groups + if (!$this->getRequestGroups($id, $patron)) { + return false; + } + } + return true; } @@ -610,6 +691,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte */ public function getPickUpLocations($patron = false, $holdDetails = null) { + $params = array(); if ($this->ws_pickUpLocations) { foreach ($this->ws_pickUpLocations as $code => $library) { $pickResponse[] = array( @@ -618,16 +700,32 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte ); } } else { - $sql = "SELECT CIRC_POLICY_LOCS.LOCATION_ID as location_id, " . - "NVL(LOCATION.LOCATION_DISPLAY_NAME, LOCATION.LOCATION_NAME) " . - "as location_name from " . - $this->dbName . ".CIRC_POLICY_LOCS, $this->dbName.LOCATION " . - "where CIRC_POLICY_LOCS.PICKUP_LOCATION = 'Y' ". - "and CIRC_POLICY_LOCS.LOCATION_ID = LOCATION.LOCATION_ID"; + if ($this->requestGroupsEnabled + && $this->pickupLocationsInRequestGroup + && !empty($holdDetails['requestGroupId']) + ) { + $sql = "SELECT CIRC_POLICY_LOCS.LOCATION_ID as location_id, " . + "NVL(LOCATION.LOCATION_DISPLAY_NAME, LOCATION.LOCATION_NAME) " . + "as location_name from " . + $this->dbName . ".CIRC_POLICY_LOCS, $this->dbName.LOCATION, " . + "$this->dbName.REQUEST_GROUP_LOCATION rgl " . + "where CIRC_POLICY_LOCS.PICKUP_LOCATION = 'Y' ". + "and CIRC_POLICY_LOCS.LOCATION_ID = LOCATION.LOCATION_ID " . + "and rgl.GROUP_ID=:requestGroupId " . + "and rgl.LOCATION_ID = LOCATION.LOCATION_ID"; + $params['requestGroupId'] = $holdDetails['requestGroupId']; + } else { + $sql = "SELECT CIRC_POLICY_LOCS.LOCATION_ID as location_id, " . + "NVL(LOCATION.LOCATION_DISPLAY_NAME, LOCATION.LOCATION_NAME) " . + "as location_name from " . + $this->dbName . ".CIRC_POLICY_LOCS, $this->dbName.LOCATION " . + "where CIRC_POLICY_LOCS.PICKUP_LOCATION = 'Y' ". + "and CIRC_POLICY_LOCS.LOCATION_ID = LOCATION.LOCATION_ID"; + } try { $sqlStmt = $this->db->prepare($sql); - $sqlStmt->execute(); + $sqlStmt->execute($params); } catch (PDOException $e) { throw new ILSException($e->getMessage()); } @@ -655,7 +753,8 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte * placeHold, minus the patron data. May be used to limit the pickup options * or may be ignored. * - * @return string The default pickup location for the patron. + * @return false|string The default pickup location for the patron or false + * if the user has to choose. * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getDefaultPickUpLocation($patron = false, $holdDetails = null) @@ -663,7 +762,245 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte return $this->defaultPickUpLocation; } - /** + /** + * Get Default Request Group + * + * Returns the default request group set in VoyagerRestful.ini + * + * @param array $patron Patron information returned by the patronLogin + * method. + * @param array $holdDetails Optional array, only passed in when getting a list + * in the context of placing a hold; contains most of the same values passed to + * placeHold, minus the patron data. May be used to limit the request group + * options or may be ignored. + * + * @return false|string The default request group for the patron or false if + * the user has to choose. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getDefaultRequestGroup($patron = false, $holdDetails = null) + { + return $this->defaultRequestGroup; + } + + /** + * Sort function for sorting request groups + * + * @param array $a Request group + * @param array $b Request group + * + * @return number + */ + protected function requestGroupSortFunction($a, $b) + { + $requestGroupOrder = isset($this->config['Holds']['requestGroupOrder']) + ? explode(':', $this->config['Holds']['requestGroupOrder']) + : array(); + $requestGroupOrder = array_flip($requestGroupOrder); + if (isset($requestGroupOrder[$a['id']])) { + if (isset($requestGroupOrder[$b['id']])) { + return $requestGroupOrder[$a['id']] - $requestGroupOrder[$b['id']]; + } + return -1; + } + if (isset($requestGroupOrder[$b['id']])) { + return 1; + } + return strcasecmp($a['name'], $b['name']); + } + + /** + * Get request groups + * + * @param integer $bibId BIB ID + * @param array $patron Patron information returned by the patronLogin + * method. + * + * @return array False if request groups not in use or an array of + * associative arrays with id and name keys + */ + public function getRequestGroups($bibId, $patron) + { + if (!$this->requestGroupsEnabled) { + return false; + } + + if ($this->checkItemsExist) { + // First get hold information for the list of items Voyager + // thinks are holdable + $request = $this->determineHoldType($patron['id'], $bibId); + if ($request != 'hold' && $result != 'recall') { + return false; + } + + $hierarchy = array(); + + // Build Hierarchy + $hierarchy['record'] = $bibId; + $hierarchy[$request] = false; + + // Add Required Params + $params = array( + "patron" => $patron['id'], + "patron_homedb" => $this->ws_patronHomeUbId, + "view" => "full" + ); + + $results = $this->makeRequest($hierarchy, $params, "GET", false); + + if ($results === false) { + throw new ILSException('Could not fetch hold information'); + } + + $items = array(); + foreach ($results->hold as $hold) { + foreach ($hold->items->item as $item) { + $items[(string)$item->item_id] = 1; + } + } + } + + // Find request groups (with items if item check is enabled) + if ($this->checkItemsExist) { + $sqlExpressions = array( + 'rg.GROUP_ID', + 'rg.GROUP_NAME', + 'bi.ITEM_ID' + ); + + $sqlFrom = array( + "$this->dbName.BIB_ITEM bi", + "$this->dbName.MFHD_ITEM mi", + "$this->dbName.MFHD_MASTER mm", + "$this->dbName.REQUEST_GROUP rg", + "$this->dbName.REQUEST_GROUP_LOCATION rgl", + ); + + $sqlWhere = array( + 'bi.BIB_ID=:bibId', + 'mi.ITEM_ID=bi.ITEM_ID', + 'mm.MFHD_ID=mi.MFHD_ID', + 'rgl.LOCATION_ID=mm.LOCATION_ID', + 'rg.GROUP_ID=rgl.GROUP_ID' + ); + + $sqlBind = array( + 'bibId' => $bibId + ); + } else { + $sqlExpressions = array( + 'rg.GROUP_ID', + 'rg.GROUP_NAME', + ); + + $sqlFrom = array( + "$this->dbName.REQUEST_GROUP rg", + "$this->dbName.REQUEST_GROUP_LOCATION rgl" + ); + + $sqlWhere = array( + 'rg.GROUP_ID=rgl.GROUP_ID' + ); + + $sqlBind = array( + ); + + if ($this->pickupLocationsInRequestGroup) { + // Limit to request groups that have valid pickup locations + $sqlFrom[] = "$this->dbName.REQUEST_GROUP_LOCATION rgl"; + $sqlFrom[] = "$this->dbName.CIRC_POLICY_LOCS cpl"; + + $sqlWhere[] = "rgl.GROUP_ID=rg.GROUP_ID"; + $sqlWhere[] = "cpl.LOCATION_ID=rgl.LOCATION_ID"; + $sqlWhere[] = "cpl.PICKUP_LOCATION='Y'"; + } + } + + if ($this->checkItemsNotAvailable) { + + // Build inner query first + $subExpressions = array( + 'sub_rgl.GROUP_ID', + 'sub_i.ITEM_ID', + 'max(sub_ist.ITEM_STATUS) as STATUS' + ); + + $subFrom = array( + "$this->dbName.ITEM_STATUS sub_ist", + "$this->dbName.BIB_ITEM sub_bi", + "$this->dbName.ITEM sub_i", + "$this->dbName.REQUEST_GROUP_LOCATION sub_rgl", + "$this->dbName.MFHD_ITEM sub_mi", + "$this->dbName.MFHD_MASTER sub_mm" + ); + + $subWhere = array( + 'sub_bi.BIB_ID=:subBibId', + 'sub_i.ITEM_ID=sub_bi.ITEM_ID', + 'sub_ist.ITEM_ID=sub_i.ITEM_ID', + 'sub_mi.ITEM_ID=sub_i.ITEM_ID', + 'sub_mm.MFHD_ID=sub_mi.MFHD_ID', + 'sub_rgl.LOCATION_ID=sub_mm.LOCATION_ID' + ); + + $subGroup = array( + 'sub_rgl.GROUP_ID', + 'sub_i.ITEM_ID' + ); + + $sqlBind['subBibId'] = $bibId; + + $subArray = array( + 'expressions' => $subExpressions, + 'from' => $subFrom, + 'where' => $subWhere, + 'group' => $subGroup, + 'bind' => array() + ); + + $subSql = $this->buildSqlFromArray($subArray); + + $sqlWhere[] = "not exists (select status.GROUP_ID from " . + "({$subSql['string']}) status where status.status=1 " . + "and status.GROUP_ID = rgl.GROUP_ID)"; + } + + $sqlArray = array( + 'expressions' => $sqlExpressions, + 'from' => $sqlFrom, + 'where' => $sqlWhere, + 'bind' => $sqlBind + ); + + $sql = $this->buildSqlFromArray($sqlArray); + + try { + $this->debugSQL(__FUNCTION__, $sql['string'], $sql['bind']); + $sqlStmt = $this->db->prepare($sql['string']); + $sqlStmt->execute($sql['bind']); + } catch (PDOException $e) { + return new PEAR_Error($e->getMessage()); + } + + $groups = array(); + while ($row = $sqlStmt->fetch(PDO::FETCH_ASSOC)) { + if (!$this->checkItemsExist || isset($items[$row['ITEM_ID']])) { + $groups[$row['GROUP_ID']] = utf8_encode($row['GROUP_NAME']); + } + } + + $results = array(); + foreach ($groups as $groupId => $groupName) { + $results[] = array('id' => $groupId, 'name' => $groupName); + } + + // Sort request groups + usort($results, array($this, 'requestGroupSortFunction')); + + return $results; + } + + /** * Make Request * * Makes a request to the Voyager Restful API @@ -1080,88 +1417,93 @@ EOT; /** * Make Item Requests * - * Places a Hold or Recall for a particular item + * Places a Hold or Recall for a particular title or item * - * @param string $patronId The user's Patron ID - * @param string $request The request type (hold or recall) - * @param string $level The request level (title or copy) - * @param array $requestData An array of data to submit with the request, - * may include comment, lastInterestDate and pickUpLocation - * @param string $bibId An item's Bib ID - * @param string $itemId An item's Item ID (optional) + * @param string $patron Patron information from patronLogin + * @param string $type The request type (hold or recall) + * @param array $requestData An array of parameters to submit with the request * * @return array An array of data from the attempted request * including success, status and a System Message (if available) */ - protected function makeItemRequests($patronId, $request, $level, - $requestData, $bibId, $itemId = false + protected function makeItemRequests($patron, $type, $requestData ) { - $response = array('success' => false, 'status' =>"hold_error_fail"); - - if (!empty($bibId) && !empty($patronId) && !empty($requestData) - && !empty($request) + if (empty($patron) || empty($requestData) || empty($requestData['bibId']) + || empty($type) ) { - $hierarchy = array(); - - // Build Hierarchy - $hierarchy['record'] = $bibId; - - if ($itemId) { - $hierarchy['items'] = $itemId; - } - - $hierarchy[$request] = false; - - // Add Required Params - $params = array( - "patron" => $patronId, - "patron_homedb" => $this->ws_patronHomeUbId, - "view" => "full" - ); - - if ("title" == $level) { - $xmlParameter = ("recall" == $request) - ? "recall-title-parameters" : "hold-title-parameters"; - $request = $request . "-title"; - } else { - $xmlParameter = ("recall" == $request) - ? "recall-parameters" : "hold-request-parameters"; - } + return array('success' => false, 'status' =>"hold_error_fail"); + } - $xml[$xmlParameter] = array( - "pickup-location" => $requestData['pickupLocation'], - "last-interest-date" => $requestData['lastInterestDate'], - "comment" => $requestData['comment'], - "dbkey" => $this->ws_dbKey - ); + // Build request + $patronId = htmlspecialchars($patron['id'], ENT_COMPAT, 'UTF-8'); + $lastname = htmlspecialchars($patron['lastname'], ENT_COMPAT, 'UTF-8'); + $barcode = htmlspecialchars($patron['cat_username'], ENT_COMPAT, 'UTF-8'); + $localUbId = htmlspecialchars($this->ws_patronHomeUbId, ENT_COMPAT, 'UTF-8'); + $type = strtoupper($type); + $cval = 'anyCopy'; + if (isset($requestData['itemId'])) { + $cval = 'thisCopy'; + } elseif (isset($requestData['requestGroupId'])) { + $cval = 'anyCopyAt'; + } - // Generate XML - $requestXML = $this->buildBasicXML($xml); + // Build 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="bibDbCode"> + <ser:value>LOCAL</ser:value> + </ser:parameter> + <ser:parameter key="requestCode"> + <ser:value>$type</ser:value> + </ser:parameter> + <ser:parameter key="requestSiteId"> + <ser:value>$localUbId</ser:value> + </ser:parameter> + <ser:parameter key="CVAL"> + <ser:value>$cval</ser:value> + </ser:parameter> - // Get Data - $result = $this->makeRequest($hierarchy, $params, "PUT", $requestXML); +EOT; + foreach ($requestData as $key => $value) { + $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + $xml .= <<<EOT + <ser:parameter key="$key"> + <ser:value>$value</ser:value> + </ser:parameter> - if ($result) { - // Process - $result = $result->children(); - $node = "reply-text"; - $reply = (string)$result->$node; +EOT; + } + $xml .= <<<EOT + </ser:parameters> + <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$localUbId" patronId="$patronId"> + <ser:authFactor type="B">$barcode</ser:authFactor> + </ser:patronIdentifier> +</ser:serviceParameters> +EOT; - $responseNode = "create-".$request; - $note = (isset($result->$responseNode)) - ? trim((string)$result->$responseNode->note) : false; + $response = $this->makeRequest(array('SendPatronRequestService' => false), array(), 'POST', $xml); - // Valid Response - if ($reply == "ok" && $note == "Your request was successful.") { - $response['success'] = true; - $response['status'] = "hold_success"; - } else { - // Failed - $response['sysMessage'] = $note; - } + if ($response === false) { + return $this->holdError('hold_error_system'); + } + // 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' => 'hold_request_success' + ); + } + if ($message->attributes()->type == 'system') { + return $this->holdError('hold_error_system'); } } - return $response; + + return $this->holdError('hold_error_blocked'); } /** @@ -1241,7 +1583,7 @@ EOT; $bibId = $holdDetails['id']; // Request was initiated before patron was logged in - - //Let's determine Hold Type now + // Let's determine Hold Type now if ($type == "auto") { $type = $this->determineHoldType($patron['id'], $bibId, $itemId); if (!$type || $type == "block") { @@ -1280,22 +1622,31 @@ EOT; return $this->holdError("hold_invalid_pickup"); } + if ($this->requestGroupsEnabled && !$itemId + && empty($holdDetails['requestGroupId']) + ) { + return $this->holdError('hold_invalid_request_group'); + } + // Build Request Data $requestData = array( - 'pickupLocation' => $pickUpLocation, - 'lastInterestDate' => $lastInterestDate, - 'comment' => $comment + 'bibId' => $bibId, + 'PICK' => $pickUpLocation, + 'REQNNA' => $lastInterestDate, + 'REQCOMMENTS' => $comment ); + if ($level == 'copy' && $itemId) { + $requestData['itemId'] = $itemId; + } elseif (isset($holdDetails['requestGroupId'])) { + $requestData['requestGroupId'] = $holdDetails['requestGroupId']; + } - if ($this->checkItemRequests($patron['id'], $type, $bibId, $itemId)) { - // Attempt Request - $result = $this->makeItemRequests( - $patron['id'], $type, $level, $requestData, $bibId, $itemId - ); - if ($result) { - return $result; - } + // Attempt Request + $result = $this->makeItemRequests($patron, $type, $requestData); + if ($result) { + return $result; } + return $this->holdError("hold_error_blocked"); } @@ -2289,7 +2640,7 @@ EOT; 'sysMessage' => 'ill_request_place_fail_missing' ); } - + // Attempt Request $xml = <<<EOT <?xml version="1.0" encoding="UTF-8"?> diff --git a/themes/blueprint/css/styles.css b/themes/blueprint/css/styles.css index c1d15a3ba3d..a0f8da98f98 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_ill_request_availability, .ajax_ill_request_loading { +.ajax_availability, .ajax_hold_availability, .ajax_storage_retrieval_request_availability, .ajax_ill_request_availability, .ajax_ill_request_loading, .ajax_hold_request_loading { background: url(../images/ajax_loading.gif) no-repeat left top; padding:0 .5em .5em 20px; } diff --git a/themes/blueprint/js/hold.js b/themes/blueprint/js/hold.js new file mode 100644 index 00000000000..33d785d3cbd --- /dev/null +++ b/themes/blueprint/js/hold.js @@ -0,0 +1,41 @@ +function setUpHoldRequestForm(recordId) { + $('#requestGroupId').change(function() { + var $emptyOption = $("#pickUpLocation option[value='']"); + $("#pickUpLocation option[value!='']").remove(); + if ($('#requestGroupId').val() === '') { + $('#pickUpLocation').attr('disabled', 'disabled'); + return; + } + $('#pickUpLocationLabel').addClass("ajax_hold_request_loading"); + var params = { + method: 'getRequestGroupPickupLocations', + id: recordId, + requestGroupId: $('#requestGroupId').val() + }; + $.ajax({ + data: params, + dataType: 'json', + cache: false, + url: path + '/AJAX/JSON', + success: function(response) { + if (response.status == 'OK') { + var defaultValue = $('#pickUpLocation').data('default'); + $.each(response.data.locations, function() { + var option = $('<option></option>').attr('value', this.locationID).text(this.locationDisplay); + if (this.locationID == defaultValue || (defaultValue == '' && this.isDefault && $emptyOption.length == 0)) { + option.attr('selected', 'selected'); + } + $('#pickUpLocation').append(option); + }); + } + $('#pickUpLocationLabel').removeClass("ajax_hold_request_loading"); + $('#pickUpLocation').removeAttr('disabled'); + }, + fail: function() { + $('#pickUpLocationLabel').removeClass("ajax_hold_request_loading"); + $('#pickUpLocation').removeAttr('disabled'); + } + }); + }); + $('#requestGroupId').change(); +} diff --git a/themes/blueprint/templates/myresearch/holds.phtml b/themes/blueprint/templates/myresearch/holds.phtml index 9be0cdb6ac8..4207757b4b6 100644 --- a/themes/blueprint/templates/myresearch/holds.phtml +++ b/themes/blueprint/templates/myresearch/holds.phtml @@ -94,6 +94,11 @@ <br /> <? endif; ?> + <? if (!empty($ilsDetails['requestGroup'])): ?> + <strong><?=$this->transEsc('hold_request_group') ?>:</strong> <?=$this->transEsc('location_' . $ilsDetails['requestGroup'], array(), $ilsDetails['requestGroup'])?> + <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 = ''; ?> diff --git a/themes/blueprint/templates/record/hold.phtml b/themes/blueprint/templates/record/hold.phtml index 0d5ec0b35d6..90d8c0c1c32 100644 --- a/themes/blueprint/templates/record/hold.phtml +++ b/themes/blueprint/templates/record/hold.phtml @@ -1,4 +1,7 @@ <? + // Set up hold script: + $this->headScript()->appendFile("hold.js"); + // Set page title. $this->headTitle($this->translate('request_place_text') . ': ' . $this->driver->getBreadcrumb()); @@ -26,18 +29,54 @@ </div> <? endif; ?> - <? if (in_array("pickUpLocation", $this->extraHoldFields)): ?> + <? if ($this->requestGroupNeeded): ?> <div> - <? if (count($this->pickup) > 1): ?> <? - if (isset($this->gatheredDetails['pickUpLocation']) && $this->gatheredDetails['pickUpLocation'] !== "") { - $selected = $this->gatheredDetails['pickUpLocation']; - } elseif (isset($this->homeLibrary) && $this->homeLibrary !== "") { - $selected = $this->homeLibrary; + if (isset($this->gatheredDetails['requestGroupId']) && $this->gatheredDetails['requestGroupId'] !== "") { + $selected = $this->gatheredDetails['requestGroupId']; } else { - $selected = $this->defaultPickup; + $selected = $this->defaultRequestGroup; } - ?> + ?> + <strong><?=$this->transEsc("hold_request_group")?>:</strong> + <select id="requestGroupId" name="gatheredDetails[requestGroupId]"> + <? if ($selected === false): ?> + <option value="" selected="selected"> + <?=$this->transEsc('select_request_group')?> + </option> + <? endif; ?> + <? foreach ($this->requestGroups as $group): ?> + <option value="<?=$this->escapeHtml($group['id'])?>"<?=($selected == $group['id']) ? ' selected="selected"' : ''?>> + <?=$this->escapeHtml($group['name'])?> + </option> + <? endforeach; ?> + </select> + </div> + <? endif; ?> + + <? if (in_array("pickUpLocation", $this->extraHoldFields)): ?> + <? + 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; + } + ?> + <div> + <? if ($this->requestGroupNeeded): ?> + <span id="pickUpLocationLabel"><strong><?=$this->transEsc("pick_up_location")?>: + <noscript> (<?=$this->transEsc("Please enable JavaScript.")?>)</noscript> + </strong></span> + <select id="pickUpLocation" name="gatheredDetails[pickUpLocation]" data-default="<?=$this->escapeHtml($selected)?>"> + <? if ($selected === false): ?> + <option value="" selected="selected"> + <?=$this->transEsc('select_pickup_location')?> + </option> + <? endif; ?> + </select> + <? elseif (count($this->pickup) > 1): ?> <strong><?=$this->transEsc("pick_up_location")?>:</strong><br/> <select name="gatheredDetails[pickUpLocation]"> <? if ($selected === false): ?> @@ -62,3 +101,9 @@ </form> </div> + +<script type="text/javascript"> +$(document).ready(function(){ + setUpHoldRequestForm('<?=$this->escapeHtml($this->driver->getUniqueId()) ?>'); +}); +</script> diff --git a/themes/bootprint/templates/myresearch/holds.phtml b/themes/bootprint/templates/myresearch/holds.phtml index f971db65f7c..6c4d7f12614 100644 --- a/themes/bootprint/templates/myresearch/holds.phtml +++ b/themes/bootprint/templates/myresearch/holds.phtml @@ -104,6 +104,11 @@ <br /> <? endif; ?> + <? if (!empty($ilsDetails['requestGroup'])): ?> + <strong><?=$this->transEsc('hold_request_group') ?>:</strong> <?=$this->transEsc('location_' . $ilsDetails['requestGroup'], array(), $ilsDetails['requestGroup'])?> + <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 = ''; ?> diff --git a/themes/bootstrap/js/hold.js b/themes/bootstrap/js/hold.js new file mode 100644 index 00000000000..70a0fbdd153 --- /dev/null +++ b/themes/bootstrap/js/hold.js @@ -0,0 +1,41 @@ +function setUpHoldRequestForm(recordId) { + $('#requestGroupId').change(function() { + var $emptyOption = $("#pickUpLocation option[value='']"); + $("#pickUpLocation option[value!='']").remove(); + if ($('#requestGroupId').val() === '') { + $('#pickUpLocation').attr('disabled', 'disabled'); + return; + } + $('#pickUpLocationLabel i').addClass("icon-spinner icon-spin"); + var params = { + method: 'getRequestGroupPickupLocations', + id: recordId, + requestGroupId: $('#requestGroupId').val() + }; + $.ajax({ + data: params, + dataType: 'json', + cache: false, + url: path + '/AJAX/JSON', + success: function(response) { + if (response.status == 'OK') { + var defaultValue = $('#pickUpLocation').data('default'); + $.each(response.data.locations, function() { + var option = $('<option></option>').attr('value', this.locationID).text(this.locationDisplay); + if (this.locationID == defaultValue || (defaultValue == '' && this.isDefault && $emptyOption.length == 0)) { + option.attr('selected', 'selected'); + } + $('#pickUpLocation').append(option); + }); + } + $('#pickUpLocationLabel i').removeClass("icon-spinner icon-spin"); + $('#pickUpLocation').removeAttr('disabled'); + }, + fail: function() { + $('#pickUpLocationLabel i').removeClass("icon-spinner icon-spin"); + $('#pickUpLocation').removeAttr('disabled'); + } + }); + }); + $('#requestGroupId').change(); +} diff --git a/themes/bootstrap/templates/myresearch/holds.phtml b/themes/bootstrap/templates/myresearch/holds.phtml index c37a9d5184a..5ccc7178aa5 100644 --- a/themes/bootstrap/templates/myresearch/holds.phtml +++ b/themes/bootstrap/templates/myresearch/holds.phtml @@ -104,6 +104,11 @@ <br /> <? endif; ?> + <? if (!empty($ilsDetails['requestGroup'])): ?> + <strong><?=$this->transEsc('hold_request_group') ?>:</strong> <?=$this->transEsc('location_' . $ilsDetails['requestGroup'], array(), $ilsDetails['requestGroup'])?> + <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 = ''; ?> diff --git a/themes/bootstrap/templates/record/hold.phtml b/themes/bootstrap/templates/record/hold.phtml index 131362674bb..c90edbf6a74 100644 --- a/themes/bootstrap/templates/record/hold.phtml +++ b/themes/bootstrap/templates/record/hold.phtml @@ -11,6 +11,7 @@ <?=$this->flashmessages()?> <div class="hold-form"> <form action="" class="form-horizontal" method="post" name="placeHold"> + <? if (in_array("comments", $this->extraHoldFields)): ?> <div class="control-group"> <label class="control-label"><?=$this->transEsc("Comments")?>:</label> @@ -30,21 +31,69 @@ </div> <? endif; ?> + <? $showRequestGroups = in_array("requestGroup", $this->extraHoldFields) + && (empty($this->gatheredDetails['level']) + || $this->gatheredDetails['level'] != 'copy'); + ?> + <? if ($this->requestGroupNeeded): ?> + <div class="control-group"> + <? + if (isset($this->gatheredDetails['requestGroupId']) && $this->gatheredDetails['requestGroupId'] !== "") { + $selected = $this->gatheredDetails['requestGroupId']; + } else { + $selected = $this->defaultRequestGroup; + } + ?> + <label class="control-label"><?=$this->transEsc("hold_request_group")?>:</label> + <div class="controls"> + <select id="requestGroupId" name="gatheredDetails[requestGroupId]"> + <? if ($selected === false): ?> + <option value="" selected="selected"> + <?=$this->transEsc('select_request_group')?> + </option> + <? endif; ?> + <? foreach ($this->requestGroups as $group): ?> + <option value="<?=$this->escapeHtml($group['id'])?>"<?=($selected == $group['id']) ? ' selected="selected"' : ''?>> + <?=$this->escapeHtml($group['name'])?> + </option> + <? endforeach; ?> + </select> + </div> + </div> + <? endif; ?> + <? if (in_array("pickUpLocation", $this->extraHoldFields)): ?> - <? if (count($this->pickup) > 1): ?> + <? + 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; + } + ?> + <? if ($this->requestGroupNeeded): ?> + <div class="control-group"> + <label id="pickUpLocationLabel" class="control-label"><i></i> <?=$this->transEsc("pick_up_location")?>: + <? if (in_array("requestGroup", $this->extraHoldFields)): ?> + <noscript> (<?=$this->transEsc("Please enable JavaScript.")?>)</noscript> + <? endif; ?> + </label> + <div class="controls"> + <select id="pickUpLocation" name="gatheredDetails[pickUpLocation]" data-default="<?=$this->escapeHtml($selected)?>"> + <? if ($selected === false): ?> + <option value="" selected="selected"> + <?=$this->transEsc('select_pickup_location')?> + </option> + <? endif; ?> + </select> + </div> + </div> + <? elseif (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]"> + <select id="pickUpLocation" name="gatheredDetails[pickUpLocation]"> <? if ($selected === false): ?> <option value="" selected="selected"> <?=$this->transEsc('select_pickup_location')?> @@ -69,3 +118,20 @@ </div> </form> </div> + +<? + // Set up hold script; we do this inline instead of in the header for lightbox compatibility: + $this->inlineScript()->appendFile('hold.js'); + + $js = <<<JS + if ($.isReady) { + setUpHoldRequestForm("{$this->escapeHtml($this->driver->getUniqueId())}"); + } else { + $(document).ready(function(){ + setUpHoldRequestForm("{$this->escapeHtml($this->driver->getUniqueId())}"); + }); + } +JS; + + echo $this->inlineScript()->appendScript($js); +?> diff --git a/themes/jquerymobile/css/styles.css b/themes/jquerymobile/css/styles.css index a86d3ce6838..18d5439e0fa 100644 --- a/themes/jquerymobile/css/styles.css +++ b/themes/jquerymobile/css/styles.css @@ -51,7 +51,7 @@ ul.comments .ui-li-aside { ul.comments p { white-space: normal; margin-top: .3em; - font-size: 14px;; + font-size: 14px; } ul.comments p.posted-by { font-size: 12px; @@ -64,7 +64,7 @@ ul.history .ui-icon-plus { } ul.history p { white-space: normal; - font-size: 12px;; + font-size: 12px; } .result { white-space: normal !important; @@ -204,6 +204,11 @@ div.footer-text { color:#ff890f; padding-left:18px } +.ajax_hold_request_loading { + background: url(../images/loading.gif) no-repeat left top; + padding:0 .5em .5em 20px; +} + .error, .alert, .info { text-align:center; padding:10px 0; diff --git a/themes/jquerymobile/js/hold.js b/themes/jquerymobile/js/hold.js new file mode 100644 index 00000000000..ae84317ff62 --- /dev/null +++ b/themes/jquerymobile/js/hold.js @@ -0,0 +1,44 @@ +function setUpHoldRequestForm(recordId) { + $('#requestGroupId').change(function() { + var $emptyOption = $("#pickUpLocation option[value='']"); + $("#pickUpLocation option[value!='']").remove(); + try { + $("#pickUpLocation").selectmenu("refresh", true); + } catch (e) {} + if ($('#requestGroupId').val() === '') { + return; + } + $('#pickUpLocationLabel').addClass("ajax_hold_request_loading"); + var params = { + method: 'getRequestGroupPickupLocations', + id: recordId, + requestGroupId: $('#requestGroupId').val() + }; + $.ajax({ + data: params, + dataType: 'json', + cache: false, + url: path + '/AJAX/JSON', + success: function(response) { + if (response.status == 'OK') { + var defaultValue = $('#pickUpLocation').data('default'); + $.each(response.data.locations, function() { + var option = $('<option></option>').attr('value', this.locationID).text(this.locationDisplay); + if (this.locationID == defaultValue || (defaultValue == '' && this.isDefault && $emptyOption.length == 0)) { + option.attr('selected', 'selected'); + } + $('#pickUpLocation').append(option); + }); + try { + $("#pickUpLocation").selectmenu("refresh", true); + } catch (e) {} + } + $('#pickUpLocationLabel').removeClass("ajax_hold_request_loading"); + }, + fail: function() { + $('#pickUpLocationLabel').removeClass("ajax_hold_request_loading"); + } + }); + }); + $('#requestGroupId').change(); +} diff --git a/themes/jquerymobile/templates/myresearch/holds.phtml b/themes/jquerymobile/templates/myresearch/holds.phtml index ed0533e61d2..7c07672843d 100644 --- a/themes/jquerymobile/templates/myresearch/holds.phtml +++ b/themes/jquerymobile/templates/myresearch/holds.phtml @@ -56,6 +56,13 @@ <p><strong><?=$this->transEsc('Year of Publication')?>:</strong> <?=$this->escapeHtml($ilsDetails['publication_year'])?></p> <? endif; ?> + <? if (!empty($ilsDetails['requestGroup'])): ?> + <p> + <strong><?=$this->transEsc('hold_request_group') ?>:</strong> + <?=$this->transEsc('location_' . $ilsDetails['requestGroup'], array(), $ilsDetails['requestGroup'])?> + </p> + <? endif; ?> + <? /* Depending on the ILS driver, the "location" value may be a string or an ID; figure out the best value to display... */ ?> <? $pickupDisplay = ''; ?> diff --git a/themes/jquerymobile/templates/record/hold.phtml b/themes/jquerymobile/templates/record/hold.phtml index b2e248d0b23..04bef4b36b4 100644 --- a/themes/jquerymobile/templates/record/hold.phtml +++ b/themes/jquerymobile/templates/record/hold.phtml @@ -1,4 +1,7 @@ <? + // Set up hold script: + $this->headScript()->appendFile("hold.js"); + // Set page title. $this->headTitle($this->translate('request_place_text') . ': ' . $this->driver->getBreadcrumb()); ?> @@ -25,18 +28,54 @@ </div> <? endif; ?> - <? if (in_array("pickUpLocation", $this->extraHoldFields)): ?> + <? if ($this->requestGroupNeeded): ?> <div> - <? if (count($this->pickup) > 1): ?> <? - if (isset($this->gatheredDetails['pickUpLocation']) && $this->gatheredDetails['pickUpLocation'] !== "") { - $selected = $this->gatheredDetails['pickUpLocation']; - } elseif (isset($this->homeLibrary) && $this->homeLibrary !== "") { - $selected = $this->homeLibrary; + if (isset($this->gatheredDetails['requestGroupId']) && $this->gatheredDetails['requestGroupId'] !== "") { + $selected = $this->gatheredDetails['requestGroupId']; } else { - $selected = $this->defaultPickup; + $selected = $this->defaultRequestGroup; } - ?> + ?> + <strong><?=$this->transEsc("hold_request_group")?>:</strong> + <select id="requestGroupId" name="gatheredDetails[requestGroupId]"> + <? if ($selected === false): ?> + <option value="" selected="selected"> + <?=$this->transEsc('select_request_group')?> + </option> + <? endif; ?> + <? foreach ($this->requestGroups as $group): ?> + <option value="<?=$this->escapeHtml($group['id'])?>"<?=($selected == $group['id']) ? ' selected="selected"' : ''?>> + <?=$this->transEsc('location_' . $group['name'], array(), $group['name'])?> + </option> + <? endforeach; ?> + </select> + </div> + <? endif; ?> + + <? if (in_array("pickUpLocation", $this->extraHoldFields)): ?> + <? + 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; + } + ?> + <? if ($this->requestGroupNeeded): ?> + <div> + <span id="pickUpLocationLabel"><strong><?=$this->transEsc("pick_up_location")?>: + <noscript> (<?=$this->transEsc("Please enable JavaScript.")?>)</noscript> + </strong></span> + <select id="pickUpLocation" name="gatheredDetails[pickUpLocation]" data-default="<?=$this->escapeHtml($selected)?>"> + <? if ($selected === false): ?> + <option value="" selected="selected"> + <?=$this->transEsc('select_pickup_location')?> + </option> + <? endif; ?> + </select> + <? elseif (count($this->pickup) > 1): ?> <strong><?=$this->transEsc("pick_up_location")?>:</strong><br/> <select name="gatheredDetails[pickUpLocation]"> <? if ($selected === false): ?> @@ -64,3 +103,9 @@ </div> <?=$this->mobileMenu()->footer()?> </div> + +<script type="text/javascript"> +$(document).bind("pageinit", function(){ + setUpHoldRequestForm('<?=$this->escapeHtml($this->driver->getUniqueId()) ?>'); +}); +</script> -- GitLab