diff --git a/.github/workflows/pull_request_qa.yml b/.github/workflows/pull_request_qa.yml index 144c3ef67d..e03a5a88cb 100644 --- a/.github/workflows/pull_request_qa.yml +++ b/.github/workflows/pull_request_qa.yml @@ -62,7 +62,7 @@ jobs: echo "Found modified file: $file"; if [[ $file == *.php || $file == *.js || $file == *.java ]]; then #echo "Checking $file for whitespace issues" - DIFF=$(git diff official/$DEFAULT_BRANCH HEAD -- $file) + DIFF=$(git diff official/$DEFAULT_BRANCH HEAD -- $file | grep "^\+\s* {2,}") #echo "DIFF: $DIFF" if [[ $DIFF =~ " " ]]; then echo "Detected spaces instead of tabs in $file" diff --git a/code/aspen_app/app-configs/version.json b/code/aspen_app/app-configs/version.json index e64102c80a..8556ad677b 100644 --- a/code/aspen_app/app-configs/version.json +++ b/code/aspen_app/app-configs/version.json @@ -1,5 +1,5 @@ { - "version": "24.08.00", - "build": "270", - "patch": "0" + "version": "24.09.00", + "build": "275", + "patch": "2" } \ No newline at end of file diff --git a/code/aspen_app/src/components/Action/ActionButton.js b/code/aspen_app/src/components/Action/ActionButton.js index 4ce971e257..ea439049d8 100644 --- a/code/aspen_app/src/components/Action/ActionButton.js +++ b/code/aspen_app/src/components/Action/ActionButton.js @@ -41,6 +41,8 @@ export const ActionButton = (data) => { setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, + userHasAlternateLibraryCard, + shouldPromptAlternateLibraryCard, } = data; if (_.isObject(action)) { if (action.type === 'overdrive_sample') { @@ -81,6 +83,9 @@ export const ActionButton = (data) => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} + recordSource={recordSource} /> ); } else if (action.type === 'vdx_request') { @@ -136,6 +141,9 @@ export const ActionButton = (data) => { cancelHoldConfirmationRef={cancelHoldConfirmationRef} holdConfirmationResponse={holdConfirmationResponse} setHoldConfirmationResponse={setHoldConfirmationResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} + recordSource={recordSource} /> ); } diff --git a/code/aspen_app/src/components/Action/AddAlternateLibraryCard.js b/code/aspen_app/src/components/Action/AddAlternateLibraryCard.js new file mode 100644 index 0000000000..92f56cfa15 --- /dev/null +++ b/code/aspen_app/src/components/Action/AddAlternateLibraryCard.js @@ -0,0 +1,223 @@ +import React from 'react'; +import _ from 'lodash'; +import { CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Heading, Button, ButtonGroup, ButtonText, SelectTrigger, SelectInput, SelectIcon, SelectPortal, SelectBackdrop, SelectContent, SelectDragIndicatorWrapper, SelectDragIndicator, SelectItem, Icon, ChevronDownIcon, ButtonSpinner, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; +import { LanguageContext, LibrarySystemContext, ThemeContext, UserContext } from '../../context/initialContext'; +import { getTermFromDictionary } from '../../translations/TranslationService'; +import { refreshProfile, updateAlternateLibraryCard } from '../../util/api/user'; +import { decodeHTML } from '../../util/apiAuth'; +import { completeAction } from '../../util/recordActions'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; +import { EyeOff, Eye } from 'lucide-react-native'; + +export const AddAlternateLibraryCard = (props) => { + const { + id, + title, + action, + volumeInfo, + holdTypeForFormat, + variationId, + prevRoute, + isEContent, + response, + setResponse, + responseIsOpen, + setResponseIsOpen, + onResponseClose, + cancelResponseRef, + holdConfirmationResponse, + setHoldConfirmationResponse, + holdConfirmationIsOpen, + setHoldConfirmationIsOpen, + onHoldConfirmationClose, + cancelHoldConfirmationRef, + holdSelectItemResponse, + setHoldSelectItemResponse, + holdItemSelectIsOpen, + setHoldItemSelectIsOpen, + onHoldItemSelectClose, + cancelHoldItemSelectRef, + recordSource, + activeAccount, + } = props; + + let isPlacingHold = false; + if (_.isObject(action)) { + isPlacingHold = action.includes('hold'); + } + + const { library } = React.useContext(LibrarySystemContext); + const { user, updateUser } = React.useContext(UserContext); + const { language } = React.useContext(LanguageContext); + const { theme, textColor, colorMode } = React.useContext(ThemeContext); + const queryClient = useQueryClient(); + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + const [showModal, setShowModal] = React.useState(true); + const [loading, setLoading] = React.useState(false); + + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + }; + + return ( + setShowModal(false)} closeOnOverlayClick={false} size="lg"> + + + + + {isPlacingHold ? getTermFromDictionary(language, 'hold_options') : getTermFromDictionary(language, 'checkout_options')} + + + + + + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/code/aspen_app/src/components/Action/CheckOut/CheckOut.js b/code/aspen_app/src/components/Action/CheckOut/CheckOut.js index ae467b92b2..594d1978e1 100644 --- a/code/aspen_app/src/components/Action/CheckOut/CheckOut.js +++ b/code/aspen_app/src/components/Action/CheckOut/CheckOut.js @@ -1,23 +1,27 @@ -import { Box, Button, ButtonSpinner, ButtonGroup, ButtonIcon, ButtonText, Text } from '@gluestack-ui/themed'; +import { Box, Button, ButtonSpinner, ButtonGroup, ButtonIcon, ButtonText, Text, Heading, Icon, CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; import React from 'react'; import _ from 'lodash'; import { useQueryClient } from '@tanstack/react-query'; +import { EyeOff, Eye } from 'lucide-react-native'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; // custom components and helper files import { LanguageContext, LibraryBranchContext, LibrarySystemContext, ThemeContext, UserContext } from '../../../context/initialContext'; +import { decodeHTML } from '../../../util/apiAuth'; import { completeAction } from '../../../util/recordActions'; -import { refreshProfile } from '../../../util/api/user'; +import { refreshProfile, updateAlternateLibraryCard } from '../../../util/api/user'; import { HoldPrompt } from '../Holds/HoldPrompt'; import { getTermFromDictionary } from '../../../translations/TranslationService'; export const CheckOut = (props) => { const queryClient = useQueryClient(); - const { id, title, type, record, prevRoute, response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef } = props; + const { id, title, type, record, prevRoute, response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef, userHasAlternateLibraryCard, shouldPromptAlternateLibraryCard } = props; const { user, updateUser, accounts } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); const { language } = React.useContext(LanguageContext); const [loading, setLoading] = React.useState(false); - const { theme } = React.useContext(ThemeContext); + const { theme, colorMode, textColor } = React.useContext(ThemeContext); const volumeInfo = { numItemsWithVolumes: 0, @@ -47,8 +51,145 @@ export const CheckOut = (props) => { cancelHoldConfirmationRef={cancelHoldConfirmationRef} holdConfirmationResponse={holdConfirmationResponse} setHoldConfirmationResponse={setHoldConfirmationResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> ); + } else if (shouldPromptAlternateLibraryCard && !userHasAlternateLibraryCard) { + const [showAddAlternateLibraryCardModal, setShowAddAlternateLibraryCardModal] = React.useState(false); + + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + setCard(''); + setPassword(''); + }; + return ( + <> + + setShowAddAlternateLibraryCardModal(false)} closeOnOverlayClick={false} size="lg"> + + + + + {getTermFromDictionary(language, 'add_alternate_library_card')} + + + + + + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + + + + + ); } else { return ( <> diff --git a/code/aspen_app/src/components/Action/Holds/HoldPrompt.js b/code/aspen_app/src/components/Action/Holds/HoldPrompt.js index 85a61cbee1..e8604ec715 100644 --- a/code/aspen_app/src/components/Action/Holds/HoldPrompt.js +++ b/code/aspen_app/src/components/Action/Holds/HoldPrompt.js @@ -1,9 +1,13 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import _ from 'lodash'; -import { CheckIcon, CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Heading, Select, Button, ButtonGroup, ButtonText, SelectTrigger, SelectInput, SelectIcon, SelectPortal, SelectBackdrop, SelectContent, SelectDragIndicatorWrapper, SelectDragIndicator, SelectItem, Icon, ChevronDownIcon, ButtonSpinner, SelectScrollView } from '@gluestack-ui/themed'; +import { CloseIcon, Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, FormControl, FormControlLabel, FormControlLabelText, Heading, Select, Button, ButtonGroup, ButtonText, SelectTrigger, SelectInput, SelectIcon, SelectPortal, SelectBackdrop, SelectContent, SelectDragIndicatorWrapper, SelectDragIndicator, SelectItem, Icon, ChevronDownIcon, ButtonSpinner, SelectScrollView, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; import React from 'react'; -import { Platform } from 'react-native'; +import { EyeOff, Eye } from 'lucide-react-native'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; import { HoldsContext, LibrarySystemContext, ThemeContext, UserContext } from '../../../context/initialContext'; +import { refreshProfile, updateAlternateLibraryCard } from '../../../util/api/user'; +import { decodeHTML } from '../../../util/apiAuth'; import { completeAction } from '../../../util/recordActions'; import { getTermFromDictionary } from '../../../translations/TranslationService'; import { getCopies } from '../../../util/api/item'; @@ -41,9 +45,14 @@ export const HoldPrompt = (props) => { setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, + recordSource, } = props; + + const [userHasAlternateLibraryCard, setUserHasAlternateLibraryCard] = React.useState(props.userHasAlternateLibraryCard ?? false); + const [promptAlternateLibraryCard, setPromptAlternateLibraryCard] = React.useState(props.shouldPromptAlternateLibraryCard ?? false); const [loading, setLoading] = React.useState(false); const [showModal, setShowModal] = React.useState(false); + const [showAddAlternateLibraryCardModal, setShowAddAlternateLibraryCardModal] = React.useState(false); const { user, updateUser, accounts, locations } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); @@ -117,8 +126,115 @@ export const HoldPrompt = (props) => { const [volume, setVolume] = React.useState(''); const [item, setItem] = React.useState(''); + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + const [activeAccount, setActiveAccount] = React.useState(user.id ?? ''); + const updateActiveAccount = (newId) => { + setActiveAccount(newId); + if (newId !== user.id) { + let newAccount = _.filter(accounts, ['id', newId]); + if (newAccount[0]) { + newAccount = newAccount[0]; + + // we need to recalculate if the linked account is eligible for using alternate library cards + if (newAccount) { + if (typeof newAccount.alternateLibraryCard !== 'undefined') { + const alternateLibraryCardOptions = newAccount?.alternateLibraryCardOptions ?? []; + if (alternateLibraryCardOptions) { + if (alternateLibraryCardOptions.showAlternateLibraryCard === '1' || alternateLibraryCardOptions.showAlternateLibraryCard === 1) { + if (recordSource === 'cloud_library' && (alternateLibraryCardOptions.useAlternateLibraryCardForCloudLibrary === '1' || alternateLibraryCardOptions.useAlternateLibraryCardForCloudLibrary === 1)) { + setPromptAlternateLibraryCard(true); + } + } + + if (newAccount.alternateLibraryCard && newAccount.alternateLibraryCard !== '') { + if (alternateLibraryCardOptions?.showAlternateLibraryCardPassword === '1') { + if (newAccount.alternateLibraryCardPassword !== '') { + setUserHasAlternateLibraryCard(true); + } else { + setUserHasAlternateLibraryCard(false); + } + } else { + setUserHasAlternateLibraryCard(true); + } + } else { + setUserHasAlternateLibraryCard(false); + } + + if (alternateLibraryCardOptions?.alternateLibraryCardLabel) { + cardLabel = alternateLibraryCardOptions.alternateLibraryCardLabel; + } + + if (alternateLibraryCardOptions?.alternateLibraryCardPasswordLabel) { + passwordLabel = alternateLibraryCardOptions.alternateLibraryCardPasswordLabel; + } + + if (alternateLibraryCardOptions?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(alternateLibraryCardOptions.alternateLibraryCardFormMessage); + } + + if (alternateLibraryCardOptions?.showAlternateLibraryCardPassword) { + if (alternateLibraryCardOptions.showAlternateLibraryCardPassword === '1' || alternateLibraryCardOptions.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + } else { + setUserHasAlternateLibraryCard(false); + setPromptAlternateLibraryCard(false); + } + } else { + setUserHasAlternateLibraryCard(false); + setPromptAlternateLibraryCard(false); + } + } + } + } else { + //revert back to primary user id + setUserHasAlternateLibraryCard(props.userHasAlternateLibraryCard); + setPromptAlternateLibraryCard(props.shouldPromptAlternateLibraryCard); + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + } + }; + let userPickupLocationId = user.pickupLocationId ?? user.homeLocationId; if (_.isNumber(user.pickupLocationId)) { userPickupLocationId = _.toString(user.pickupLocationId); @@ -144,6 +260,154 @@ export const HoldPrompt = (props) => { const [location, setLocation] = React.useState(pickupLocation); + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + setCard(''); + setPassword(''); + }; + + if (showAddAlternateLibraryCardModal) { + return ( + setShowAddAlternateLibraryCardModal(false)} closeOnOverlayClick={false} size="lg"> + + + + + {getTermFromDictionary(language, 'add_alternate_library_card')} + + + + + + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + + + + ); + } + return ( <> - + ) : ( + + }); + }}> + {loading ? : {title}} + + )} diff --git a/code/aspen_app/src/components/Action/Holds/PlaceHold.js b/code/aspen_app/src/components/Action/Holds/PlaceHold.js index 876fea5ca1..4e97cfe5ca 100644 --- a/code/aspen_app/src/components/Action/Holds/PlaceHold.js +++ b/code/aspen_app/src/components/Action/Holds/PlaceHold.js @@ -38,6 +38,8 @@ export const PlaceHold = (props) => { setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, + userHasAlternateLibraryCard, + shouldPromptAlternateLibraryCard, } = props; const { user, updateUser, accounts, locations } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); @@ -73,7 +75,7 @@ export const PlaceHold = (props) => { let promptForHoldNotifications = user.promptForHoldNotifications ?? false; let loadHoldPrompt = false; - if (volumeInfo.numItemsWithVolumes >= 1 || _.size(accounts) > 0 || _.size(locations) > 1 || promptForHoldNotifications || holdTypeForFormat === 'item' || holdTypeForFormat === 'either') { + if (volumeInfo.numItemsWithVolumes >= 1 || _.size(accounts) > 0 || _.size(locations) > 1 || promptForHoldNotifications || holdTypeForFormat === 'item' || holdTypeForFormat === 'either' || (shouldPromptAlternateLibraryCard && !userHasAlternateLibraryCard)) { loadHoldPrompt = true; } diff --git a/code/aspen_app/src/navigations/drawer/DrawerContent.js b/code/aspen_app/src/navigations/drawer/DrawerContent.js index db2c11a2f9..d312bef0e1 100644 --- a/code/aspen_app/src/navigations/drawer/DrawerContent.js +++ b/code/aspen_app/src/navigations/drawer/DrawerContent.js @@ -415,6 +415,7 @@ export const DrawerContent = () => { + @@ -811,6 +812,39 @@ const UserPreferences = () => { ); }; +const AlternateLibraryCard = () => { + const { library } = React.useContext(LibrarySystemContext); + const { language } = React.useContext(LanguageContext); + const version = formatDiscoveryVersion(library.discoveryVersion); + + let shouldShowAlternateLibraryCard = false; + if (typeof library.showAlternateLibraryCard !== 'undefined') { + shouldShowAlternateLibraryCard = library.showAlternateLibraryCard; + } + + if (version >= '24.09.00' && (shouldShowAlternateLibraryCard === '1' || shouldShowAlternateLibraryCard === 1)) { + return ( + { + navigateStack('LibraryCardTab', 'MyAlternateLibraryCard', { + prevRoute: 'AccountDrawer', + hasPendingChanges: false, + }); + }}> + + + {getTermFromDictionary(language, 'alternate_library_card')} + + + ); + } + + return null; +}; + const Fines = () => { const { user } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); diff --git a/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js b/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js index f51c4f27eb..534a62bb7d 100644 --- a/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js +++ b/code/aspen_app/src/navigations/stack/LibraryCardStackNavigator.js @@ -1,5 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; +import { MyAlternateLibraryCard } from '../../screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard'; import { MyLibraryCard } from '../../screens/MyAccount/MyLibraryCard/MyLibraryCard'; import { LanguageContext, LibrarySystemContext } from '../../context/initialContext'; @@ -24,6 +25,13 @@ const LibraryCardStackNavigator = () => { libraryContext: JSON.stringify(React.useContext(LibrarySystemContext)), }} /> + ); }; diff --git a/code/aspen_app/src/screens/GroupedWork/Editions.js b/code/aspen_app/src/screens/GroupedWork/Editions.js index d649256853..fe0fbfe656 100644 --- a/code/aspen_app/src/screens/GroupedWork/Editions.js +++ b/code/aspen_app/src/screens/GroupedWork/Editions.js @@ -17,7 +17,7 @@ import { stripHTML } from '../../util/apiAuth'; import { placeHold } from '../../util/recordActions'; import { getStatusIndicator } from './StatusIndicator'; import { ActionButton } from '../../components/Action/ActionButton'; -import { LanguageContext, LibrarySystemContext, ThemeContext } from '../../context/initialContext'; +import { LanguageContext, LibrarySystemContext, ThemeContext, UserContext } from '../../context/initialContext'; import { getTermFromDictionary } from '../../translations/TranslationService'; export const Editions = () => { @@ -28,6 +28,7 @@ export const Editions = () => { const params = route[0].params; const { id, recordId, format, source, volumeInfo, prevRoute } = params; const { library } = React.useContext(LibrarySystemContext); + const { user } = React.useContext(UserContext); const { language } = React.useContext(LanguageContext); const { colorMode, theme, textColor } = React.useContext(ThemeContext); const [isLoading, setLoading] = React.useState(false); @@ -54,6 +55,39 @@ export const Editions = () => { const [holdSelectItemResponse, setHoldSelectItemResponse] = React.useState(''); const [placingItemHold, setPlacingItemHold] = React.useState(false); + let shouldPromptAlternateLibraryCard = false; + let shouldShowAlternateLibraryCard = false; + let useAlternateCardForCloudLibrary = false; + let userHasAlternateLibraryCard = false; + + if (typeof library.showAlternateLibraryCard !== 'undefined') { + if (library.showAlternateLibraryCard === '1' || library.showAlternateLibraryCard === 1) { + shouldShowAlternateLibraryCard = true; + } + } + + if (typeof library.useAlternateCardForCloudLibrary !== 'undefined') { + if (library.useAlternateCardForCloudLibrary === '1' || library.useAlternateCardForCloudLibrary === 1) { + useAlternateCardForCloudLibrary = true; + } + } + + if (shouldShowAlternateLibraryCard && useAlternateCardForCloudLibrary && source === 'cloud_library') { + shouldPromptAlternateLibraryCard = true; + } + + if (typeof user.alternateLibraryCard !== 'undefined') { + if (user.alternateLibraryCard && user.alternateLibraryCard !== '') { + if (library.alternateLibraryCardConfig?.showAlternateLibraryCardPassword === '1') { + if (user.alternateLibraryCardPassword !== '') { + userHasAlternateLibraryCard = true; + } + } else { + userHasAlternateLibraryCard = true; + } + } + } + const handleNavigation = (action) => { if (prevRoute === 'DiscoveryScreen' || prevRoute === 'SearchResults' || prevRoute === 'HomeScreen') { if (action.includes('Checkouts')) { @@ -117,6 +151,8 @@ export const Editions = () => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> )} /> @@ -256,7 +292,7 @@ export const Editions = () => { const Edition = (payload) => { const { language } = React.useContext(LanguageContext); const { theme, textColor } = React.useContext(ThemeContext); - const { response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef, holdSelectItemResponse, setHoldSelectItemResponse, holdItemSelectIsOpen, setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef } = payload; + const { response, setResponse, responseIsOpen, setResponseIsOpen, onResponseClose, cancelResponseRef, holdConfirmationResponse, setHoldConfirmationResponse, holdConfirmationIsOpen, setHoldConfirmationIsOpen, onHoldConfirmationClose, cancelHoldConfirmationRef, holdSelectItemResponse, setHoldSelectItemResponse, holdItemSelectIsOpen, setHoldItemSelectIsOpen, onHoldItemSelectClose, cancelHoldItemSelectRef, userHasAlternateLibraryCard, shouldPromptAlternateLibraryCard } = payload; const prevRoute = payload.prevRoute; const records = payload.records; const id = payload.id; @@ -343,6 +379,8 @@ const Edition = (payload) => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> )} /> diff --git a/code/aspen_app/src/screens/GroupedWork/Variations.js b/code/aspen_app/src/screens/GroupedWork/Variations.js index 844583954c..0395889176 100644 --- a/code/aspen_app/src/screens/GroupedWork/Variations.js +++ b/code/aspen_app/src/screens/GroupedWork/Variations.js @@ -257,6 +257,7 @@ export const Variations = (props) => { }; const Variation = (payload) => { + const { user } = React.useContext(UserContext); const { library } = React.useContext(LibrarySystemContext); const { language } = React.useContext(LanguageContext); const { textColor, colorMode, theme } = React.useContext(ThemeContext); @@ -273,6 +274,39 @@ const Variation = (payload) => { const isbn = variation.isbn ?? null; const oclcNumber = variation.oclcNumber ?? null; + let shouldPromptAlternateLibraryCard = false; + let shouldShowAlternateLibraryCard = false; + let useAlternateCardForCloudLibrary = false; + let userHasAlternateLibraryCard = false; + + if (typeof library.showAlternateLibraryCard !== 'undefined') { + if (library.showAlternateLibraryCard === '1' || library.showAlternateLibraryCard === 1) { + shouldShowAlternateLibraryCard = true; + } + } + + if (typeof library.useAlternateCardForCloudLibrary !== 'undefined') { + if (library.useAlternateCardForCloudLibrary === '1' || library.useAlternateCardForCloudLibrary === 1) { + useAlternateCardForCloudLibrary = true; + } + } + + if (shouldShowAlternateLibraryCard && useAlternateCardForCloudLibrary && source === 'cloud_library') { + shouldPromptAlternateLibraryCard = true; + } + + if (typeof user.alternateLibraryCard !== 'undefined') { + if (user.alternateLibraryCard && user.alternateLibraryCard !== '') { + if (library.alternateLibraryCardConfig?.showAlternateLibraryCardPassword === '1') { + if (user.alternateLibraryCardPassword !== '') { + userHasAlternateLibraryCard = true; + } + } else { + userHasAlternateLibraryCard = true; + } + } + } + let fullRecordId = _.split(variation.id, ':'); const recordId = _.toString(fullRecordId[1]); @@ -357,6 +391,8 @@ const Variation = (payload) => { cancelHoldItemSelectRef={cancelHoldItemSelectRef} holdSelectItemResponse={holdSelectItemResponse} setHoldSelectItemResponse={setHoldSelectItemResponse} + userHasAlternateLibraryCard={userHasAlternateLibraryCard} + shouldPromptAlternateLibraryCard={shouldPromptAlternateLibraryCard} /> )} /> diff --git a/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard.js b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard.js new file mode 100644 index 0000000000..95880aebe2 --- /dev/null +++ b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyAlternateLibraryCard.js @@ -0,0 +1,186 @@ +import _ from 'lodash'; +import { EyeOff, Eye } from 'lucide-react-native'; +import { Pressable, ChevronLeftIcon, Box, ScrollView, ButtonGroup, Button, ButtonText, FormControl, FormControlLabel, FormControlLabelText, Input, InputField, InputSlot, InputIcon } from '@gluestack-ui/themed'; +import React from 'react'; +import { useWindowDimensions } from 'react-native'; +import RenderHtml from 'react-native-render-html'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRoute, useNavigation, CommonActions, StackActions } from '@react-navigation/native'; +import { LoadingSpinner } from '../../../components/loadingSpinner'; + +// custom components and helper files +import { LanguageContext, LibrarySystemContext, SystemMessagesContext, ThemeContext, UserContext } from '../../../context/initialContext'; +import { DisplaySystemMessage } from '../../../components/Notifications'; +import { BackIcon } from '../../../themes/theme'; +import { getTermFromDictionary } from '../../../translations/TranslationService'; +import { refreshProfile, updateAlternateLibraryCard } from '../../../util/api/user'; +import { decodeHTML } from '../../../util/apiAuth'; + +export const MyAlternateLibraryCard = () => { + const navigation = useNavigation(); + const route = useRoute(); + const { library } = React.useContext(LibrarySystemContext); + const { user, updateUser } = React.useContext(UserContext); + const { language } = React.useContext(LanguageContext); + const { theme, textColor, colorMode } = React.useContext(ThemeContext); + const queryClient = useQueryClient(); + const { systemMessages, updateSystemMessages } = React.useContext(SystemMessagesContext); + const { width } = useWindowDimensions(); + const [card, setCard] = React.useState(user?.alternateLibraryCard ?? ''); + const [password, setPassword] = React.useState(user?.alternateLibraryCardPassword ?? ''); + + const [isLoading, setIsLoading] = React.useState(false); + const [showPassword, setShowPassword] = React.useState(false); + const toggleShowPassword = () => setShowPassword(!showPassword); + + const handleGoBack = () => { + console.log(route?.params); + if (route?.params?.prevRoute === 'AccountDrawer') { + navigation.dispatch(CommonActions.setParams({ prevRoute: null })); + navigation.dispatch(StackActions.replace('LibraryCard')); + } else { + navigation.goBack(); + } + }; + + React.useLayoutEffect(() => { + navigation.setOptions({ + headerLeft: () => ( + + + + ), + }); + }, [navigation]); + let cardLabel = getTermFromDictionary(language, 'alternate_library_card'); + let passwordLabel = getTermFromDictionary(language, 'password'); + let formMessage = ''; + let showAlternateLibraryCardPassword = false; + let alternateLibraryCardStyle = 'none'; + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardLabel) { + cardLabel = library.alternateLibraryCardConfig.alternateLibraryCardLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardPasswordLabel) { + passwordLabel = library.alternateLibraryCardConfig.alternateLibraryCardPasswordLabel; + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardFormMessage) { + formMessage = decodeHTML(library.alternateLibraryCardConfig.alternateLibraryCardFormMessage); + } + + if (library?.alternateLibraryCardConfig?.showAlternateLibraryCardPassword) { + if (library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === '1' || library.alternateLibraryCardConfig.showAlternateLibraryCardPassword === 1) { + showAlternateLibraryCardPassword = true; + } + } + + if (library?.alternateLibraryCardConfig?.alternateLibraryCardStyle) { + alternateLibraryCardStyle = library.alternateLibraryCardConfig.alternateLibraryCardStyle; + } + + const showSystemMessage = () => { + if (_.isArray(systemMessages)) { + return systemMessages.map((obj, index, collection) => { + if (obj.showOn === '0' || obj.showOn === '1' || obj.showOn === '5') { + return ; + } + }); + } + return null; + }; + + const source = { + baseUrl: library.baseUrl, + html: formMessage, + }; + + const tagsStyles = { + body: { + color: textColor, + }, + a: { + color: textColor, + textDecorationColor: textColor, + }, + }; + + const deleteCard = async () => { + await updateAlternateLibraryCard('', '', true, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + }; + + const updateCard = async () => { + await updateAlternateLibraryCard(card, password, false, library.baseUrl, language); + await refreshProfile(library.baseUrl).then(async (result) => { + updateUser(result); + }); + setCard(''); + setPassword(''); + }; + + return ( + + {isLoading ? ( + LoadingSpinner() + ) : ( + + {showSystemMessage()} + + {formMessage ? : null} + + + + {cardLabel} + + + + setCard(value)} /> + + + {showAlternateLibraryCardPassword ? ( + + + + {passwordLabel} + + + + setPassword(value)} /> + + + + + + ) : null} + + + + + + + )} + + ); +}; \ No newline at end of file diff --git a/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js index 6a2e315a75..9927c0bc8f 100644 --- a/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js +++ b/code/aspen_app/src/screens/MyAccount/MyLibraryCard/MyLibraryCard.js @@ -15,8 +15,10 @@ import Carousel from 'react-native-reanimated-carousel'; // custom components and helper files import { PermissionsPrompt } from '../../../components/PermissionsPrompt'; import { LanguageContext, LibrarySystemContext, UserContext } from '../../../context/initialContext'; +import { navigateStack } from '../../../helpers/RootNavigator'; import { getTermFromDictionary, getTranslationsWithValues } from '../../../translations/TranslationService'; import { getLinkedAccounts, updateScreenBrightnessStatus } from '../../../util/api/user'; +import { formatDiscoveryVersion } from '../../../util/loadLibrary'; export const MyLibraryCard = () => { const queryClient = useQueryClient(); @@ -165,6 +167,15 @@ export const MyLibraryCard = () => { return ; } + const version = formatDiscoveryVersion(library.discoveryVersion); + let shouldShowAlternateLibraryCard = false; + if (typeof library.showAlternateLibraryCard !== 'undefined') { + shouldShowAlternateLibraryCard = library.showAlternateLibraryCard; + } + if (version >= '24.09.00' && (shouldShowAlternateLibraryCard === '1' || shouldShowAlternateLibraryCard === 1)) { + shouldShowAlternateLibraryCard = true; + } + /* useFocusEffect( React.useCallback(() => { console.log("numCards listener > " + numCards); @@ -193,6 +204,21 @@ export const MyLibraryCard = () => { return ( <> + {shouldShowAlternateLibraryCard ? ( +
+ +
+ ) : null} ); }; diff --git a/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js b/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js index 17e61a8e20..c5cccb0279 100644 --- a/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js +++ b/code/aspen_app/src/screens/MyAccount/TitlesOnHold/MyHold.js @@ -1,16 +1,16 @@ import { MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/native'; -import CachedImage from 'expo-cached-image'; +import DateTimePickerModal from 'react-native-modal-datetime-picker'; import { Image } from 'expo-image'; import _ from 'lodash'; -import { Actionsheet, Box, Button, Center, Checkbox, HStack, Icon, Pressable, Text, useDisclose, VStack } from 'native-base'; +import { Actionsheet, Box, Button, Center, Checkbox, HStack, Icon, Pressable, Text, useDisclose, VStack, useToken, useColorModeValue } from 'native-base'; import React from 'react'; import { popAlert } from '../../../components/loadError'; import { HoldsContext, LanguageContext, LibrarySystemContext, UserContext } from '../../../context/initialContext'; import { getAuthor, getBadge, getCleanTitle, getExpirationDate, getFormat, getOnHoldFor, getPickupLocation, getPosition, getStatus, getTitle, getType } from '../../../helpers/item'; import { navigateStack } from '../../../helpers/RootNavigator'; -import { getTermFromDictionary, getTranslationsWithValues } from '../../../translations/TranslationService'; -import { cancelHold, cancelHolds, cancelVdxRequest, thawHold, thawHolds } from '../../../util/accountActions'; +import { getTermFromDictionary } from '../../../translations/TranslationService'; +import { cancelHold, cancelHolds, cancelVdxRequest, freezeHold, freezeHolds, thawHold, thawHolds } from '../../../util/accountActions'; import { formatDiscoveryVersion } from '../../../util/loadLibrary'; import { checkoutItem } from '../../../util/recordActions'; import { SelectPickupLocation } from './SelectPickupLocation'; @@ -89,6 +89,9 @@ export const MyHold = (props) => { } } + const freezingHoldLabel = getTermFromDictionary(language, 'freezing_hold'); + const freezeHoldLabel = getTermFromDictionary(language, 'freeze_hold'); + const openGroupedWork = (item, title) => { navigateStack('AccountScreenTab', 'MyHold', { id: item, @@ -251,7 +254,7 @@ export const MyHold = (props) => { ); } else { - return ; + return ; } } else { return null; @@ -371,6 +374,8 @@ export const ManageSelectedHolds = (props) => { const numToFreezeLabel = getTermFromDictionary(language, 'freeze_selected_holds') + ' (' + numToFreeze + ')'; const numToThawLabel = getTermFromDictionary(language, 'thaw_selected_holds') + ' (' + numToThaw + ')'; const numSelectedLabel = getTermFromDictionary(language, 'manage_selected') + ' (' + numSelected + ')'; + const freezingHoldLabel = getTermFromDictionary(language, 'freezing_hold'); + const freezeHoldLabel = getTermFromDictionary(language, 'freeze_hold'); const cancelActionItem = () => { if (numToCancel > 0) { @@ -424,7 +429,7 @@ export const ManageSelectedHolds = (props) => { {cancelActionItem()} - + {thawActionItem()} @@ -489,6 +494,8 @@ export const ManageAllHolds = (props) => { const numToCancelLabel = getTermFromDictionary(language, 'cancel_all_holds') + ' (' + numToCancel + ')'; const numToFreezeLabel = getTermFromDictionary(language, 'freeze_all_holds') + ' (' + numToFreeze + ')'; const numToThawLabel = getTermFromDictionary(language, 'thaw_all_holds') + ' (' + numToThaw + ')'; + const freezingHoldLabel = getTermFromDictionary(language, 'freezing_hold'); + const freezeHoldLabel = getTermFromDictionary(language, 'freeze_hold'); if (numToManage >= 1) { return ( @@ -511,7 +518,7 @@ export const ManageAllHolds = (props) => { }}> {numToCancelLabel} - + { - const { label, language, libraryContext, onClose, freezeId, recordId, source, userId, resetGroup, isOpen } = props; + const { freezingLabel, freezeLabel, label, libraryContext, onClose, freezeId, recordId, source, userId, resetGroup, isOpen } = props; let data = props.data; + const { language } = React.useContext(LanguageContext); const [loading, setLoading] = React.useState(false); const textColor = useToken('colors', useColorModeValue('text.500', 'text.50')); const colorMode = useColorModeValue(false, true); - let actionLabel = getTermFromDictionary(language, 'freeze_hold'); + let actionLabel = freezeLabel; if (label) { actionLabel = label; } @@ -57,7 +59,7 @@ export const SelectThawDate = (props) => { } onPress={showDatePicker}> {actionLabel} - + ); }; \ No newline at end of file diff --git a/code/aspen_app/src/screens/Search/Facets/RadioGroup.js b/code/aspen_app/src/screens/Search/Facets/RadioGroup.js index 3be58f3f7e..3f0a399060 100644 --- a/code/aspen_app/src/screens/Search/Facets/RadioGroup.js +++ b/code/aspen_app/src/screens/Search/Facets/RadioGroup.js @@ -41,7 +41,9 @@ export default class Facet_RadioGroup extends Component { componentDidUpdate(prevProps, prevState) { if (prevState.value !== this.state.applied) { - this.renderValue(); + console.log('prevState.value', prevState.value); + console.log('this.state.applied', this.state.applied); + //this.renderValue(); } } @@ -59,17 +61,22 @@ export default class Facet_RadioGroup extends Component { const { category, value } = this.state; if (category !== 'sort_by') { console.log('payload > ', payload); + console.log('value > ', value); if (payload === value) { + console.log('new is same as old. removing.'); removeAppliedFilter(category, payload); this.setState({ value: '', }); } else { + console.log('new value. adding.'); addAppliedFilter(category, payload, false); this.setState({ value: payload, }); } + + console.log('current state value: ' + this.state.value); } else { console.log('payload > ', payload); console.log('value > ', value); @@ -94,6 +101,8 @@ export default class Facet_RadioGroup extends Component { const { items, category, title, updater, applied } = this.state; const name = category + '_group'; + console.log(items); + if (category === 'sort_by') { return ( diff --git a/code/aspen_app/src/translations/defaults.json b/code/aspen_app/src/translations/defaults.json index 53ad04bfd7..62a4ff3d57 100644 --- a/code/aspen_app/src/translations/defaults.json +++ b/code/aspen_app/src/translations/defaults.json @@ -570,5 +570,8 @@ "mark_as_unread": "Mark As Unread", "date_sent": "Date Sent", "sent_on": "Sent on %1%", - "open": "Open" + "open": "Open", + "alternate_library_card": "Alternate Library Card", + "manage_alternate_library_card": "Manage Alternate Library Card", + "add_alternate_library_card": "Add Alternate Library Card" } \ No newline at end of file diff --git a/code/aspen_app/src/util/api/user.js b/code/aspen_app/src/util/api/user.js index 47774941f5..099ca3285c 100644 --- a/code/aspen_app/src/util/api/user.js +++ b/code/aspen_app/src/util/api/user.js @@ -188,6 +188,42 @@ export async function logoutUser(url) { } } +/** + * Updates the users alternate library card + * @param {string} cardNumber + * @param {string} cardPassword + * @param {boolean} deleteCard + * @param {string} url + * @param {string} language + **/ +export async function updateAlternateLibraryCard(cardNumber = '', cardPassword = '', deleteCard = false, url, language = 'en') { + const postBody = await postData(); + postBody.append('alternateLibraryCard', cardNumber); + postBody.append('alternateLibraryCardPassword', cardPassword); + + const api = create({ + baseURL: url + '/API', + headers: getHeaders(true), + auth: createAuthTokens(), + params: { + deleteAlternateLibraryCard: deleteCard, + language, + }, + }); + + const response = await api.post('/UserAPI?method=updateAlternateLibraryCard', postBody); + let data = []; + if (response.ok) { + data = response.data; + } + + return { + success: data?.success ?? false, + title: data?.title ?? null, + message: data?.message ?? null, + }; +} + /** ******************************************************************* * Checkouts and Holds ******************************************************************* **/ diff --git a/code/aspen_app/yarn.lock b/code/aspen_app/yarn.lock index 7fe537a275..0d1e4bc41f 100644 --- a/code/aspen_app/yarn.lock +++ b/code/aspen_app/yarn.lock @@ -7481,7 +7481,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2: +braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -11687,11 +11687,11 @@ metro@0.80.8, metro@^0.80.3: yargs "^17.6.2" micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": diff --git a/code/symphony_export/src/com/turning_leaf_technologies/symphony/SymphonyExportMain.java b/code/symphony_export/src/com/turning_leaf_technologies/symphony/SymphonyExportMain.java index 7fd7d6d5f1..c74aac26e6 100644 --- a/code/symphony_export/src/com/turning_leaf_technologies/symphony/SymphonyExportMain.java +++ b/code/symphony_export/src/com/turning_leaf_technologies/symphony/SymphonyExportMain.java @@ -40,6 +40,8 @@ public class SymphonyExportMain { private static boolean hadErrors = false; + private static boolean volumesInTextFile = true; + public static void main(String[] args){ if (args.length == 0) { serverName = AspenStringUtils.getInputFromCommandLine("Please enter the server name"); @@ -534,18 +536,9 @@ private static void exportVolumes(Connection dbConn, IndexingProfile indexingPro try { VolumeUpdateInfo volumeUpdateInfo = new VolumeUpdateInfo(); logEntry.addNote("Updating Volumes, loading existing volumes from database"); - PreparedStatement allRecordsWithVolumesStmt = dbConn.prepareStatement("SELECT DISTINCT(recordId) from ils_volume_info where recordId like '" + profileToLoad + ":%'", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); PreparedStatement addVolumeStmt = dbConn.prepareStatement("INSERT INTO ils_volume_info (recordId, volumeId, displayLabel, relatedItems, displayOrder) VALUES (?,?,?,?, ?) ON DUPLICATE KEY update recordId = VALUES(recordId), displayLabel = VALUES(displayLabel), relatedItems = VALUES(relatedItems), displayOrder = VALUES(displayOrder)"); PreparedStatement deleteVolumeStmt = dbConn.prepareStatement("DELETE from ils_volume_info where recordId = ?"); - - //Get the existing records with volumes from the database, we will use this to figure out which records no longer have volumes - HashSet allRecordsWithVolumes = new HashSet<>(); - ResultSet allRecordsWithVolumesRS = allRecordsWithVolumesStmt.executeQuery(); - while (allRecordsWithVolumesRS.next()){ - allRecordsWithVolumes.add(allRecordsWithVolumesRS.getString("recordId") ); - } - allRecordsWithVolumesRS.close(); - allRecordsWithVolumesStmt.close(); + HashSet allRecordsWithVolumes = getAllRecordsWithVolumes(dbConn, profileToLoad); //Load all volumes in the export logEntry.addNote("Updating Volumes, loading volumes from the export"); @@ -666,11 +659,25 @@ private static void exportVolumes(Connection dbConn, IndexingProfile indexingPro logEntry.saveResults(); } }else{ + volumesInTextFile = false; logEntry.addNote("Volume export file (volumes.txt) did not exist in " + SymphonyExportMain.indexingProfile.getMarcPath()); logEntry.saveResults(); } } + private static HashSet getAllRecordsWithVolumes(Connection dbConn, String profileToLoad) throws SQLException { + PreparedStatement allRecordsWithVolumesStmt = dbConn.prepareStatement("SELECT DISTINCT(recordId) from ils_volume_info where recordId like '" + profileToLoad + ":%'", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + //Get the existing records with volumes from the database, we will use this to figure out which records no longer have volumes + HashSet allRecordsWithVolumes = new HashSet<>(); + ResultSet allRecordsWithVolumesRS = allRecordsWithVolumesStmt.executeQuery(); + while (allRecordsWithVolumesRS.next()){ + allRecordsWithVolumes.add(allRecordsWithVolumesRS.getString("recordId") ); + } + allRecordsWithVolumesRS.close(); + allRecordsWithVolumesStmt.close(); + return allRecordsWithVolumes; + } + private static void saveVolumes(String recordId, HashMap volumesForRecord, PreparedStatement addVolumeStmt, VolumeUpdateInfo volumeUpdateInfo, PreparedStatement deleteVolumeStmt) { //Update the database try { @@ -708,6 +715,35 @@ private static void saveVolumes(String recordId, HashMap vol } } + private static HashMap generateVolumesForRecord(org.marc4j.marc.Record bibRecord, RecordIdentifier recordIdentifier) { + HashMap volumesForRecord = new HashMap<>(); + List items = bibRecord.getDataFields(indexingProfile.getItemTag()); + for (int i = 0; i < items.size(); i++) { + Subfield volumeSubfield = items.get(i).getSubfield(indexingProfile.getVolume()); + if (volumeSubfield != null) { + String volume = volumeSubfield.getData(); + VolumeInfo curVolume; + + if (volumesForRecord.containsKey(volume)) { + curVolume = volumesForRecord.get(volume); + } else { + curVolume = new VolumeInfo(); + curVolume.bibNumber = recordIdentifier.toString(); + curVolume.volume = volume; + // Symphony doesn't have volume identifiers - just keys for each call number (format shortID:key, all numbers) + // But the key isn't in the MARC record - we will look it up later while placing the hold + // However we still need a unique volume ID, so use LOOKUP:recordId:displayVolume + curVolume.volumeIdentifier = "LOOKUP:" + recordIdentifier.getIdentifier() + ":" + volume; + curVolume.displayOrder = i + 1; + volumesForRecord.put(volume, curVolume); + } + curVolume.relatedItems.add(items.get(i).getSubfield('i').getData()); + } + } + + return volumesForRecord; + } + private static int updateRecords(Connection dbConn){ //Check to see if we should regroup all existing records try { @@ -943,6 +979,17 @@ private static int updateRecordsUsingMarcExtract(ArrayList exportedMarcFil } } + //Set up for adding volumes by item + String profileToLoad = "ils"; + VolumeUpdateInfo volumeUpdateInfo = new VolumeUpdateInfo(); + HashSet allRecordsWithVolumes = new HashSet(); + try { + logEntry.addNote("Updating Volumes, loading existing volumes from database"); + allRecordsWithVolumes = getAllRecordsWithVolumes(dbConn, profileToLoad); + } catch (Exception e){ + logEntry.incErrors("Error checking database for existing volumes: " + e); + } + GroupedWorkIndexer reindexer = getGroupedWorkIndexer(dbConn); for (File curBibFile : exportedMarcFiles) { logEntry.addNote("Processing file " + curBibFile.getAbsolutePath()); @@ -994,6 +1041,16 @@ private static int updateRecordsUsingMarcExtract(ArrayList exportedMarcFil marcStatus = reindexer.saveMarcRecordToDatabase(indexingProfile, recordNumber, curBib); } + // If volumes weren't provided in a text file, check for volumes in MARC data + if (!volumesInTextFile) { + HashMap volumesForRecord = generateVolumesForRecord(curBib, recordIdentifier); + if (!volumesForRecord.isEmpty() || allRecordsWithVolumes.contains(recordIdentifier.toString())) { + PreparedStatement addVolumeStmt = dbConn.prepareStatement("INSERT INTO ils_volume_info (recordId, volumeId, displayLabel, relatedItems, displayOrder) VALUES (?,?,?,?, ?) ON DUPLICATE KEY update recordId = VALUES(recordId), displayLabel = VALUES(displayLabel), relatedItems = VALUES(relatedItems), displayOrder = VALUES(displayOrder)"); + PreparedStatement deleteVolumeStmt = dbConn.prepareStatement("DELETE from ils_volume_info where recordId = ?"); + saveVolumes(recordIdentifier.toString(), volumesForRecord, addVolumeStmt, volumeUpdateInfo, deleteVolumeStmt); + } + } + marc245 = curBib.getDataField(245); if (marc245 != null) { if (marcStatus != GroupedWorkIndexer.MarcStatus.UNCHANGED || indexingProfile.isRunFullUpdate()) { diff --git a/code/symphony_export/symphony_export.jar b/code/symphony_export/symphony_export.jar index 7b7fb2eab5..3aaeda0ba7 100644 Binary files a/code/symphony_export/symphony_export.jar and b/code/symphony_export/symphony_export.jar differ diff --git a/code/web/.idea/dataSources.local.xml b/code/web/.idea/dataSources.local.xml index 171413b11b..5a58003517 100644 --- a/code/web/.idea/dataSources.local.xml +++ b/code/web/.idea/dataSources.local.xml @@ -1,6 +1,6 @@ - + #@ diff --git a/code/web/Drivers/Koha.php b/code/web/Drivers/Koha.php index 18e97a2a02..f4304268d6 100644 --- a/code/web/Drivers/Koha.php +++ b/code/web/Drivers/Koha.php @@ -2028,12 +2028,20 @@ public function placeVolumeHold(User $patron, $recordId, $volumeId, $pickupBranc } else { $apiUrl = $this->getWebServiceUrl() . "/api/v1/holds"; if ($this->getKohaVersion() >= 22.11) { - $postParams = [ - 'patron_id' => $patron->unique_ils_id, - 'pickup_library_id' => $pickupBranch, - 'item_group_id' => (int)$volumeId, - 'biblio_id' => $recordId, - ]; + if ($volumeId != 0){ + $postParams = [ + 'patron_id' => $patron->unique_ils_id, + 'pickup_library_id' => $pickupBranch, + 'item_group_id' => (int)$volumeId, + 'biblio_id' => $recordId, + ]; + } else { //if there is no item group id + $postParams = [ + 'patron_id' => $patron->unique_ils_id, + 'pickup_library_id' => $pickupBranch, + 'biblio_id' => $recordId, + ]; + } } else { $postParams = [ 'patron_id' => $patron->unique_ils_id, diff --git a/code/web/Drivers/Nashville.php b/code/web/Drivers/Nashville.php index 76d2fc81e0..2469b5a275 100644 --- a/code/web/Drivers/Nashville.php +++ b/code/web/Drivers/Nashville.php @@ -524,8 +524,8 @@ public function getHoldsReportData($location): array { ), item_level_holds as ( select - pb.branchname as PICKUP_BRANCH - , p.name as PATRON_NAME + p.name as PATRON_NAME + , pb.branchname as PICKUP_BRANCH , p.sponsor as HOME_ROOM , bb.btyname as GRD_LVL , p.patronid as P_BARCODE diff --git a/code/web/Drivers/OverDriveDriver.php b/code/web/Drivers/OverDriveDriver.php index 784f885943..04babdcbfb 100644 --- a/code/web/Drivers/OverDriveDriver.php +++ b/code/web/Drivers/OverDriveDriver.php @@ -132,7 +132,11 @@ public function getProductUrl($crossRefId) { if (substr($baseUrl, -1) != '/') { $baseUrl .= '/'; } - $baseUrl .= 'media/' . $crossRefId; + if (str_contains($baseUrl, 'lexisdl')) { + $baseUrl .= 'title/' . $crossRefId; + } else{ + $baseUrl .= 'media/' . $crossRefId; + } }else{ $baseUrl = ''; } diff --git a/code/web/Drivers/Sierra.php b/code/web/Drivers/Sierra.php index 7b80c35a9a..96425438e7 100644 --- a/code/web/Drivers/Sierra.php +++ b/code/web/Drivers/Sierra.php @@ -1402,7 +1402,9 @@ public function findNewUser($patronBarcode, $patronUsername) { } $forceDisplayNameUpdate = false; - $primaryName = reset($patronInfo->names); + if ($patronInfo->names != null) { + $primaryName = reset($patronInfo->names); + } if (strpos($primaryName, ',') !== false) { [ $lastName, @@ -1778,6 +1780,7 @@ public function selfRegister(): array { $params = []; if ($formFields != null) { + $params['patronType'] = $selfRegistrationForm->selfRegPatronCode; foreach ($formFields as $fieldObj){ $field = $fieldObj->ilsName; if ($field == 'firstName') { @@ -2453,6 +2456,9 @@ public function checkoutByAPI(User $patron, $barcode, Location $currentLocation) if (!empty($patron->ils_password)) { $params['patronPin'] = $patron->ils_password; } + if (!empty($currentLocation->circulationUsername)) { + $params['username'] = $currentLocation->circulationUsername; + } if (!empty($currentLocation->statGroup) && $currentLocation->statGroup != -1) { $params['statgroup'] = $currentLocation->statGroup; } @@ -2627,6 +2633,14 @@ public function checkInByAPI(User $patron, $barcode, Location $currentLocation): if (!empty($currentLocation->statGroup) && $currentLocation->statGroup != -1) { $sierraUrl .= '?statgroup=' . $currentLocation->statGroup; } + if (!empty($currentLocation->circulationUsername)) { + if (strpos($sierraUrl, '?') === false) { + $sierraUrl .= '?'; + }else{ + $sierraUrl .= '&'; + } + $sierraUrl .= 'username=' . $currentLocation->circulationUsername; + } $checkoutResult = $this->_sendPage( 'sierra.checkin', 'DELETE', $sierraUrl); if ($this->lastResponseCode >= 200 && $this->lastResponseCode < 300) { diff --git a/code/web/Drivers/SirsiDynixROA.php b/code/web/Drivers/SirsiDynixROA.php index 347566e438..b4c37159af 100644 --- a/code/web/Drivers/SirsiDynixROA.php +++ b/code/web/Drivers/SirsiDynixROA.php @@ -1314,6 +1314,13 @@ function placeSirsiHold($patron, $recordId, $itemId, $volume = null, $pickupBran ], ]; + //Check whether we need to look up the volume key because we are no longer getting it from a txt file + if (str_starts_with($volume, "LOOKUP")) { + $idParts = explode(":", $volume); + $displayVolume = $idParts[2] ?? ''; + $volume = $this->getMissingVolumeKey($webServiceURL, $shortId, $sessionToken, $displayVolume); + } + if (!empty($volume)) { $holdData['call'] = [ 'resource' => '/catalog/call', @@ -1486,6 +1493,23 @@ public function placeVolumeHold(User $patron, $recordId, $volumeId, $pickupBranc } } + private function getMissingVolumeKey($webServiceURL, $shortId, $sessionToken, $volume) { + // We need a call number key (formatted bibId:itemId) to place a volume hold, but the itemId isn't in the MARC record + // First get all the keys for the record + $numericId = str_replace('a', '', $shortId); + $getKeysForBib = $this->getWebServiceResponse('catalogBib', $webServiceURL . "/catalog/bib/key/" . $numericId . "?includeFields=callList", null, $sessionToken); + foreach ($getKeysForBib->fields->callList as $call) { + // Get the item that matches that key + $item = $this->getWebServiceResponse('catalogCall', $webServiceURL . "/catalog/call/key/" . $call->key, null, $sessionToken); + // Create a lookup array that returns the first key that matches the volume number + if ($item->fields->volumetric == $volume) { + return $item->key; + } + } + // Return a blank string if there was no matching volume key + return ""; + } + private function getSessionToken(User $patron) { if (UserAccount::isUserMasquerading()) { diff --git a/code/web/RecordDrivers/GroupedWorkDriver.php b/code/web/RecordDrivers/GroupedWorkDriver.php index 71c032e12c..7dd8ce12db 100644 --- a/code/web/RecordDrivers/GroupedWorkDriver.php +++ b/code/web/RecordDrivers/GroupedWorkDriver.php @@ -2,16 +2,14 @@ require_once ROOT_DIR . '/RecordDrivers/IndexRecordDriver.php'; require_once ROOT_DIR . '/sys/File/MARC.php'; -// require_once ROOT_DIR . '/RecordDrivers/MarcRecordDriver.php'; -// require_once ROOT_DIR . '/RecordDrivers/GroupedWorkSubDriver.php'; class GroupedWorkDriver extends IndexRecordDriver { - private $permanentId = null; - public $isValid = true; + private ?string $permanentId = null; + public bool $isValid = true; /** @var SearchObject_AbstractGroupedWorkSearcher */ - private static $recordLookupSearcher = null; + private static ?SearchObject_AbstractGroupedWorkSearcher $recordLookupSearcher = null; // private $marcRecordDriver; @@ -407,24 +405,75 @@ function compareRelatedRecords($a, $b) { } } + private ?GroupedWorkFormatSortingGroup $_formatSorting = null; /** * @param Grouping_Record $a * @param Grouping_Record $b * @return int */ function compareRelatedManifestations($a, $b) { - //First sort by format - $format1 = $a->format; - $format2 = $b->format; - $formatComparison = strcasecmp($format1, $format2); - //Make sure that book is the very first format always - if ($formatComparison != 0) { - if ($format1 == 'Book') { - return -1; - } elseif ($format2 == 'Book') { - return 1; + if ($this->_formatSorting == null) { + global $library; + $groupedWorkDisplaySettings = $library->getGroupedWorkDisplaySettings(); + $this->_formatSorting = $groupedWorkDisplaySettings->getFormatSortingGroup(); + } + $groupedWork = $this->getGroupedWorkObject(); + if ($groupedWork->grouping_category == 'book') { + $sortMethod = $this->_formatSorting->bookSortMethod; + }elseif ($groupedWork->grouping_category == 'comic') { + $sortMethod = $this->_formatSorting->comicSortMethod; + }elseif ($groupedWork->grouping_category == 'movie') { + $sortMethod = $this->_formatSorting->movieSortMethod; + }elseif ($groupedWork->grouping_category == 'music') { + $sortMethod = $this->_formatSorting->musicSortMethod; + }else{ + $sortMethod = $this->_formatSorting->otherSortMethod; + } + + if ($sortMethod === 1) { + //First sort by format + $format1 = $a->format; + $format2 = $b->format; + $formatComparison = strcasecmp($format1, $format2); + //Make sure that book is the very first format always + if ($formatComparison != 0) { + if ($format1 == 'Book') { + return -1; + } elseif ($format2 == 'Book') { + return 1; + } + } + }else{ + $weight1 = 999; + $weight2 = 999; + $sortFormats = $this->_formatSorting->getSortedFormats($groupedWork->grouping_category); + foreach ($sortFormats as $format) { + if ($format->format == $a->format) { + $weight1 = $format->weight; + }elseif ($format->format == $b->format) { + $weight2 = $format->weight; + } + } + + if ($weight1 < $weight2){ + $formatComparison = -1; + }elseif ($weight1 == $weight2){ + $format1 = $a->format; + $format2 = $b->format; + $formatComparison = strcasecmp($format1, $format2); + //Make sure that book is the very first format always + if ($formatComparison != 0) { + if ($format1 == 'Book') { + $formatComparison = -1; + } elseif ($format2 == 'Book') { + $formatComparison = 1; + } + } + }elseif ($weight1 > $weight2){ + $formatComparison = 1; } } + return $formatComparison; } @@ -893,6 +942,7 @@ public function getContributors() { function getDescription() { $description = null; $cleanIsbn = $this->getCleanISBN(); + /** @var Library $library */ global $library; if ($description == null) { $description = $this->getDescriptionFast(); @@ -1742,9 +1792,11 @@ public function getRelatedManifestations() { return $this->_relatedManifestations; } - private $relatedRecords = null; - private $childRecords = null; - private $relatedItemsByRecordId = null; + private ?array $relatedRecords = null; + + /** @noinspection PhpPropertyOnlyWrittenInspection */ + private ?array $childRecords = null; + private ?array $relatedItemsByRecordId = null; /** * @param bool $forCovers @@ -2160,6 +2212,25 @@ public function getShortTitle($useHighlighting = false) { } } + private GroupedWork|null|false $_groupedWork = false; + public function getGroupedWorkObject() : ?GroupedWork { + if ($this->_groupedWork === false) { + if (empty($this->getUniqueID())) { + $this->_groupedWork = null; + }else{ + require_once ROOT_DIR . '/sys/Grouping/GroupedWork.php'; + $this->_groupedWork = new GroupedWork(); + $this->_groupedWork->permanent_id = $this->getUniqueID(); + if (!$this->_groupedWork->find(true)) { + $this->_groupedWork = null; + } + } + + } + return $this->_groupedWork; + + } + /** * Assign necessary Smarty variables and return a template name to * load in order to display the full record information on the Staff @@ -2179,9 +2250,8 @@ public function getStaffView() { if (IPAddress::showDebuggingInformation()) { require_once ROOT_DIR . '/sys/Grouping/GroupedWork.php'; - $groupedWork = new GroupedWork(); - $groupedWork->permanent_id = $this->getUniqueID(); - if (!empty($groupedWork->permanent_id) && $groupedWork->find(true)) { + $groupedWork = $this->getGroupedWorkObject(); + if ( $groupedWork != null) { global $aspen_db; //Get the scopeId for the active scope global $solrScope; @@ -2290,6 +2360,7 @@ public function getSolrField($fieldName) { } public function loadSubjects() { + /** @var Library $library */ global $library; global $interface; @@ -3139,7 +3210,7 @@ protected function setupRelatedRecordDetails($recordDetails, $groupedWork, $time $i = 0; foreach ($this->relatedItemsByRecordId[$relatedRecord->id] as $curItem) { require_once ROOT_DIR . '/sys/Grouping/Item.php'; - $item = new Grouping_Item($curItem, $scopingInfo, $searchLocation, $library, false, false, false, false, false, false); + $item = new Grouping_Item($curItem, $scopingInfo, $searchLocation, $library, false, false, false, false, false, false, false); $relatedRecord->addItem($item); $description = $item->shelfLocation . ':' . $item->callNumber; @@ -3461,104 +3532,104 @@ public function formatGroupedWorkCitation() { if(is_array($format) && count($format) > 0) { $format = implode(', ', $format); - switch ($format) { - case 'book': - $format = 'BOOK'; - break; - case 'BOOK': - $format = 'BOOK'; - break; - case 'BOOKS': - $format = 'BOOK'; - break; - case 'Book': - $format = 'BOOK'; - break; - case 'books': - $format = 'BOOK'; - break; - case 'BK': - $format = 'BOOK'; - break; - case 'Books': - $format = 'BOOK'; - break; - case 'JOURNAL': - $format = 'BOOK'; - break; - case 'Journal Article': - $format = 'JOUR'; - break; - case 'JOURNAL ARTICLE': - $format = 'JOUR'; - break; - case 'Journal': - $format = 'BOOK'; - break; - case 'Audio-Visual': - $format = 'SOUND'; - break; - case 'AudioBook': - $format = 'SOUND'; - break; - case 'Catalog': - $format = 'CTLG'; - break; - case 'Dictionary': - $format = 'DICT'; - break; - case 'Electronic Article': - $format = 'EJOUR'; - break; - case 'Electronic Book': - $format = 'EBOOK'; - break; - case 'E-Book': - $format = 'EBOOK'; - break; - case 'Magazine': - $format = 'MGZN'; - break; - case 'Magazine Article': - $format = 'MGZN'; - break; - case 'Music': - $format = 'MUSIC'; - break; - case 'MUSIC': - $format = 'MUSIC'; - break; - case 'Newspaper': - $format = 'NEWS'; - break; - case 'Newspaper Article': - $format = 'NEWS'; - break; - case 'Web Page': - $format = 'ELEC'; - break; - case 'Visual Materials': - $format = 'VIDEO'; - break; - case 'Movie': - $format = 'VIDEO'; - break; - case 'Movie -- DVD': - $format = 'VIDEO'; - break; - case 'Movie -- VHS': - $format = 'VIDEO'; - break; - case 'Electronic Database': - $format = 'EBOOK'; + switch ($format) { + case 'book': + $format = 'BOOK'; break; - case 'Reference': - $format = 'BOOK'; - break; - } + case 'BOOK': + $format = 'BOOK'; + break; + case 'BOOKS': + $format = 'BOOK'; + break; + case 'Book': + $format = 'BOOK'; + break; + case 'books': + $format = 'BOOK'; + break; + case 'BK': + $format = 'BOOK'; + break; + case 'Books': + $format = 'BOOK'; + break; + case 'JOURNAL': + $format = 'BOOK'; + break; + case 'Journal Article': + $format = 'JOUR'; + break; + case 'JOURNAL ARTICLE': + $format = 'JOUR'; + break; + case 'Journal': + $format = 'BOOK'; + break; + case 'Audio-Visual': + $format = 'SOUND'; + break; + case 'AudioBook': + $format = 'SOUND'; + break; + case 'Catalog': + $format = 'CTLG'; + break; + case 'Dictionary': + $format = 'DICT'; + break; + case 'Electronic Article': + $format = 'EJOUR'; + break; + case 'Electronic Book': + $format = 'EBOOK'; + break; + case 'E-Book': + $format = 'EBOOK'; + break; + case 'Magazine': + $format = 'MGZN'; + break; + case 'Magazine Article': + $format = 'MGZN'; + break; + case 'Music': + $format = 'MUSIC'; + break; + case 'MUSIC': + $format = 'MUSIC'; + break; + case 'Newspaper': + $format = 'NEWS'; + break; + case 'Newspaper Article': + $format = 'NEWS'; + break; + case 'Web Page': + $format = 'ELEC'; + break; + case 'Visual Materials': + $format = 'VIDEO'; + break; + case 'Movie': + $format = 'VIDEO'; + break; + case 'Movie -- DVD': + $format = 'VIDEO'; + break; + case 'Movie -- VHS': + $format = 'VIDEO'; + break; + case 'Electronic Database': + $format = 'EBOOK'; + break; + case 'Reference': + $format = 'BOOK'; + break; + } - $risFields[] = "TY - ".$format; - } + $risFields[] = "TY - ".$format; + } //RIS Tag: AU - Author $authors = array(); $primaryAuthor = $this->getPrimaryAuthor(); @@ -3573,9 +3644,9 @@ public function formatGroupedWorkCitation() { if (!empty($authors)) { foreach ($authors as $author){ - $risFields[] = "AU - " . $author; + $risFields[] = "AU - " . $author; } - } + } // RIS Tag: TI - Title $title = $this->getTitle(); @@ -3613,23 +3684,23 @@ public function formatGroupedWorkCitation() { } } - // //RIS Tag: ET - Editions - $editions = $this->getEdition(); - if(is_array($editions) && count($editions) > 0) { - $editions = implode(', ', $editions); + // //RIS Tag: ET - Editions + $editions = $this->getEdition(); + if(is_array($editions) && count($editions) > 0) { + $editions = implode(', ', $editions); + $risFields[] = "ET - ".$editions; + } else { + if(!empty($editions)) { $risFields[] = "ET - ".$editions; - } else { - if(!empty($editions)) { - $risFields[] = "ET - ".$editions; - } } + } - //RIS UR - URL - $url = $this->getRecordUrl(); - if(is_array($url) && count($url) > 0) { - $url = implode(', ', $url); - $risFields[] = "UR - ".$url; - } + //RIS UR - URL + $url = $this->getRecordUrl(); + if(is_array($url) && count($url) > 0) { + $url = implode(', ', $url); + $risFields[] = "UR - ".$url; + } //RIS Tag: N1 - Info $notes = $this->getTableOfContentsNotes(); @@ -3637,8 +3708,8 @@ public function formatGroupedWorkCitation() { $notes = implode(', ', $notes); $risFields[] = "N1 - ".$notes; }else{ - if(!empty($notes)) { - $risFields[] = "N1 - ".$notes; + if(!empty($notes)) { + $risFields[] = "N1 - ".$notes; } } diff --git a/code/web/RecordDrivers/MarcRecordDriver.php b/code/web/RecordDrivers/MarcRecordDriver.php index 606249ed5f..956c3f35d2 100644 --- a/code/web/RecordDrivers/MarcRecordDriver.php +++ b/code/web/RecordDrivers/MarcRecordDriver.php @@ -1598,6 +1598,7 @@ public function getUPCs() { public function getMoreDetailsOptions() { global $interface; + /** @var Library $library */ global $library; $isbn = $this->getCleanISBN(); @@ -2431,7 +2432,7 @@ public function loadPeriodicalInformation() { } foreach ($issueSummaries as $key => $issueSummary) { if (isset($issueSummary['holdings']) && is_array($issueSummary['holdings'])) { - krsort($issueSummary['holdings']); + krsort($issueSummary['holdings'], SORT_NATURAL); $issueSummaries[$key] = $issueSummary; } } diff --git a/code/web/bootstrap.php b/code/web/bootstrap.php index 2a9a2d6d82..0d89618537 100644 --- a/code/web/bootstrap.php +++ b/code/web/bootstrap.php @@ -81,6 +81,11 @@ if (strlen($userAgentString) > 512) { $userAgentString = substr($userAgentString, 0, 512); } + if (isSpammyUserAgent($userAgentString)) { + http_response_code(404); + echo("Page Not Found

404

We're sorry, but the page you are looking for can't be found.

"); + die(); + } $userAgent->userAgent = $userAgentString; if ($userAgent->find(true)) { $userAgentId = $userAgent->id; @@ -431,4 +436,52 @@ function getGitBranch() { } return $branchName; +} + +//Look for spammy user agents and kill them +function isSpammyUserAgent($userAgentString): bool { + if (stripos($userAgentString, 'DBMS_PIPE.RECEIVE_MESSAGE') !== false) { + return true; + } elseif (stripos($userAgentString, 'PG_SLEEP') !== false) { + return true; + } elseif (stripos($userAgentString, 'SELECT') !== false) { + return true; + } elseif (stripos($userAgentString, 'SLEEP') !== false) { + return true; + } elseif (stripos($userAgentString, 'ORDER BY') !== false) { + return true; + } elseif (stripos($userAgentString, 'WAITFOR') !== false) { + return true; + } elseif (stripos($userAgentString, 'nvOpzp') !== false) { + return true; + } elseif (stripos($userAgentString, 'window.location') !== false) { + return true; + } elseif (stripos($userAgentString, 'window.top') !== false) { + return true; + } elseif (stripos($userAgentString, 'nslookup') !== false) { + return true; + } elseif (stripos($userAgentString, 'if(') !== false) { + return true; + } elseif (stripos($userAgentString, 'now(') !== false) { + return true; + } elseif (stripos($userAgentString, 'sysdate()') !== false) { + return true; + } elseif (stripos($userAgentString, 'sleep(') !== false) { + return true; + } elseif (stripos($userAgentString, 'cast(') !== false) { + return true; + } elseif (stripos($userAgentString, 'current_database') !== false) { + return true; + } elseif (stripos($userAgentString, 'response.write') !== false) { + return true; + } elseif (stripos($userAgentString, 'CONVERT(') !== false) { + return true; + } elseif (stripos($userAgentString, 'EXTRACTVALUE(') !== false) { + return true; + } + $termWithoutTags = strip_tags($userAgentString); + if ($termWithoutTags != $userAgentString) { + return true; + } + return false; } \ No newline at end of file diff --git a/code/web/init_oauth.php b/code/web/init_oauth.php index d57da3204b..8135e0a10c 100644 --- a/code/web/init_oauth.php +++ b/code/web/init_oauth.php @@ -5,6 +5,7 @@ require_once ROOT_DIR . '/sys/Authentication/OAuthAuthentication.php'; global $logger; +/** @var Library $library */ global $library; $auth = new OAuthAuthentication(); diff --git a/code/web/interface/themes/responsive/Admin/adminMenu.tpl b/code/web/interface/themes/responsive/Admin/adminMenu.tpl index f1a83f2a4a..386c1b695c 100644 --- a/code/web/interface/themes/responsive/Admin/adminMenu.tpl +++ b/code/web/interface/themes/responsive/Admin/adminMenu.tpl @@ -7,8 +7,7 @@