diff --git a/code/reindexer/src/org/aspen_discovery/format_classification/MarcRecordFormatClassifier.java b/code/reindexer/src/org/aspen_discovery/format_classification/MarcRecordFormatClassifier.java index 6de12ed125..cf1cbcc485 100644 --- a/code/reindexer/src/org/aspen_discovery/format_classification/MarcRecordFormatClassifier.java +++ b/code/reindexer/src/org/aspen_discovery/format_classification/MarcRecordFormatClassifier.java @@ -254,7 +254,7 @@ public void getFormatFromPhysicalDescription(AbstractGroupedWorkSolr groupedWork } else if (audioDiscPattern.matcher(physicalDescriptionData).matches() && !(physicalDescriptionData.contains("cd player") || physicalDescriptionData.contains("cd boombox") || physicalDescriptionData.contains("cd boom box") || physicalDescriptionData.contains("cd/mp3 player"))) { //Check to see if there is a subfield e. If so, this could be a combined format Subfield subfieldE = field.getSubfield('e'); - if (subfieldE != null && subfieldE.getData().toLowerCase().contains("book")){ + if (subfieldE != null && subfieldE.getData().toLowerCase().contains("book") && !subfieldE.getData().toLowerCase().contains("booklet")){ if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Adding bib level format CD+Book based on 300 Physical Description", 2);} result.add("CD+Book"); }else{ @@ -363,6 +363,9 @@ public void getFormatFromSubjects(AbstractGroupedWorkSolr groupedWork, org.marc4 }else if (subfieldData.contains("pop-up")) { if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Adding bib level format Pop-UpBook based on 655 Genre", 2);} result.add("Pop-UpBook"); + }else if (subfieldData.equals("zines")) { + if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Adding bib level format Zines based on 655 Genre", 2);} + result.add("Zines"); }else if (subfieldData.startsWith("manga graphic novel") || subfieldData.equals("manga") || subfieldData.equals("manga.")) { if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Adding bib level format Manga based on 655 Genre", 2);} result.add("Manga"); diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/CarlXRecordProcessor.java b/code/reindexer/src/org/aspen_discovery/reindexer/CarlXRecordProcessor.java index 0c02459516..2125792004 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/CarlXRecordProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/CarlXRecordProcessor.java @@ -59,7 +59,7 @@ ItemInfoWithNotes createPrintIlsItem(AbstractGroupedWorkSolr groupedWork, Record if (shelfLocationField != null) { String shelfLocation = shelfLocationField.getData().toLowerCase(); //noinspection SpellCheckingInspection - if (shelfLocation.equals("xord")) { + if (shelfLocation.equalsIgnoreCase("xord")) { item.itemInfo.setIsOrderItem(); } } diff --git a/code/web/Drivers/Koha.php b/code/web/Drivers/Koha.php index 8992534702..bfed91a77e 100644 --- a/code/web/Drivers/Koha.php +++ b/code/web/Drivers/Koha.php @@ -3071,7 +3071,7 @@ function freezeHold($patron, $recordId, $itemToFreezeId, $dateToReactivate) : ar $result = [ 'success' => false, 'message' => translate([ - 'text' => 'Unable to freeze your hold.', + 'text' => 'Unable to freeze your hold. ', 'isPublicFacing' => true, ]), ]; @@ -3086,16 +3086,10 @@ function freezeHold($patron, $recordId, $itemToFreezeId, $dateToReactivate) : ar 'isPublicFacing' => true, ]); - $postParams = []; + $postParams = (object) []; if (strlen($dateToReactivate) > 0) { - $postParams = []; - [ - $year, - $month, - $day, - ] = explode('-', $dateToReactivate); - $postParams['end_date'] = "$year-$month-$day"; - } + $postParams['end_date'] = $dateToReactivate; + } $endpoint = "/api/v1/holds/$itemToFreezeId/suspension"; $extraHeaders = ['Accept-Encoding: gzip, deflate','x-koha-library: ' . $patron->getHomeLocationCode()]; @@ -3104,17 +3098,29 @@ function freezeHold($patron, $recordId, $itemToFreezeId, $dateToReactivate) : ar if ($response) { $holdResponse = $response['content']; if ($response['code'] != 201) { - $result['title'] = translate([ - 'text' => 'Hold frozen', - 'isPublicFacing' => true, - ]); - $result['message'] = translate([ - 'text' => $holdResponse['error'], - 'isPublicFacing' => true, - ]); - $result['success'] = false; - // Result for API or app use - $result['api']['message'] = $holdResponse['error']; + if (isset($holdResponse['error'])){ + $result['title'] = translate([ + 'text' => 'Hold frozen', + 'isPublicFacing' => true, + ]); + $result['message'] = translate([ + 'text' => $holdResponse['error'], + 'isPublicFacing' => true, + ]); + $result['success'] = false; + // Result for API or app use + $result['api']['message'] = $holdResponse['error']; + } elseif (isset($holdResponse['errors'])) { + foreach ($holdResponse['errors'] as $error) { + $result['message'] .= translate([ + 'text' => $error['message'], + 'isPublicFacing' => true, + ]) . '
'; + } + } else { + $result['message'] = $holdResponse; + } + } else { $result['message'] = translate([ 'text' => 'Your hold was frozen successfully.', diff --git a/code/web/Drivers/KohaApiUserAgent.php b/code/web/Drivers/KohaApiUserAgent.php index 9d2a245a85..2666407a30 100644 --- a/code/web/Drivers/KohaApiUserAgent.php +++ b/code/web/Drivers/KohaApiUserAgent.php @@ -77,7 +77,7 @@ public function get(string $endpoint, string $caller, array $dataToSanitize = [] * Recover specific data of it as properties. * * @param string $endpoint e.g "/api/v1/auth/password/validation" - * @param array $requestParameters e.g ['identifier' => $username,'password' => $password,] + * @param array|object $requestParameters e.g ['identifier' => $username,'password' => $password,] * @param string $caller e.g "koha.PatronLogin" * @param array $dataToSanitize e.g ['password' => $password] * @param array|null $extraHeaders e.g ['x-koha-embed: +strings,extended_attributes'] @@ -85,11 +85,11 @@ public function get(string $endpoint, string $caller, array $dataToSanitize = [] * @return array|bool An array containing the response body and response code retrieved by the request or false if authorization fails. * @access public */ - public function post(string $endpoint, array $requestParameters, string $caller, array $dataToSanitize = [], array $extraHeaders = null): array|bool { + public function post(string $endpoint, array|object $requestParameters, string $caller, array $dataToSanitize = [], array $extraHeaders = null): array|bool { // Preparing request $apiURL = $this->baseURL . $endpoint; $jsonEncodedParams = json_encode($requestParameters); - + if ($this->getAuthorizationHeader($caller)) { $this->apiCurlWrapper->addCustomHeaders([ $this->getAuthorizationHeader($caller) @@ -106,6 +106,7 @@ public function post(string $endpoint, array $requestParameters, string $caller, $response = $this->apiCurlWrapper->curlSendPage($apiURL, 'POST', $jsonEncodedParams); $responseCode = $this->apiCurlWrapper->getResponseCode(); $jsonResponse = $this->jsonValidate($response); + ExternalRequestLogEntry::logRequest($caller, 'POST', $apiURL, $this->apiCurlWrapper->getHeaders(), $jsonEncodedParams, $responseCode, $response, $dataToSanitize); return [ 'content' => $jsonResponse, diff --git a/code/web/Drivers/SirsiDynixROA.php b/code/web/Drivers/SirsiDynixROA.php index 41e56c51d7..cb288f9985 100644 --- a/code/web/Drivers/SirsiDynixROA.php +++ b/code/web/Drivers/SirsiDynixROA.php @@ -23,7 +23,7 @@ public function getWebServiceResponse($requestType, $url, $params = null, $sessi if (!empty($physicalLocation)) { $workingLibraryId = $physicalLocation->code; } - //If we still don't have a working library id, get the + //If we still don't have a working library id, get the first location for the library if (empty($workingLibraryId)) { $libraryLocations = $library->getLocations(); $firstLocation = reset($libraryLocations); @@ -901,7 +901,7 @@ protected function loginViaWebService($username, $password) { $password, ]); if ($loginUserResponse && isset($loginUserResponse->sessionToken)) { - //We got at valid user (A bad call will have isset($loginUserResponse->messageList) ) + //We got a valid user (A bad call will have isset($loginUserResponse->messageList) ) $sirsiRoaUserID = $loginUserResponse->patronKey; $sessionToken = $loginUserResponse->sessionToken; SirsiDynixROA::$sessionIdsForUsers[(string)$sirsiRoaUserID] = $sessionToken; @@ -1119,7 +1119,7 @@ public function getHolds($patron): array { //Get a list of holds for the user // (Call now includes Item information for when the hold is an item level hold.) - $includeFields = urlencode("holdRecordList{*,bib{title,author},selectedItem{call{*},itemType{*}}}"); + $includeFields = urlencode("holdRecordList{*,bib{title,author},selectedItem{call{*},itemType{*},barcode}}"); $patronHolds = $this->getWebServiceResponse('getHolds', $webServiceURL . '/user/patron/key/' . $patron->unique_ils_id . '?includeFields=' . $includeFields, null, $sessionToken); if ($patronHolds && isset($patronHolds->fields)) { require_once ROOT_DIR . '/RecordDrivers/MarcRecordDriver.php'; @@ -1538,8 +1538,7 @@ private function getSessionToken(User $patron) { if (UserAccount::isUserMasquerading()) { //If the user is masquerading, we will use the staff login since we might not have the patron PIN //list($userValid, $sessionToken) = $this->loginViaWebService(UserAccount::getGuidingUserObject()->cat_username, UserAccount::getGuidingUserObject()->cat_password); - $sessionToken = $this->getStaffSessionToken(); - return $sessionToken; + return $this->getStaffSessionToken(); } [ , @@ -3564,4 +3563,317 @@ public function showHoldPlacedDate(): bool { public function showDateInFines(): bool { return false; } + + public function hasAPICheckout() : bool { + return true; + } + + public function checkoutByAPI(User $patron, $barcode, Location $currentLocation): array { + $result = [ + 'success' => false, + 'message' => translate([ + 'text' => 'There was an error checking out this title.', + 'isPublicFacing' => true, + ]), + 'title' => translate([ + 'text' => 'Unable to checkout title', + 'isPublicFacing' => true, + ]), + 'api' => [ + 'title' => translate([ + 'text' => 'Unable to checkout title', + 'isPublicFacing' => true, + ]), + 'message' => translate([ + 'text' => 'There was an error checking out this title.', + 'isPublicFacing' => true, + ]), + ], + 'itemData' => [] + ]; + + //Find the correct stat group to use + $doCheckout = false; + $addOverrideCode = false; + + //Use the current location for the item + //To get the current location, we need to determine if the item is already on hold. + //If it is, make sure it is on hold for the active user and use the pickup_location + //If it is not on hold, use the current location for the item + $sessionToken = $this->getStaffSessionToken(); + if (!$sessionToken) { + return $result; + } + + //Now that we have the session token, get holds information + $webServiceURL = $this->getWebServiceURL(); + + $lookupItemResponse = $this->getWebServiceResponse('lookupItem', $webServiceURL . '/catalog/item/barcode/' . $barcode, null, $sessionToken); + if (empty($lookupItemResponse) || !empty($lookupItemResponse->messageList)) { + $result['message'] = translate([ + 'text' => 'Could not find an item with that barcode, unable to checkout item.', + 'isPublicFacing' => true, + ]); + $result['api']['message'] = translate([ + 'text' => 'Could not find an item with that barcode, unable to checkout item.', + 'isPublicFacing' => true, + ]); + }else{ + require_once ROOT_DIR . '/sys/AspenLiDA/SelfCheckSetting.php'; + $scoSettings = new AspenLiDASelfCheckSetting(); + $checkoutLocationSetting = $scoSettings->getCheckoutLocationSetting($currentLocation->code); + + if ($checkoutLocationSetting == 0) { + //Use the active location, no change needed + $doCheckout = true; + }elseif ($checkoutLocationSetting == 1) { + //Use home location for the user + $currentLocation = $patron->getHomeLocation(); + $doCheckout = true; + }else { + $doCheckout = true; + + $currentItemLocation = $lookupItemResponse->fields->currentLocation->key; + if ($currentItemLocation == 'CHECKEDOUT') { + $result['message'] = translate([ + 'text' => 'This title is already checked out, cannot check it out again.', + 'isPublicFacing' => true, + ]); + $result['api']['message'] = translate([ + 'text' => 'This title is already checked out, cannot check it out again.', + 'isPublicFacing' => true, + ]); + $doCheckout = false; + }elseif ($currentItemLocation == 'HOLDS') { + //The title is on the hold shelf, make sure it is on the hold shelf for the current patron + $doCheckout = false; + + $itemKey = $lookupItemResponse->key; + + //Get holds for the patron + $includeFields = urlencode("holdRecordList{*,bib{title,author},selectedItem{call{*},itemType{*},barcode}}"); + $patronHolds = $this->getWebServiceResponse('getHolds', $webServiceURL . '/user/patron/key/' . $patron->unique_ils_id . '?includeFields=' . $includeFields, null, $sessionToken); + if ($patronHolds && isset($patronHolds->fields)) { + foreach ($patronHolds->fields->holdRecordList as $hold) { + if (isset($hold->fields->status)) { + $holdStatus = strtolower($hold->fields->status); + if ($holdStatus == "being_held") { + $holdItemId = empty($hold->fields->item->key) ? '' : $hold->fields->item->key; + if ($holdItemId == $itemKey) { + $doCheckout = true; + //For titles that are on hold, we need to add an override. + $addOverrideCode = true; + $curPickupBranch = new Location(); + $curPickupBranch->code = $hold->fields->pickupLibrary->key; + if ($curPickupBranch->find(true)) { + $currentLocation = $curPickupBranch; + }else{ + //We didn't get a valid code, use the passed in location + } + break; + } + } + } + } + }else{ + $doCheckout = true; + $curPickupBranch = new Location(); + $curPickupBranch->code = $currentItemLocation; + if ($curPickupBranch->find(true)) { + $currentLocation = $curPickupBranch; + }else{ + //We didn't get a valid code, use the passed in location + } + } + + if (!$doCheckout) { + $result['message'] = translate([ + 'text' => 'This title is on hold for another user or is not available yet and cannot be checked out.', + 'isPublicFacing' => true, + ]); + $result['api']['message'] = translate([ + 'text' => 'This title is on hold for another user or is not available yet and cannot be checked out.', + 'isPublicFacing' => true, + ]); + } + } + } + } + + if ($doCheckout) { + $checkOutParams = [ + 'itemBarcode' => $barcode, + 'patronBarcode' => $patron->ils_barcode + ]; + + $additionalHeaders = [ + 'SD-Preferred-Role: STAFF' + ]; + //For titles that are on hold, we need to add an override. + if ($addOverrideCode && !empty($this->accountProfile->overrideCode)) { + $additionalHeaders[] = 'SD-Prompt-Return: CKOBLOCKS/' . $this->accountProfile->overrideCode; + } + + $checkOutResponse = $this->getWebServiceResponse('checkOutItem', $webServiceURL . '/circulation/circRecord/checkOut', $checkOutParams, $sessionToken, 'POST', $additionalHeaders, [], $currentLocation->code); + + if (!empty($checkOutResponse)) { + $checkOutMessage = ''; + if (!empty($checkOutResponse->messageList)){ + foreach ($checkOutResponse->messageList as $message) { + if (!empty($checkOutMessage)) { + $checkOutMessage .= '
'; + }else{ + $checkOutMessage .= translate([ + 'text' => $message->message, + 'isPublicFacing' => true, + ]); + } + } + } + + $result['message'] = $checkOutMessage; + if ($this->lastWebServiceResponseCode == 200) { + $result['success'] = true; + $result['api']['title'] = translate([ + 'text' => 'Check Out successful', + 'isPublicFacing' => true, + ]); + } + + $result['api']['message'] = $checkOutMessage; + } + } + + return $result; + } + + public function hasAPICheckIn() { + return true; + } + + public function checkInByAPI(User $patron, $barcode, Location $currentLocation): array { + $result = [ + 'success' => false, + 'message' => translate([ + 'text' => 'There was an error checking in this title.', + 'isPublicFacing' => true, + ]), + 'title' => translate([ + 'text' => 'Unable to check in title', + 'isPublicFacing' => true, + ]), + 'api' => [ + 'title' => translate([ + 'text' => 'Unable to check in title', + 'isPublicFacing' => true, + ]), + 'message' => translate([ + 'text' => 'There was an error checking in this title.', + 'isPublicFacing' => true, + ]), + ], + 'itemData' => [] + ]; + + //Find the correct stat group to use + $doCheckout = false; + require_once ROOT_DIR . '/sys/AspenLiDA/SelfCheckSetting.php'; + $scoSettings = new AspenLiDASelfCheckSetting(); + $checkInLocationSetting = $scoSettings->getCheckoutLocationSetting($currentLocation->code); + if ($checkInLocationSetting == 0) { + //Use the active location, no change needed + $doCheckIn = true; + }elseif ($checkInLocationSetting == 1) { + //Use home location for the user + $currentLocation = $patron->getHomeLocation(); + $doCheckIn = true; + }else { + $doCheckIn = true; + + } + + if ($doCheckIn) { + $sessionToken = $this->getStaffSessionToken(); + if (!$sessionToken) { + return $result; + } + + //Now that we have the session token, get holds information + $webServiceURL = $this->getWebServiceURL(); + + $lookupItemResponse = $this->getWebServiceResponse('lookupItem', $webServiceURL . '/catalog/item/barcode/' . $barcode, null, $sessionToken); + if (!empty($lookupItemResponse)) { + $currentLocation = $lookupItemResponse->fields->currentLocation->key; + if ($currentLocation !== 'CHECKEDOUT') { + $result['message'] = translate([ + 'text' => 'This title is not currently checked out. Cannot check it in.', + 'isPublicFacing' => true, + ]); + $result['api']['message'] = translate([ + 'text' => 'This title is not currently checked out. Cannot check it in.', + 'isPublicFacing' => true, + ]); + }else{ + $checkInParams = [ + 'itemBarcode' => $barcode + ]; + + $additionalHeaders = []; + + $checkInResponse = $this->getWebServiceResponse('checkInItem', $webServiceURL . '/circulation/circRecord/checkIn', $checkInParams, $sessionToken, 'POST', $additionalHeaders); + + if (!empty($checkInResponse)) { + $checkInMessage = ''; + if (!empty($checkInResponse->messageList)) { + foreach ($checkInResponse->messageList as $message) { + if (!empty($checkInMessage)) { + $checkInMessage .= '
'; + } else { + $checkInMessage .= translate([ + 'text' => $message->message, + 'isPublicFacing' => true, + ]); + } + } + } + + $result['message'] = $checkInMessage; + if ($this->lastWebServiceResponseCode == 200) { + $result['success'] = true; + $result['api']['title'] = translate([ + 'text' => 'Check in successful', + 'isPublicFacing' => true, + ]); + } + + $result['api']['message'] = $checkInMessage; + } + } + } + } + + return $result; + } + + public function describePath(string $path) : ?stdClass { + $sessionToken = $this->getStaffSessionToken(); + if (!$sessionToken) { + return null; + } + + //Now that we have the session token, get holds information + $webServiceURL = $this->getWebServiceURL(); + return $this->getWebServiceResponse('describePath', "$webServiceURL/$path/describe", null, $sessionToken); + } + + public function getRequest(string $path): ?stdClass { + $sessionToken = $this->getStaffSessionToken(); + if (!$sessionToken) { + return null; + } + + //Now that we have the session token, get holds information + $webServiceURL = $this->getWebServiceURL(); + return $this->getWebServiceResponse('describePath', "$webServiceURL/$path", null, $sessionToken); + } } \ No newline at end of file diff --git a/code/web/Drivers/marmot_inc/SearchSources.php b/code/web/Drivers/marmot_inc/SearchSources.php index 09c84b6d13..f06787af71 100644 --- a/code/web/Drivers/marmot_inc/SearchSources.php +++ b/code/web/Drivers/marmot_inc/SearchSources.php @@ -64,6 +64,7 @@ private static function getSearchSourcesDefault() { global $library; global $enabledModules; + /** @var Location $locationSingleton */ global $locationSingleton; $location = $locationSingleton->getActiveLocation(); if ($location != null && $location->useScope && $location->restrictSearchByLocation) { @@ -369,8 +370,8 @@ private static function getSearchSourcesDefault() { } /** - * @param $location - * @param $library + * @param Location $location + * @param Library $library * @return array */ static function getCombinedSearchSetupParameters($location, $library) { diff --git a/code/web/RecordDrivers/GroupedWorkSubDriver.php b/code/web/RecordDrivers/GroupedWorkSubDriver.php index 5b7e081af1..d573fd1159 100644 --- a/code/web/RecordDrivers/GroupedWorkSubDriver.php +++ b/code/web/RecordDrivers/GroupedWorkSubDriver.php @@ -426,7 +426,7 @@ public function getRatingData() { } /** - * @param Grouping_Record $relatedRecord + * @param null|Grouping_Record $relatedRecord * @param boolean $isAvailable * @param boolean $isHoldable * @param null|IlsVolumeInfo[] $volumeData diff --git a/code/web/interface/themes/responsive/Admin/symphonyApiTester.tpl b/code/web/interface/themes/responsive/Admin/symphonyApiTester.tpl new file mode 100644 index 0000000000..7fd4549b77 --- /dev/null +++ b/code/web/interface/themes/responsive/Admin/symphonyApiTester.tpl @@ -0,0 +1,42 @@ +{strip} +
+

Symphony API Tester

+

Get Endpoint documentation

+
+
+ + +
Enter the path to describe, i.e. user/patron The /describe at the end of the path should be omitted and there should not be a leading /.
+
+
+
+ +
+
+
+ {if !empty($describeResults)} +
+				{$describeResults|print_r}
+			
+ {/if} + +

GET request

+
+
+ + +
Enter the path to retrieve. I.e., catalog/item/barcode/barcode there should not be a leading /.
+
+
+
+ +
+
+
+ {if !empty($getRequestResults)} +
+				{$getRequestResults|print_r}
+			
+ {/if} +
+{/strip} \ No newline at end of file diff --git a/code/web/interface/themes/responsive/footer_responsive.tpl b/code/web/interface/themes/responsive/footer_responsive.tpl index f05c504764..1afaeee42f 100644 --- a/code/web/interface/themes/responsive/footer_responsive.tpl +++ b/code/web/interface/themes/responsive/footer_responsive.tpl @@ -3,7 +3,11 @@