diff --git a/code/axis_360_export/axis_360_export.jar b/code/axis_360_export/axis_360_export.jar index db63e861e1..091e27ff3e 100644 Binary files a/code/axis_360_export/axis_360_export.jar and b/code/axis_360_export/axis_360_export.jar differ diff --git a/code/carlx_export/carlx_export.jar b/code/carlx_export/carlx_export.jar index a975b0bed8..cb2d2a6469 100644 Binary files a/code/carlx_export/carlx_export.jar and b/code/carlx_export/carlx_export.jar differ diff --git a/code/cloud_library_export/cloud_library_export.jar b/code/cloud_library_export/cloud_library_export.jar index ad67acb641..a667c2c3e5 100644 Binary files a/code/cloud_library_export/cloud_library_export.jar and b/code/cloud_library_export/cloud_library_export.jar differ diff --git a/code/course_reserves_indexer/course_reserves_indexer.jar b/code/course_reserves_indexer/course_reserves_indexer.jar index aae6351803..36733e4368 100644 Binary files a/code/course_reserves_indexer/course_reserves_indexer.jar and b/code/course_reserves_indexer/course_reserves_indexer.jar differ diff --git a/code/cron/cron.jar b/code/cron/cron.jar index 111eae5ad8..cbbcac4dc5 100644 Binary files a/code/cron/cron.jar and b/code/cron/cron.jar differ diff --git a/code/events_indexer/events_indexer.jar b/code/events_indexer/events_indexer.jar index cd9a80f33c..fad38d5889 100644 Binary files a/code/events_indexer/events_indexer.jar and b/code/events_indexer/events_indexer.jar differ diff --git a/code/events_indexer/src/com/turning_leaf_technologies/events/AspenEventsIndexer.java b/code/events_indexer/src/com/turning_leaf_technologies/events/AspenEventsIndexer.java index 4eca6357d7..20d748e9cd 100644 --- a/code/events_indexer/src/com/turning_leaf_technologies/events/AspenEventsIndexer.java +++ b/code/events_indexer/src/com/turning_leaf_technologies/events/AspenEventsIndexer.java @@ -20,18 +20,21 @@ public class AspenEventsIndexer { private final long settingsId; + @SuppressWarnings("FieldCanBeLocal") private final String name; private final int numberOfDaysToIndex; private final boolean runFullUpdate; + @SuppressWarnings("FieldCanBeLocal") private final long lastUpdateOfAllEvents; private final long lastUpdateOfChangedEvents; private final Connection aspenConn; private final EventsIndexerLogEntry logEntry; private final HashMap eventInstances = new HashMap<>(); - private final HashSet librariesToShowFor = new HashSet<>(); + private final HashSet librariesToShowAllFor = new HashSet<>(); + private final HashMap librariesToShowSeparatelyFor = new HashMap<>(); + private final HashMap libraryIdsByLocation = new HashMap<>(); private final static CRC32 checksumCalculator = new CRC32(); private final String coverPath; - private final List idsToDelete = new ArrayList<>(); private final ConcurrentUpdateHttp2SolrClient solrUpdateServer; @@ -59,6 +62,10 @@ public class AspenEventsIndexer { private final SimpleDateFormat eventWeekFormatter = new SimpleDateFormat("yyyy-ww"); private final SimpleDateFormat eventMonthFormatter = new SimpleDateFormat("yyyy-MM"); private final SimpleDateFormat eventYearFormatter = new SimpleDateFormat("yyyy"); + + /** + * Load events for the given settings ID + */ private void loadEvents() { try { // Calculate date for numberOfDaysToIndex into the future to add to where statement @@ -66,33 +73,42 @@ private void loadEvents() { lastDateToIndex.setTime(new Date()); lastDateToIndex.add(DAY_OF_YEAR, this.numberOfDaysToIndex); - // Get total number of events for log - PreparedStatement eventCountStmt = aspenConn.prepareStatement("SELECT COUNT(*) FROM event_instance LEFT JOIN event ON event_instance.eventId = event.id WHERE event_instance.deleted = 0 AND event.locationID IN (SELECT locationId from location_events_setting WHERE settingId = ?);"); - eventCountStmt.setLong(1, settingsId); + // Get the total number of events to update the log + PreparedStatement eventCountStmt = aspenConn.prepareStatement("SELECT COUNT(*) FROM event_instance LEFT JOIN event ON event_instance.eventId = event.id WHERE event_instance.deleted = 0;"); ResultSet eventCountRS = eventCountStmt.executeQuery(); if (eventCountRS.next()) { logEntry.incNumEvents(eventCountRS.getInt("COUNT(*)")); } - PreparedStatement getLibraryScopesStmt = aspenConn.prepareStatement("SELECT subdomain from library inner join library_events_setting on library.libraryId = library_events_setting.libraryId WHERE settingSource = 'aspenEvents' AND settingId = ?"); - getLibraryScopesStmt.setLong(1, settingsId); - ResultSet getLibraryScopesRS = getLibraryScopesStmt.executeQuery(); - while (getLibraryScopesRS.next()){ - librariesToShowFor.add(getLibraryScopesRS.getString("subdomain").toLowerCase()); + PreparedStatement getLibraryIdsForLocationsStmt = aspenConn.prepareStatement("SELECT locationId, libraryId FROM location"); + ResultSet getLibraryIdsForLocationsRS = getLibraryIdsForLocationsStmt.executeQuery(); + while (getLibraryIdsForLocationsRS.next()) { + libraryIdsByLocation.put(getLibraryIdsForLocationsRS.getLong("locationId"), getLibraryIdsForLocationsRS.getLong("libraryId")); + } + + PreparedStatement getLibrariesToShowAllEventsForStmt = aspenConn.prepareStatement("SELECT library.libraryId, subdomain from library WHERE aspenEventsToInclude = 1"); + ResultSet getLibrariesToShowAllEventsForRS = getLibrariesToShowAllEventsForStmt.executeQuery(); + //Load a list of all libraries that want to see all events + while (getLibrariesToShowAllEventsForRS.next()){ + librariesToShowAllFor.add(getLibrariesToShowAllEventsForRS.getString("subdomain").toLowerCase()); + } + //Load a list of libraries that want to see events for their library only + PreparedStatement getLibrariesToShowLocalEventsForStmt = aspenConn.prepareStatement("SELECT library.libraryId, subdomain from library WHERE aspenEventsToInclude = 2"); + ResultSet getLibrariesToShowLocalEventsForRS = getLibrariesToShowLocalEventsForStmt.executeQuery(); + while (getLibrariesToShowLocalEventsForRS.next()){ + librariesToShowSeparatelyFor.put(getLibrariesToShowLocalEventsForRS.getLong("libraryId"), getLibrariesToShowLocalEventsForRS.getString("subdomain").toLowerCase()); } PreparedStatement eventsStmt; PreparedStatement deleteEventsStmt; if (runFullUpdate) { // Get event instance and event info - eventsStmt = aspenConn.prepareStatement("SELECT ei.*, e.title, e.description, e.eventTypeId, e.locationId, l.displayName, sl.name AS sublocationName, sl2.name AS sublocationOverride, e.sublocationId, e.cover, e.private FROM event_instance AS ei LEFT JOIN event as e ON e.id = ei.eventID LEFT JOIN location AS l ON e.locationId = l.locationId LEFT JOIN sublocation AS sl on e.sublocationId = sl.id LEFT JOIN sublocation AS sl2 ON ei.sublocationId = sl2.id WHERE ei.date < ? AND ei.deleted = 0 AND e.locationId IN (SELECT locationId from location_events_setting WHERE settingId = ?);"); - eventsStmt.setLong(2, settingsId); + eventsStmt = aspenConn.prepareStatement("SELECT ei.*, e.title, e.description, e.eventTypeId, e.locationId, l.displayName, sl.name AS sublocationName, sl2.name AS sublocationOverride, e.sublocationId, e.cover, e.private FROM event_instance AS ei LEFT JOIN event as e ON e.id = ei.eventID LEFT JOIN location AS l ON e.locationId = l.locationId LEFT JOIN sublocation AS sl on e.sublocationId = sl.id LEFT JOIN sublocation AS sl2 ON ei.sublocationId = sl2.id WHERE ei.date < ? AND ei.deleted = 0;"); } else { - eventsStmt = aspenConn.prepareStatement("SELECT ei.*, e.title, e.description, e.eventTypeId, e.locationId, l.displayName, sl.name AS sublocationName, sl2.name AS sublocationOverride, e.sublocationId, e.cover, e.private FROM event_instance AS ei LEFT JOIN event as e ON e.id = ei.eventID LEFT JOIN location AS l ON e.locationId = l.locationId LEFT JOIN sublocation AS sl on e.sublocationId = sl.id LEFT JOIN sublocation AS sl2 ON ei.sublocationId = sl2.id WHERE ei.date < ? AND (e.dateUpdated > ? OR ei.dateUpdated > ?) AND ei.deleted = 0 AND e.locationId IN (SELECT locationId from location_events_setting WHERE settingId = ?);"); + eventsStmt = aspenConn.prepareStatement("SELECT ei.*, e.title, e.description, e.eventTypeId, e.locationId, l.displayName, sl.name AS sublocationName, sl2.name AS sublocationOverride, e.sublocationId, e.cover, e.private FROM event_instance AS ei LEFT JOIN event as e ON e.id = ei.eventID LEFT JOIN location AS l ON e.locationId = l.locationId LEFT JOIN sublocation AS sl on e.sublocationId = sl.id LEFT JOIN sublocation AS sl2 ON ei.sublocationId = sl2.id WHERE ei.date < ? AND (e.dateUpdated > ? OR ei.dateUpdated > ?) AND ei.deleted = 0;"); deleteEventsStmt = aspenConn.prepareStatement("SELECT id FROM event_instance WHERE deleted = 1 AND dateUpdated > ?;"); eventsStmt.setLong(2, lastUpdateOfChangedEvents); eventsStmt.setLong(3, lastUpdateOfChangedEvents); - eventsStmt.setLong(4, settingsId); deleteEventsStmt.setLong(1, lastUpdateOfChangedEvents); ResultSet deleteEventsRS = deleteEventsStmt.executeQuery(); while (deleteEventsRS.next()) { @@ -129,7 +145,7 @@ private void loadEvents() { void indexEvents() { - // Delete everything and start fresh for full index + // Delete everything and start fresh for a full index if (runFullUpdate) { try { solrUpdateServer.deleteByQuery("type:event_aspenEvent AND source:" + this.settingsId); @@ -242,8 +258,16 @@ void indexEvents() { solrDocument.addField("description", eventInfo.getDescription()); + ArrayList librariesToShowEventFor = new ArrayList<>(librariesToShowAllFor); + + //Add any libraries that want to see their events only + long libraryForLocation = libraryIdsByLocation.get(eventInfo.getLocationId()); + if (librariesToShowSeparatelyFor.containsKey(libraryForLocation)) { + librariesToShowEventFor.add(librariesToShowSeparatelyFor.get(libraryForLocation)); + } + // Libraries scopes - solrDocument.addField("library_scopes", librariesToShowFor); + solrDocument.addField("library_scopes", librariesToShowEventFor); solrDocument.addField("boost", boost); solrUpdateServer.add(solrDocument); diff --git a/code/events_indexer/src/com/turning_leaf_technologies/events/SpringshareLibCalIndexer.java b/code/events_indexer/src/com/turning_leaf_technologies/events/SpringshareLibCalIndexer.java index 2d1efd9407..04ae2cd818 100644 --- a/code/events_indexer/src/com/turning_leaf_technologies/events/SpringshareLibCalIndexer.java +++ b/code/events_indexer/src/com/turning_leaf_technologies/events/SpringshareLibCalIndexer.java @@ -668,7 +668,7 @@ public static void cleanOrphanEvents(ConcurrentUpdateHttp2SolrClient solrUpdateS //run index events logEntry.addNote("Checking for orphaned events..."); try { - PreparedStatement getEventsSitesToIndexStmt = aspenConn.prepareStatement("SELECT unique(settingsId) from springshare_libcal_events where settingsId not in (select id from springshare_libcal_settings) and deleted = 0"); + PreparedStatement getEventsSitesToIndexStmt = aspenConn.prepareStatement("SELECT DISTINCT(settingsId) from springshare_libcal_events where settingsId not in (select id from springshare_libcal_settings) and deleted = 0"); PreparedStatement deleteOrphans = aspenConn.prepareStatement("UPDATE springshare_libcal_events SET deleted = 1 where settingsId = ?"); ResultSet eventsSitesRS = getEventsSitesToIndexStmt.executeQuery(); while (eventsSitesRS.next()) { diff --git a/code/evergreen_export/evergreen_export.jar b/code/evergreen_export/evergreen_export.jar index 931d20f7d9..b6b5f00144 100644 Binary files a/code/evergreen_export/evergreen_export.jar and b/code/evergreen_export/evergreen_export.jar differ diff --git a/code/evolve_export/evolve_export.jar b/code/evolve_export/evolve_export.jar index abdeebb493..f0e6028801 100644 Binary files a/code/evolve_export/evolve_export.jar and b/code/evolve_export/evolve_export.jar differ diff --git a/code/hoopla_export/hoopla_export.jar b/code/hoopla_export/hoopla_export.jar index 5e49da12ce..a564369c37 100644 Binary files a/code/hoopla_export/hoopla_export.jar and b/code/hoopla_export/hoopla_export.jar differ diff --git a/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaExportMain.java b/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaExportMain.java index e592b02cf3..946bf9d55b 100644 --- a/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaExportMain.java +++ b/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaExportMain.java @@ -20,7 +20,6 @@ import java.nio.charset.StandardCharsets; import java.sql.*; -import java.text.SimpleDateFormat; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.*; @@ -35,7 +34,6 @@ public class HooplaExportMain { private static Long startTimeForLogging; private static HooplaExtractLogEntry logEntry; - private static String hooplaAPIBaseURL; private static Connection aspenConn; private static PreparedStatement getAllExistingHooplaItemsStmt; @@ -60,7 +58,7 @@ public static void main(String[] args){ boolean extractSingleWork = false; String singleWorkId = null; String singleWorkType = null; - String hooplaType = null; + String hooplaType; if (args.length == 0) { serverName = AspenStringUtils.getInputFromCommandLine("Please enter the server name"); if (serverName.isEmpty()) { @@ -70,14 +68,13 @@ public static void main(String[] args){ String extractSingleWorkResponse = AspenStringUtils.getInputFromCommandLine("Process a single work? (y/N)"); if (extractSingleWorkResponse.equalsIgnoreCase("y")) { extractSingleWork = true; - String extractSingleWorkType = AspenStringUtils.getInputFromCommandLine("Enter the type of work to extract (Instant/Flex)"); + String extractSingleWorkType = AspenStringUtils.getInputFromCommandLine("Enter the type of work to extract (INSTANT/Flex)"); if (extractSingleWorkType.equalsIgnoreCase("Instant")) { singleWorkType = "Instant"; } else if (extractSingleWorkType.equalsIgnoreCase("Flex")) { singleWorkType = "Flex"; } else { - System.out.println("Invalid work type. Please enter Instant or Flex."); - System.exit(1); + singleWorkType = "Instant"; } } @@ -100,6 +97,15 @@ public static void main(String[] args){ if (args.length > 3) { singleWorkId = args[3]; } + } else { + String extractSingleWorkType = AspenStringUtils.getInputFromCommandLine("Enter the type of work to extract (INSTANT/Flex)"); + if (extractSingleWorkType.equalsIgnoreCase("Instant")) { + singleWorkType = "Instant"; + } else if (extractSingleWorkType.equalsIgnoreCase("Flex")) { + singleWorkType = "Flex"; + } else { + singleWorkType = "Instant"; + } } } } @@ -117,7 +123,7 @@ public static void main(String[] args){ long timeAtStart = new Date().getTime(); while (true) { - //Hoopla only needs to run once a day so just run it in cron + //Hoopla only needs to run once a day, so run it in cron Date startTime = new Date(); startTimeForLogging = startTime.getTime() / 1000; logger.info(startTime + ": Starting Hoopla Export"); @@ -128,7 +134,7 @@ public static void main(String[] args){ //Connect to the Aspen database aspenConn = connectToDatabase(); - //Check to see if the jar has changes before processing records, and if so quit + //Check to see if the jar has changes before processing records, and if so, quit if (myChecksumAtStart != JarUtil.getChecksumForJar(logger, processName, "./" + processName + ".jar")){ IndexingUtils.markNightlyIndexNeeded(aspenConn, logger); disconnectDatabase(aspenConn); @@ -200,7 +206,7 @@ public static void main(String[] args){ break; } - //Check to see if the jar has changes, and if so quit + //Check to see if the jar has changes, and if so, quit if (myChecksumAtStart != JarUtil.getChecksumForJar(logger, processName, "./" + processName + ".jar")){ IndexingUtils.markNightlyIndexNeeded(aspenConn, logger); disconnectDatabase(aspenConn); @@ -211,7 +217,7 @@ public static void main(String[] args){ disconnectDatabase(aspenConn); break; } - //Check to see if it's between midnight and 1 am and the jar has been running more than 15 hours. If so, restart just to clean up memory. + //Check to see if it's between midnight and 1 am, and the jar has been running more than 15 hours. If so, restart just to clean up memory. GregorianCalendar nowAsCalendar = new GregorianCalendar(); Date now = new Date(); nowAsCalendar.setTime(now); @@ -229,7 +235,7 @@ public static void main(String[] args){ disconnectDatabase(aspenConn); - //Check to see if nightly indexing is running and if so, wait until it is done. + //Check to see if nightly indexing is running, and if so, wait until it is done. if (IndexingUtils.isNightlyIndexRunning(configIni, serverName, logger)) { //Quit and we will restart after if finishes System.exit(0); @@ -438,6 +444,7 @@ private static boolean exportHooplaContent(HooplaSettings settings, String hoopl String accessToken = settings.getAccessToken(); long tokenExpirationTime = settings.getTokenExpirationTime(); + int indexingTime = settings.getIndexingTime(); if (accessToken == null || tokenExpirationTime < (System.currentTimeMillis() / 1000)) { accessToken = getAccessToken(settings); @@ -448,6 +455,9 @@ private static boolean exportHooplaContent(HooplaSettings settings, String hoopl return true; } + logEntry.addNote("Starting " + hooplaType + " content extraction using a batch size of " + settings.getRecordExtractionBatchSize() + " at " + indexingTime); + logEntry.saveResults(); + try { if (doFullReload){ //Unset that a full update needs to be done @@ -464,12 +474,12 @@ private static boolean exportHooplaContent(HooplaSettings settings, String hoopl ZonedDateTime thirtyTwoHoursAgoTime = nowLocalTime.minusHours(32); long thirtyTwoHoursAgo = thirtyTwoHoursAgoTime.toInstant().getEpochSecond(); - if (curHour == 1){ + if (curHour == indexingTime){ if (lastUpdateOfChangedRecords >= startOfTodaySeconds) { - logger.warn("Already completed today's " + hooplaType + " extraction at 1 AM. Skipping until tomorrow."); - return updatedContent; + logger.warn("Already completed today's " + hooplaType + " extraction at " + indexingTime + ". Skipping until tomorrow."); + return false; } - //Set last update time to 32 hours ago (go bigger to get more updates) + //Set the last update time to 32 hours ago (go bigger to get more updates) if (thirtyTwoHoursAgo < lastUpdate){ lastUpdate = thirtyTwoHoursAgo; } @@ -479,14 +489,14 @@ private static boolean exportHooplaContent(HooplaSettings settings, String hoopl //It's not 1 am Local time, skip for now. //Figure out when we last indexed this collection. if (lastUpdate >= thirtyTwoHoursAgo) { - //Go ahead and index even if we are off schedule - return updatedContent; + //Do not index unless it has been 32 hours + return false; } // If we don't have updates for 32 hours, we will try 3 times // If we exceed 3 times and fail, we will wait until 1 AM if (numRetries32HoursAfter >= 3){ - logger.warn("Exceeded 3 retries for 32 hours catch up, waiting until 1 AM"); - return updatedContent; + logger.warn("Exceeded 3 retries for 32 hours catch up, waiting until next indexing time at " + indexingTime); + return false; } numRetries32HoursAfter++; logEntry.addNote("Retrying " + hooplaType + " extraction after 32 hours " + numRetries32HoursAfter + " of 3"); @@ -501,11 +511,12 @@ private static boolean exportHooplaContent(HooplaSettings settings, String hoopl //Give a 2-minute buffer for the extract lastUpdate -= 120; logEntry.addNote("Extracting records since " + new Date(lastUpdate * 1000)); - url += "?startTime=" + lastUpdate + "&limit=500&purchaseModel=" + purchaseModel; + url += "?startTime=" + lastUpdate + "&limit=" + settings.getRecordExtractionBatchSize() + "&purchaseModel=" + purchaseModel; } else { - url += "?limit=500&purchaseModel=" + purchaseModel; + url += "?limit=" + settings.getRecordExtractionBatchSize() + "&purchaseModel=" + purchaseModel; } + @SuppressWarnings("DuplicatedCode") HashMap headers = new HashMap<>(); headers.put("Authorization", "Bearer " + accessToken); headers.put("Content-Type", "application/json"); @@ -513,7 +524,7 @@ private static boolean exportHooplaContent(HooplaSettings settings, String hoopl WebServiceResponse response = NetworkUtils.getURL(url, logger, headers); if (!response.isSuccess()){ logEntry.incErrors("Could not get titles from " + url + " " + response.getMessage() + " " + response.getResponseCode()); - return updatedContent; + return false; }else { JSONObject responseJSON = new JSONObject(response.getMessage()); if (responseJSON.has("titles")) { @@ -532,9 +543,9 @@ private static boolean exportHooplaContent(HooplaSettings settings, String hoopl int numTries = 0; while (startToken != null) { if (!doFullReload && lastUpdate > 0) { - url = hooplaAPIBaseURL + "/api/v1/libraries/" + hooplaLibraryId + "/content?startTime=" + lastUpdate + "&startToken=" + startToken + "&limit=500&purchaseModel=" + purchaseModel; + url = hooplaAPIBaseURL + "/api/v1/libraries/" + hooplaLibraryId + "/content?startTime=" + lastUpdate + "&startToken=" + startToken + "&limit=" + settings.getRecordExtractionBatchSize() + "&purchaseModel=" + purchaseModel; }else { - url = hooplaAPIBaseURL + "/api/v1/libraries/" + hooplaLibraryId + "/content?startToken=" + startToken + "&limit=500&purchaseModel=" + purchaseModel; + url = hooplaAPIBaseURL + "/api/v1/libraries/" + hooplaLibraryId + "/content?startToken=" + startToken + "&limit=" + settings.getRecordExtractionBatchSize() + "&purchaseModel=" + purchaseModel; } response = NetworkUtils.getURL(url, logger, headers); if (response.isSuccess()){ @@ -654,9 +665,9 @@ private static boolean getFlexAvailability(HooplaSettings settings) { while (flexTitlesRS.next()) { long hooplaId = flexTitlesRS.getLong("hooplaId"); boolean existingInDB = flexTitlesRS.getString("status") != null; - Integer existingHoldsQueueSize = existingInDB ? flexTitlesRS.getInt("holdsQueueSize") : 0; - Integer existingAvailableCopies = existingInDB ? flexTitlesRS.getInt("availableCopies") : 0; - Integer existingTotalCopies = existingInDB ? flexTitlesRS.getInt("totalCopies") : 0; + int existingHoldsQueueSize = existingInDB ? flexTitlesRS.getInt("holdsQueueSize") : 0; + int existingAvailableCopies = existingInDB ? flexTitlesRS.getInt("availableCopies") : 0; + int existingTotalCopies = existingInDB ? flexTitlesRS.getInt("totalCopies") : 0; String existingStatus = existingInDB ? flexTitlesRS.getString("status") : null; if (!doFullReloadFlex && existingInDB){ @@ -665,6 +676,7 @@ private static boolean getFlexAvailability(HooplaSettings settings) { String url = hooplaAPIBaseURL + "/api/v1/libraries/" + hooplaLibraryId + "/content/info?contentIds=" + hooplaId; + @SuppressWarnings("DuplicatedCode") HashMap headers = new HashMap<>(); headers.put("Authorization", "Bearer " + accessToken); headers.put("Content-Type", "application/json"); @@ -676,15 +688,15 @@ private static boolean getFlexAvailability(HooplaSettings settings) { } try { JSONArray availabilityArray = new JSONArray(response.getMessage()); - if (availabilityArray.length() > 0) { + if (!availabilityArray.isEmpty()) { JSONObject titleInfo = availabilityArray.getJSONObject(0); - Long contentId = titleInfo.getLong("contentId"); + long contentId = titleInfo.getLong("contentId"); if (hooplaId != contentId) { logEntry.incErrors("Response content ID " + contentId + " mismatch for title " + hooplaId); continue; } JSONObject availability = titleInfo.getJSONObject("availability"); - if (availability.length() > 0) { + if (!availability.isEmpty()) { String newStatus = availability.getString("status"); int newHoldsQueueSize = newStatus.equals("BORROW") ? 0 : availability.has("holdsQueueSize") ? availability.getInt("holdsQueueSize") : 0; @@ -722,11 +734,10 @@ private static boolean getFlexAvailability(HooplaSettings settings) { if (numUpdates > 0) { logEntry.addNote("Updated availability for " + numUpdates + " Flex titles"); - return true; } else { logEntry.addNote("No availability changes found for Hoopla Flex titles"); - return true; } + return true; } catch (Exception e) { @@ -778,7 +789,7 @@ private static void exportSingleHooplaTitle(String singleWorkId, String singleWo logEntry.saveResults(); if (singleWorkType.equalsIgnoreCase("Flex")) { - if (responseTitles != null && !responseTitles.isEmpty()) { + if (!responseTitles.isEmpty()) { JSONObject titleObj = responseTitles.getJSONObject(0); boolean isActive = titleObj.getBoolean("active"); if (!isActive) { @@ -791,7 +802,7 @@ private static void exportSingleHooplaTitle(String singleWorkId, String singleWo } else { try { JSONArray availabilityArray = new JSONArray(availResponse.getMessage()); - if (availabilityArray.length() > 0) { + if (!availabilityArray.isEmpty()) { JSONObject titleInfo = availabilityArray.getJSONObject(0); JSONObject availability = titleInfo.getJSONObject("availability"); @@ -831,7 +842,7 @@ private static void exportSingleHooplaTitle(String singleWorkId, String singleWo } } if (numSettings == 0){ - logger.error("Unable to find settings for Hoopla, please add settings to the database"); + logger.error("Unable to find settings for Hoopla when processing single title, please add settings to the database"); } }catch (Exception e){ logEntry.incErrors("Error exporting hoopla data", e); @@ -850,7 +861,7 @@ private static void updateTitlesInDB(JSONArray responseTitles, boolean forceRegr long rawChecksum = checksumCalculator.getValue(); boolean curTitleActive = curTitle.getBoolean("active"); - long hooplaId = curTitle.getLong("id"); //formerly titleId was used but this is not unique for tv series + long hooplaId = curTitle.getLong("id"); //formerly titleId was used, but this is not unique for TV series HooplaTitle existingTitle = existingRecords.get(hooplaId); boolean recordUpdated = false; @@ -919,8 +930,8 @@ private static void updateTitlesInDB(JSONArray responseTitles, boolean forceRegr addHooplaTitleToDB.setString(8, curTitle.has("rating") ? curTitle.getString("rating") : ""); addHooplaTitleToDB.setBoolean(9, curTitle.getBoolean("abridged")); addHooplaTitleToDB.setBoolean(10, curTitle.getBoolean("children")); - // Flex titles don't have a price so set it to 0.0 - if (hooplaType.equalsIgnoreCase("Flex")) { + // Flex titles don't have a price, so set it to 0.0 or set to 0 if the record has no price + if (hooplaType.equalsIgnoreCase("Flex") || !curTitle.has("price")) { addHooplaTitleToDB.setDouble(11, 0.0); } else { addHooplaTitleToDB.setDouble(11, curTitle.getDouble("price")); @@ -949,8 +960,8 @@ private static void updateTitlesInDB(JSONArray responseTitles, boolean forceRegr updateHooplaTitleInDB.setString(7, curTitle.has("rating") ? curTitle.getString("rating") : ""); updateHooplaTitleInDB.setBoolean(8, curTitle.getBoolean("abridged")); updateHooplaTitleInDB.setBoolean(9, curTitle.getBoolean("children")); - // Flex titles don't have a price so set it to 0.0 - if (hooplaType.equalsIgnoreCase("Flex")) { + // Flex titles don't have a price, so set it to 0.0 or set to 0 if the record has no price + if (hooplaType.equalsIgnoreCase("Flex") || !curTitle.has("price")) { updateHooplaTitleInDB.setDouble(10, 0.0); } else { updateHooplaTitleInDB.setDouble(10, curTitle.getDouble("price")); @@ -1090,7 +1101,7 @@ private static RecordGroupingProcessor getRecordGroupingProcessor(){ private static void regroupAllRecords(Connection dbConn, long settingsId, GroupedWorkIndexer indexer, HooplaExtractLogEntry logEntry) throws SQLException { logEntry.addNote("Starting to regroup all records"); PreparedStatement getAllRecordsToRegroupStmt = dbConn.prepareStatement("SELECT hooplaId, UNCOMPRESS(rawResponse) as rawResponse from hoopla_export where active = 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); - //It turns out to be quite slow to look this up repeatedly, just grab the existing values for all and store in memory + //It turns out to be quite slow to look this up repeatedly, grab the existing values for all and store in memory PreparedStatement getOriginalPermanentIdForRecordStmt = dbConn.prepareStatement("SELECT identifier, permanent_id from grouped_work_primary_identifiers join grouped_work on grouped_work_id = grouped_work.id WHERE type = 'hoopla'", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); HashMap allPermanentIdsForHoopla = new HashMap<>(); ResultSet getOriginalPermanentIdForRecordRS = getOriginalPermanentIdForRecordStmt.executeQuery(); diff --git a/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaSettings.java b/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaSettings.java index 38f6df252b..8cf7867309 100644 --- a/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaSettings.java +++ b/code/hoopla_export/src/com/turning_leaf_technologies/hoopla/HooplaSettings.java @@ -9,6 +9,8 @@ class HooplaSettings { private final int libraryId; private final String apiUsername; private final String apiPassword; + private final int recordExtractionBatchSize; + private final int indexingTime; // Instant settings private final boolean hooplaInstantEnabled; @@ -35,6 +37,8 @@ public HooplaSettings(ResultSet settingsRS) throws SQLException { apiUsername = settingsRS.getString("apiUsername"); apiPassword = settingsRS.getString("apiPassword"); + recordExtractionBatchSize = settingsRS.getInt("recordExtractionBatchSize"); + indexingTime = settingsRS.getInt("indexingTime"); hooplaInstantEnabled = settingsRS.getBoolean("hooplaInstantEnabled"); runFullUpdateInstant = settingsRS.getBoolean("runFullUpdateInstant"); lastUpdateOfChangedRecordsInstant = settingsRS.getLong("lastUpdateOfChangedRecordsInstant"); @@ -45,7 +49,7 @@ public HooplaSettings(ResultSet settingsRS) throws SQLException { lastUpdateOfChangedRecordsFlex = settingsRS.getLong("lastUpdateOfChangedRecordsFlex"); lastUpdateOfAllRecordsFlex = settingsRS.getLong("lastUpdateOfAllRecordsFlex"); - accessToken = settingsRS.getString("accessToken"); + accessToken = settingsRS.getString("accessToken"); tokenExpirationTime = settingsRS.getLong("tokenExpirationTime"); regroupAllRecords = settingsRS.getBoolean("regroupAllRecords"); @@ -118,4 +122,12 @@ public long getTokenExpirationTime() { public boolean isRegroupAllRecords() { return regroupAllRecords; } + + public int getRecordExtractionBatchSize() { + return recordExtractionBatchSize; + } + + public int getIndexingTime() { + return indexingTime; + } } diff --git a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRule.java b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRule.java index 375ddf9ea6..89bb1327d9 100644 --- a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRule.java +++ b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRule.java @@ -9,159 +9,96 @@ import java.util.regex.Pattern; class InclusionRule { - private final String recordType; - private final boolean matchAllLocations; - private boolean isLocationExactMatch; - private String locationCodeToMatch; - private Pattern locationCodePattern; - private Pattern locationsToExcludePattern = null; + //Checks boolean inclusion options + private InclusionRuleBasics inclusionRuleBasics; + //Checks inclusion by location, this is where most things are weeded out + private InclusionRuleLocation inclusionRuleByLocation; + //Checks detailed inclusion options, can be null if no inclusion rules apply + private InclusionRuleDetails detailedInclusionRule; - private final boolean matchAllSubLocations; - private final Pattern subLocationCodePattern; - private Pattern subLocationsToExcludePattern = null; - - private boolean matchAlliTypes = false; - private final Pattern iTypePattern; - private Pattern iTypesToExcludePattern = null; - - private boolean matchAllAudiences = false; - private final Pattern audiencePattern; - private Pattern audiencesToExcludePattern = null; - - private boolean matchAllFormats = false; - private final Pattern formatPattern; - private Pattern formatsToExcludePattern = null; - - private boolean matchAllShelfLocations = false; - private final Pattern shelfLocationPattern; - private Pattern shelfLocationsToExcludePattern = null; - - private boolean matchAllCollectionCodes = false; - private final Pattern collectionCodePattern; - private Pattern collectionCodesToExcludePattern = null; - - private final boolean includeHoldableOnly; - private final boolean includeItemsOnOrder; - private final boolean includeEContent; - private final String marcTagToMatch; - private final Pattern marcValueToMatchPattern; - private final boolean includeExcludeMatches; private final String urlToMatch; private final String urlReplacement; - private final String locationPatternString; - private final String formatPatternString; - - private static final Pattern isRegexPattern = Pattern.compile("[.*?{}\\\\^\\[\\]|$]"); - InclusionRule(String recordType, String locationCode, String subLocationCode, String locationsToExclude, String subLocationsToExclude, String iType, String iTypesToExclude, String audience, String audiencesToExclude,String format, String formatsToExclude, String shelfLocation, String shelfLocationsToExclude, String collectionCode, String collectionCodesToExclude, boolean includeHoldableOnly, boolean includeItemsOnOrder, boolean includeEContent, String marcTagToMatch, String marcValueToMatch, boolean includeExcludeMatches, String urlToMatch, String urlReplacement){ - this.recordType = recordType; - this.includeHoldableOnly = includeHoldableOnly; - this.includeItemsOnOrder = includeItemsOnOrder; - this.includeEContent = includeEContent; - //Location & Sublocation Code Inclusion/Exclusion Check - if (locationCode.isEmpty()){ - locationCode = ".*"; - } - this.locationPatternString = locationCode; - matchAllLocations = locationCode.equals(".*"); - if (!matchAllLocations){ - if (isRegexPattern.matcher(locationCode).find()) { - this.locationCodePattern = Pattern.compile(locationCode, Pattern.CASE_INSENSITIVE); - }else{ - this.locationCodeToMatch = locationCode; - isLocationExactMatch = true; - } - } - if (subLocationCode.isEmpty()){ - subLocationCode = ".*"; - } - matchAllSubLocations = subLocationCode.equals(".*"); - this.subLocationCodePattern = Pattern.compile(subLocationCode, Pattern.CASE_INSENSITIVE); + private static final HashMap basicInclusionRules = new HashMap<>(); + private static final HashMap locationInclusionRules = new HashMap<>(); + private static final HashMap detailedInclusionRules = new HashMap<>(); - if (locationsToExclude != null && !locationsToExclude.isEmpty()){ - this.locationsToExcludePattern = Pattern.compile(locationsToExclude, Pattern.CASE_INSENSITIVE); - } - if (subLocationsToExclude != null && !subLocationsToExclude.isEmpty()){ - this.subLocationsToExcludePattern = Pattern.compile(subLocationsToExclude, Pattern.CASE_INSENSITIVE); - } - - //iType Inclusion/Exclusion Check - if (iType == null || iType.isEmpty()){ - iType = ".*"; - } - if (iTypesToExclude == null) { - iTypesToExclude = ""; - } - if (iType.equals(".*") && (iTypesToExclude.isEmpty())){ - matchAlliTypes = true; - } - this.iTypePattern = Pattern.compile(iType, Pattern.CASE_INSENSITIVE); - if (!iTypesToExclude.isEmpty()){ - this.iTypesToExcludePattern = Pattern.compile(iTypesToExclude, Pattern.CASE_INSENSITIVE); - } - - //Audience Inclusion/Exclusion Check - if (audience == null || audience.isEmpty()) { - audience = ".*"; - } - if (audiencesToExclude == null) { - audiencesToExclude = ""; - } - if (audience.equals(".*") && audiencesToExclude.isEmpty()){ - matchAllAudiences = true; - } - this.audiencePattern = Pattern.compile(audience, Pattern.CASE_INSENSITIVE); - if (audiencesToExclude != null && !audiencesToExclude.isEmpty()){ - this.audiencesToExcludePattern = Pattern.compile(audiencesToExclude, Pattern.CASE_INSENSITIVE); - } - - //Format Inclusion/Exclusion Check - if (format == null || format.isEmpty()){ - format = ".*"; - } - this.formatPatternString = format; - if (formatsToExclude == null) { - formatsToExclude = ""; - } - if (format.equals(".*") && formatsToExclude.isEmpty()){ - matchAllFormats = true; - } - this.formatPattern = Pattern.compile(format, Pattern.CASE_INSENSITIVE); - if (!formatsToExclude.isEmpty()){ - this.formatsToExcludePattern = Pattern.compile(formatsToExclude, Pattern.CASE_INSENSITIVE); - } - - //Shelf Location Inclusion/Exclusion Check - if (shelfLocation == null || shelfLocation.isEmpty()) { - shelfLocation = ".*"; - } - if (shelfLocation.equals(".*") && (shelfLocationsToExclude == null || shelfLocationsToExclude.isEmpty())){ - matchAllShelfLocations = true; - } - this.shelfLocationPattern = Pattern.compile(shelfLocation, Pattern.CASE_INSENSITIVE); - if (shelfLocationsToExclude != null && !shelfLocationsToExclude.isEmpty()){ - this.shelfLocationsToExcludePattern = Pattern.compile(shelfLocationsToExclude, Pattern.CASE_INSENSITIVE); + //TODO Switch this back to the previous constructor and create local static caches for all the options + InclusionRule(String recordType, String locationCode, String subLocationCode, String locationsToExclude, String subLocationsToExclude, String iType, String iTypesToExclude, String audience, String audiencesToExclude,String format, String formatsToExclude, String shelfLocation, String shelfLocationsToExclude, String collectionCode, String collectionCodesToExclude, boolean includeHoldableOnly, boolean includeItemsOnOrder, boolean includeEContent, String marcTagToMatch, String marcValueToMatch, boolean includeExcludeMatches, String urlToMatch, String urlReplacement){ + String basicInclusionRuleKey = + Boolean.toString(includeHoldableOnly) + "~" + + Boolean.toString(includeItemsOnOrder) + "~" + + Boolean.toString(includeEContent); + if (basicInclusionRules.containsKey(basicInclusionRuleKey)){ + this.inclusionRuleBasics = basicInclusionRules.get(basicInclusionRuleKey); + }else{ + InclusionRuleBasics inclusionRuleBasics = new InclusionRuleBasics(includeHoldableOnly, includeItemsOnOrder, includeEContent); + basicInclusionRules.put(basicInclusionRuleKey, inclusionRuleBasics); + this.inclusionRuleBasics = inclusionRuleBasics; } - //Collection Code Inclusion/Exclusion Check - if (collectionCode == null || collectionCode.isEmpty()) { - collectionCode = ".*"; - } - if (collectionCode.equals(".*") && collectionCodesToExclude.isEmpty()){ - matchAllCollectionCodes = true; - } - this.collectionCodePattern = Pattern.compile(collectionCode, Pattern.CASE_INSENSITIVE); - if (!collectionCodesToExclude.isEmpty()){ - this.collectionCodesToExcludePattern = Pattern.compile(collectionCodesToExclude, Pattern.CASE_INSENSITIVE); + String locationInclusionRuleKey = recordType + "~" + locationCode + "~" + locationsToExclude; + if (locationInclusionRules.containsKey(locationInclusionRuleKey)){ + this.inclusionRuleByLocation = locationInclusionRules.get(locationInclusionRuleKey); + }else{ + InclusionRuleLocation inclusionRuleByLocation = new InclusionRuleLocation(recordType, locationCode, locationsToExclude); + locationInclusionRules.put(locationInclusionRuleKey, inclusionRuleByLocation); + this.inclusionRuleByLocation = inclusionRuleByLocation; + } + + //Check to see if detailed inclusion will result in no restrictions. + boolean detailedInclusionRuleHasNoRestrictions = false; + if (subLocationCode == null || subLocationCode.isEmpty() || subLocationCode.equals(".*")){ + if (subLocationsToExclude == null || subLocationsToExclude.isEmpty()){ + if (audience == null || audience.isEmpty() || audience.equals(".*")){ + if (audiencesToExclude == null || audiencesToExclude.isEmpty()){ + if (format == null || format.isEmpty() || format.equals(".*")){ + if (formatsToExclude == null || formatsToExclude.isEmpty()){ + if (shelfLocation == null || shelfLocation.isEmpty() || shelfLocation.equals(".*")){ + if (shelfLocationsToExclude == null || shelfLocationsToExclude.isEmpty()){ + if (collectionCode == null || collectionCode.isEmpty() || collectionCode.equals(".*")){ + if (collectionCodesToExclude == null || collectionCodesToExclude.isEmpty()){ + if (marcTagToMatch == null || marcTagToMatch.isEmpty() || marcTagToMatch.equals(".*")){ + if (marcValueToMatch == null || marcValueToMatch.isEmpty() || marcValueToMatch.equals(".*")){ + detailedInclusionRuleHasNoRestrictions = true; + } + } + } + } + } + } + } + } + } + } + } } - this.marcTagToMatch = Objects.requireNonNullElse(marcTagToMatch, ""); - - if (marcValueToMatch == null || marcValueToMatch.isEmpty()){ - marcValueToMatch = ".*"; + if (detailedInclusionRuleHasNoRestrictions) { + this.detailedInclusionRule = null; + }else { + String detailedInclusionRuleKey = subLocationCode + "~" + + subLocationsToExclude + "~" + + iType + "~" + + iTypesToExclude + "~" + + audience + "~" + + audiencesToExclude + "~" + + format + "~" + + formatsToExclude + "~" + + shelfLocation + "~" + + shelfLocationsToExclude + "~" + + collectionCode + "~" + + collectionCodesToExclude + "~" + + marcTagToMatch + "~" + + marcValueToMatch + "~" + + Boolean.toString(includeExcludeMatches); + if (detailedInclusionRules.containsKey(detailedInclusionRuleKey)) { + this.detailedInclusionRule = detailedInclusionRules.get(detailedInclusionRuleKey); + } else { + InclusionRuleDetails detailedInclusionRule = new InclusionRuleDetails(subLocationCode, subLocationsToExclude, iType, iTypesToExclude, audience, audiencesToExclude, format, formatsToExclude, shelfLocation, shelfLocationsToExclude, collectionCode, collectionCodesToExclude, marcTagToMatch, marcValueToMatch, includeExcludeMatches); + detailedInclusionRules.put(detailedInclusionRuleKey, detailedInclusionRule); + this.detailedInclusionRule = detailedInclusionRule; + } } - this.marcValueToMatchPattern = Pattern.compile(marcValueToMatch); - - this.includeExcludeMatches = includeExcludeMatches; this.urlToMatch = urlToMatch; this.urlReplacement = urlReplacement; @@ -178,230 +115,27 @@ boolean isItemIncluded(String itemIdentifier, String recordType, String location if (lastIdentifier != null && lastIdentifier.equals(itemIdentifier)){ return lastIdentifierResult; } - lastIdentifier = itemIdentifier; - //Do the quick checks first - if (!isEContent && (includeHoldableOnly && !isHoldable)) { - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because 'Include Holdable Only' is enabled but item is not holdable (holdable=" + isHoldable + ", format=" + format + ")", 3); - } + if (!inclusionRuleBasics.isItemIncluded(itemIdentifier, isHoldable, isOnOrder, isEContent, debugLogger)){ lastIdentifierResult = false; return false; - } else if (!includeItemsOnOrder && isOnOrder){ - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because 'Include Items On Order' is disabled but item is on order", 3); - } - lastIdentifierResult = false; - return false; - } else if (!includeEContent && isEContent){ - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because 'Include eContent' is disabled but item is eContent", 3); - } - lastIdentifierResult = false; - return false; - } else if (!this.recordType.equals(recordType)) { - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because record type '" + recordType + "' does not match rule record type '" + this.recordType + "'", 3); - } - lastIdentifierResult = false; - return false; - } - - //Determine if we have already determined this already - boolean hasCachedValue = true; - if (locationCode == null){ - if (matchAllLocations){ - locationCode = "null"; - }else { - lastIdentifierResult = false; - return false; - } - } - - if (matchAlliTypes){ - iType = "any"; - } - String audienceKey = audiencesAsString; - if (matchAllAudiences){ - audienceKey = "all"; - } - if (matchAllFormats){ - format = "any"; - } - if(matchAllShelfLocations){ - shelfLocation = "all"; - } - if(matchAllCollectionCodes){ - collectionCode = "any"; } - String inclusionCacheKey = locationCode + subLocationCode + iType + audienceKey + format + shelfLocation + collectionCode; - Boolean cachedInclusion = inclusionCache.get(inclusionCacheKey); - if (cachedInclusion == null){ - hasCachedValue = false; - } - - boolean isIncluded; - - if (!hasCachedValue){ - isIncluded = true; - if (!matchAllLocations) { - if (isLocationExactMatch) { - if (!locationCodeToMatch.equalsIgnoreCase(locationCode)) { - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because location '" + locationCode + "' does not match required location '" + locationCodeToMatch + "'", 3); - } - isIncluded = false; - } - } else { - if (!locationCodePattern.matcher(locationCode).matches()) { - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because location '" + locationCode + "' does not match pattern '" + locationPatternString + "'", 3); - } - isIncluded = false; - } - } - } - if (isIncluded && !locationCode.isEmpty() && locationsToExcludePattern != null) { - if (locationsToExcludePattern.matcher(locationCode).matches()) { - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because location '" + locationCode + "' matches exclusion pattern", 3); - } - isIncluded = false; - } - } - - if (isIncluded && !subLocationCode.isEmpty()){ - if (!matchAllSubLocations) { - if (!subLocationCodePattern.matcher(subLocationCode).matches()) { - isIncluded = false; - } - if (isIncluded && subLocationsToExcludePattern != null) { - if (subLocationsToExcludePattern.matcher(subLocationCode).matches()){ - isIncluded = false; - } - } - } - } - - //Check Formats to include & exclude - if (isIncluded && format != null && !format.isEmpty()){ - if (!matchAllFormats) { - if (!formatPattern.matcher(format).matches()) { - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because format '" + format + "' does not match pattern '" + formatPatternString + "'", 3); - } - isIncluded = false; - } - if (isIncluded && formatsToExcludePattern != null) { - if(formatsToExcludePattern.matcher(format).matches()){ - if (debugLogger != null && debugLogger.isDebugEnabled()) { - debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because format '" + format + "' matches exclusion pattern", 3); - } - isIncluded = false; - } - } - } - } - - //Check iTypes to include & exclude - if (isIncluded && iType != null){ - if (!matchAlliTypes) { - if (!iTypePattern.matcher(iType).matches()) { - isIncluded = false; - } - if (isIncluded && iTypesToExcludePattern != null) { - if(iTypesToExcludePattern.matcher(iType).matches()) { - isIncluded = false; - } - } - } - } - //Check Shelf Location to include & exclude - if (isIncluded && !matchAllShelfLocations){ //still want to process empty shelf locations, don't check for length > 0 - if (shelfLocation != null && !shelfLocation.isEmpty()){ - if (!shelfLocationPattern.matcher(shelfLocation).matches()) { - isIncluded = false; - } - if (isIncluded && shelfLocationsToExcludePattern != null) { - if(shelfLocationsToExcludePattern.matcher(shelfLocation).matches()) { - isIncluded = false; - } - } - } - else { - if (!shelfLocationPattern.pattern().equals(".*")) { - isIncluded = false; - } - } - - } - //Check Collection Code to include & exclude - if (isIncluded && !matchAllCollectionCodes){ - if (collectionCode != null && !collectionCode.isEmpty()) { - if (!collectionCodePattern.matcher(collectionCode).matches()) { - isIncluded = false; - } - if (isIncluded && collectionCodesToExcludePattern != null) { - if(collectionCodesToExcludePattern.matcher(collectionCode).matches()) { - isIncluded = false; - } - } - } - else { - if (!collectionCodePattern.pattern().equals(".*")) { - isIncluded = false; - } - } - } - //Check audiences to include & exclude - if (isIncluded && !matchAllAudiences){ - boolean audienceMatched = false; - for (String audience : audiences) { - //As soon as something is either matched or excluded we can stop checking. - if (audiencePattern.matcher(audience).matches()) { - audienceMatched = true; - break; - } - } - if (audienceMatched){ - for (String audience : audiences) { - //As soon as something is either matched or excluded we can stop checking. - if (audiencesToExcludePattern != null && audiencesToExcludePattern.matcher(audience).matches()) { - audienceMatched = false; - break; - } - } - } - isIncluded = audienceMatched; - } - - //Make sure not to cache marc tag determination - inclusionCache.put(inclusionCacheKey, isIncluded); - }else{ - isIncluded = cachedInclusion; + if (!inclusionRuleByLocation.isItemIncluded(itemIdentifier, recordType, locationCode, debugLogger)){ + lastIdentifierResult = false; + return false; } - //Make sure not to cache marc tag determination - //TODO: *Someday* if the marc tag to match is the item tag, only get the marc tag for the item we are on. - if (isIncluded && !marcTagToMatch.isEmpty()) { - boolean hasMatch = false; - Set marcValuesToCheck = MarcUtil.getFieldList(marcRecord, marcTagToMatch); - for (String marcValueToCheck : marcValuesToCheck) { - if (marcValueToMatchPattern.matcher(marcValueToCheck).matches()) { - hasMatch = true; - break; - } - } - isIncluded = hasMatch && includeExcludeMatches; + if (detailedInclusionRule != null && !detailedInclusionRule.isItemIncluded(itemIdentifier, subLocationCode, iType, audiences, audiencesAsString, format, shelfLocation, collectionCode, marcRecord, debugLogger)){ + lastIdentifierResult = false; + return false; } if (debugLogger != null && debugLogger.isDebugEnabled()) { - if (isIncluded) { + if (lastIdentifierResult) { debugLogger.addDebugMessage("Item " + itemIdentifier + " included in scope (location='" + locationCode + "', format='" + format + "', holdable=" + isHoldable + ")", 3); } } - - lastIdentifierResult = isIncluded; - return isIncluded; + lastIdentifierResult = true; + return true; } String getLocalUrl(String url){ diff --git a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleBasics.java b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleBasics.java new file mode 100644 index 0000000000..352f3d7e80 --- /dev/null +++ b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleBasics.java @@ -0,0 +1,46 @@ +package com.turning_leaf_technologies.indexing; + +import com.turning_leaf_technologies.marc.MarcUtil; + +import java.util.HashMap; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +/** + * Inclusion rule for basic information that can be checked very quickly + */ +class InclusionRuleBasics { + private final boolean includeHoldableOnly; + private final boolean includeItemsOnOrder; + private final boolean includeEContent; + + InclusionRuleBasics(boolean includeHoldableOnly, boolean includeItemsOnOrder, boolean includeEContent){ + this.includeHoldableOnly = includeHoldableOnly; + this.includeItemsOnOrder = includeItemsOnOrder; + this.includeEContent = includeEContent; + } + + //TODO: We can potentially just pass in the ItemInfo object instead of all or most of these parameters + // This would likely require creating an interface for ItemInfo under java_shared_libraries. + boolean isItemIncluded(String itemIdentifier, boolean isHoldable, boolean isOnOrder, boolean isEContent, DebugLogger debugLogger){ + if (!isEContent && (includeHoldableOnly && !isHoldable)) { + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because 'Include Holdable Only' is enabled but item is not holdable", 3); + } + return false; + } else if (!includeItemsOnOrder && isOnOrder){ + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because 'Include Items On Order' is disabled but item is on order", 3); + } + return false; + } else if (!includeEContent && isEContent){ + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because 'Include eContent' is disabled but item is eContent", 3); + } + return false; + } + return true; + } +} diff --git a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleDetails.java b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleDetails.java new file mode 100644 index 0000000000..7b4fe4699f --- /dev/null +++ b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleDetails.java @@ -0,0 +1,309 @@ +package com.turning_leaf_technologies.indexing; + +import com.turning_leaf_technologies.marc.MarcUtil; + +import java.util.HashMap; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +/** + * Inclusion rule based on everything except the location code. + * These generally don't change so we can optimize checking. + */ +class InclusionRuleDetails { + private final boolean matchAllSubLocations; + private final Pattern subLocationCodePattern; + private Pattern subLocationsToExcludePattern = null; + + private boolean matchAlliTypes = false; + private final Pattern iTypePattern; + private Pattern iTypesToExcludePattern = null; + + private boolean matchAllAudiences = false; + private final Pattern audiencePattern; + private Pattern audiencesToExcludePattern = null; + + private boolean matchAllFormats = false; + private final Pattern formatPattern; + private Pattern formatsToExcludePattern = null; + + private boolean matchAllShelfLocations = false; + private final Pattern shelfLocationPattern; + private Pattern shelfLocationsToExcludePattern = null; + + private boolean matchAllCollectionCodes = false; + private final Pattern collectionCodePattern; + private Pattern collectionCodesToExcludePattern = null; + + private final String marcTagToMatch; + private final Pattern marcValueToMatchPattern; + private final boolean includeExcludeMatches; + + private final String formatPatternString; + + private static final Pattern isRegexPattern = Pattern.compile("[.*?{}\\\\^\\[\\]|$]"); + InclusionRuleDetails(String subLocationCode, String subLocationsToExclude, String iType, String iTypesToExclude, String audience, String audiencesToExclude, String format, String formatsToExclude, String shelfLocation, String shelfLocationsToExclude, String collectionCode, String collectionCodesToExclude, String marcTagToMatch, String marcValueToMatch, boolean includeExcludeMatches){ + + //Location & Sublocation Code Inclusion/Exclusion Check + if (subLocationCode.isEmpty()){ + subLocationCode = ".*"; + } + matchAllSubLocations = subLocationCode.equals(".*"); + this.subLocationCodePattern = Pattern.compile(subLocationCode, Pattern.CASE_INSENSITIVE); + + if (subLocationsToExclude != null && !subLocationsToExclude.isEmpty()){ + this.subLocationsToExcludePattern = Pattern.compile(subLocationsToExclude, Pattern.CASE_INSENSITIVE); + } + + //iType Inclusion/Exclusion Check + if (iType == null || iType.isEmpty()){ + iType = ".*"; + } + if (iTypesToExclude == null) { + iTypesToExclude = ""; + } + if (iType.equals(".*") && (iTypesToExclude.isEmpty())){ + matchAlliTypes = true; + } + this.iTypePattern = Pattern.compile(iType, Pattern.CASE_INSENSITIVE); + if (!iTypesToExclude.isEmpty()){ + this.iTypesToExcludePattern = Pattern.compile(iTypesToExclude, Pattern.CASE_INSENSITIVE); + } + + //Audience Inclusion/Exclusion Check + if (audience == null || audience.isEmpty()) { + audience = ".*"; + } + if (audiencesToExclude == null) { + audiencesToExclude = ""; + } + if (audience.equals(".*") && audiencesToExclude.isEmpty()){ + matchAllAudiences = true; + } + this.audiencePattern = Pattern.compile(audience, Pattern.CASE_INSENSITIVE); + if (audiencesToExclude != null && !audiencesToExclude.isEmpty()){ + this.audiencesToExcludePattern = Pattern.compile(audiencesToExclude, Pattern.CASE_INSENSITIVE); + } + + //Format Inclusion/Exclusion Check + if (format == null || format.isEmpty()){ + format = ".*"; + } + this.formatPatternString = format; + if (formatsToExclude == null) { + formatsToExclude = ""; + } + if (format.equals(".*") && formatsToExclude.isEmpty()){ + matchAllFormats = true; + } + this.formatPattern = Pattern.compile(format, Pattern.CASE_INSENSITIVE); + if (!formatsToExclude.isEmpty()){ + this.formatsToExcludePattern = Pattern.compile(formatsToExclude, Pattern.CASE_INSENSITIVE); + } + + //Shelf Location Inclusion/Exclusion Check + if (shelfLocation == null || shelfLocation.isEmpty()) { + shelfLocation = ".*"; + } + if (shelfLocation.equals(".*") && (shelfLocationsToExclude == null || shelfLocationsToExclude.isEmpty())){ + matchAllShelfLocations = true; + } + this.shelfLocationPattern = Pattern.compile(shelfLocation, Pattern.CASE_INSENSITIVE); + if (shelfLocationsToExclude != null && !shelfLocationsToExclude.isEmpty()){ + this.shelfLocationsToExcludePattern = Pattern.compile(shelfLocationsToExclude, Pattern.CASE_INSENSITIVE); + } + + //Collection Code Inclusion/Exclusion Check + if (collectionCode == null || collectionCode.isEmpty()) { + collectionCode = ".*"; + } + if (collectionCode.equals(".*") && collectionCodesToExclude.isEmpty()){ + matchAllCollectionCodes = true; + } + this.collectionCodePattern = Pattern.compile(collectionCode, Pattern.CASE_INSENSITIVE); + if (!collectionCodesToExclude.isEmpty()){ + this.collectionCodesToExcludePattern = Pattern.compile(collectionCodesToExclude, Pattern.CASE_INSENSITIVE); + } + this.marcTagToMatch = Objects.requireNonNullElse(marcTagToMatch, ""); + + if (marcValueToMatch == null || marcValueToMatch.isEmpty()){ + marcValueToMatch = ".*"; + } + this.marcValueToMatchPattern = Pattern.compile(marcValueToMatch); + + this.includeExcludeMatches = includeExcludeMatches; + } + + private String lastIdentifier = null; + private boolean lastIdentifierResult = false; + + HashMap inclusionCache = new HashMap<>(); + + //TODO: We can potentially just pass in the ItemInfo object instead of all or most of these parameters + // This would likely require creating an interface for ItemInfo under java_shared_libraries. + boolean isItemIncluded(String itemIdentifier, String subLocationCode, String iType, TreeSet audiences, String audiencesAsString, String format, String shelfLocation, String collectionCode, org.marc4j.marc.Record marcRecord, DebugLogger debugLogger){ + if (lastIdentifier != null && lastIdentifier.equals(itemIdentifier)){ + return lastIdentifierResult; + } + + lastIdentifier = itemIdentifier; + //Determine if we have already determined this already + boolean hasCachedValue = true; + + if (matchAlliTypes){ + iType = "any"; + } + String audienceKey = audiencesAsString; + if (matchAllAudiences){ + audienceKey = "all"; + } + if (matchAllFormats){ + format = "any"; + } + if(matchAllShelfLocations){ + shelfLocation = "all"; + } + if(matchAllCollectionCodes){ + collectionCode = "any"; + } + String inclusionCacheKey = subLocationCode + iType + audienceKey + format + shelfLocation + collectionCode; + Boolean cachedInclusion = inclusionCache.get(inclusionCacheKey); + if (cachedInclusion == null){ + hasCachedValue = false; + } + + boolean isIncluded; + + if (!hasCachedValue){ + isIncluded = true; + + if (!subLocationCode.isEmpty()){ + if (!matchAllSubLocations) { + if (!subLocationCodePattern.matcher(subLocationCode).matches()) { + isIncluded = false; + } + if (isIncluded && subLocationsToExcludePattern != null) { + if (subLocationsToExcludePattern.matcher(subLocationCode).matches()){ + isIncluded = false; + } + } + } + } + + //Check Formats to include & exclude + if (isIncluded && format != null && !format.isEmpty()){ + if (!matchAllFormats) { + if (!formatPattern.matcher(format).matches()) { + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because format '" + format + "' does not match pattern '" + formatPatternString + "'", 3); + } + isIncluded = false; + } + if (isIncluded && formatsToExcludePattern != null) { + if(formatsToExcludePattern.matcher(format).matches()){ + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because format '" + format + "' matches exclusion pattern", 3); + } + isIncluded = false; + } + } + } + } + + //Check iTypes to include & exclude + if (isIncluded && iType != null){ + if (!matchAlliTypes) { + if (!iTypePattern.matcher(iType).matches()) { + isIncluded = false; + } + if (isIncluded && iTypesToExcludePattern != null) { + if(iTypesToExcludePattern.matcher(iType).matches()) { + isIncluded = false; + } + } + } + } + //Check Shelf Location to include & exclude + if (isIncluded && !matchAllShelfLocations){ //still want to process empty shelf locations, don't check for length > 0 + if (shelfLocation != null && !shelfLocation.isEmpty()){ + if (!shelfLocationPattern.matcher(shelfLocation).matches()) { + isIncluded = false; + } + if (isIncluded && shelfLocationsToExcludePattern != null) { + if(shelfLocationsToExcludePattern.matcher(shelfLocation).matches()) { + isIncluded = false; + } + } + } + else { + if (!shelfLocationPattern.pattern().equals(".*")) { + isIncluded = false; + } + } + + } + //Check Collection Code to include & exclude + if (isIncluded && !matchAllCollectionCodes){ + if (collectionCode != null && !collectionCode.isEmpty()) { + if (!collectionCodePattern.matcher(collectionCode).matches()) { + isIncluded = false; + } + if (isIncluded && collectionCodesToExcludePattern != null) { + if(collectionCodesToExcludePattern.matcher(collectionCode).matches()) { + isIncluded = false; + } + } + } + else { + if (!collectionCodePattern.pattern().equals(".*")) { + isIncluded = false; + } + } + } + //Check audiences to include & exclude + if (isIncluded && !matchAllAudiences){ + boolean audienceMatched = false; + for (String audience : audiences) { + //As soon as something is either matched or excluded we can stop checking. + if (audiencePattern.matcher(audience).matches()) { + audienceMatched = true; + break; + } + } + if (audienceMatched){ + for (String audience : audiences) { + //As soon as something is either matched or excluded we can stop checking. + if (audiencesToExcludePattern != null && audiencesToExcludePattern.matcher(audience).matches()) { + audienceMatched = false; + break; + } + } + } + isIncluded = audienceMatched; + } + + //Make sure not to cache marc tag determination + inclusionCache.put(inclusionCacheKey, isIncluded); + }else{ + isIncluded = cachedInclusion; + } + //Make sure not to cache marc tag determination + //TODO: *Someday* if the marc tag to match is the item tag, only get the marc tag for the item we are on. + if (isIncluded && !marcTagToMatch.isEmpty()) { + boolean hasMatch = false; + Set marcValuesToCheck = MarcUtil.getFieldList(marcRecord, marcTagToMatch); + for (String marcValueToCheck : marcValuesToCheck) { + if (marcValueToMatchPattern.matcher(marcValueToCheck).matches()) { + hasMatch = true; + break; + } + } + isIncluded = hasMatch && includeExcludeMatches; + } + + lastIdentifierResult = isIncluded; + return isIncluded; + } +} diff --git a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleLocation.java b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleLocation.java new file mode 100644 index 0000000000..0872726ff7 --- /dev/null +++ b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/InclusionRuleLocation.java @@ -0,0 +1,97 @@ +package com.turning_leaf_technologies.indexing; + +import com.turning_leaf_technologies.marc.MarcUtil; + +import java.util.HashMap; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +/** + * Inclusion Rule based on record type and location code only. + * These are generally different for each library and location although there is some sharing. + * By pulling this code out we can optimize checking before proceeding to details + */ +class InclusionRuleLocation { + private final String recordType; + private final boolean matchAllLocations; + private boolean isLocationExactMatch; + private String locationCodeToMatch; + private Pattern locationCodePattern; + private Pattern locationsToExcludePattern = null; + private final String locationPatternString; + + private static final Pattern isRegexPattern = Pattern.compile("[.*?{}\\\\^\\[\\]|$]"); + InclusionRuleLocation(String recordType, String locationCode, String locationsToExclude){ + this.recordType = recordType; + + //Location & Sublocation Code Inclusion/Exclusion Check + if (locationCode.isEmpty()){ + locationCode = ".*"; + } + this.locationPatternString = locationCode; + matchAllLocations = locationCode.equals(".*"); + if (!matchAllLocations){ + if (isRegexPattern.matcher(locationCode).find()) { + this.locationCodePattern = Pattern.compile(locationCode, Pattern.CASE_INSENSITIVE); + }else{ + this.locationCodeToMatch = locationCode; + isLocationExactMatch = true; + } + } + + if (locationsToExclude != null && !locationsToExclude.isEmpty()){ + this.locationsToExcludePattern = Pattern.compile(locationsToExclude, Pattern.CASE_INSENSITIVE); + } + } + + //TODO: We can potentially just pass in the ItemInfo object instead of all or most of these parameters + // This would likely require creating an interface for ItemInfo under java_shared_libraries. + boolean isItemIncluded(String itemIdentifier, String recordType, String locationCode, DebugLogger debugLogger){ + //Determine if we have already determined this already + if (matchAllLocations){ + if (!recordType.equals(this.recordType)){ + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because record type '" + recordType + "' does not match rule record type '" + this.recordType + "'", 3); + } + return false; + }else{ + return true; + } + }else{ + if (!this.recordType.equals(recordType)) { + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because record type '" + recordType + "' does not match rule record type '" + this.recordType + "'", 3); + } + return false; + } + + if (isLocationExactMatch) { + if (!locationCodeToMatch.equalsIgnoreCase(locationCode)) { + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because location '" + locationCode + "' does not match required location '" + locationCodeToMatch + "'", 3); + } + return false; + } + } else { + if (!locationCodePattern.matcher(locationCode).matches()) { + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because location '" + locationCode + "' does not match pattern '" + locationPatternString + "'", 3); + } + return false; + } + } + if (!locationCode.isEmpty() && locationsToExcludePattern != null) { + if (locationsToExcludePattern.matcher(locationCode).matches()) { + if (debugLogger != null && debugLogger.isDebugEnabled()) { + debugLogger.addDebugMessage("Item " + itemIdentifier + " excluded from scope because location '" + locationCode + "' matches exclusion pattern", 3); + } + return false; + } + } + + return true; + } + } +} diff --git a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingProfile.java b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingProfile.java index 4056eecf52..82f038b62a 100644 --- a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingProfile.java +++ b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingProfile.java @@ -106,6 +106,15 @@ public class IndexingProfile extends BaseIndexingSettings { private Pattern customFacet3ValuesToIncludePattern; private Pattern customFacet3ValuesToExcludePattern; + //Series Fields + private boolean index896asSeries; + private boolean index897asSeries; + private boolean index898asSeries; + private boolean index899asSeries; + + // Display Title processing + private Pattern displayTitleStripPattern; + //Evergreen settings private final int numRetriesForBibLookups; private final int numMillisecondsToPauseAfterBibLookups; @@ -270,6 +279,20 @@ public IndexingProfile(String serverName, ResultSet indexingProfileRS, Connectio prioritizeAvailableRecordsForTitleSelection = indexingProfileRS.getBoolean("prioritizeAvailableRecordsForTitleSelection"); + index896asSeries = indexingProfileRS.getBoolean("index896asSeries"); + index897asSeries = indexingProfileRS.getBoolean("index897asSeries"); + index898asSeries = indexingProfileRS.getBoolean("index898asSeries"); + index899asSeries = indexingProfileRS.getBoolean("index899asSeries"); + + try { + String pattern = indexingProfileRS.getString("displayTitleStripRegex"); + if (pattern != null && !pattern.isEmpty()) { + displayTitleStripPattern = Pattern.compile(pattern); + } + } catch (Exception e) { + logEntry.incErrors("Could not load displayTitleStripRegex pattern", e); + } + //Custom Facet 1 this.customFacet1SourceField = indexingProfileRS.getString("customFacet1SourceField"); String customFacet1ValuesToInclude = indexingProfileRS.getString("customFacet1ValuesToInclude"); @@ -1179,4 +1202,31 @@ public boolean getPrioritizeAvailableRecordsForTitleSelection() { public void setPrioritizeAvailableRecordsForTitleSelection(boolean prioritizeAvailableRecordsForTitleSelection) { this.prioritizeAvailableRecordsForTitleSelection = prioritizeAvailableRecordsForTitleSelection; } + + public boolean isIndex896asSeries() { + return index896asSeries; + } + + public boolean isIndex897asSeries() { + return index897asSeries; + } + + public boolean isIndex898asSeries() { + return index898asSeries; + } + + public boolean isIndex899asSeries() { + return index899asSeries; + } + + /** + * Gets the compiled regex pattern for stripping text from display titles. + * This case-sensitive pattern is only applied to ILS records. + * + * @return The compiled Pattern to match text that should be removed from display titles, + * or null if no pattern is configured. + */ + public Pattern getDisplayTitleStripPattern() { + return displayTitleStripPattern; + } } diff --git a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingUtils.java b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingUtils.java index d344047d30..82048e4ef9 100644 --- a/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingUtils.java +++ b/code/java_shared_libraries/src/com/turning_leaf_technologies/indexing/IndexingUtils.java @@ -474,28 +474,28 @@ private static void loadLocationScopes(TreeSet scopes, HashMap scopes, HashMap scopes, HashMap scopes, HashMap{ //Determine if this is a library scope or location scope and store related information private boolean isLibraryScope; //If this is a library scope, we want to store pointers to the individual location scopes - private final HashSet locationScopes = new HashSet<>(); + private final ArrayList locationScopes = new ArrayList<>(); private boolean isLocationScope; private Scope libraryScope; @@ -30,9 +31,9 @@ public class Scope implements Comparable{ //Called restrictOwningBranchesAndSystems in PHP admin interface private boolean restrictOwningLibraryAndLocationFacets; private boolean isConsortialCatalog; - private final HashSet ownershipRules = new HashSet<>(); + private final ArrayList ownershipRules = new ArrayList<>(); //Inclusion rules indicate records owned by someone else that should be shown within the scope - private final HashSet inclusionRules = new HashSet<>(); + private final ArrayList inclusionRules = new ArrayList<>(); private String ilsCode; private int publicListsToInclude; @@ -119,7 +120,7 @@ public InclusionResult isItemPartOfScope(String itemIdentifier, String recordTyp * Determine if the item is part of the current scope based on location code and other information */ public boolean isItemOwnedByScope(String itemIdentifier, String recordType, String locationCode, String subLocationCode, String iType, TreeSet audiences, String audiencesAsString, String format, String shelfLocation, String collectionCode, boolean isHoldable, boolean isOnOrder, boolean isEContent, Record marcRecord, DebugLogger debugLogger){ - for(InclusionRule curRule: ownershipRules){ + for (InclusionRule curRule: ownershipRules){ if (curRule.isItemIncluded(itemIdentifier, recordType, locationCode, subLocationCode, iType, audiences, audiencesAsString, format, shelfLocation, collectionCode, isHoldable, isOnOrder, isEContent, marcRecord, debugLogger)){ return true; } @@ -162,15 +163,21 @@ public boolean isLocationScope() { } void addOwnershipRule(InclusionRule ownershipRule) { - ownershipRules.add(ownershipRule); + if (!ownershipRules.contains( ownershipRule ) ) { + ownershipRules.add(ownershipRule); + } } void addInclusionRule(InclusionRule inclusionRule) { - inclusionRules.add(inclusionRule); + if (!inclusionRules.contains( inclusionRule ) ) { + inclusionRules.add(inclusionRule); + } } void addLocationScope(Scope locationScope) { - this.locationScopes.add(locationScope); + if (!locationScopes.contains( locationScope ) ) { + this.locationScopes.add(locationScope); + } } void setLibraryScope(Scope libraryScope) { @@ -190,7 +197,7 @@ void setRestrictOwningLibraryAndLocationFacets(boolean restrictOwningLibraryAndL this.restrictOwningLibraryAndLocationFacets = restrictOwningLibraryAndLocationFacets; } - public HashSet getLocationScopes() { + public ArrayList getLocationScopes() { return locationScopes; } @@ -329,4 +336,4 @@ public static class InclusionResult{ this.isOwned = isOwned; } } -} \ No newline at end of file +} diff --git a/code/java_shared_libraries/src/com/turning_leaf_technologies/marc/MarcUtil.java b/code/java_shared_libraries/src/com/turning_leaf_technologies/marc/MarcUtil.java index 4aef8ebc29..4fc353f3d8 100644 --- a/code/java_shared_libraries/src/com/turning_leaf_technologies/marc/MarcUtil.java +++ b/code/java_shared_libraries/src/com/turning_leaf_technologies/marc/MarcUtil.java @@ -371,6 +371,10 @@ public static List getDataFields(Record marcRecord, int[] tags) { return marcRecord.getDataFields(tags); } + public static List getDataFields(Record marcRecord, ArrayList tags) { + return marcRecord.getDataFields(tags); + } + public static ControlField getControlField(Record marcRecord, String tag){ List variableFields = marcRecord.getControlFields(tag); ControlField variableFieldReturn = null; diff --git a/code/java_shared_libraries/src/org/marc4j/marc/Record.java b/code/java_shared_libraries/src/org/marc4j/marc/Record.java index 14c8a1b4ef..f4c5e7885a 100644 --- a/code/java_shared_libraries/src/org/marc4j/marc/Record.java +++ b/code/java_shared_libraries/src/org/marc4j/marc/Record.java @@ -21,6 +21,7 @@ package org.marc4j.marc; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import org.marc4j.MarcError; @@ -118,7 +119,7 @@ public interface Record extends Serializable { /** * Returns the control number or null if no control number is * available. - * + * * This method returns the data for a ControlField with tag * 001. * @@ -194,6 +195,8 @@ public interface Record extends Serializable { List getDataFields(int[] tags); + List getDataFields(ArrayList tags); + /** * Returns the Leader. * @@ -254,12 +257,12 @@ public interface Record extends Serializable { List find(int[] tag, String pattern); /** - * Logs an error message using the stated severity level. Uses the values passed + * Logs an error message using the stated severity level. Uses the values passed * in id, field, and subfield to note the location of the error. - * + * * @param field - the tag of the field currently being processed * @param subfield - the subfield tag of the subfield currently being processed - * @param severity - An indication of the relative severity of the error that was + * @param severity - An indication of the relative severity of the error that was * encountered. * @param message - A descriptive message about the error that was encountered. */ @@ -267,7 +270,7 @@ public interface Record extends Serializable { /** * Copies a List of errors into the current error handler - * + * * @param newErrors - A list of Errors. */ void addErrors(List newErrors); diff --git a/code/java_shared_libraries/src/org/marc4j/marc/impl/RecordImpl.java b/code/java_shared_libraries/src/org/marc4j/marc/impl/RecordImpl.java index 638418ed67..1f10364a94 100644 --- a/code/java_shared_libraries/src/org/marc4j/marc/impl/RecordImpl.java +++ b/code/java_shared_libraries/src/org/marc4j/marc/impl/RecordImpl.java @@ -340,7 +340,7 @@ public List getVariableFields() { /** * Gets a {@link List} of {@link VariableField}s from the {@link Record} * including the LEADER recast as ControlField for field matching purposes. - * + * * @return a List of all VariableFields plus the Leader represented as a ControlField */ public List getVariableFieldsWithLeader() { @@ -422,6 +422,15 @@ public List getDataFields(final int[] tags) { return result; } + public List getDataFields(final ArrayList tags) { + final List result = new ArrayList<>(); + for (int tag : tags) { + result.addAll(getDataFields(tag)); + } + + return result; + } + String stringRepresentation = null; /** * Returns a string representation of this record. @@ -584,12 +593,12 @@ public Long getId() { } /** - * Logs an error message using the stated severity level. Uses the values passed + * Logs an error message using the stated severity level. Uses the values passed * in id, field, and subfield to note the location of the error. - * + * * @param field - the tag of the field currently being processed * @param subfield - the subfield tag of the subfield currently being processed - * @param severity - An indication of the relative severity of the error that was + * @param severity - An indication of the relative severity of the error that was * encountered. * @param message - A descriptive message about the error that was encountered. */ @@ -607,7 +616,7 @@ public void addError(final String field, final String subfield, final int severi /** * Copies a List of errors into the current error handler - * + * * @param newErrors - A list of Errors. */ @Override diff --git a/code/koha_export/koha_export.jar b/code/koha_export/koha_export.jar index d0aa39a60b..50d7b7ae6a 100644 Binary files a/code/koha_export/koha_export.jar and b/code/koha_export/koha_export.jar differ diff --git a/code/oai_indexer/oai_indexer.jar b/code/oai_indexer/oai_indexer.jar index 07ba7ee59b..bd0f03b044 100644 Binary files a/code/oai_indexer/oai_indexer.jar and b/code/oai_indexer/oai_indexer.jar differ diff --git a/code/oai_indexer/src/com/turning_leaf_technologies/oai/OaiIndexerMain.java b/code/oai_indexer/src/com/turning_leaf_technologies/oai/OaiIndexerMain.java index 764b59315f..0b67a9d26f 100644 --- a/code/oai_indexer/src/com/turning_leaf_technologies/oai/OaiIndexerMain.java +++ b/code/oai_indexer/src/com/turning_leaf_technologies/oai/OaiIndexerMain.java @@ -575,7 +575,13 @@ private static boolean indexElement(Element curRecordElement, Long collectionId, case "dc:identifier": case "mods:identifier": String textContentLower = textContent.toLowerCase(); - if (textContentLower.startsWith("http") && !textContentLower.endsWith(".jpg") && !textContentLower.endsWith(".mp3") && !textContentLower.endsWith(".pdf")) { + if (textContentLower.startsWith("http") && + !textContentLower.matches(".*\\.jpg([?&].*|$)") && + !textContentLower.matches(".*\\.png([?&].*|$)") && + !textContentLower.matches(".*\\.tif([?&].*|$)") && + !textContentLower.matches(".*\\.mp3([?&].*|$)") && + !textContentLower.matches(".*\\.pdf([?&].*|$)")) + { if (solrRecord.getIdentifier() == null || !solrRecord.getIdentifier().startsWith("http")) { solrRecord.setIdentifier(textContent); } else { diff --git a/code/overdrive_extract/overdrive_extract.jar b/code/overdrive_extract/overdrive_extract.jar index 33e8b004b2..ed6e2442f7 100644 Binary files a/code/overdrive_extract/overdrive_extract.jar and b/code/overdrive_extract/overdrive_extract.jar differ diff --git a/code/overdrive_extract/src/com/turning_leaf_technologies/overdrive/ExtractOverDriveInfo.java b/code/overdrive_extract/src/com/turning_leaf_technologies/overdrive/ExtractOverDriveInfo.java index 551ad873ba..3c8f40cbef 100644 --- a/code/overdrive_extract/src/com/turning_leaf_technologies/overdrive/ExtractOverDriveInfo.java +++ b/code/overdrive_extract/src/com/turning_leaf_technologies/overdrive/ExtractOverDriveInfo.java @@ -162,7 +162,7 @@ int extractOverDriveInfo(Ini configIni, String serverName, Connection dbConn, Ov logEntry.addNote("There are " + numProductsToUpdate + " products that need to be checked for updates"); logEntry.saveResults(); - //Do some counts of numbers of records that will be updated for logging purposes + //Do some counts of the records that will be updated for logging purposes int numRecordsToUpdate = 0; int numNewRecords = 0; int totalRecordsWithChanges = 0; @@ -247,7 +247,7 @@ int extractOverDriveInfo(Ini configIni, String serverName, Connection dbConn, Ov } if (checkForDeletedRecords) { - //Remove any records that no longer exist + //Remove any records that no longer exist. //There is currently an issue with Libby Search APIs that cause all records to not be returned, //so we will avoid deleting records if we are deleting more than 500 records or 5% of the collection int totalRecordsToDelete = 0; @@ -334,8 +334,9 @@ int extractOverDriveInfo(Ini configIni, String serverName, Connection dbConn, Ov if (settings.isRunFullUpdate()){ columnToUpdate = "lastUpdateOfAllRecords"; } - updateExtractTime = dbConn.prepareStatement("UPDATE overdrive_settings set " + columnToUpdate + " = ?"); + updateExtractTime = dbConn.prepareStatement("UPDATE overdrive_settings set " + columnToUpdate + " = ? WHERE id = ?"); updateExtractTime.setLong(1, extractStartTime / 1000); + updateExtractTime.setLong(2, settings.getId()); updateExtractTime.executeUpdate(); logger.debug("Setting last extract time to " + extractStartTime + " " + new Date(extractStartTime)); } @@ -361,7 +362,7 @@ int processSingleWork(String singleWorkId, Ini configIni, String serverName, Con if (settings.getClientSecret() == null || settings.getClientKey() == null || settings.getAccountId() == null || settings.getClientSecret().isEmpty() || settings.getClientKey().isEmpty() || settings.getAccountId().isEmpty()) { logEntry.addNote("Did not find correct configuration in settings, not loading Libby titles"); } else { - //Load products from database this lets us know what is new, what has been deleted, and what has been updated + //Load products from the database this lets us know what is new, what has been deleted, and what has been updated singleWorkId = singleWorkId.toLowerCase(); OverDriveRecordInfo recordInfo = new OverDriveRecordInfo(); recordInfo.setId(singleWorkId); @@ -495,7 +496,7 @@ private void initOverDriveExtract(Connection dbConn, OverDriveExtractLogEntry lo logger.info("Loading all records that have changed since " + lastUpdateFormat.format(lastExtractDate)); logEntry.addNote("Loading all records that have changed since " + lastUpdateFormat.format(lastExtractDate)); lastUpdateTimeParam = lastUpdateFormat.format(lastExtractDate); - //Simple Date Format doesn't give us quite the right timezone format so adjust + //Simple Date Format doesn't give us quite the right timezone format, so adjust lastUpdateTimeParam = lastUpdateTimeParam.substring(0, lastUpdateTimeParam.length() - 2) + ":" + lastUpdateTimeParam.substring(lastUpdateTimeParam.length() - 2); //lastUpdateTimeParam = lastUpdateTimeParam.substring(0, lastUpdateTimeParam.length() - 5) + "Z"; }else{ @@ -563,6 +564,7 @@ private void updateProductInDB(long databaseId, String overDriveId, Long crossRe //Update the product in the database long curTime = new Date().getTime() / 1000; int curCol = 0; + //noinspection DuplicatedCode updateProductStmt.setLong(++curCol, crossRefId); updateProductStmt.setString(++curCol, mediaType); updateProductStmt.setString(++curCol, title); @@ -584,7 +586,7 @@ private void updateProductInDB(long databaseId, String overDriveId, Long crossRe } catch (SQLException e) { logEntry.incErrors("Error updating Libby product " + overDriveId, e); } - + } private synchronized long addProductToDB(String overDriveId, Long crossRefId, String mediaType, String title, String subtitle, String series, String primaryCreatorRole, String primaryCreatorName, String coverUrl) { @@ -594,6 +596,7 @@ private synchronized long addProductToDB(String overDriveId, Long crossRefId, St long curTime = new Date().getTime() / 1000; addProductStmt.setString(++curCol, overDriveId); addProductStmt.setLong(++curCol, crossRefId); + //noinspection DuplicatedCode addProductStmt.setString(++curCol, mediaType); addProductStmt.setString(++curCol, title); addProductStmt.setString(++curCol, subtitle); @@ -609,7 +612,7 @@ private synchronized long addProductToDB(String overDriveId, Long crossRefId, St if (newIdRS.next()) { databaseId = newIdRS.getLong(1); }else{ - //get the id of the title in Libby. This happens when we are adding titles in multiple threads. + //Get the id of the title in Libby. This happens when we are adding titles in multiple threads, //or when the title was not previously available in a setting, but did exist in another setting getProductIdByOverDriveIdStmt.setString(1, overDriveId); ResultSet getProductIdByOverDriveIdRS = getProductIdByOverDriveIdStmt.executeQuery(); @@ -662,6 +665,7 @@ private boolean loadProductsFromAPI(int loadType, long startTime) throws SocketT JSONObject libraryInfo = libraryInfoResponse.getJSONResponse(); //noinspection CommentedOutCode try { + //noinspection DuplicatedCode String mainProductUrl = libraryInfo.getJSONObject("links").getJSONObject("products").getString("href"); if (mainProductUrl.contains("?")) { mainProductUrl += "&minimum=true"; @@ -772,6 +776,7 @@ private void loadProductsForAdvantageAccount(int loadType, JSONObject curAdvanta AdvantageCollectionInfo collectionInfo = null; if (loadType == LOAD_ALL_PRODUCTS || loadCollectionInfo) { collectionInfo = new AdvantageCollectionInfo(); + //noinspection DuplicatedCode collectionInfo.setAdvantageId(curAdvantageAccount.getInt("id")); collectionInfo.setName(curAdvantageAccount.getString("name")); collectionInfo.setCollectionToken(curAdvantageAccount.getString("collectionToken")); @@ -814,6 +819,7 @@ private void loadProductsForAdvantageAccount(int loadType, JSONObject curAdvanta if (advantageWebServiceResponse.getResponseCode() == 200) { JSONObject advantageSelfInfo = advantageWebServiceResponse.getJSONResponse(); if (advantageSelfInfo != null) { + //noinspection DuplicatedCode String productUrl = advantageSelfInfo.getJSONObject("links").getJSONObject("products").getString("href"); if (productUrl.contains("?")) { productUrl += "&minimum=true"; @@ -866,6 +872,7 @@ private boolean loadAccountInformationFromAPI() throws SocketTimeoutException { JSONObject curAdvantageAccount = advantageAccounts.getJSONObject(i); AdvantageCollectionInfo collectionInfo = new AdvantageCollectionInfo(); + //noinspection DuplicatedCode collectionInfo.setAdvantageId(curAdvantageAccount.getInt("id")); collectionInfo.setName(curAdvantageAccount.getString("name")); collectionInfo.setCollectionToken(curAdvantageAccount.getString("collectionToken")); @@ -926,7 +933,7 @@ private void loadProductsFromUrl(AdvantageCollectionInfo collectionInfo, String } int batchSize = 300; for (int i = 0; i < numProducts; i += batchSize) { - //Just search for the specific product + //Search for the specific product String batchUrl = mainProductUrl; if (mainProductUrl.contains("?")) { batchUrl += "&"; @@ -954,7 +961,7 @@ private void loadProductsFromUrl(AdvantageCollectionInfo collectionInfo, String setLastSeenForProduct(startTime, curRecord); totalProductsInCollection++; } else { - //By definition the record has changes if we are loading just changes + //By definition, the record has changes if we are loading just changes curRecord.hasChanges = true; if (previouslyLoadedProduct == null) { allProductsInOverDrive.put(curRecord.getId(), curRecord); @@ -980,9 +987,9 @@ private void loadProductsFromUrl(AdvantageCollectionInfo collectionInfo, String } break; }else{ - //This happens, retry and don't log the error unless we're at the end of trying. + //This seems to be a normal thing if a batch has no titles in it. Log the condition and move on. if (tries == maxTries - 1) { - logEntry.incErrors("Batch " + i + " did not have any products in it, but we got back a 200 code"); + logEntry.addNote("Batch " + i + " did not have any products in it, but we got back a 200 code"); } } } else { @@ -1146,10 +1153,11 @@ private void saveMetadataToDatabase(OverDriveRecordInfo overDriveInfo, long curT if (getExistingMetadataIdRS.next()){ long metadataId = getExistingMetadataIdRS.getLong("id"); + //noinspection DuplicatedCode updateMetaDataStmt.setLong(++curCol, metadataChecksum); updateMetaDataStmt.setString(++curCol, metaData.has("sortTitle") ? metaData.getString("sortTitle") : ""); updateMetaDataStmt.setString(++curCol, metaData.has("publisher") ? metaData.getString("publisher") : ""); - //Grab the textual version of publish date rather than the actual date + //Grab the textual version of publishDate rather than the actual date if (metaData.has("publishDateText")){ String publishDateText = metaData.getString("publishDateText"); if (publishDateText.matches("\\d{2}/\\d{2}/\\d{4}")){ @@ -1187,10 +1195,11 @@ private void saveMetadataToDatabase(OverDriveRecordInfo overDriveInfo, long curT updateMetaDataStmt.executeUpdate(); }else{ addMetadataStmt.setLong(++curCol, overDriveInfo.getDatabaseId()); + //noinspection DuplicatedCode addMetadataStmt.setLong(++curCol, metadataChecksum); addMetadataStmt.setString(++curCol, metaData.has("sortTitle") ? metaData.getString("sortTitle") : ""); addMetadataStmt.setString(++curCol, metaData.has("publisher") ? metaData.getString("publisher") : ""); - //Grab the textual version of publish date rather than the actual date + //Grab the textual version of publishDate rather than the actual date if (metaData.has("publishDateText")){ String publishDateText = metaData.getString("publishDateText"); if (publishDateText.matches("\\d{2}/\\d{2}/\\d{4}")){ @@ -1227,7 +1236,7 @@ private void saveMetadataToDatabase(OverDriveRecordInfo overDriveInfo, long curT try { addMetadataStmt.executeUpdate(); }catch (SQLIntegrityConstraintViolationException e) { - //Another thread already created it, since we don't need the ID for additional work + //Another thread already created it, since we don't need the ID for additional work, // and since the metadata doesn't normally change between collections, we can just ignore this } } @@ -1307,7 +1316,7 @@ private void saveMetadataToDatabase(OverDriveRecordInfo overDriveInfo, long curT } private synchronized boolean updateOverDriveAvailability(OverDriveRecordInfo overDriveInfo, long databaseId, boolean singleWork) { - //Don't need to load availability if we already have availability and the availability was checked within the last hour + //Don't need to load availability if we already have availability, and the availability was checked within the last hour long curTime = new Date().getTime() / 1000; final boolean[] changesMade = {false}; @@ -1364,7 +1373,7 @@ private synchronized boolean updateOverDriveAvailability(OverDriveRecordInfo ove //404 is a message that availability has been deleted. if (availabilityResponse.getResponseCode() == 404) { //Add a note and skip to the next collection, in reality, this is probably deleted, - //but Nashville was having issues with 404s coming incorrectly, so we can just keep retrying + //but Nashville was having issues with 404 errors coming incorrectly, so we can just keep retrying //No longer needed for logging //logEntry.addNote("Got a 404 availability response code for " + url + " not updating for " + collectionInfo.getName()); } else if (availabilityResponse.getResponseCode() != 200) { @@ -1638,9 +1647,7 @@ private boolean connectToOverDriveAPI() throws SocketTimeoutException { JSONObject parser = new JSONObject(response.toString()); overDriveAPIToken = parser.getString("access_token"); overDriveAPITokenType = parser.getString("token_type"); - //logger.debug("Token expires in " + parser.getLong("expires_in") + " seconds"); overDriveAPIExpiration = new Date().getTime() + (parser.getLong("expires_in") * 1000) - 10000; - //logger.debug("Libby token is " + overDriveAPIToken); } else { logger.error("Received error " + conn.getResponseCode() + " connecting to Libby authentication service. Encoded auth header: " + encoded); // Get any errors @@ -1694,6 +1701,7 @@ void close(){ allAdvantageCollections.clear(); try { + //noinspection DuplicatedCode addProductStmt.close(); getProductIdByOverDriveIdStmt.close(); updateProductStmt.close(); diff --git a/code/palace_project_export/palace_project_export.jar b/code/palace_project_export/palace_project_export.jar index 3b62419920..2317e0b02b 100644 Binary files a/code/palace_project_export/palace_project_export.jar and b/code/palace_project_export/palace_project_export.jar differ diff --git a/code/palace_project_export/src/org/aspendiscovery/palace_project/PalaceProjectExportMain.java b/code/palace_project_export/src/org/aspendiscovery/palace_project/PalaceProjectExportMain.java index f50157f95d..2835d1d628 100644 --- a/code/palace_project_export/src/org/aspendiscovery/palace_project/PalaceProjectExportMain.java +++ b/code/palace_project_export/src/org/aspendiscovery/palace_project/PalaceProjectExportMain.java @@ -572,17 +572,15 @@ private static void processDeletedCollections( for (String deletedCollectionName : allAspenCollections) { PalaceProjectCollection deletedColl = palaceProjectCollections.get(deletedCollectionName); - if (!deletedColl.includeInAspen) { - logEntry.addNote("Deleting titles from deleted collection " + deletedCollectionName + - ", and removing the collection because it is no longer in the Palace Project."); - HashMap titlesForCollection = - getTitlesForCollection(getTitlesForCollectionStmt, deletedColl); - for (PalaceProjectTitleAvailability titleAvailability : titlesForCollection.values()) { - removePalaceProjectTitleFromCollection(titleAvailability.id, titleAvailability.titleId); - } - deleteCollectionStmt.setLong(1, deletedColl.id); - deleteCollectionStmt.executeUpdate(); - } + logEntry.addNote("Deleting titles from deleted collection " + deletedCollectionName + + ", and removing the collection because it is no longer in the Palace Project."); + HashMap titlesForCollection = + getTitlesForCollection(getTitlesForCollectionStmt, deletedColl); + for (PalaceProjectTitleAvailability titleAvailability : titlesForCollection.values()) { + removePalaceProjectTitleFromCollection(titleAvailability.id, titleAvailability.titleId); + } + deleteCollectionStmt.setLong(1, deletedColl.id); + deleteCollectionStmt.executeUpdate(); } } diff --git a/code/polaris_export/polaris_export.jar b/code/polaris_export/polaris_export.jar index 42330c0409..0731b49a21 100644 Binary files a/code/polaris_export/polaris_export.jar and b/code/polaris_export/polaris_export.jar differ diff --git a/code/polaris_export/src/com/turning_leaf_technologies/polaris/ExistingLocation.java b/code/polaris_export/src/com/turning_leaf_technologies/polaris/ExistingLocation.java index e01d676f08..e3d02d010d 100644 --- a/code/polaris_export/src/com/turning_leaf_technologies/polaris/ExistingLocation.java +++ b/code/polaris_export/src/com/turning_leaf_technologies/polaris/ExistingLocation.java @@ -13,6 +13,7 @@ public void setCode(String code) { this.code = code; } + @SuppressWarnings("unused") public String getName() { return name; } diff --git a/code/polaris_export/src/com/turning_leaf_technologies/polaris/PolarisExportMain.java b/code/polaris_export/src/com/turning_leaf_technologies/polaris/PolarisExportMain.java index e411aff1c3..5142421695 100644 --- a/code/polaris_export/src/com/turning_leaf_technologies/polaris/PolarisExportMain.java +++ b/code/polaris_export/src/com/turning_leaf_technologies/polaris/PolarisExportMain.java @@ -87,8 +87,9 @@ public class PolarisExportMain { private static PreparedStatement getRecordIdForItemIdStmt; private static PreparedStatement sourceForPolarisStmt; + private static SecretKeySpec signingKey; + private static Set bibIdsUpdatedDuringContinuous; - private static Set itemIdsUpdatedDuringContinuous; private static String singleWorkId = null; @@ -254,7 +255,7 @@ public static void main(String[] args) { //Don't exit, we will try again in a few minutes } - //Check to see if the jar has changes, and if so quit + //Check to see if the jar has changes, and if so, quit if (myChecksumAtStart != JarUtil.getChecksumForJar(logger, processName, "./" + processName + ".jar")){ IndexingUtils.markNightlyIndexNeeded(dbConn, logger); disconnectDatabase(); @@ -265,7 +266,7 @@ public static void main(String[] args) { disconnectDatabase(); break; } - //Check to see if it's between midnight and 1 am and the jar has been running more than 15 hours. If so, restart just to clean up memory. + //Check to see if it's between midnight and 1 am, and the jar has been running more than 15 hours. If so, restart just to clean up memory. GregorianCalendar nowAsCalendar = new GregorianCalendar(); Date now = new Date(); nowAsCalendar.setTime(now); @@ -287,7 +288,7 @@ public static void main(String[] args) { } disconnectDatabase(); - //Check to see if nightly indexing is running and if so, wait until it is done. + //Check to see if nightly indexing is running, and if so, wait until it is done. if (IndexingUtils.isNightlyIndexRunning(configIni, serverName, logger)) { //Quit and we will restart after if finishes System.exit(0); @@ -340,13 +341,15 @@ private static boolean use7_4DateFormatFunctionality() { private static void updateSublocationInfo(Connection dbConn) { try { - //In Polaris, Sub locations can be connected to multiple locations so need to account for that when loading and saving + //In Polaris, Sub locations can be connected to multiple locations, so need to account for that when loading and saving logEntry.addNote("Loading Pickup Areas into Sub-locations"); PreparedStatement existingAspenSublocationStmt = dbConn.prepareStatement("SELECT id, name, isValidHoldPickupAreaILS, weight from sublocation where locationId = ? AND ilsId = ?"); PreparedStatement updateAspenSublocationStmt = dbConn.prepareStatement("UPDATE sublocation SET name = ?, isValidHoldPickupAreaILS = ?, weight = ? where id = ?"); PreparedStatement addAspenSublocationStmt = dbConn.prepareStatement("INSERT INTO sublocation (locationId, name, ilsId, isValidHoldPickupAreaILS, isValidHoldPickupAreaAspen, weight) VALUES (?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS); PreparedStatement activateSublocationForAllPTypesStmt = dbConn.prepareStatement("INSERT INTO sublocation_ptype (sublocationId, patronTypeId) SELECT ?, id from ptype"); PreparedStatement getExistingAspenLocationsStmt = dbConn.prepareStatement("SELECT locationId, displayName, code from location", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + PreparedStatement getExistingSublocationsStmt = dbConn.prepareStatement("select distinct (ilsId) from sublocation where ilsId REGEXP '^[0-9]+$'", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + PreparedStatement deleteExistingSublocationStmt = dbConn.prepareStatement("UPDATE sublocation set isValidHoldPickupAreaILS = 0 where ilsId = ?"); ResultSet allExistingLocationsRS = getExistingAspenLocationsStmt.executeQuery(); HashMap existingLocations = new HashMap<>(); while (allExistingLocationsRS.next()) { @@ -358,7 +361,14 @@ private static void updateSublocationInfo(Connection dbConn) { existingLocations.put(existingLocation.getCode(), existingLocation); } - //Lookup the sub-locations in Polaris. It does not look like filtering by orgID works in the API so we will just grab them all + HashSet existingSublocationIds = new HashSet<>(); + ResultSet allExistingSublocationsRS = getExistingSublocationsStmt.executeQuery(); + while (allExistingSublocationsRS.next()) { + existingSublocationIds.add(allExistingSublocationsRS.getLong("ilsId")); + } + + + //Look up the sub-locations in Polaris. It does not look like filtering by orgID works in the API, so we will just grab them all String getPickupAreasUrl = "/PAPIService/REST/public/v1/1033/100/1/pickupareas"; WebServiceResponse pickupAreasResponse = callPolarisAPI(getPickupAreasUrl, null, "GET", "application/json", null); if (pickupAreasResponse.isSuccess()){ @@ -372,6 +382,8 @@ private static void updateSublocationInfo(Connection dbConn) { long sequence = pickupAreaInfo.getLong("SequenceID"); boolean selected = pickupAreaInfo.getBoolean("Selected"); + existingSublocationIds.remove(pickupAreaId); + //Get the parent location for the sublocation ExistingLocation parentLocation = existingLocations.get(parentOrganizationId); if (parentLocation != null) { @@ -380,7 +392,7 @@ private static void updateSublocationInfo(Connection dbConn) { existingAspenSublocationStmt.setLong(2, pickupAreaId); ResultSet existingAspenSublocationRS = existingAspenSublocationStmt.executeQuery(); if (existingAspenSublocationRS.next()) { - //We have an existing sublocation, make sure the name, sequence, and selection has not changed + //We have an existing sublocation, make sure the name, sequence, and selection have not changed long existingId = existingAspenSublocationRS.getLong("id"); String existingName = existingAspenSublocationRS.getString("name"); boolean existingSelected = existingAspenSublocationRS.getBoolean("isValidHoldPickupAreaILS"); @@ -418,6 +430,13 @@ private static void updateSublocationInfo(Connection dbConn) { } } allExistingLocationsRS.close(); + + //Remove any pickup locations that no longer exist + for (Long existingSublocationId : existingSublocationIds) { + deleteExistingSublocationStmt.setLong(1, existingSublocationId); + deleteExistingSublocationStmt.executeUpdate(); + } + logEntry.addNote("Finished Loading Pickup Areas into Sub-locations"); } catch (Exception e) { @@ -462,7 +481,7 @@ private static void importLibraryBranchData(Connection dbConn) { ResultSet existingLibraryRS = existingAspenLibraryStmt.executeQuery(); long libraryId = 0; if (!existingLibraryRS.next()) { - // Check if subdomain already exists. + // Check if the subdomain already exists. existingAspenLibraryBySubdomainStmt.setString(1, abbreviation); ResultSet existingSubdomainRS = existingAspenLibraryBySubdomainStmt.executeQuery(); if (existingSubdomainRS.next()) { @@ -511,7 +530,7 @@ private static void importLibraryBranchData(Connection dbConn) { addAspenLocationRecordsOwnedStmt.setLong(3, ilsId); addAspenLocationRecordsOwnedStmt.executeUpdate(); - //Add records owned for the library, since we have multiple locations defined by ID, we will add separate rows for each. + //Add records owned by the library. Because we have multiple locations defined by ID, we will add separate rows for each. addAspenLibraryRecordsOwnedStmt.setLong(1, libraryId); addAspenLibraryRecordsOwnedStmt.setLong(2, indexingProfile.getId()); addAspenLibraryRecordsOwnedStmt.setLong(3, ilsId); @@ -640,7 +659,7 @@ private static void updateTranslationMaps(Connection dbConn){ } } } - //For shelf locations, we also get the text version so pull that too + //For shelf locations, we also get the text version, so pull that too if (!shelfLocationName.isEmpty()){ if (!existingShelfLocations.containsKey(shelfLocationName.toLowerCase())){ try { @@ -682,7 +701,7 @@ private static void updateTranslationMaps(Connection dbConn){ } } - //For material types, we also get the text version so pull that too + //For material types, we also get the text version, so pull that too if (!materialTypeName.isEmpty()){ if (!existingITypes.containsKey(materialTypeName.toLowerCase())){ try { @@ -777,6 +796,7 @@ private static boolean loadAccountProfile(Connection dbConn) { } clientId = accountProfileRS.getString("oAuthClientId"); clientSecret = accountProfileRS.getString("oAuthClientSecret"); + signingKey = new SecretKeySpec(clientSecret.getBytes(), HMAC_SHA1_ALGORITHM); accountProfileId = accountProfileRS.getLong("id"); } else { logger.error("Could not find an account profile for Polaris stopping"); @@ -807,7 +827,6 @@ private static int updateRecords(String singleWorkId) { getRecordIdForItemIdStmt = dbConn.prepareStatement("SELECT groupedWorkRecordId FROM grouped_work_record_items WHERE itemId = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); if (singleWorkId != null){ bibIdsUpdatedDuringContinuous = Collections.synchronizedSet(new HashSet<>()); - itemIdsUpdatedDuringContinuous = Collections.synchronizedSet(new HashSet<>()); updateBibFromPolaris(singleWorkId, null, 0, true); }else { long lastExtractTime = 0; @@ -825,7 +844,6 @@ private static int updateRecords(String singleWorkId) { //Check to see if we should regroup all records if (indexingProfile.isRegroupAllRecords()){ - //Regrouping takes a long time, and we don't need koha DB connection so close it while we regroup MarcRecordGrouper recordGrouper = getRecordGroupingProcessor(); recordGrouper.regroupAllRecords(dbConn, indexingProfile, getGroupedWorkIndexer(), logEntry); } @@ -946,12 +964,6 @@ private static int updateBibsFromPolarisFull(long lastExtractTime) { //Get a paged list of all bibs String lastId = "0"; MarcFactory marcFactory = MarcFactory.newInstance(); - DateTimeFormatter dateFormatter; - if (use7_4DateFormatFunctionality()) { - dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).withZone(ZoneId.systemDefault()); - } else { - dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).withZone(ZoneId.systemDefault()); - } if (indexingProfile.isRunFullUpdate() && indexingProfile.getLastChangeProcessed() > 0){ lastId = Long.toString(indexingProfile.getLastChangeProcessed()); logEntry.incSkipped(indexingProfile.getLastChangeProcessed()); @@ -970,7 +982,7 @@ private static int updateBibsFromPolarisFull(long lastExtractTime) { boolean doneLoading = false; long highestIdProcessed = 0; while (!doneLoading) { - //Polaris has an "include items" field, but it does not seem to contain all information we need for Aspen. + //Polaris has an "include items" field, but it does not seem to contain all the information we need for Aspen. long lastIdForThisBatch = Long.parseLong(lastId); if (lastIdForThisBatch > highestIdProcessed){ highestIdProcessed = lastIdForThisBatch; @@ -982,7 +994,7 @@ private static int updateBibsFromPolarisFull(long lastExtractTime) { //Polaris has an issue where if there are more than 100 suppressed titles, it will return 0 as the lastId. We need to account for that long lastIdLong = Long.parseLong(response.lastId); logEntry.setCurrentId(response.lastId); - //MDN this seems to be normal if nothing has changed since the last extract. + //MDN this seems normal if nothing has changed since the last extract. if (lastIdLong == 0) { highestIdProcessed = maxBibId; }else if (lastIdLong > highestIdProcessed){ @@ -1002,7 +1014,7 @@ private static int updateBibsFromPolarisFull(long lastExtractTime) { logEntry.addNote("Finished updating bibs"); logEntry.saveResults(); - + return numChanges; } @@ -1010,7 +1022,6 @@ private static int updateBibsFromPolarisIncremental(long lastExtractTime) { int numChanges = 0; bibIdsUpdatedDuringContinuous = Collections.synchronizedSet(new HashSet<>()); - itemIdsUpdatedDuringContinuous = Collections.synchronizedSet(new HashSet<>()); //Get a paged list of all bibs MarcFactory marcFactory = MarcFactory.newInstance(); @@ -1020,77 +1031,35 @@ private static int updateBibsFromPolarisIncremental(long lastExtractTime) { } else { dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).withZone(ZoneId.systemDefault()); } - String formattedLastExtractTime = ""; - formattedLastExtractTime = dateFormatter.format(Instant.ofEpochSecond(lastExtractTime)); + String formattedLastExtractTime = dateFormatter.format(Instant.ofEpochSecond(lastExtractTime)); logEntry.addNote("Looking for changed records since " + formattedLastExtractTime); formattedLastExtractTime = URLEncoder.encode(formattedLastExtractTime, StandardCharsets.UTF_8); - String formattedTimeNow = dateFormatter.format(Instant.now()); //Get the highest bib from Polaris //noinspection SpellCheckingInspection WebServiceResponse maxBibResponse = callPolarisAPI("/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/bibs/maxid", null, "GET", "application/json", accessSecret); - long maxBibId = -1; if (maxBibResponse.isSuccess()){ - maxBibId = maxBibResponse.getJSONResponse().getJSONArray("BibIDListRows").getJSONObject(0).getLong("BibliographicRecordID"); + long maxBibId = maxBibResponse.getJSONResponse().getJSONArray("BibIDListRows").getJSONObject(0).getLong("BibliographicRecordID"); logEntry.addNote("The maximum bib id in the Polaris Database is " + maxBibId); } //If we are doing a continuous index, get a list of any items that have been updated or changed or bib ids that have been replaced - HashSet bibsToUpdate = new HashSet<>(); + TreeSet bibsToUpdate = new TreeSet<>(); - // Get a list of all the bibs that have been updated since the last extract - String getUpdatedBibsUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/bibs/updated?updatedate=" + formattedLastExtractTime; - WebServiceResponse updatedBibs = callPolarisAPI(getUpdatedBibsUrl, null, "GET", "application/json", accessSecret); - if (updatedBibs.isSuccess()){ - try { - JSONObject response = updatedBibs.getJSONResponse(); - int errorCode = response.getInt("PAPIErrorCode"); - if (errorCode != 0) { - String errorMessage = response.getString("ErrorMessage"); - logEntry.incErrors("Polaris API error " + errorCode + ": " + errorMessage); - return numChanges; - } - JSONArray allBibs = response.getJSONArray("BibIDListRows"); - for (int i = 0; i < allBibs.length(); i++) { - JSONObject bibObj = allBibs.getJSONObject(i); - String bibId = Long.toString(bibObj.getLong("BibliographicRecordID")); - bibsToUpdate.add(bibId); - } - } catch (Exception e) { - logEntry.incErrors("error getting updated bibs list from Polaris", e); - } + // Get a list of all the bibs that have been updated since the last extract + boolean updatingBibsHadErrors; + updatingBibsHadErrors = getBibsUpdatedSinceLastExtract(formattedLastExtractTime, bibsToUpdate); + if (updatingBibsHadErrors) { + return numChanges; } + logEntry.addNote("A total of " + bibsToUpdate.size() + " bibs have been updated since the last extract"); + logEntry.saveResults(); //Get a list of any bibs that have been replaced. - DateTimeFormatter dateReplacedFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH).withZone(ZoneId.systemDefault()); - String formattedLastItemExtractDate = URLEncoder.encode(dateReplacedFormatter.format(Instant.ofEpochSecond(lastExtractTime)), StandardCharsets.UTF_8); - //noinspection SpellCheckingInspection - String getBibReplacedUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/bibs/replacementids?startdate=" + formattedLastItemExtractDate; - WebServiceResponse bibsReplaced = callPolarisAPI(getBibReplacedUrl, null, "GET", "application/json", accessSecret); - if (bibsReplaced.isSuccess()){ - try { - JSONObject response = bibsReplaced.getJSONResponse(); - JSONArray allBibs = response.getJSONArray("BibReplacementIDRows"); - logEntry.addNote("There were " + allBibs.length() + "bibs where the id has been replaced"); - for (int i = 0; i < allBibs.length(); i++) { - JSONObject curBibReplacement = allBibs.getJSONObject(i); - String originalId = Long.toString(curBibReplacement.getLong("OriginalBibRecordID")); - String newId = Long.toString(curBibReplacement.getLong("NewBibliographicRecordID")); - RemoveRecordFromWorkResult result = getRecordGroupingProcessor().removeRecordFromGroupedWork(indexingProfile.getName(), originalId); - if (result.reindexWork){ - getGroupedWorkIndexer().processGroupedWork(result.permanentId); - }else if (result.deleteWork){ - //Delete the work from solr and the database - getGroupedWorkIndexer().deleteRecord(result.permanentId, result.groupedWorkId); - } - logEntry.incDeleted(); - bibsToUpdate.add(newId); - } - logEntry.saveResults(); - } catch (Exception e) { - logEntry.incErrors("Unable to parse document for replaced bibs response", e); - } - } + getBibsReplacedSinceLastExtract(lastExtractTime, bibsToUpdate); + + logEntry.addNote("A total of " + bibsToUpdate.size() + " bibs have been updated or replaced since the last extract"); + logEntry.saveResults(); DateTimeFormatter itemDateFormatter; if (use7_4DateFormatFunctionality()) { @@ -1099,8 +1068,6 @@ private static int updateBibsFromPolarisIncremental(long lastExtractTime) { itemDateFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss", Locale.ENGLISH).withZone(ZoneId.systemDefault()); } String formattedLastItemExtractTime = URLEncoder.encode(itemDateFormatter.format(Instant.ofEpochSecond(lastExtractTime)), StandardCharsets.UTF_8); - logEntry.addNote("Getting a list of all items that have been updated"); - logEntry.saveResults(); long sourceId = -1; try { @@ -1117,194 +1084,286 @@ private static int updateBibsFromPolarisIncremental(long lastExtractTime) { // Get a list of items that have been deleted and update those MARC records too if (sourceId != -1) { - DateTimeFormatter itemDeleteDateFormatter; - if (use7_4DateFormatFunctionality()) { - itemDeleteDateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH).withZone(ZoneId.systemDefault()); - }else{ - itemDeleteDateFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss", Locale.ENGLISH).withZone(ZoneId.systemDefault()); + HashSet updatedAndDeletedItemIds = new HashSet<>(); + getBibsToUpdateForDeletedItems(lastExtractTime, updatedAndDeletedItemIds); + getBibsToUpdateBasedOnUpdatedItems(formattedLastItemExtractTime, updatedAndDeletedItemIds); + + getBibsToUpdateBasedOnItemChangesAndDeletions(sourceId, updatedAndDeletedItemIds, bibsToUpdate); + } + + logEntry.setNumProducts(bibsToUpdate.size()); + logEntry.addNote("A total of " + bibsToUpdate.size() + " bibs need to be updated."); + logEntry.saveResults(); + + + //Now that we have a list of all bibs that need to be updated based on item changes, reindex the bib + try { + int batchSize = 50; + List batch = null; + for (String bibNumber : bibsToUpdate) { + if (batch == null) { + batch = new ArrayList<>(); + } + batch.add(bibNumber); + if (batch.size() == batchSize) { + String bibIds = String.join(",", batch); + numChanges += updateBibFromPolaris(bibIds, marcFactory, lastExtractTime, false); + logEntry.setCurrentId(batch.get(batch.size() - 1)); + batch = null; + } + } + //Process the final batch + if (batch != null) { + String bibIds = String.join(",", batch); + numChanges += updateBibFromPolaris(bibIds, marcFactory, lastExtractTime, false); + logEntry.setCurrentId(batch.get(batch.size() - 1)); } - String formattedItemDeleteDate = URLEncoder.encode(itemDeleteDateFormatter.format(Instant.ofEpochSecond(lastExtractTime)), StandardCharsets.UTF_8); + logEntry.addNote("Finished updating bibs"); + logEntry.saveResults(); + }catch (Exception e){ + logEntry.incErrors("Error updating bibs", e); + } + + return numChanges; + } + + private static void getBibsToUpdateBasedOnUpdatedItems(String formattedLastItemExtractTime, HashSet updatedAndDeletedItemIds) { + //Get the highest item id from Polaris + //noinspection SpellCheckingInspection + WebServiceResponse maxItemResponse = callPolarisAPI("/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/items/maxid", null, "GET", "application/json", accessSecret); + long maxItemId = -1; + if (maxItemResponse.isSuccess()){ + maxItemId = maxItemResponse.getJSONResponse().getJSONArray("ItemIDListRows").getJSONObject(0).getLong("ItemRecordID"); + logEntry.addNote("The maximum item id in the Polaris Database is " + maxItemId); + } + logEntry.addNote("Looking for items that have been updated since " + formattedLastItemExtractTime); + logEntry.saveResults(); + + boolean doneLoading = false; + long highestItemIdProcessed = 0; + while (!doneLoading) { //noinspection SpellCheckingInspection - String getDeletedItemsUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/items/deleted?deletedate=" + formattedItemDeleteDate; - WebServiceResponse pagedDeletedItems = callPolarisAPI(getDeletedItemsUrl, null, "GET", "application/json", accessSecret); - int bibsToUpdateBasedOnDeletedItems = 0; - if (pagedDeletedItems.isSuccess()) { + String getItemsUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/items/updated/paged?updatedate=" + formattedLastItemExtractTime + "&nrecs=100&lastId=" + highestItemIdProcessed; + WebServiceResponse pagedItems = callPolarisAPI(getItemsUrl, null, "GET", "application/json", accessSecret); + if (pagedItems.isSuccess()) { try { - JSONObject response = pagedDeletedItems.getJSONResponse(); - JSONArray allItems = response.getJSONArray("ItemIDListRows"); - logEntry.addNote("There were " + allItems.length() + " items that have been deleted"); - logEntry.saveResults(); + JSONObject response = pagedItems.getJSONResponse(); + JSONArray allItems = response.optJSONArray("ItemIDListRows"); + + if (allItems == null) { + logEntry.addNote("ItemIDListRows was null when getting updated items"); + break; + }else if (allItems.isEmpty()){ + doneLoading = true; + } for (int i = 0; i < allItems.length(); i++) { JSONObject curItem = allItems.getJSONObject(i); long itemId = curItem.getLong("ItemRecordID"); - //Figure out the bib record based on the item id. - String bibForItem = getBibIdForItemIdFromAspen(itemId, sourceId); - if (bibForItem == null) { - bibForItem = getBibIdForItemId(itemId); + updatedAndDeletedItemIds.add(itemId); + if (itemId > highestItemIdProcessed) { + highestItemIdProcessed = itemId; } - if (bibForItem != null) { - if (!bibsToUpdate.contains(bibForItem)) { - logEntry.incProducts(); - bibsToUpdate.add(bibForItem); - bibsToUpdateBasedOnDeletedItems++; - if (logEntry.getNumProducts() % 250 == 0) { - logEntry.saveResults(); - } - } - } - if (i > 0 && (i % 1000 == 0)) { - logEntry.addNote("Processed " + i + " items looking for the bib that was deleted"); + if (updatedAndDeletedItemIds.size() % 1000 == 0){ + logEntry.addNote("Found " + updatedAndDeletedItemIds.size() + " items that have been updated since the last extract"); logEntry.saveResults(); } } + + if (highestItemIdProcessed >= maxItemId){ + doneLoading = true; + } } catch (Exception e) { - logEntry.incErrors("Unable to parse document for deleted items response", e); + logEntry.incErrors("Unable to parse document for paged items response", e); + doneLoading = true; } + }else{ + logEntry.incErrors("Could not get paged items"); + doneLoading = true; } - logEntry.addNote("There are " + bibsToUpdateBasedOnDeletedItems + " records to be updated based on deleted items."); - logEntry.saveResults(); - - //Get the highest item id from Polaris - //noinspection SpellCheckingInspection - WebServiceResponse maxItemResponse = callPolarisAPI("/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/items/maxid", null, "GET", "application/json", accessSecret); - long maxItemId = -1; - if (maxItemResponse.isSuccess()){ - maxItemId = maxItemResponse.getJSONResponse().getJSONArray("ItemIDListRows").getJSONObject(0).getLong("ItemRecordID"); - logEntry.addNote("The maximum item id in the Polaris Database is " + maxItemId); - } + } + logEntry.addNote("There are " + updatedAndDeletedItemIds.size() + " items to be updated based on changes to the items."); + logEntry.saveResults(); + } - boolean doneLoading = false; - long highestItemIdProcessed = 0; - int bibsToUpdateBasedOnChangedItems = 0; - while (!doneLoading) { - //noinspection SpellCheckingInspection - String getItemsUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/items/updated/paged?updatedate=" + formattedLastItemExtractTime + "&nrecs=100&lastId=" + highestItemIdProcessed; - WebServiceResponse pagedItems = callPolarisAPI(getItemsUrl, null, "GET", "application/json", accessSecret); - if (pagedItems.isSuccess()) { - try { - JSONObject response = pagedItems.getJSONResponse(); - JSONArray allItems = response.getJSONArray("ItemIDListRows"); + private static void getBibsToUpdateForDeletedItems(long lastExtractTime, HashSet updatedAndDeletedItemIds) { + DateTimeFormatter itemDeleteDateFormatter; + if (use7_4DateFormatFunctionality()) { + itemDeleteDateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH).withZone(ZoneId.systemDefault()); + }else{ + itemDeleteDateFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss", Locale.ENGLISH).withZone(ZoneId.systemDefault()); + } + String formattedItemDeleteDate = URLEncoder.encode(itemDeleteDateFormatter.format(Instant.ofEpochSecond(lastExtractTime)), StandardCharsets.UTF_8); - if (allItems.isEmpty()) { - doneLoading = true; - } - for (int i = 0; i < allItems.length(); i++) { - JSONObject curItem = allItems.getJSONObject(i); - long itemId = curItem.getLong("ItemRecordID"); - if (!itemIdsUpdatedDuringContinuous.contains(itemId)) { - //Figure out the bib record based on the item id. - //Getting from Aspen is faster if we can get it. - HashSet bibsForItem = getBibIdsForItemIdFromAspen(itemId, sourceId); - for (String bibForItem : bibsForItem) { - //check we've already updated this bib, if so it's ok to skip - if (!bibIdsUpdatedDuringContinuous.contains(bibForItem)) { - logEntry.incProducts(); - bibsToUpdate.add(bibForItem); - bibsToUpdateBasedOnChangedItems++; - if (logEntry.getNumProducts() % 250 == 0) { - logEntry.saveResults(); - } - } - } - } else { - logger.info("Not updating item " + itemId + "because it was already processed when updating bibs"); - } - if (itemId > highestItemIdProcessed) { - highestItemIdProcessed = itemId; - } - if (i > 0 && (i % 500 == 0)) { - logEntry.addNote("Processed " + i + " items to load bib id for the item"); - } - } + logEntry.addNote("Getting a list of all items that have been deleted since " + formattedItemDeleteDate); + logEntry.saveResults(); - if (highestItemIdProcessed >= maxItemId){ - doneLoading = true; - } - } catch (Exception e) { - logEntry.incErrors("Unable to parse document for paged items response", e); + //noinspection SpellCheckingInspection + String getDeletedItemsUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/items/deleted?deletedate=" + formattedItemDeleteDate; + WebServiceResponse pagedDeletedItems = callPolarisAPI(getDeletedItemsUrl, null, "GET", "application/json", accessSecret); + final int[] numItemsProcessed = {0}; + if (pagedDeletedItems.isSuccess()) { + try { + JSONObject response = pagedDeletedItems.getJSONResponse(); + JSONArray allItems = response.optJSONArray("ItemIDListRows"); + if (allItems == null) { + logEntry.addNote("There were no items that have been deleted"); + }else { + for (int i = 0; i < allItems.length(); i++) { + JSONObject curItem = allItems.getJSONObject(i); + updatedAndDeletedItemIds.add(curItem.getLong("ItemRecordID")); } - - }else{ - logEntry.incErrors("Could not get paged items"); } + } catch (Exception e) { + logEntry.incErrors("Unable to parse document for deleted items response", e); } - logEntry.addNote("There are " + bibsToUpdateBasedOnChangedItems + " records to be updated based on changes to the items."); - logEntry.saveResults(); } + logEntry.addNote("There are " + numItemsProcessed[0] + " records to be updated based on deleted items."); + logEntry.saveResults(); + } - - //Now that we have a list of all bibs that need to be updated based on item changes, reindex the bib - List bibsNumber = new ArrayList<>(bibsToUpdate); - int batchSize = 50; - - for (int i = 0; i < bibsNumber.size(); i += batchSize) { - List batch = bibsNumber.subList(i, Math.min(i + batchSize, bibsNumber.size())); - String bibIds = String.join(",", batch); - numChanges += updateBibFromPolaris(bibIds, marcFactory, lastExtractTime, false); - logEntry.setCurrentId(batch.get(batch.size() - 1)); + private static void getBibsToUpdateBasedOnItemChangesAndDeletions(long sourceId, HashSet updatedAndDeletedItemIds, TreeSet bibsToUpdate) { + logEntry.addNote("There were " + updatedAndDeletedItemIds.size() + " items that have been updated or deleted"); + logEntry.saveResults(); + ThreadPoolExecutor es = (ThreadPoolExecutor) Executors.newFixedThreadPool(5); + int[] numItemsProcessed = {0}; + for (Long itemId : updatedAndDeletedItemIds) { + es.execute(() -> { + //Figure out the bib record based on the item id. + //Getting from Aspen is faster if we can get it, but if it does not exist in Aspen, we will get the Bib ID from Polaris + //noinspection DuplicatedCode + HashSet bibsForItem = getBibIdsForItemIdFromAspen(itemId, sourceId); + for (String bibForItem : bibsForItem) { + if (bibForItem != null) { + logEntry.incProducts(); + bibsToUpdate.add(bibForItem); + }else{ + logger.error("Got a null bib id for item id " + itemId); + } + } + numItemsProcessed[0]++; + if (numItemsProcessed[0] > 0 && (numItemsProcessed[0] % 1000 == 0)) { + logEntry.addNote("Retrieved bib id for " + numItemsProcessed[0] + " items."); + logEntry.saveResults(); + } + }); } - logEntry.addNote("Finished updating bibs"); + //Run the threads to get bibs for all items + es.shutdown(); + while (true) { + try { + boolean terminated = es.awaitTermination(15, TimeUnit.SECONDS); + if (terminated){ + break; + } + } catch (InterruptedException e) { + logger.error("Error waiting for all bib ids to be loaded based on item id."); + } + } + logEntry.addNote("Finished getting bib ids for items, processed " + numItemsProcessed[0] + " items"); logEntry.saveResults(); - - return numChanges; } - private static String getBibIdForItemIdFromAspen(long itemId, long sourceId) { - if (sourceId == -1){ - //No records have been saved yet - return null; - } - try { - String itemIdString = Long.toString(itemId); - getRecordIdForItemIdStmt.setString(1, itemIdString); - ResultSet getRecordIdForItemIdRS = getRecordIdForItemIdStmt.executeQuery(); - while (getRecordIdForItemIdRS.next()){ - long recordId = getRecordIdForItemIdRS.getLong("groupedWorkRecordId"); - //If records are merged, we can have more than one record for the item in that case, don't return anything - if (getRecordIdForItemIdRS.next()) { - return null; + private static void getBibsReplacedSinceLastExtract(long lastExtractTime, TreeSet bibsToUpdate) { + DateTimeFormatter dateReplacedFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH).withZone(ZoneId.systemDefault()); + String formattedLastItemExtractDate = URLEncoder.encode(dateReplacedFormatter.format(Instant.ofEpochSecond(lastExtractTime)), StandardCharsets.UTF_8); + //noinspection SpellCheckingInspection + String getBibReplacedUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/bibs/replacementids?startdate=" + formattedLastItemExtractDate; + WebServiceResponse bibsReplaced = callPolarisAPI(getBibReplacedUrl, null, "GET", "application/json", accessSecret); + if (bibsReplaced.isSuccess()){ + try { + JSONObject response = bibsReplaced.getJSONResponse(); + JSONArray allBibs = response.optJSONArray("BibReplacementIDRows"); + if (allBibs == null) { + logEntry.addNote("There were no bibs where the id has been replaced"); + }else{ + logEntry.addNote("There were " + allBibs.length() + " bibs where the id has been replaced"); + for (int i = 0; i < allBibs.length(); i++) { + JSONObject curBibReplacement = allBibs.getJSONObject(i); + String originalId = Long.toString(curBibReplacement.getLong("OriginalBibRecordID")); + String newId = Long.toString(curBibReplacement.getLong("NewBibliographicRecordID")); + RemoveRecordFromWorkResult result = getRecordGroupingProcessor().removeRecordFromGroupedWork(indexingProfile.getName(), originalId); + if (result.reindexWork){ + getGroupedWorkIndexer().processGroupedWork(result.permanentId); + }else if (result.deleteWork){ + //Delete the work from solr and the database + getGroupedWorkIndexer().deleteRecord(result.permanentId, result.groupedWorkId); + } + logEntry.incDeleted(); + bibsToUpdate.add(newId); + } } - getBibIdForItemIdStmt.setLong(1, recordId); - getBibIdForItemIdStmt.setLong(2, sourceId); - ResultSet getBibIdForItemIdRS = getBibIdForItemIdStmt.executeQuery(); - if (getBibIdForItemIdRS.next()){ - return getBibIdForItemIdRS.getString("recordIdentifier"); + logEntry.saveResults(); + } catch (Exception e) { + logEntry.incErrors("Unable to parse document for replaced bibs response", e); + } + } + } + + private static boolean getBibsUpdatedSinceLastExtract(String formattedLastExtractTime, TreeSet bibsToUpdate) { + boolean updatingBibsHadErrors = false; + String getUpdatedBibsUrl = "/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/bibs/updated?updatedate=" + formattedLastExtractTime; + WebServiceResponse updatedBibs = callPolarisAPI(getUpdatedBibsUrl, null, "GET", "application/json", accessSecret); + if (updatedBibs.isSuccess()){ + try { + JSONObject response = updatedBibs.getJSONResponse(); + int errorCode = response.getInt("PAPIErrorCode"); + if (errorCode != 0) { + String errorMessage = response.getString("ErrorMessage"); + logEntry.incErrors("Polaris API error " + errorCode + ": " + errorMessage); + updatingBibsHadErrors = true; + } + if (!updatingBibsHadErrors) { + JSONArray allBibs = response.getJSONArray("BibIDListRows"); + for (int i = 0; i < allBibs.length(); i++) { + JSONObject bibObj = allBibs.getJSONObject(i); + String bibId = Long.toString(bibObj.getLong("BibliographicRecordID")); + bibsToUpdate.add(bibId); + } } + } catch (Exception e) { + logEntry.incErrors("error getting updated bibs list from Polaris", e); } - } catch (SQLException e) { - logEntry.incErrors("Error getting bib for item id from Aspen", e); } - return null; + return updatingBibsHadErrors; } + static final Boolean getRecordIdForItemLock = false; private static HashSet getBibIdsForItemIdFromAspen(long itemId, long sourceId) { HashSet bibIds = new HashSet<>(); if (sourceId == -1){ return bibIds; } try { - String itemIdString = Long.toString(itemId); - getRecordIdForItemIdStmt.setString(1, itemIdString); - ResultSet getRecordIdForItemIdRS = getRecordIdForItemIdStmt.executeQuery(); - //If records are merged, we can have more than one record for the item in that case, don't return anything boolean bibFound = false; - while (getRecordIdForItemIdRS.next()){ - long recordId = getRecordIdForItemIdRS.getLong("groupedWorkRecordId"); - getBibIdForItemIdStmt.setLong(1, recordId); - getBibIdForItemIdStmt.setLong(2, sourceId); - ResultSet getBibIdForItemIdRS = getBibIdForItemIdStmt.executeQuery(); - if (getBibIdForItemIdRS.next()){ - bibIds.add(getBibIdForItemIdRS.getString("recordIdentifier")); - bibFound = true; + synchronized (getRecordIdForItemLock) { + String itemIdString = Long.toString(itemId); + getRecordIdForItemIdStmt.setString(1, itemIdString); + ResultSet getRecordIdForItemIdRS = getRecordIdForItemIdStmt.executeQuery(); + //If records are merged, we can have more than one record for the item in that case, don't return anything + while (getRecordIdForItemIdRS.next()) { + long recordId = getRecordIdForItemIdRS.getLong("groupedWorkRecordId"); + getBibIdForItemIdStmt.setLong(1, recordId); + getBibIdForItemIdStmt.setLong(2, sourceId); + ResultSet getBibIdForItemIdRS = getBibIdForItemIdStmt.executeQuery(); + if (getBibIdForItemIdRS.next()) { + bibIds.add(getBibIdForItemIdRS.getString("recordIdentifier")); + bibFound = true; + } + getBibIdForItemIdRS.close(); } } if (!bibFound) { //No records have been saved yet, get them from Polaris String bibForItem = getBibIdForItemId(itemId); - bibIds.add(bibForItem); + if (bibForItem != null) { + bibIds.add(bibForItem); + }else{ + //This seems normal if both the bib and all items have been deleted + logger.debug("No bib id found for item id " + itemId); + } } } catch (SQLException e) { logEntry.incErrors("Error getting bib for item id from Aspen", e); @@ -1448,6 +1507,9 @@ private static void processPolarisBibAndReindex(MarcFactory marcFactory, long la logEntry.addNote("Did not get a bib record for " + bibliographicRecordId + " skipping"); return; } + }else{ + logEntry.incErrors("Did not get a valid response calling " + getBibUrl + " " + getBibResponse.getResponseCode()); + return; } try { String bibRecordXML = bibPagedRow.getElementsByTagName("BibliographicRecordXML").item(0).getTextContent(); @@ -1540,14 +1602,12 @@ private static synchronized void updateVolumeInfoForIdentifier(Record marcRecord deleteAllVolumesStmt.setString(1, fullIdentifier); deleteAllVolumesStmt.executeUpdate(); }else { -// logger.info(fullIdentifier + " has volumes " + volumesForRecord.size()); HashMap existingVolumes = new HashMap<>(); getExistingVolumesStmt.setString(1, fullIdentifier); ResultSet existingVolumesRS = getExistingVolumesStmt.executeQuery(); while (existingVolumesRS.next()) { existingVolumes.put(existingVolumesRS.getString("volumeId"), existingVolumesRS.getLong("id")); } -// logger.info(" -- existing volume count " + existingVolumes.size()); int numVolumes = 0; for (String volume : volumesForRecord.keySet()) { VolumeInfo volumeInfo = volumesForRecord.get(volume); @@ -1607,7 +1667,7 @@ private static boolean getItemsForBibFromPolaris(MarcFactory marcFactory, String JSONObject bibHoldingsResponseJSON = bibHoldingsResponse.getJSONResponse(); allHoldings = bibHoldingsResponseJSON.getJSONArray("BibHoldingsGetRows"); }catch (Exception e){ - //Just add a note for this + //Add a note for this logEntry.addNote("Error loading bib holdings " + e); } } @@ -1668,8 +1728,6 @@ private static boolean getItemsForBibFromPolaris(MarcFactory marcFactory, String } marcRecord.addVariableField(itemField); - - itemIdsUpdatedDuringContinuous.add(curItem.getLong("ItemRecordID")); } } gotItems = true; @@ -1708,7 +1766,7 @@ private static String getBibIdForItemId(long itemId) { JSONObject itemInfo = itemInfoRows.getJSONObject(0); bibForItem = Long.toString(itemInfo.getLong("BibliographicRecordID")); } else { - //This does not look like an error, just return a null bib. + //This does not look like an error, return a null bib. logger.info("Failed to get bib id for item id " + itemId + ", could not find the item."); logger.info(getItemResponse.getMessage()); } @@ -1772,11 +1830,7 @@ private static void disconnectDatabase() { private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH).withZone(ZoneId.of("GMT")); - private static SecretKeySpec signingKey; private static WebServiceResponse callPolarisAPI(String url, String postData, String method, String contentType, String accessSecret){ - if (signingKey == null){ - signingKey = new SecretKeySpec(clientSecret.getBytes(), HMAC_SHA1_ALGORITHM); - } String fullUrl = webServiceUrl + url; String authorization = "PWS " + clientId + ":"; @@ -1786,8 +1840,7 @@ private static WebServiceResponse callPolarisAPI(String url, String postData, St signatureUnencoded += accessSecret; } try { - Mac mac; - mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); + Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); mac.init(signingKey); byte[] rawHmac = mac.doFinal(signatureUnencoded.getBytes()); authorization += Base64.encodeBase64String(rawHmac, false); @@ -1803,7 +1856,8 @@ private static WebServiceResponse callPolarisAPI(String url, String postData, St headers.put("Authorization", authorization); if (method.equals("GET")) { - return NetworkUtils.getURL(fullUrl, logger, headers); + WebServiceResponse response = NetworkUtils.getURL(fullUrl, logger, headers); + return response; }else{ return NetworkUtils.postToURL(fullUrl, postData, contentType, null, logger, null, 10000, 60000, StandardCharsets.UTF_8, headers, !serverName.contains(".localhost")); } @@ -1866,7 +1920,7 @@ private static void checkForDeletedBibsInPolaris() { //noinspection SpellCheckingInspection WebServiceResponse maxBibResponse = callPolarisAPI("/PAPIService/REST/protected/v1/1033/100/1/" + accessToken + "/synch/bibs/maxid", null, "GET", "application/json", accessSecret); - long maxBibId = -1; + long maxBibId; if (maxBibResponse.isSuccess()){ maxBibId = maxBibResponse.getJSONResponse().getJSONArray("BibIDListRows").getJSONObject(0).getLong("BibliographicRecordID"); }else{ @@ -1917,7 +1971,7 @@ private static void checkForDeletedBibsInPolaris() { //This bib has been deleted bibsToDelete.add(bibId); } else { - //Remove it faster to lookup information in the future + //Remove it faster to look up information in the future allBibsInPolaris.remove(bibId); } } @@ -1943,4 +1997,4 @@ private static void checkForDeletedBibsInPolaris() { logEntry.saveResults(); } -} \ No newline at end of file +} diff --git a/code/reindexer/reindexer.jar b/code/reindexer/reindexer.jar index eaa9f8edcc..648d3a9b11 100644 Binary files a/code/reindexer/reindexer.jar and b/code/reindexer/reindexer.jar differ 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 b03aabb506..31a3070525 100644 --- a/code/reindexer/src/org/aspen_discovery/format_classification/MarcRecordFormatClassifier.java +++ b/code/reindexer/src/org/aspen_discovery/format_classification/MarcRecordFormatClassifier.java @@ -473,6 +473,7 @@ private boolean titleMatchesPattern(org.marc4j.marc.Record record, Pattern patte return false; } + public Pattern graphicNovelSubtitle = Pattern.compile("\\b(the|a) graphic novel\\b", Pattern.CASE_INSENSITIVE); public void getFormatFromTitle(AbstractGroupedWorkSolr groupedWork, org.marc4j.marc.Record record, Set printFormats) { String titleMedium = MarcUtil.getFirstFieldVal(record, "245h"); if (titleMedium != null){ @@ -552,6 +553,13 @@ public void getFormatFromTitle(AbstractGroupedWorkSolr groupedWork, org.marc4j.m printFormats.add("BookClubKit"); } } + String subTitle = MarcUtil.getFirstFieldVal(record, "245b"); + if (subTitle != null){ + if (graphicNovelSubtitle.matcher(subTitle).find()){ + if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Adding bib level format GraphicNovel based on 245b", 2);} + printFormats.add("GraphicNovel"); + } + } } public Pattern whazoodlePattern = Pattern.compile("^WZ.*"); diff --git a/code/reindexer/src/org/aspen_discovery/grouping/RecordGroupingProcessor.java b/code/reindexer/src/org/aspen_discovery/grouping/RecordGroupingProcessor.java index 64f0947c52..626c22850d 100644 --- a/code/reindexer/src/org/aspen_discovery/grouping/RecordGroupingProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/grouping/RecordGroupingProcessor.java @@ -62,14 +62,15 @@ public class RecordGroupingProcessor { private PreparedStatement getCloudLibraryRecordStmt; private PreparedStatement getHooplaRecordStmt; private PreparedStatement getPalaceProjectRecordStmt; - private PreparedStatement getProductIdForPalaceProjectIdStmt; - + private PreparedStatement getManualGroupForRecordStmt; + private PreparedStatement getRecordGroupingOverrideStmt; + private PreparedStatement setPermIdForManuallyGroupedRecordsStmt; HashMap> translationMaps = new HashMap<>(); - - //A list of grouped works that have been manually merged. private final HashSet recordsToNotGroup = new HashSet<>(); + private final HashMap recordGroupingOverrides = new HashMap<>(); + private final HashMap manuallyGroupedRecords = new HashMap<>(); private final Long updateTime = new Date().getTime() / 1000; protected static long numAuthorAuthoritiesUsed = 0; @@ -96,8 +97,10 @@ public RecordGroupingProcessor(Connection dbConnection, String serverName, BaseI public void close(){ translationMaps.clear(); recordsToNotGroup.clear(); + recordGroupingOverrides.clear(); updatedAndInsertedWorksThisRun.clear(); formatsWarned.clear(); + manuallyGroupedRecords.clear(); try { insertGroupedWorkStmt.close(); updateDateUpdatedForGroupedWorkStmt.close(); @@ -118,6 +121,7 @@ public void close(){ updateReadingHistoryStmt.close(); updateNotInterestedStmt.close(); updateUserListEntriesStmt.close(); + //noinspection DuplicatedCode updateNovelistStmt.close(); updateDisplayInfoStmt.close(); updateUploadedCoverInfoStmt.close(); @@ -132,6 +136,9 @@ public void close(){ getHooplaRecordStmt.close(); getPalaceProjectRecordStmt.close(); getProductIdForPalaceProjectIdStmt.close(); + getRecordGroupingOverrideStmt.close(); + getManualGroupForRecordStmt.close(); + setPermIdForManuallyGroupedRecordsStmt.close(); } catch (Exception e) { logEntry.incErrors("Error closing prepared statements in record grouping processor", e); @@ -145,7 +152,7 @@ public void close(){ * @param source - The source of the record being removed * @param id - The id of the record being removed */ - public RemoveRecordFromWorkResult removeRecordFromGroupedWork(String source, String id) { + public synchronized RemoveRecordFromWorkResult removeRecordFromGroupedWork(String source, String id) { RemoveRecordFromWorkResult result = new RemoveRecordFromWorkResult(); try { //Check to see if the identifier is in the grouped work primary identifiers table @@ -174,7 +181,7 @@ public RemoveRecordFromWorkResult removeRecordFromGroupedWork(String source, Str }//If not true, already deleted skip this getWorkForPrimaryIdentifierRS.close(); } catch (Exception e) { - logEntry.incErrors("Error processing deleted bibs", e); + logEntry.incErrors("Error processing removing record from grouped work", e); } return result; } @@ -269,9 +276,22 @@ void setupDatabaseStatements(Connection dbConnection) { nonGroupedRecordsRS.close(); recordsToNotGroupStmt.close(); - getWorkByAlternateTitleAuthorStmt = dbConnection.prepareStatement("SELECT permanent_id from grouped_work_alternate_titles where alternateTitle = ? and alternateAuthor = ? and alternateGroupingCategory = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + PreparedStatement recordGroupingOverridesStmt = dbConnection.prepareStatement("SELECT source, record_id, grouped_work_permanent_id FROM record_grouping_overrides"); + ResultSet recordGroupingOverridesRS = recordGroupingOverridesStmt.executeQuery(); + while (recordGroupingOverridesRS.next()) { + String identifier = recordGroupingOverridesRS.getString("source") + ":" + recordGroupingOverridesRS.getString("record_id"); + String permanentId = recordGroupingOverridesRS.getString("grouped_work_permanent_id"); + recordGroupingOverrides.put(identifier.toLowerCase(), permanentId); + } + recordGroupingOverridesRS.close(); + recordGroupingOverridesStmt.close(); + + getRecordGroupingOverrideStmt = dbConnection.prepareStatement("SELECT grouped_work_permanent_id FROM record_grouping_overrides WHERE source = ? AND record_id = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + getWorkByAlternateTitleAuthorStmt = dbConnection.prepareStatement("SELECT permanent_id from grouped_work_alternate_titles where alternateTitle = ? and alternateAuthor = ? and alternateGroupingCategory = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + getManualGroupForRecordStmt = dbConnection.prepareStatement("SELECT mgw.title FROM manually_grouped_work_records mgwr JOIN manually_grouped_works mgw ON mgwr.manually_grouped_work_id = mgw.id WHERE mgwr.type = ? AND mgwr.identifier = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + setPermIdForManuallyGroupedRecordsStmt = dbConnection.prepareStatement("UPDATE manually_grouped_works mgw JOIN manually_grouped_work_records mgwr ON mgwr.manually_grouped_work_id = mgw.id SET mgw.grouped_work_permanent_id = ? WHERE mgwr.type = ? AND mgwr.identifier = ?"); } catch (Exception e) { logEntry.incErrors("Error setting up prepared statements", e); } @@ -283,29 +303,47 @@ List getDataFields(org.marc4j.marc.Record marcRecord, int tag) { } /** - * Add a work to the database + * Add a work to the database with proper grouping logic applied in priority order: + * 1. Manual grouping (highest priority): if the record is manually grouped. + * 2. Record grouping overrides: if an override exists for the record. + * 3. Ungrouping (non-grouped records): if the record is marked to not be grouped. + * 4. Alternate title/author grouping: if alternate grouping exists from merged works. + * 5. Default grouping based on title, author, format, etc. * - * @param primaryIdentifier The primary identifier we are updating the work for - * @param groupedWork Information about the work itself + * @param primaryIdentifier The primary identifier for the work being updated. + * @param groupedWork Information about the work itself. + * @param primaryDataChanged Whether the primary data has changed (affects date_updated). + * @param originalGroupedWorkId The record's current grouped work ID if already known; null to perform a database lookup; the string "false" to skip lookup when known to not exist. */ void addGroupedWorkToDatabase(RecordIdentifier primaryIdentifier, GroupedWork groupedWork, boolean primaryDataChanged, String originalGroupedWorkId) { String groupedWorkPermanentId = groupedWork.getPermanentId(); - - //Check to see if we need to ungroup the record. String primaryIdentifierString = primaryIdentifier.toString(); - if (recordsToNotGroup.contains(primaryIdentifierString.toLowerCase())) { - groupedWork.makeUnique(primaryIdentifierString); - groupedWorkPermanentId = groupedWork.getPermanentId(); - }else{ - String alternateGroupedWorkPermanentId = checkForAlternateTitleAuthor(groupedWork); - if (alternateGroupedWorkPermanentId != null) { - if (alternateGroupedWorkPermanentId.length() > 36) { - alternateGroupedWorkPermanentId = alternateGroupedWorkPermanentId.substring(0, 36); - } - alternateGroupedWorkPermanentId += "-" + groupedWork.getLanguage(); - groupedWorkPermanentId = alternateGroupedWorkPermanentId; + + String manualGroupId = checkForManualGrouping(primaryIdentifier, groupedWork.getLanguage()); + if (manualGroupId != null) { + groupedWorkPermanentId = manualGroupId; + groupedWork.overridePermanentId(groupedWorkPermanentId); + logger.debug("Using manual grouping for {} -> {}.", primaryIdentifierString, groupedWorkPermanentId); + } else { + String overridePermanentId = recordGroupingOverrides.get(primaryIdentifierString.toLowerCase()); + if (overridePermanentId != null) { + groupedWorkPermanentId = overridePermanentId; groupedWork.overridePermanentId(groupedWorkPermanentId); - } + logger.debug("Using override record grouping for {} -> {}.", primaryIdentifierString, groupedWorkPermanentId); + } else if (recordsToNotGroup.contains(primaryIdentifierString.toLowerCase())) { + groupedWork.makeUnique(primaryIdentifierString); + groupedWorkPermanentId = groupedWork.getPermanentId(); + } else { + String alternateGroupedWorkPermanentId = checkForAlternateTitleAuthor(groupedWork); + if (alternateGroupedWorkPermanentId != null) { + if (alternateGroupedWorkPermanentId.length() > 36) { + alternateGroupedWorkPermanentId = alternateGroupedWorkPermanentId.substring(0, 36); + } + alternateGroupedWorkPermanentId += "-" + groupedWork.getLanguage(); + groupedWorkPermanentId = alternateGroupedWorkPermanentId; + groupedWork.overridePermanentId(groupedWorkPermanentId); + } + } } //Check to see if the record is already on an existing work. If so, remove from the old work. @@ -361,7 +399,6 @@ void addGroupedWorkToDatabase(RecordIdentifier primaryIdentifier, GroupedWork gr ResultSet existingIdRS = getGroupedWorkIdByPermanentIdStmt.executeQuery(); if (existingIdRS.next()) { - //grouped work already exists groupedWorkId = existingIdRS.getLong("id"); //Mark that the work has been updated @@ -426,7 +463,7 @@ private String checkForAlternateTitleAuthor(GroupedWork groupedWork) { private void moveGroupedWorkEnrichment(String oldPermanentId, String newPermanentId) { try{ - //First make sure the old record does not have items attached to it still + //First, make sure the old record does not have items attached to it still getGroupedWorkIdByPermanentIdStmt.setString(1, oldPermanentId); ResultSet getWorkIdByPermanentIdRS = getGroupedWorkIdByPermanentIdStmt.executeQuery(); if (getWorkIdByPermanentIdRS.next()){ @@ -440,15 +477,18 @@ private void moveGroupedWorkEnrichment(String oldPermanentId, String newPermanen getAdditionalPrimaryIdentifierForWorkRS.close(); //At the point this is called, we have not removed the record from the work so count should be 1 if (numPrimaryIdentifiers <= 1) { - //If there are no items attached to the old record - //Move ratings + //If there are no items attached to the old record, move ratings int numUpdatedRatings = 0; - try{ + try { updateRatingsStmt.setString(1, newPermanentId); updateRatingsStmt.setString(2, oldPermanentId); numUpdatedRatings = updateRatingsStmt.executeUpdate(); }catch (SQLException e){ - logEntry.incErrors("Error moving ratings", e); + //Make sure this isn't just an error that the user had previously rated the destination title. + //If they have, we preserve the previous value + if (!(e instanceof SQLIntegrityConstraintViolationException)) { + logEntry.incErrors("Error moving ratings", e); + } } //Move reading history @@ -458,7 +498,11 @@ private void moveGroupedWorkEnrichment(String oldPermanentId, String newPermanen updateReadingHistoryStmt.setString(2, oldPermanentId); numUpdatedReadingHistory = updateReadingHistoryStmt.executeUpdate(); }catch (SQLException e){ - logEntry.incErrors("Error moving reading history from " + oldPermanentId + " to " + newPermanentId, e); + //Make sure this isn't just an error that the user has reading history for both the old and new works. + //If they do, we preserve the previous value + if (!(e instanceof SQLIntegrityConstraintViolationException)) { + logEntry.incErrors("Error moving reading history from " + oldPermanentId + " to " + newPermanentId, e); + } } //Update list entries @@ -468,17 +512,25 @@ private void moveGroupedWorkEnrichment(String oldPermanentId, String newPermanen updateUserListEntriesStmt.setString(2, oldPermanentId); numUpdatedListEntries = updateUserListEntriesStmt.executeUpdate(); }catch (SQLException e){ - logEntry.incErrors("Error moving list entries", e); + //Make sure this isn't just an error that the list already has the new value + //If it does, we preserve the previous value + if (!(e instanceof SQLIntegrityConstraintViolationException)) { + logEntry.incErrors("Error moving list entries", e); + } } - //User Not Interested + //User is Not Interested in Work data int numUpdatedNotInterested = 0; try{ updateNotInterestedStmt.setString(1, newPermanentId); updateNotInterestedStmt.setString(2, oldPermanentId); numUpdatedNotInterested = updateNotInterestedStmt.executeUpdate(); }catch (SQLException e){ - logEntry.incErrors("Error moving not interested info", e); + //Make sure this isn't just an error that the user is not interested in the work already + //If they have, we preserve the previous value + if (!(e instanceof SQLIntegrityConstraintViolationException)) { + logEntry.incErrors("Error moving not interested info", e); + } } //Novelist @@ -488,7 +540,11 @@ private void moveGroupedWorkEnrichment(String oldPermanentId, String newPermanen updateNovelistStmt.setString(2, oldPermanentId); numUpdatedNovelist = updateNovelistStmt.executeUpdate(); }catch (SQLException e){ - logEntry.incErrors("Error moving novelist info", e); + //Make sure this isn't just an error that novelist data already exists + //If it does, we preserve the previous value + if (!(e instanceof SQLIntegrityConstraintViolationException)) { + logEntry.incErrors("Error moving novelist info", e); + } } //Display info @@ -498,7 +554,11 @@ private void moveGroupedWorkEnrichment(String oldPermanentId, String newPermanen updateDisplayInfoStmt.setString(2, oldPermanentId); numUpdatedDisplayInfo = updateDisplayInfoStmt.executeUpdate(); }catch (SQLException e){ - logEntry.incErrors("Error moving display info", e); + //Make sure this isn't just an error that the new value already has display info set + //If it does, we preserve the previous value + if (!(e instanceof SQLIntegrityConstraintViolationException)) { + logEntry.incErrors("Error moving display info", e); + } } int uploadedCoverInfo = 0; @@ -515,7 +575,11 @@ private void moveGroupedWorkEnrichment(String oldPermanentId, String newPermanen } } }catch (SQLException e){ - logEntry.incErrors("Error moving uploaded covers", e); + //Make sure this isn't just an error that the new value already has an uploaded cover + //If it does, we preserve the previous value + if (!(e instanceof SQLIntegrityConstraintViolationException)) { + logEntry.incErrors("Error moving uploaded covers", e); + } } logger.debug("Updated " + numUpdatedRatings + " ratings, " + numUpdatedListEntries + " list entries, " + numUpdatedReadingHistory + " reading history entries, " + numUpdatedNotInterested + " not interested entries, " + numUpdatedNovelist + " novelist entries, " + numUpdatedDisplayInfo + " display info entries, " + uploadedCoverInfo + " uploaded covers"); @@ -701,18 +765,7 @@ void loadAuthorities(Connection dbConn) { PreparedStatement addAuthorAuthorityStmt = dbConn.prepareStatement("INSERT into author_authorities (originalName, authoritativeName) VALUES (?, ?)"); try { CSVReader csvReader = new CSVReader(new FileReader("../reindexer/author_authorities.properties")); - String[] curLine = csvReader.readNext(); - while (curLine != null) { - try { - addAuthorAuthorityStmt.setString(1, curLine[0]); - addAuthorAuthorityStmt.setString(2, curLine[1]); - addAuthorAuthorityStmt.executeUpdate(); - } catch (SQLException e) { - logEntry.incErrors("Error adding authority " + curLine[0]); - } - curLine = csvReader.readNext(); - } - csvReader.close(); + loadDefaultAuthorityFile(addAuthorAuthorityStmt, csvReader); } catch (IOException e) { logEntry.incErrors("Unable to load author authorities", e); } @@ -732,18 +785,7 @@ void loadAuthorities(Connection dbConn) { PreparedStatement addTitleAuthorityStmt = dbConn.prepareStatement("INSERT into title_authorities (originalName, authoritativeName) VALUES (?, ?)"); try { CSVReader csvReader = new CSVReader(new FileReader("../reindexer/title_authorities.properties")); - String[] curLine = csvReader.readNext(); - while (curLine != null) { - try { - addTitleAuthorityStmt.setString(1, curLine[0]); - addTitleAuthorityStmt.setString(2, curLine[1]); - addTitleAuthorityStmt.executeUpdate(); - } catch (SQLException e) { - logEntry.incErrors("Error adding authority " + curLine[0]); - } - curLine = csvReader.readNext(); - } - csvReader.close(); + loadDefaultAuthorityFile(addTitleAuthorityStmt, csvReader); } catch (IOException e) { logEntry.incErrors("Unable to load title authorities", e); } @@ -786,6 +828,21 @@ void loadAuthorities(Connection dbConn) { logger.info("Done loading authorities"); } + private void loadDefaultAuthorityFile(PreparedStatement addAuthorityStmt, CSVReader defaultAuthorityCsvReader) throws IOException { + String[] curLine = defaultAuthorityCsvReader.readNext(); + while (curLine != null) { + try { + addAuthorityStmt.setString(1, curLine[0]); + addAuthorityStmt.setString(2, curLine[1]); + addAuthorityStmt.executeUpdate(); + } catch (SQLException e) { + logEntry.incErrors("Error adding authority " + curLine[0]); + } + curLine = defaultAuthorityCsvReader.readNext(); + } + defaultAuthorityCsvReader.close(); + } + String getAuthoritativeAuthor(String originalAuthor) { if (lookupAuthorAuthoritiesInDB && !originalAuthor.isEmpty()) { try { @@ -976,6 +1033,7 @@ public String groupCloudLibraryRecord(String cloudLibraryId, org.marc4j.marc.Rec } private String getAxis360FieldValue(JSONObject itemDetails, String fieldName) { + //noinspection DuplicatedCode JSONArray fields = itemDetails.getJSONArray("fields"); for (int i = 0; i < fields.length(); i++){ JSONObject field = fields.getJSONObject(i); @@ -1129,9 +1187,11 @@ public String groupPalaceProjectRecord(JSONObject titleDetails, long palaceProje String type = titleMetadata.getString("@type"); String primaryFormat; switch (type) { + //noinspection HttpUrlsUsage case "http://bib.schema.org/Audiobook": primaryFormat = "eAudiobook"; break; + //noinspection HttpUrlsUsage case "http://schema.org/EBook": //TODO: May need to check the subjects to determine if this is a comic/graphic novel primaryFormat = "eBook"; @@ -1153,4 +1213,45 @@ public String groupPalaceProjectRecord(JSONObject titleDetails, long palaceProje public long getNumAuthoritiesUsed() { return numAuthorAuthoritiesUsed; } + + /** + * Check if the record is part of a manually grouped work. + * + * @param primaryIdentifier The primary identifier to check. + * @return The manually grouped work ID or null if not manually grouped. + */ + private String checkForManualGrouping(RecordIdentifier primaryIdentifier, String recordLanguage) { + String primaryIdentifierString = primaryIdentifier.toString(); + if (manuallyGroupedRecords.containsKey(primaryIdentifierString)) { + return manuallyGroupedRecords.get(primaryIdentifierString); + } + + try { + getManualGroupForRecordStmt.setString(1, primaryIdentifier.getType()); + getManualGroupForRecordStmt.setString(2, primaryIdentifier.getIdentifier()); + ResultSet manualGroupRS = getManualGroupForRecordStmt.executeQuery(); + + if (manualGroupRS.next()) { + String manualGroupTitle = manualGroupRS.getString("title"); + // Create a unique ID for the manual group based on the title. + GroupedWork manualGroupWork = new GroupedWork(this); + manualGroupWork.setLanguage(recordLanguage); + manualGroupWork.makeUnique("manual_group_" + manualGroupTitle); + String manualGroupId = manualGroupWork.getPermanentId(); + manuallyGroupedRecords.put(primaryIdentifierString, manualGroupId); + + setPermIdForManuallyGroupedRecordsStmt.setString(1, manualGroupId); + setPermIdForManuallyGroupedRecordsStmt.setString(2, primaryIdentifier.getType()); + setPermIdForManuallyGroupedRecordsStmt.setString(3, primaryIdentifier.getIdentifier()); + setPermIdForManuallyGroupedRecordsStmt.executeUpdate(); + + return manualGroupId; + } + manualGroupRS.close(); + } catch (SQLException e) { + logEntry.incErrors("Error checking for manual grouping for " + primaryIdentifierString + ": ", e); + } + + return null; + } } diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/AbstractGroupedWorkSolr.java b/code/reindexer/src/org/aspen_discovery/reindexer/AbstractGroupedWorkSolr.java index 99b8aee314..cc8192ef2b 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/AbstractGroupedWorkSolr.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/AbstractGroupedWorkSolr.java @@ -14,6 +14,7 @@ public abstract class AbstractGroupedWorkSolr implements DebugLogger { protected String id; protected HashMap relatedRecords = new HashMap<>(); + protected HashSet overriddenRecords = new HashSet<>(); protected String acceleratedReaderInterestLevel; protected String acceleratedReaderReadingLevel; @@ -24,6 +25,7 @@ public abstract class AbstractGroupedWorkSolr implements DebugLogger { protected HashSet authorAdditional = new HashSet<>(); protected String authorDisplay; protected String authorFormat; + protected boolean authorFromOverriddenRecord = false; protected HashSet author2 = new HashSet<>(); protected HashSet authAuthor2 = new HashSet<>(); protected HashSet author2Role = new HashSet<>(); @@ -93,6 +95,7 @@ public abstract class AbstractGroupedWorkSolr implements DebugLogger { protected HashSet titleNew = new HashSet<>(); protected String titleSort; protected String titleFormat = ""; + protected boolean titleFromOverriddenRecord = false; private boolean hasNotForLoanRecord = false; protected HashSet topics = new HashSet<>(); protected HashSet topicFacets = new HashSet<>(); @@ -232,6 +235,12 @@ protected void cloneCollectionData(AbstractGroupedWorkSolr clonedWork){ abstract SolrInputDocument getSolrDocument(BaseIndexingLogEntry logEntry); + void setDisplayDescription(String description) { + if (description != null && !description.isEmpty()) { + this.displayDescription = description; + } + } + public void addScopingInfo(String scopeName, ScopingInfo scopingInfo){ ArrayList scopingInfoForScope = relatedScopes.computeIfAbsent(scopeName, k -> new ArrayList<>()); scopingInfoForScope.add(scopingInfo); @@ -419,19 +428,32 @@ public void setId(String id) { this.id = id.toLowerCase(Locale.ROOT); } - private final static Pattern removeBracketsPattern = Pattern.compile("\\[.*?]"); + public void setOverriddenRecords(HashSet overriddenRecords) { + this.overriddenRecords = overriddenRecords; + } + + //private final static Pattern removeBracketsPattern = Pattern.compile("\\[.*?]"); private final static Pattern commonSubtitlePattern = Pattern.compile("(?i)([(]?(?:\\s?a\\s?|\\s?the\\s?)?audio cd|book club kit|large print[)]?)$"); private final static Pattern punctuationPattern = Pattern.compile("[.\\\\/()\\[\\]:;]"); - void setTitle(String shortTitle, String subTitle, String displayTitle, String sortableTitle, String recordFormat, String formatCategory) { - this.setTitle(shortTitle, subTitle, displayTitle, sortableTitle, recordFormat, formatCategory, false, null); + void setTitle(String shortTitle, String subTitle, String sortableTitle, String formatCategory) { + this.setTitle(shortTitle, subTitle, sortableTitle, formatCategory, false, null, null); + } + + void setTitle(String shortTitle, String subTitle, String sortableTitle, String formatCategory, boolean isDisplayInfo, RecordInfo recordInfo) { + this.setTitle(shortTitle, subTitle, sortableTitle, formatCategory, isDisplayInfo, recordInfo, null); } - void setTitle(String shortTitle, String subTitle, String displayTitle, String sortableTitle, String recordFormat, String formatCategory, boolean isDisplayInfo, RecordInfo recordInfo) { + void setTitle(String shortTitle, String subTitle, String sortableTitle, String formatCategory, boolean isDisplayInfo, RecordInfo recordInfo, MarcRecordProcessor processor) { if (shortTitle != null) { shortTitle = AspenStringUtils.trimTrailingPunctuation(shortTitle); + boolean isOverridden = false; + if (recordInfo != null) { + String recordKey = recordInfo.getSource() + ":" + recordInfo.getRecordIdentifier(); + isOverridden = overriddenRecords.contains(recordKey); + } - // Figure out if we want to use this title or if the one we have is better. + // Determine whether to use this title or if the chosen one is better. boolean updateTitle = false; if (this.title == null) { updateTitle = true; @@ -439,33 +461,39 @@ void setTitle(String shortTitle, String subTitle, String displayTitle, String so } else { // Skip unavailable records for title selection if we have any other title. if (recordInfo == null || !recordInfo.hasNotForLoanStatus()) { - // Only overwrite if we get a better format. - if (formatCategory.equals("Books")) { - // We have a book, update if we didn't have a book before. - if (!formatCategory.equals(titleFormat)) { - updateTitle = true; - // Or, update if we had a book before and this title is longer. - } else if (shortTitle.length() > this.title.length()) { + // Only overwrite if there is a better format. + // Do not overwrite if the record was manually moved to this work. + if (!isOverridden) { + // If the current title is from an overridden record, replace it with this non-overridden record. + if (titleFromOverriddenRecord) { updateTitle = true; - } else if (hasNotForLoanRecord) { - // Not for loan record was processed first, and it updated the title, so make sure to override it. - updateTitle = true; - } - } else if (formatCategory.equals("eBook")) { - // Update if the format we had before is not a book. - if (!titleFormat.equals("Books")) { - // And the new format was not an eBook or the new title is longer than what we had before. + } else if (formatCategory.equals("Books")) { + // There is a book, update if no book from before. if (!formatCategory.equals(titleFormat)) { updateTitle = true; - // Or, update if we had a book before and this title is longer. + // Or, update if there was a book before, but this title is longer. } else if (shortTitle.length() > this.title.length()) { updateTitle = true; + } else if (hasNotForLoanRecord) { + // Not for loan record was processed first, and it updated the title, so make sure to override it. + updateTitle = true; + } + } else if (formatCategory.equals("eBook")) { + // Update if the format from before is not a book. + if (!titleFormat.equals("Books")) { + // And the new format was not an eBook or the new title is longer from before. + if (!formatCategory.equals(titleFormat)) { + updateTitle = true; + // Or, update if there was a book before, but this title is longer. + } else if (shortTitle.length() > this.title.length()) { + updateTitle = true; + } + } + } else if (!titleFormat.equals("Books") && !titleFormat.equals("eBook")) { + // If there isn't a Book or an eBook, then update the title if there is a longer title. + if (shortTitle.length() > this.title.length()) { + updateTitle = true; } - } - } else if (!titleFormat.equals("Books") && !titleFormat.equals("eBook")) { - // If we don't have a Book or an eBook, then we can update the title if we get a longer title. - if (shortTitle.length() > this.title.length()) { - updateTitle = true; } } } @@ -486,6 +514,7 @@ void setTitle(String shortTitle, String subTitle, String displayTitle, String so //} this.title = shortTitle; this.titleFormat = formatCategory; + this.titleFromOverriddenRecord = isOverridden; //Strip out anything in brackets unless that would cause us to show nothing // tmpTitle = removeBracketsPattern.matcher(sortableTitle).replaceAll("").trim(); // if (!tmpTitle.isEmpty()) { @@ -512,6 +541,14 @@ void setTitle(String shortTitle, String subTitle, String displayTitle, String so subTitle = AspenStringUtils.trimTrailingPunctuation(subTitle); this.displayTitle = shortTitle.concat(": ").concat(subTitle); } + + // Apply regex stripping to display title for ILS records only. + if (processor instanceof IlsRecordProcessor) { + IlsRecordProcessor ilsProcessor = (IlsRecordProcessor) processor; + if (ilsProcessor.settings != null && ilsProcessor.settings.getDisplayTitleStripPattern() != null) { + this.displayTitle = ilsProcessor.settings.getDisplayTitleStripPattern().matcher(this.displayTitle).replaceAll("").trim(); + } + } } //replace apostrophes in contractions @@ -578,39 +615,58 @@ void setAuthor(String author) { } protected String getPrimaryAuthor() { + if (authorDisplay != null && !authorDisplay.isEmpty()) { + return authorDisplay; + } String mostUsedAuthor = null; - long numUses = -1; + long highestUsage = -1; for (String curAuthor : primaryAuthors.keySet()) { - if (primaryAuthors.get(curAuthor) > numUses) { + long numUses = primaryAuthors.get(curAuthor); + if (numUses > highestUsage) { + highestUsage = numUses; mostUsedAuthor = curAuthor; } } return mostUsedAuthor; } - void setAuthorDisplay(String newAuthor, String formatCategory) { + void setAuthorDisplay(String newAuthor) { + this.setAuthorDisplay(newAuthor, "Books", null); + } + + void setAuthorDisplay(String newAuthor, String formatCategory, RecordInfo recordInfo) { + boolean isOverridden = false; + if (recordInfo != null) { + String recordKey = recordInfo.getSource() + ":" + recordInfo.getRecordIdentifier(); + isOverridden = overriddenRecords.contains(recordKey); + } + boolean updateAuthor = false; if (this.authorDisplay == null) { updateAuthor = true; } else { - if (formatCategory.equals("Books")) { - //We have a book, update if we didn't have a book before - if (!formatCategory.equals(authorFormat)) { + // Do not overwrite if the record was manually moved to this work. + if (!isOverridden) { + // If the current author is from an overridden record, replace it with this non-overridden record. + if (authorFromOverriddenRecord) { updateAuthor = true; - } - } else if (formatCategory.equals("eBook")) { - //Update if the format we had before is not a book - if (!authorFormat.equals("Books")) { - //And the new format was not an eBook or the new title is longer than what we had before + } else if (formatCategory.equals("Books")) { + // There is a book, update if no book from before. if (!formatCategory.equals(authorFormat)) { updateAuthor = true; } + } else if (formatCategory.equals("eBook")) { + // Update if the format from before was neither a Book nor eBook. + if (!authorFormat.equals("Books") && !formatCategory.equals(authorFormat)) { + updateAuthor = true; + } } } } if (updateAuthor) { this.authorDisplay = AspenStringUtils.trimTrailingPunctuation(newAuthor); authorFormat = formatCategory; + this.authorFromOverriddenRecord = isOverridden; } } @@ -773,40 +829,46 @@ void addSeriesWithVolume(String seriesName, String volume, int priority) { } if (!this.seriesWithVolume.containsKey(normalizedSeriesInfoWithVolume)) { boolean okToAdd = true; - for (String existingSeries2 : this.seriesWithVolume.keySet()) { - String[] existingSeriesInfo = existingSeries2.split("\\|", 2); - String existingSeriesName = existingSeriesInfo[0]; - String existingVolume = ""; - if (existingSeriesInfo.length > 1) { - existingVolume = existingSeriesInfo[1]; - } - //Get the longer series name - if (existingSeriesName.contains(seriesInfoLower)) { - //Use the old one unless it doesn't have a volume - if (existingVolume.isEmpty()) { - this.seriesWithVolume.remove(existingSeries2); - break; - } else { - if (volumeLower.equals(existingVolume)) { - okToAdd = false; + //Check to see if we have a similar series name (where one series name is fully contained in the other series). + // This helps to prevent cases where series of "Dark" and "Dark Series" both appear. + // When this occurs the more specific series (longer or with a volume) will be preserved. + // This logic only applies if the series module is NOT active. + if (!groupedWorkIndexer.hasSeriesModuleEnabled()) { + for (String existingSeries2 : this.seriesWithVolume.keySet()) { + String[] existingSeriesInfo = existingSeries2.split("\\|", 2); + String existingSeriesName = existingSeriesInfo[0]; + String existingVolume = ""; + if (existingSeriesInfo.length > 1) { + existingVolume = existingSeriesInfo[1]; + } + //Get the longer series name + if (existingSeriesName.contains(seriesInfoLower)) { + //Use the old one unless it doesn't have a volume + if (existingVolume.isEmpty()) { + this.seriesWithVolume.remove(existingSeries2); + break; + } else { + if (volumeLower.equals(existingVolume)) { + okToAdd = false; + break; + } else if (volumeLower.isEmpty()) { + okToAdd = false; + break; + } + } + } else if (seriesInfoLower.contains(existingSeriesName)) { + //Before removing the old series, make sure the new one has a volume + if (!existingVolume.isEmpty() && existingVolume.equals(volumeLower)) { + this.seriesWithVolume.remove(existingSeries2); break; - } else if (volumeLower.isEmpty()) { + } else if (volume.isEmpty() && !existingVolume.isEmpty()) { okToAdd = false; break; + } else if (volume.isEmpty()) { + this.seriesWithVolume.remove(existingSeries2); + break; } } - } else if (seriesInfoLower.contains(existingSeriesName)) { - //Before removing the old series, make sure the new one has a volume - if (!existingVolume.isEmpty() && existingVolume.equals(volumeLower)) { - this.seriesWithVolume.remove(existingSeries2); - break; - } else if (volume.isEmpty() && !existingVolume.isEmpty()) { - okToAdd = false; - break; - } else if (volume.isEmpty()) { - this.seriesWithVolume.remove(existingSeries2); - break; - } } } if (okToAdd) { @@ -1367,7 +1429,7 @@ RecordInfo addRelatedRecord(String source, String subSource, String recordIdenti /** * Get the RecordInfo for a specific source and identifier. * - * @param source The source of the record (e.g. "koha"). + * @param source The source of the record (e.g. "ils"). * @param recordIdentifier The identifier of the record. * @return The {@code RecordInfo} object if found, null otherwise. */ @@ -1436,61 +1498,62 @@ void removeRedundantHooplaRecords() { //Loop through all the scopes to see if we should remove the hoopla record from that scope. for (ItemInfo curItem1 : record1.getRelatedItems()){ HashSet scopesToRemove = new HashSet<>(); - for (ScopingInfo item1Scope : curItem1.getScopingInfo().values()) { - String item1ScopeName = item1Scope.getScope().getScopeName(); - //Get information about the scope to determine how this scope should be processed. - switch (item1Scope.getScope().getHooplaScope().getExcludeTitlesWithCopiesFromOtherVendors()) { - case 0: - //Don't remove items that have the same record someplace else - break; - case 1: - //Remove if there is an available copy for the scope - for (ItemInfo curItem2 : record2.getRelatedItems()){ - if (curItem2.isAvailable()){ + if (!curItem1.geteContentSubSource().equals("Flex")) { + for (ScopingInfo item1Scope : curItem1.getScopingInfo().values()) { + String item1ScopeName = item1Scope.getScope().getScopeName(); + //Get information about the scope to determine how this scope should be processed. + switch (item1Scope.getScope().getHooplaScope().getExcludeTitlesWithCopiesFromOtherVendors()) { + case 0: + //Don't remove items that have the same record someplace else + break; + case 1: + //Remove if there is an available copy for the scope + for (ItemInfo curItem2 : record2.getRelatedItems()){ + if (curItem2.isAvailable()){ + if (curItem2.getScopingInfo().containsKey(item1ScopeName)){ + scopesToRemove.add(item1ScopeName); + break; + } + } + } + break; + case 2: + //Remove if there is another copy in the scope (does not have to be available) + for (ItemInfo curItem2 : record2.getRelatedItems()){ if (curItem2.getScopingInfo().containsKey(item1ScopeName)){ scopesToRemove.add(item1ScopeName); break; } } - } - break; - case 2: - //Remove if there is another copy in the scope (does not have to be available) - for (ItemInfo curItem2 : record2.getRelatedItems()){ - if (curItem2.getScopingInfo().containsKey(item1ScopeName)){ - scopesToRemove.add(item1ScopeName); - break; + break; + } + } + for (String scopeToRemove : scopesToRemove){ + curItem1.getScopingInfo().remove(scopeToRemove); + //Remove from related scopes as well + ArrayList scopingInfo = relatedScopes.get(scopeToRemove); + if (scopingInfo != null) { + ArrayList scopingInfoClone; + //noinspection unchecked + scopingInfoClone = (ArrayList) scopingInfo.clone(); + for (ScopingInfo relatedScopeInfo : scopingInfoClone) { + if (relatedScopeInfo.getItem().equals(curItem1)) { + scopingInfo.remove(relatedScopeInfo); } } - break; - } - } - for (String scopeToRemove : scopesToRemove){ - curItem1.getScopingInfo().remove(scopeToRemove); - //Remove from related scopes as well - ArrayList scopingInfo = relatedScopes.get(scopeToRemove); - if (scopingInfo != null) { - ArrayList scopingInfoClone; - //noinspection unchecked - scopingInfoClone = (ArrayList) scopingInfo.clone(); - for (ScopingInfo relatedScopeInfo : scopingInfoClone) { - if (relatedScopeInfo.getItem().equals(curItem1)) { - scopingInfo.remove(relatedScopeInfo); + if (scopingInfo.isEmpty()) { + relatedScopes.remove(scopeToRemove); } } - if (scopingInfo.isEmpty()) { - relatedScopes.remove(scopeToRemove); - } } - } - //Remove the item entirely if it is no longer valid for any scope - if (curItem1.getScopingInfo().isEmpty()){ - record1.getRelatedItems().remove(curItem1); - break; + //Remove the item entirely if it is no longer valid for any scope + if (curItem1.getScopingInfo().isEmpty()){ + record1.getRelatedItems().remove(curItem1); + break; + } } - } } } diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/AvailabilityToggleInfo.java b/code/reindexer/src/org/aspen_discovery/reindexer/AvailabilityToggleInfo.java index 79bb372e4f..0812adbaf9 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/AvailabilityToggleInfo.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/AvailabilityToggleInfo.java @@ -1,13 +1,13 @@ package org.aspen_discovery.reindexer; -import java.util.HashSet; +import java.util.ArrayList; public class AvailabilityToggleInfo { public boolean local; public boolean available; public boolean availableOnline; - public HashSet getValues(){ + public ArrayList getValues(){ if (local) { if (available) { if (availableOnline) { @@ -44,14 +44,14 @@ public HashSet getValues(){ private final static String availableStr = "available"; private final static String availableOnlineStr = "available_online"; - private final static HashSet globalOnly = new HashSet<>(); - private final static HashSet globalLocal = new HashSet<>(); - private final static HashSet globalLocalAvailable = new HashSet<>(); - private final static HashSet globalLocalAvailableOnline = new HashSet<>(); - private final static HashSet globalLocalOnline = new HashSet<>(); - private final static HashSet globalAvailable = new HashSet<>(); - private final static HashSet globalAvailableOnline = new HashSet<>(); - private final static HashSet globalOnline = new HashSet<>(); + private final static ArrayList globalOnly = new ArrayList<>(); + private final static ArrayList globalLocal = new ArrayList<>(); + private final static ArrayList globalLocalAvailable = new ArrayList<>(); + private final static ArrayList globalLocalAvailableOnline = new ArrayList<>(); + private final static ArrayList globalLocalOnline = new ArrayList<>(); + private final static ArrayList globalAvailable = new ArrayList<>(); + private final static ArrayList globalAvailableOnline = new ArrayList<>(); + private final static ArrayList globalOnline = new ArrayList<>(); static { globalOnly.add(globalStr); diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/Axis360Processor.java b/code/reindexer/src/org/aspen_discovery/reindexer/Axis360Processor.java index 54087cb91c..2bf7dc5852 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/Axis360Processor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/Axis360Processor.java @@ -85,12 +85,12 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI if (rawResponse.has("subTitle")) { subTitle = rawResponse.getString("subTitle"); } - groupedWork.setTitle(title, subTitle, title, title, primaryFormat, formatCategory); + groupedWork.setTitle(title, subTitle, title, formatCategory, false, axis360Record); String primaryAuthor = productRS.getString("primaryAuthor"); groupedWork.setAuthor(primaryAuthor); groupedWork.setAuthAuthor(primaryAuthor); - groupedWork.setAuthorDisplay(primaryAuthor, formatCategory); + groupedWork.setAuthorDisplay(primaryAuthor, formatCategory, axis360Record); String series = getFieldValue(rawResponse,"series"); if (!series.isEmpty()){ diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkIndexer.java b/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkIndexer.java index eb0c1a6dd1..eef0ded391 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkIndexer.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkIndexer.java @@ -82,6 +82,7 @@ public class GroupedWorkIndexer { private TreeSet scopes ; private PreparedStatement getGroupedWorkPrimaryIdentifiers; + private PreparedStatement getOverriddenRecordsForWork; private PreparedStatement getGroupedWorkInfoStmt; private PreparedStatement getArBookIdForIsbnStmt; private PreparedStatement getArBookInfoStmt; @@ -256,6 +257,7 @@ public GroupedWorkIndexer(String serverName, Connection dbConn, Ini configIni, b //Load a few statements we will need later try{ getGroupedWorkPrimaryIdentifiers = dbConn.prepareStatement("SELECT * FROM grouped_work_primary_identifiers where grouped_work_id = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + getOverriddenRecordsForWork = dbConn.prepareStatement("SELECT source, record_id FROM record_grouping_overrides where grouped_work_permanent_id = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); //deleteGroupedWorkStmt = dbConn.prepareStatement("DELETE from grouped_work where id = ?"); getGroupedWorkInfoStmt = dbConn.prepareStatement("SELECT id, grouping_category from grouped_work where permanent_id = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); getArBookIdForIsbnStmt = dbConn.prepareStatement("SELECT arBookId from accelerated_reading_isbn where isbn = ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); @@ -359,7 +361,7 @@ public GroupedWorkIndexer(String serverName, Connection dbConn, Ini configIni, b return; } - // Check if series module is enabled + // Check if the series module is enabled try { PreparedStatement seriesModuleEnabledStmt = dbConn.prepareStatement("SELECT enabled FROM modules WHERE name = 'series'"); ResultSet enabledRS = seriesModuleEnabledStmt.executeQuery(); @@ -1086,6 +1088,17 @@ synchronized void processGroupedWork(Long id, String permanentId, String groupin groupedWork.setId(permanentId); groupedWork.setGroupingCategory(grouping_category); + HashSet overriddenRecords = new HashSet<>(); + getOverriddenRecordsForWork.setString(1, permanentId); + ResultSet overriddenRecordsRS = getOverriddenRecordsForWork.executeQuery(); + while (overriddenRecordsRS.next()) { + String source = overriddenRecordsRS.getString("source"); + String recordId = overriddenRecordsRS.getString("record_id"); + overriddenRecords.add(source + ":" + recordId); + } + overriddenRecordsRS.close(); + groupedWork.setOverriddenRecords(overriddenRecords); + getGroupedWorkPrimaryIdentifiers.setLong(1, id); ResultSet groupedWorkPrimaryIdentifiersRS = getGroupedWorkPrimaryIdentifiers.executeQuery(); ArrayList recordIdentifiers = new ArrayList<>(); @@ -1507,7 +1520,11 @@ private void updateSeriesDataForWork(AbstractGroupedWorkSolr groupedWork) { addSeriesMemberStmt.setString(2, groupedWork.getId()); if (series.length == 2) { addSeriesMemberStmt.setString(3, AspenStringUtils.trimTo(100, series[1])); // Add volume - addSeriesMemberStmt.setLong(8, NumberUtils.toLong(series[1])); // Add volume as weight if it's an integer - 0 otherwise + long seriesWeight = NumberUtils.toLong(series[1]); + if (seriesWeight > Integer.MAX_VALUE) { + seriesWeight = Integer.MAX_VALUE; + } + addSeriesMemberStmt.setLong(8, seriesWeight); // Add volume as weight if it's an integer - 0 otherwise } else { addSeriesMemberStmt.setString(3, ""); addSeriesMemberStmt.setLong(8, 0); @@ -1624,7 +1641,7 @@ private void loadDisplayInfo(AbstractGroupedWorkSolr groupedWork) { if (groupedWork.isDebugEnabled()) { groupedWork.addDebugMessage("Setting title to " + title + " based on display info", 2); } - groupedWork.setTitle(title, "", title, AspenStringUtils.makeValueSortable(title), "", "", true, null); + groupedWork.setTitle(title, "", AspenStringUtils.makeValueSortable(title), "", true, null); groupedWork.clearSubTitle(); }else{ if (groupedWork.isDebugEnabled()) { @@ -1637,7 +1654,7 @@ private void loadDisplayInfo(AbstractGroupedWorkSolr groupedWork) { groupedWork.addDebugMessage("Setting author to " + author + " based on display info", 2); } //Force a format category of Books since we want to preserve this - groupedWork.setAuthorDisplay(author, "Books"); + groupedWork.setAuthorDisplay(author); }else{ if (groupedWork.isDebugEnabled()) { groupedWork.addDebugMessage("No author set in display info, not changing author", 2); @@ -1665,6 +1682,17 @@ private void loadDisplayInfo(AbstractGroupedWorkSolr groupedWork) { groupedWork.addDebugMessage("Not applying series data for grouped work because series module is enabled", 2); } } + String description = displayInfoRS.getString("description"); + if (description != null && !description.isEmpty()){ + if (groupedWork.isDebugEnabled()) { + groupedWork.addDebugMessage("Setting description to " + description + " based on display info", 2); + } + groupedWork.setDisplayDescription(description); + } else { + if (groupedWork.isDebugEnabled()) { + groupedWork.addDebugMessage("No description set in display info, not changing description", 2); + } + } } displayInfoRS.close(); diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkSolr2.java b/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkSolr2.java index ad65e6cfa9..21710cc249 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkSolr2.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/GroupedWorkSolr2.java @@ -428,17 +428,13 @@ protected void addScopedFieldsToDocument(SolrInputDocument doc, BaseIndexingLogE scopingDetailsForScope.add(scopingInfo.getScopingDetails()); } - HashSet formatsForItem; - HashSet formatsCategoriesForItem; + ArrayList formatsForItem; + ArrayList formatsCategoriesForItem; HashSet availableAtForItem = new HashSet<>(); availabilityToggleForItem.reset(); String readerName = "Libby"; - //Loading reader name here isn't really needed since it gets set for the item based on scope -// if ((scopingInfo.getScope().getOverDriveScope()) != null){ -// readerName = scopingInfo.getScope().getOverDriveScope().getReaderName(); -// } ItemInfo curItem = scopingInfo.getItem(); try { formatsForItem = curItem.getFormatsForIndexing(); @@ -588,7 +584,9 @@ protected void addScopedFieldsToDocument(SolrInputDocument doc, BaseIndexingLogE formatCategories.add(scopeName + "#Books"); formatCategories.add(scopeName + "#Audio Books"); } - doc.addField("scoping_details_" + scopeName, scopingDetailsForScope); + if (storeRecordDetailsInSolr) { + doc.addField("scoping_details_" + scopeName, scopingDetailsForScope); + } if (daysSinceAddedForScope != null){ doc.addField("local_days_since_added_" + scopeName, daysSinceAddedForScope); @@ -627,30 +625,27 @@ protected void addScopedFieldsToDocument(SolrInputDocument doc, BaseIndexingLogE //logger.info("Work " + id + " processed " + relatedScopes.size() + " scopes"); } - private void loadScopedEditionInformation(HashSet editionInfo, String scopePrefix, HashSet formatsForItem, HashSet formatsCategoriesForItem, HashSet availableAtForItem, AvailabilityToggleInfo availabilityToggleForItem) { + private void loadScopedEditionInformation(HashSet editionInfo, String scopePrefix, ArrayList formatsForItem, ArrayList formatsCategoriesForItem, HashSet availableAtForItem, AvailabilityToggleInfo availabilityToggleForItem) { if (formatsCategoriesForItem.isEmpty()){ formatsCategoriesForItem.add(""); } if (availableAtForItem.isEmpty()) { availableAtForItem.add("none"); } - HashSet availabilityToggleValues = availabilityToggleForItem.getValues(); + ArrayList availabilityToggleValues = availabilityToggleForItem.getValues(); + ArrayList availableAtForItemArray = new ArrayList<>(availableAtForItem); for (String formatCategory : formatsCategoriesForItem) { - String scopeAndFormatCategory = scopePrefix + formatCategory; + String scopeAndFormatCategory = scopePrefix + formatCategory.replace(' ', '_'); for (String format : formatsForItem) { - String scopeFormatCategoryFormat = scopeAndFormatCategory + "#" + format; + String scopeFormatCategoryFormat = scopeAndFormatCategory + "#" + format.replace(' ', '_'); for (String availabilityToggle : availabilityToggleValues) { StringBuilder baseEditionBuilder = new StringBuilder(scopeFormatCategoryFormat) .append("#") .append(availabilityToggle) .append("#"); - for (String availableAtLocation : availableAtForItem) { - StringBuilder editionBuilder = new StringBuilder(baseEditionBuilder); - String editionString = editionBuilder.append(availableAtLocation) - .append("#") - .toString() // Get the final String - .replace(' ', '_'); + for (String availableAtLocation : availableAtForItemArray) { + String editionString = baseEditionBuilder + availableAtLocation.replace(' ', '_') + "#"; // Get the final String editionInfo.add(editionString); } } @@ -673,7 +668,7 @@ private Long loadScopedDaysAdded(ItemInfo curItem) { } else if (curItem.getGroupedStatus().equals("In Processing")) { daysSinceAdded = -1000L; } else { - //copying the code below but adding a few steps, if we copy a 3rd time + //copying the code below but adding a few steps, if we copy a 3rd time //consider extracting a separate function instead //Date Added To Catalog needs to be the earliest date added for the catalog. Date dateAdded = curItem.getDateAdded(); @@ -699,8 +694,8 @@ private Long loadScopedDaysAdded(ItemInfo curItem) { } //in order to make this appear before anything else we are going to shift it by -999 daysSinceAdded += -999L; - //clamping to -1 in case we get a value > 998 - //worst case scenario we are getting the previous behavior. + //clamping to -1 in case we get a value > 998 + //worst case scenario we are getting the previous behavior. if(daysSinceAdded < -999L) { daysSinceAdded = -999L; @@ -738,7 +733,7 @@ else if(daysSinceAdded > -1L) return daysSinceAdded; } - private void loadScopedFormatInfo(HashSet scopedFormats, HashSet scopedFormatCategories, String scopePrefix, HashSet formatsForItem, HashSet formatsCategoriesForItem) { + private void loadScopedFormatInfo(HashSet scopedFormats, HashSet scopedFormatCategories, String scopePrefix, ArrayList formatsForItem, ArrayList formatsCategoriesForItem) { for (String format : formatsForItem) { scopedFormats.add(scopePrefix + format); } diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/HooplaProcessor.java b/code/reindexer/src/org/aspen_discovery/reindexer/HooplaProcessor.java index dc9f81e65e..659276836a 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/HooplaProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/HooplaProcessor.java @@ -126,7 +126,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI String fullTitle = title + " " + subTitle; fullTitle = fullTitle.trim(); String sortableTitle = AspenStringUtils.makeValueSortable(title); - groupedWork.setTitle(title, subTitle, title, sortableTitle, primaryFormat, formatCategory); + groupedWork.setTitle(title, subTitle, sortableTitle, formatCategory, false, hooplaRecord); groupedWork.addFullTitle(fullTitle); @@ -142,7 +142,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI } groupedWork.setAuthor(primaryAuthor); groupedWork.setAuthAuthor(primaryAuthor); - groupedWork.setAuthorDisplay(primaryAuthor, formatCategory); + groupedWork.setAuthorDisplay(primaryAuthor, formatCategory, hooplaRecord); if (rawResponse.has("series")){ String series = rawResponse.getString("series"); @@ -162,38 +162,31 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isKids = true; groupedWork.addTargetAudience("Juvenile"); groupedWork.addTargetAudienceFull("Juvenile"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla record", 2);} + if ( groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla record", 2);} }else { //Todo: Also check the genres (Children's, Teen boolean foundAudience = false; if (rawResponse.has("genres")) { JSONArray genres = rawResponse.getJSONArray("genres"); for (int i = 0; i < genres.length(); i++) { - if (genres.getString(i).equals("Teen")) { + if (genres.getString(i).equals("Teen") || genres.getString(i).startsWith("Young Adult")) { isTeen = true; groupedWork.addTargetAudience("Young Adult"); groupedWork.addTargetAudienceFull("Adolescent (14-17)"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla genre", 2);} - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla genre", 2);} - foundAudience = true; - } else if (genres.getString(i).startsWith("Young Adult")) { - isTeen = true; - groupedWork.addTargetAudience("Young Adult"); - groupedWork.addTargetAudienceFull("Adolescent (14-17)"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla genre", 2);} - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla genre", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla genre", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla genre", 2);} foundAudience = true; } else if (genres.getString(i).equals("Children's")) { isKids = true; groupedWork.addTargetAudience("Juvenile"); groupedWork.addTargetAudienceFull("Juvenile"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla genre", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla genre", 2);} foundAudience = true; } else if (genres.getString(i).equals("Adult")) { isAdult = true; groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla genre", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla genre", 2);} foundAudience = true; } } @@ -206,7 +199,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isAdult = true; groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating", 2);} } else { if (kind.equals("MOVIE") || kind.equals("TELEVISION")) { switch (rating) { @@ -218,7 +211,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isAdult = true; groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} break; case "PG-13": case "PG13": @@ -231,11 +224,11 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isTeen = true; groupedWork.addTargetAudience("Young Adult"); groupedWork.addTargetAudienceFull("Adolescent (14-17)"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} break; case "TVY": case "TVY7": @@ -243,7 +236,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isKids = true; groupedWork.addTargetAudience("Juvenile"); groupedWork.addTargetAudienceFull("Juvenile"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla rating " + rating, 2);} break; case "TVG": case "G": @@ -252,7 +245,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isAdult = true; groupedWork.addTargetAudience("General"); groupedWork.addTargetAudienceFull("General"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is General based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is General based on Hoopla rating " + rating, 2);} break; default: //todo, do we want to add additional ratings here? @@ -265,21 +258,21 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isKids = true; groupedWork.addTargetAudience("Juvenile"); groupedWork.addTargetAudienceFull("Juvenile"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Juvenile based on Hoopla rating " + rating, 2);} break; case "PA": case "EX": isAdult = true; groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} break; case "T": isTeen = true; groupedWork.addTargetAudience("Young Adult"); groupedWork.addTargetAudienceFull("Adolescent (14-17)"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} break; case "T+": default: @@ -287,11 +280,11 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI isTeen = true; groupedWork.addTargetAudience("Young Adult"); groupedWork.addTargetAudienceFull("Adolescent (14-17)"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} } } else { @@ -301,16 +294,16 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI groupedWork.addTargetAudienceFull("Adolescent (14-17)"); groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target audience is Young Adult based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is Adolescent (14-17) based on Hoopla rating " + rating, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla rating " + rating, 2);} } } } else if (!foundAudience) { isAdult = true; groupedWork.addTargetAudience("Adult"); groupedWork.addTargetAudienceFull("Adult"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla record", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Target/full target audience is Adult based on Hoopla record", 2);} } } @@ -365,11 +358,11 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI if (rawResponse.getBoolean("fiction")){ Util.addToMapWithCount(literaryForm, "Fiction"); Util.addToMapWithCount(literaryFormFull, "Fiction"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Literary Form is fiction based on Hoopla record", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Literary Form is fiction based on Hoopla record", 2);} }else{ Util.addToMapWithCount(literaryForm, "Non Fiction"); Util.addToMapWithCount(literaryFormFull, "Non Fiction"); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Literary Form is non fiction based on Hoopla record", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Literary Form is non fiction based on Hoopla record", 2);} } } if (!literaryForm.isEmpty()){ @@ -416,12 +409,12 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI itemInfo.setFormatCategory(formatCategory); if (hooplaType.equalsIgnoreCase("Flex")){ + itemInfo.seteContentSubSource("Flex"); getFlexAvailabilityStmt.setString(1, identifier); ResultSet flexAvailabilityRS = getFlexAvailabilityStmt.executeQuery(); if (flexAvailabilityRS.next()){ int totalCopies = flexAvailabilityRS.getInt("totalCopies"); int availableCopies = flexAvailabilityRS.getInt("availableCopies"); - int holdsQueueSize = flexAvailabilityRS.getInt("holdsQueueSize"); itemInfo.setNumCopies(totalCopies); itemInfo.setAvailable(availableCopies > 0); @@ -438,6 +431,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI flexAvailabilityRS.close(); }else{ //Hoopla instant is always 1 copy unlimited use + itemInfo.seteContentSubSource("Instant"); itemInfo.setNumCopies(1); itemInfo.setAvailable(true); itemInfo.setDetailedStatus("Available Online"); diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/IlsRecordProcessor.java b/code/reindexer/src/org/aspen_discovery/reindexer/IlsRecordProcessor.java index 95a7c2e942..390beee1f3 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/IlsRecordProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/IlsRecordProcessor.java @@ -123,6 +123,19 @@ abstract class IlsRecordProcessor extends MarcRecordProcessor { loadTranslationMapsForProfile(dbConn, indexingProfileRS.getLong("id")); loadTimeToReshelve(dbConn, indexingProfileRS.getLong("id")); + + if (settings.isIndex896asSeries()) { + seriesFieldsToIndexWith800.add(896); + } + if (settings.isIndex897asSeries()) { + seriesFieldsToIndexWith800.add(897); + } + if (settings.isIndex898asSeries()) { + seriesFieldsToIndexWith800.add(898); + } + if (settings.isIndex899asSeries()) { + seriesFieldsToIndexWith830.add(899); + } }catch (Exception e){ indexer.getLogEntry().incErrors("Error loading indexing profile information from database", e); } @@ -1950,4 +1963,4 @@ public boolean isHideNotCodedLiteraryForm() { return settings.isHideNotCodedLiteraryForm(); } -} \ No newline at end of file +} diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/ItemInfo.java b/code/reindexer/src/org/aspen_discovery/reindexer/ItemInfo.java index 58b553fd13..b68136a6fa 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/ItemInfo.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/ItemInfo.java @@ -30,6 +30,7 @@ public class ItemInfo{ private String trimmedIType; private String ITypeCode; private String eContentSource; + private String eContentSubSource; private String trimmedEContentSource; private String eContentFilename; private String eContentUrl; @@ -291,6 +292,10 @@ String geteContentSource() { return eContentSource; } + String geteContentSubSource() { + return eContentSubSource; + } + String getTrimmedEContentSource(){ return trimmedEContentSource; } @@ -300,6 +305,10 @@ void seteContentSource(String eContentSource) { this.trimmedEContentSource = AspenStringUtils.trimTrailingPunctuation(eContentSource); } + void seteContentSubSource(String eContentSubSource) { + this.eContentSubSource = eContentSubSource; + } + String getCallNumber() { return callNumber; } @@ -552,7 +561,8 @@ private void createScopingStrings() { recordsIncludedScopes = new StringBuffer("~"); locationOwnedNames = new HashSet<>(); libraryOwnedNames = new HashSet<>(); - for (ScopingInfo scope : scopingInfo.values()){ + ArrayList scopes = new ArrayList<>(scopingInfo.values()); + for (ScopingInfo scope : scopes){ Scope curScope = scope.getScope(); if (scope.isLocallyOwned()){ locationOwnedScopes.append(curScope.getId()).append("~"); @@ -566,10 +576,10 @@ private void createScopingStrings() { } } - private HashSet formatsForIndexing = null; - public HashSet getFormatsForIndexing() { + private ArrayList formatsForIndexing = null; + public ArrayList getFormatsForIndexing() { if (formatsForIndexing == null){ - formatsForIndexing = new HashSet<>(); + formatsForIndexing = new ArrayList<>(); if (format != null){ formatsForIndexing.add(format); }else{ @@ -579,10 +589,10 @@ public HashSet getFormatsForIndexing() { return formatsForIndexing; } - private HashSet formatCategoriesForIndexing = null; - public HashSet getFormatCategoriesForIndexing() { + private ArrayList formatCategoriesForIndexing = null; + public ArrayList getFormatCategoriesForIndexing() { if (formatCategoriesForIndexing == null){ - formatCategoriesForIndexing = new HashSet<>(); + formatCategoriesForIndexing = new ArrayList<>(); if (formatCategory != null){ formatCategoriesForIndexing.add(formatCategory); }else{ diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/KohaRecordProcessor.java b/code/reindexer/src/org/aspen_discovery/reindexer/KohaRecordProcessor.java index 3647590ee4..e1389d34ab 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/KohaRecordProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/KohaRecordProcessor.java @@ -196,10 +196,10 @@ protected ItemStatus getItemStatus(DataField itemField, String recordIdentifier) status = getStatusFromSubfield(itemField, '4', "Damaged"); if (status != null) return new ItemStatus(status, ItemStatus.FROM_STATUS_FIELD, this, recordIdentifier); - status = getStatusFromSubfield(itemField, 'q', "Checked Out"); + status = getStatusFromSubfield(itemField, '7', "Library Use Only"); // Not for Loan if (status != null) return new ItemStatus(status, ItemStatus.FROM_STATUS_FIELD, this, recordIdentifier); - status = getStatusFromSubfield(itemField, '7', "Library Use Only"); //not for loan + status = getStatusFromSubfield(itemField, 'q', "Checked Out"); if (status != null) return new ItemStatus(status, ItemStatus.FROM_STATUS_FIELD, this, recordIdentifier); status = getStatusFromSubfield(itemField, 'k', null); diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/MarcRecordProcessor.java b/code/reindexer/src/org/aspen_discovery/reindexer/MarcRecordProcessor.java index 01c9312c46..5babd5afd1 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/MarcRecordProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/MarcRecordProcessor.java @@ -33,6 +33,9 @@ abstract class MarcRecordProcessor { PreparedStatement marcRecordAsSuppressedNoMarcStmt; PreparedStatement getRecordSuppressionInformationStmt; + protected ArrayList seriesFieldsToIndexWith800; + protected ArrayList seriesFieldsToIndexWith830; + MarcRecordProcessor(GroupedWorkIndexer indexer, String profileType, Connection dbConn, Logger logger) { this.indexer = indexer; this.logger = logger; @@ -45,6 +48,12 @@ abstract class MarcRecordProcessor { indexer.getLogEntry().incErrors("Error setting up prepared statements for loading MARC from the DB", e); } formatClassifier = new MarcRecordFormatClassifier(logger); + seriesFieldsToIndexWith800 = new ArrayList<>(); + seriesFieldsToIndexWith800.add(800); + seriesFieldsToIndexWith800.add(810); + seriesFieldsToIndexWith800.add(811); + seriesFieldsToIndexWith830 = new ArrayList<>(); + seriesFieldsToIndexWith830.add(830); } /** @@ -357,7 +366,7 @@ protected void loadSubjects(AbstractGroupedWorkSolr groupedWork, org.marc4j.marc } void updateGroupedWorkSolrDataBasedOnStandardMarcData(AbstractGroupedWorkSolr groupedWork, org.marc4j.marc.Record record, ArrayList printItems, String identifier, String format, String formatCategory, boolean hasParentRecord) { - loadTitles(groupedWork, record, format, formatCategory, hasParentRecord, identifier); + loadTitles(groupedWork, record, formatCategory, hasParentRecord, identifier); loadAuthors(groupedWork, record, identifier, formatCategory); loadSubjects(groupedWork, record); @@ -372,7 +381,7 @@ void updateGroupedWorkSolrDataBasedOnStandardMarcData(AbstractGroupedWorkSolr gr groupedWork.addCorporateNameSubject(name); } - List seriesFields = MarcUtil.getDataFields(record, new int[]{830, 899}); + List seriesFields = MarcUtil.getDataFields(record, seriesFieldsToIndexWith830); for (DataField seriesField : seriesFields){ String series = AspenStringUtils.trimTrailingPunctuation(MarcUtil.getSpecifiedSubfieldsAsString(seriesField, "anp"," ")).toString(); //Remove anything in parentheses since it's normally just the format @@ -393,7 +402,8 @@ void updateGroupedWorkSolrDataBasedOnStandardMarcData(AbstractGroupedWorkSolr gr groupedWork.addSeriesWithVolume(series, volume, 3); groupedWork.addSeries(series); } - seriesFields = MarcUtil.getDataFields(record, new int[]{800, 810, 811, 896, 897, 898}); + + seriesFields = MarcUtil.getDataFields(record, seriesFieldsToIndexWith800); for (DataField seriesField : seriesFields){ String subfields; if (seriesField.getNumericTag() == 800 || seriesField.getNumericTag() == 896) { @@ -709,15 +719,15 @@ protected void loadTargetAudiences(AbstractGroupedWorkSolr groupedWork, org.marc LinkedHashSet translatedAudiencesFull; if (settings == null) { translatedAudiencesFull = indexer.translateSystemCollection("target_audience_full", targetAudiences, identifier); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is " + translatedAudiencesFull + " based on system target_audience translation map", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is " + translatedAudiencesFull + " based on system target_audience translation map", 2);} }else { translatedAudiencesFull = settings.translateCollection("target_audience_full", targetAudiences, identifier, indexer.getLogEntry(), logger, true); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is " + translatedAudiencesFull + " based on target_audience translation map in settings", 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Full target audience is " + translatedAudiencesFull + " based on target_audience translation map in settings", 2);} } if (!unknownAudienceLabel.equals("Unknown") && translatedAudiencesFull.contains("Unknown")){ translatedAudiencesFull.remove("Unknown"); translatedAudiencesFull.add(unknownAudienceLabel); - if (groupedWork != null && groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Updating unknown full target audience to " + unknownAudienceLabel, 2);} + if (groupedWork.isDebugEnabled()) {groupedWork.addDebugMessage("Updating unknown full target audience to " + unknownAudienceLabel, 2);} } groupedWork.addTargetAudiencesFull(translatedAudiencesFull); } @@ -1105,7 +1115,6 @@ public static String codeToCountry(String code) { //noinspection SpellCheckingInspection countryList.put("BV", "Bouvet Island (Bouvetoya)"); countryList.put("BR", "Brazil"); - //noinspection SpellCheckingInspection countryList.put("IO", "British Indian Ocean Territory (Chagos Archipelago)"); countryList.put("VG", "British Virgin Islands"); //noinspection SpellCheckingInspection @@ -1129,7 +1138,6 @@ public static String codeToCountry(String code) { countryList.put("CD", "Congo"); countryList.put("CG", "Congo the"); countryList.put("CK", "Cook Islands"); - //noinspection SpellCheckingInspection countryList.put("CR", "Costa Rica"); //noinspection SpellCheckingInspection countryList.put("CI", "Cote d'Ivoire"); @@ -1148,7 +1156,6 @@ public static String codeToCountry(String code) { countryList.put("ER", "Eritrea"); countryList.put("EE", "Estonia"); countryList.put("ET", "Ethiopia"); - //noinspection SpellCheckingInspection countryList.put("FO", "Faroe Islands"); //noinspection SpellCheckingInspection countryList.put("FK", "Falkland Islands (Malvinas)"); @@ -1539,23 +1546,18 @@ private void loadAuthors(AbstractGroupedWorkSolr groupedWork, org.marc4j.marc.Re } contributors.add(contributor.toString()); } - groupedWork.addAuthor2Role(contributors); - //author_display = 100acq:110a:260b:710a:245c, first - //#ARL-95 Do not show display author from the 710 or from the 245c since neither are truly authors - //#ARL-200 Do not show display author from the 260b since it is also not the author - //DIS-413 Add subfield q to the display + groupedWork.addAuthor2Role(contributors); String displayAuthor = MarcUtil.getFirstFieldVal(record, "100acq:110ab"); if (displayAuthor != null && displayAuthor.indexOf(';') > 0){ displayAuthor = displayAuthor.substring(0, displayAuthor.indexOf(';') -1); } - groupedWork.setAuthorDisplay(displayAuthor, formatCategory); - } - private void loadTitles(AbstractGroupedWorkSolr groupedWork, org.marc4j.marc.Record record, String format, String formatCategory, boolean hasParentRecord, String identifier) { - //title (full title done by index process by concatenating short and subtitle + RecordInfo recordInfo = groupedWork.getRecordInfo(profileType, identifier); + groupedWork.setAuthorDisplay(displayAuthor, formatCategory, recordInfo); + } - //title short + private void loadTitles(AbstractGroupedWorkSolr groupedWork, org.marc4j.marc.Record record, String formatCategory, boolean hasParentRecord, String identifier) { DataField titleField = record.getDataField(245); String authorInTitleField = null; if (titleField != null) { @@ -1567,15 +1569,13 @@ private void loadTitles(AbstractGroupedWorkSolr groupedWork, org.marc4j.marc.Rec groupedWork.setTitle( titleField.getSubfieldsAsString("a"), subTitle, - titleField.getSubfieldsAsString("abfgnp", " "), this.getSortableTitle(record), - format, formatCategory, false, - recordInfo + recordInfo, + this ); } - //title full authorInTitleField = titleField.getSubfieldsAsString("c"); } String standardAuthorData = MarcUtil.getFirstFieldVal(record, "100abcdq:110ab"); diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/OverDriveProcessor.java b/code/reindexer/src/org/aspen_discovery/reindexer/OverDriveProcessor.java index f746e53ec1..7794771fd6 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/OverDriveProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/OverDriveProcessor.java @@ -199,7 +199,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI String fullTitle = title + " " + subtitle; fullTitle = fullTitle.trim(); - groupedWork.setTitle(title, subtitle, title, metadata.get("sortTitle"), primaryFormat, formatCategory); + groupedWork.setTitle(title, subtitle, metadata.get("sortTitle"), formatCategory, false, overDriveRecord); groupedWork.addFullTitle(fullTitle); if (series != null && !series.isEmpty()) { @@ -208,7 +208,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI } groupedWork.setAuthor(productRS.getString("primaryCreatorName")); groupedWork.setAuthAuthor(productRS.getString("primaryCreatorName")); - groupedWork.setAuthorDisplay(productRS.getString("primaryCreatorName"), formatCategory); + groupedWork.setAuthorDisplay(productRS.getString("primaryCreatorName"), formatCategory, overDriveRecord); if (rawMetadataDecoded != null) { //Loop through all creators and add them as alternate author names JSONArray creators = rawMetadataDecoded.getJSONArray("creators"); @@ -258,13 +258,22 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI } if (isEComic) { if (rawMetadataDecoded.has("edition")) { - if (rawMetadataDecoded.getString("edition").equalsIgnoreCase("Light Novel")){ + if (rawMetadataDecoded.getString("edition").equalsIgnoreCase("Light Novel") || rawMetadataDecoded.getString("edition").equalsIgnoreCase("BL Light Novel")){ isEComic = false; if (groupedWork.isDebugEnabled()) { groupedWork.addDebugMessage("Resetting format back to eBook due to an edition of Light Novel", 2); } } } + if (series != null && !series.isEmpty()) { + if (series.toLowerCase().contains("light novel")) { + isEComic = false; + if (groupedWork.isDebugEnabled()) { + groupedWork.addDebugMessage("Resetting format back to eBook due to a series including Light Novel", 2); + } + } + } + if (isEComic) { validFormats.clear(); diff --git a/code/reindexer/src/org/aspen_discovery/reindexer/PalaceProjectProcessor.java b/code/reindexer/src/org/aspen_discovery/reindexer/PalaceProjectProcessor.java index 6e0888fe08..49fe82ff51 100644 --- a/code/reindexer/src/org/aspen_discovery/reindexer/PalaceProjectProcessor.java +++ b/code/reindexer/src/org/aspen_discovery/reindexer/PalaceProjectProcessor.java @@ -131,7 +131,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI if (metadata.has("sortAs")){ sortableTitle = metadata.getString("sortAs"); } - groupedWork.setTitle(title, subTitle, title, sortableTitle, primaryFormat, formatCategory); + groupedWork.setTitle(title, subTitle, sortableTitle, formatCategory, false, palaceProjectRecord); groupedWork.addFullTitle(fullTitle); String primaryAuthor = ""; @@ -145,7 +145,7 @@ void processRecord(AbstractGroupedWorkSolr groupedWork, String identifier, BaseI } groupedWork.setAuthor(primaryAuthor); groupedWork.setAuthAuthor(primaryAuthor); - groupedWork.setAuthorDisplay(primaryAuthor, formatCategory); + groupedWork.setAuthorDisplay(primaryAuthor, formatCategory, palaceProjectRecord); //Note: Palace Project does not provide series information diff --git a/code/series_indexer/series_indexer.jar b/code/series_indexer/series_indexer.jar index 481481d4af..a585500c47 100644 Binary files a/code/series_indexer/series_indexer.jar and b/code/series_indexer/series_indexer.jar differ diff --git a/code/sideload_processing/sideload_processing.jar b/code/sideload_processing/sideload_processing.jar index 0aa3e01e4d..7da4bf8092 100644 Binary files a/code/sideload_processing/sideload_processing.jar and b/code/sideload_processing/sideload_processing.jar differ diff --git a/code/sierra_export_api/sierra_export_api.jar b/code/sierra_export_api/sierra_export_api.jar index f3cdea3a87..8150197315 100644 Binary files a/code/sierra_export_api/sierra_export_api.jar and b/code/sierra_export_api/sierra_export_api.jar differ diff --git a/code/sierra_export_api/src/com/turning_leaf_technologies/sierra/SierraExportAPIMain.java b/code/sierra_export_api/src/com/turning_leaf_technologies/sierra/SierraExportAPIMain.java index d1fd9a7653..d241689fbd 100644 --- a/code/sierra_export_api/src/com/turning_leaf_technologies/sierra/SierraExportAPIMain.java +++ b/code/sierra_export_api/src/com/turning_leaf_technologies/sierra/SierraExportAPIMain.java @@ -1002,7 +1002,17 @@ private static boolean updateMarcAndRegroupRecordId(SierraInstanceInformation si //Get Marc data from bibs response if (!bibsData.has("marc")) { - logEntry.incInvalidRecords("Record " + id + " has no MARC data"); + String longId = ".b" + id + getCheckDigit(id); + logEntry.incInvalidRecords("Record " + longId + " has no MARC data"); + RemoveRecordFromWorkResult result = getRecordGroupingProcessor().removeRecordFromGroupedWork(indexingProfile.getName(), longId); + getGroupedWorkIndexer().markIlsRecordAsDeleted(indexingProfile.getName(), longId); + if (result.reindexWork) { + getGroupedWorkIndexer().processGroupedWork(result.permanentId); + } else if (result.deleteWork) { + //Delete the work from solr and the database + getGroupedWorkIndexer().deleteRecord(result.permanentId, result.groupedWorkId); + } + logEntry.incDeleted(); return true; } JSONObject marcData = bibsData.getJSONObject("marc"); diff --git a/code/symphony_export/symphony_export.jar b/code/symphony_export/symphony_export.jar index 8744104fab..b5786c3708 100644 Binary files a/code/symphony_export/symphony_export.jar and b/code/symphony_export/symphony_export.jar differ diff --git a/code/user_list_indexer/user_list_indexer.jar b/code/user_list_indexer/user_list_indexer.jar index 308e183742..3639d8eef9 100644 Binary files a/code/user_list_indexer/user_list_indexer.jar and b/code/user_list_indexer/user_list_indexer.jar differ diff --git a/code/web/CatalogConnection.php b/code/web/CatalogConnection.php index d9719c4eea..bb55329d2d 100644 --- a/code/web/CatalogConnection.php +++ b/code/web/CatalogConnection.php @@ -1965,6 +1965,9 @@ public function updateAccountNotifications(ILSNotificationSetting $ilsNotificati $user = new User(); $user->id = $userNotificationToken->userId; if ($user->find(true)) { + $barcode = $user->getBarcode(); + $cronLogEntry->notes .= "- Updating User with $barcode.
"; + $cronLogEntry->update(); if ($user->canReceiveNotifications('notifyAccount')) { $userResult = $this->driver->updateAccountNotifications($user, $ilsNotificationSetting, $cronLogEntry); if ($userResult['success']) { @@ -1975,6 +1978,10 @@ public function updateAccountNotifications(ILSNotificationSetting $ilsNotificati $result['success'] = false; } } + }else{ + $cronLogEntry->numErrors++; + $cronLogEntry->notes .= "Could not find user {$userNotificationToken->userId}.
"; + $cronLogEntry->update(); } } if (!is_null($cronLogEntry)) { diff --git a/code/web/Drivers/CarlX.php b/code/web/Drivers/CarlX.php index 6d3f7750ec..7926413634 100644 --- a/code/web/Drivers/CarlX.php +++ b/code/web/Drivers/CarlX.php @@ -338,8 +338,14 @@ protected function doSoapRequest($requestName, $request, string $WSDL = '', arra } while (!$connectionPassed && $numTries < 2) { try { - $this->soapClient = new SoapClient($WSDL, $soapRequestOptions); - $result = $this->soapClient->$requestName($request); + try { + $this->soapClient = new SoapClient($WSDL, $soapRequestOptions); + $result = $this->soapClient->$requestName($request); + } catch (SoapFault $e) { + global $logger; + $logger->log("SOAP Fault calling $requestName: " . $e->getMessage(), Logger::LOG_ERROR); + throw $e; + } $connectionPassed = true; if (IPAddress::showDebuggingInformation()) { ExternalRequestLogEntry::logRequest('carlx.' . $requestName, 'GET', $WSDL, $this->soapClient->__getLastRequestHeaders(), $this->soapClient->__getLastRequest(), 0, $this->soapClient->__getLastResponse(), $dataToSanitize); @@ -893,6 +899,20 @@ public function updatePatronInfo(User $patron, $canUpdateContactInfo, $fromMasqu $result['success'] = true; $result['messages'][] = 'Your account was updated successfully.'; $patron->update(); + + $patronHomeLibrary = $patron->getHomeLibrary(true); + if ($patronHomeLibrary && $patronHomeLibrary->enableThirdPartySMSNotifications) { + // If checkbox is unchecked, $_REQUEST['thirdPartySMSOptIn'] won't be set, which means opt-out. + $enableThirdPartySMS = ($_REQUEST['thirdPartySMSOptIn'] ?? false) && $_REQUEST['thirdPartySMSOptIn'] == 'on'; + $smsResult = $this->updatePatronUserDefinedFields($patron, $enableThirdPartySMS); + + if ($smsResult['success']) { + $result['messages'] = array_merge($result['messages'], $smsResult['messages']); + } else { + // Don't fail the entire update if SMS update fails, just add a warning. + $result['messages'][] = 'Contact information updated, but SMS preferences could not be updated: ' . implode(', ', $smsResult['messages']); + } + } } } else { @@ -905,12 +925,109 @@ public function updatePatronInfo(User $patron, $canUpdateContactInfo, $fromMasqu $result['messages'][] = 'You can not update your information.'; } - if ($result['success'] == false && empty($result['messages'])) { + if (!$result['success'] && empty($result['messages'])) { $result['messages'][] = 'Unknown error updating your account'; } return $result; } + /** + * Update a patron's user defined fields for third-party SMS notifications. + * + * @param User $patron + * @param boolean $enableThirdPartySMS + * @return array + */ + public function updatePatronUserDefinedFields(User $patron, bool $enableThirdPartySMS): array { + $result = [ + 'success' => false, + 'messages' => [], + ]; + + $request = new stdClass(); + $request->SearchType = 'Patron ID'; + $request->SearchID = $patron->getBarcode(); + $request->UserDefinedRestrictedField = new stdClass(); + $request->UserDefinedRestrictedField->FieldID = 4; // Field ID for '3rd Party SMS' as provided by TLC + $request->UserDefinedRestrictedField->Type = 'Patron Statistic'; + // 1 to receive 3rd Party SMS notices, 2 to not receive them. + $request->UserDefinedRestrictedField->NumberCode = $enableThirdPartySMS ? 1 : 2; + $request->Modifiers = new stdClass(); + $request->Modifiers->ReportMode = false; + $request->Modifiers->Projection = 'Brief'; + + try { + $requestOptions = $this->genericResponseSOAPCallOptions; + $requestOptions['login'] = $this->accountProfile->oAuthClientId; + $requestOptions['password'] = $this->accountProfile->oAuthClientSecret; + $soapResult = $this->doSoapRequest('UpdatePatronUserDefinedFields', $request, $this->patronWsdl, $requestOptions); + if ($soapResult) { + if (isset($soapResult->ResponseStatuses->ResponseStatus)) { + $success = stripos($soapResult->ResponseStatuses->ResponseStatus->ShortMessage, 'Success') !== false; + if (!$success) { + $errorMessage = $soapResult->ResponseStatuses->ResponseStatus->LongMessage; + $result['messages'][] = 'Failed to update SMS preferences' . ($errorMessage ? ' : ' . $errorMessage : ''); + } else { + $result['success'] = true; + $result['messages'][] = 'SMS notification preferences updated successfully.'; + } + } else { + $result['messages'][] = 'Unexpected response format when updating SMS preferences.'; + global $logger; + $logger->log("CarlX: Unexpected response structure: " . print_r($soapResult, true), Logger::LOG_ERROR); + } + } else { + $result['messages'][] = 'Unable to update SMS preferences.'; + } + } catch (Exception $e) { + global $logger; + $logger->log('Exception when updating CarlX User Defined Fields: ' . $e->getMessage(), Logger::LOG_ERROR); + $result['messages'][] = 'Error updating SMS preferences: ' . $e->getMessage(); + } + + if (!$result['success'] && empty($result['messages'])) { + $result['messages'][] = 'Unknown error updating SMS preferences.'; + } + + return $result; + } + + /** + * Get the current third-party SMS opt-in status for a patron from user defined fields. + * + * @param User $patron + * @return boolean + */ + public function getThirdPartySMSOptInStatus(User $patron): bool { + $patronHomeLibrary = $patron->getHomeLibrary(true); + if (!$patronHomeLibrary || !$patronHomeLibrary->enableThirdPartySMSNotifications) { + return false; + } + + try { + $request = $this->getSearchbyPatronIdRequest($patron); + $soapResult = $this->doSoapRequest('getPatronInformation', $request, $this->patronWsdl, $this->genericResponseSOAPCallOptions); + + if (isset($soapResult->Patron->UserDefinedFields) && $soapResult) { + $userDefinedFields = $soapResult->Patron->UserDefinedFields; + if (isset($userDefinedFields->UserDefinedField)) { + $udfList = is_array($userDefinedFields->UserDefinedField) ? $userDefinedFields->UserDefinedField : [$userDefinedFields->UserDefinedField]; + foreach ($udfList as $udf) { + if (isset($udf->Field) && stripos($udf->Field, 'UDF4') !== false) { + $value = isset($udf->Value) ? strtolower(trim($udf->Value)) : ''; + return in_array($value, ['yes', 'true', '1'], true); + } + } + } + } + } catch (Exception $e) { + global $logger; + $logger->log('Exception when retrieving third-party SMS status from CarlX: ' . $e->getMessage(), Logger::LOG_ERROR); + } + + return false; + } + public function getSelfRegistrationFields() { $listOfLanguages = []; $phoneTypeList = []; @@ -1486,11 +1603,8 @@ public function getFines(User $patron, $includeMessages = false): array { $myFines = []; $request = $this->getSearchbyPatronIdRequest($patron); - // Fines $request->TransactionType = 'Fine'; $result = $this->doSoapRequest('getPatronTransactions', $request); - //global $logger; - //$logger->log("Result of getPatronTransactions (Fine)\r\n" . print_r($result, true), Logger::LOG_ERROR); if ($result && !empty($result->FineItems->FineItem)) { if (!is_array($result->FineItems->FineItem)) { $result->FineItems->FineItem = [$result->FineItems->FineItem]; @@ -1542,7 +1656,6 @@ public function getFines(User $patron, $includeMessages = false): array { // TODO: Lost Items don't have the fine amount $request->TransactionType = 'Lost'; $result = $this->doSoapRequest('getPatronTransactions', $request); - //$logger->log("Result of getPatronTransactions (Lost)\r\n" . print_r($result, true), Logger::LOG_ERROR); if ($result && !empty($result->LostItems->LostItem)) { if (!is_array($result->LostItems->LostItem)) { @@ -2417,6 +2530,7 @@ public function loadContactInformationFromSoapResult(User $user, stdClass $soapR $user->_availableHoldNotice = $soapResult->Patron->SendHoldAvailableFlag; $user->_comingDueNotice = $soapResult->Patron->SendComingDueFlag; $user->_phoneType = $soapResult->Patron->PhoneType; + $user->_thirdPartySMSOptIn = $this->getThirdPartySMSOptInStatus($user); $location = new Location(); $location->code = strtolower($soapResult->Patron->DefaultBranch); @@ -2582,4 +2696,4 @@ public function bypassReadingHistoryUpdate($patron, $isNightlyUpdate): bool { } return true; } -} +} \ No newline at end of file diff --git a/code/web/Drivers/Evergreen.php b/code/web/Drivers/Evergreen.php index 26ef702818..59dd4c43c8 100644 --- a/code/web/Drivers/Evergreen.php +++ b/code/web/Drivers/Evergreen.php @@ -1097,7 +1097,7 @@ public function getHolds(User $patron): array { $curHold->recordId = $volumeInfo->recordId; } } - } elseif ($holdInfo['hold_type'] == 'C') { + } elseif ($holdInfo['hold_type'] == 'C' || $holdInfo['hold_type'] == 'F' || $holdInfo['hold_type'] == 'R') { //This is a copy level hold, need to look it up by the item number $modsInfo = $this->getModsForCopy($holdInfo['target']); $curHold->recordId = (string)$modsInfo['doc_id']; diff --git a/code/web/Drivers/Koha.php b/code/web/Drivers/Koha.php index a9a11664d6..7d6c9fa88a 100644 --- a/code/web/Drivers/Koha.php +++ b/code/web/Drivers/Koha.php @@ -504,6 +504,7 @@ public function getCheckouts(User $patron): array { require_once ROOT_DIR . '/sys/Indexing/IlsRecord.php'; IlsRecord::preloadIlsRecords($this->getIndexingProfile()->name, $allBibNumbers); + $circControl = $this->getKohaSystemPreference('CircControl', 'PatronLibrary'); foreach ($allRows as $curRow) { $curCheckout = new Checkout(); $curCheckout->type = 'ils'; @@ -563,13 +564,20 @@ public function getCheckouts(User $patron): array { $patronType = $patron->patronType; $itemType = $curRow['itype']; - $checkoutBranch = $curRow['branchcode']; + if ($circControl == 'PatronLibrary') { + $circBranch = $patron->getHomeLocationCode(); + } else if ($circControl == 'PickupLibrary') { + $circBranch = Library::getActiveLibrary()->subdomain; + } else { + $circBranch = $curRow['branchcode']; + } $curCheckout->returnClaim = ''; //Check if patron is allowed to auto-renew based on circulation rules - $circulationRulesKey = "$patronType~$itemType~$checkoutBranch"; + $circulationRulesKey = "$patronType~$itemType~$circBranch"; + $rulesFound = []; if (array_key_exists($circulationRulesKey, $circulationRulesForCheckouts)){ $circulationRulesForCheckout = $circulationRulesForCheckouts[$circulationRulesKey]; } else { @@ -579,14 +587,19 @@ public function getCheckouts(User $patron): array { $circulationRulesSql = " SELECT * FROM circulation_rules WHERE (categorycode IN ('$patronType', '*') OR categorycode IS NULL) - AND (itemtype IN('$itemType', '*') OR itemtype is null) - AND (branchcode IN ('$checkoutBranch', '*') OR branchcode IS NULL) - ORDER BY branchcode desc, categorycode desc, itemtype desc LIMIT 1 + AND (itemtype IN('$itemType', '*') OR itemtype is null) + AND (branchcode IN ('$circBranch', '*') OR branchcode IS NULL) + AND rule_name like '%renew%' + ORDER BY branchcode desc, categorycode desc, itemtype desc "; $circulationRulesRS = mysqli_query($this->dbConnection, $circulationRulesSql); if ($circulationRulesRS !== false) { - $circulationRulesRow = $circulationRulesRS->fetch_assoc(); - $circulationRulesForCheckout[] = $circulationRulesRow; + while ($circulationRulesRow = $circulationRulesRS->fetch_assoc()) { + if (!array_key_exists($circulationRulesRow['rule_name'], $rulesFound)) { + $circulationRulesForCheckout[] = $circulationRulesRow; + $rulesFound[$circulationRulesRow['rule_name']] = $circulationRulesRow['rule_name']; + } + } $circulationRulesRS->close(); } $timer->logTime("Load circulation rules for checkout"); @@ -2331,6 +2344,8 @@ public function getHolds($patron, $page = 1, $recordsPerPage = -1, $sortOption = require_once ROOT_DIR . '/sys/Indexing/IlsRecord.php'; IlsRecord::preloadIlsRecords($this->getIndexingProfile()->name, $allBibNumbers); + $circControl = $this->getKohaSystemPreference('CircControl', 'PatronLibrary'); + foreach ($allRows as $curRow) { //Each row in the table represents a hold $curHold = new Hold(); @@ -2411,9 +2426,15 @@ public function getHolds($patron, $page = 1, $recordsPerPage = -1, $sortOption = if($this->getKohaVersion() >= 22.11) { $patronType = $patron->patronType; $itemType = $curRow['itype']; - $checkoutBranch = $curRow['branchcode']; + if ($circControl == 'PatronLibrary') { + $circBranch = $patron->getHomeLocationCode(); + } else if ($circControl == 'PickupLibrary') { + $circBranch = Library::getActiveLibrary()->subdomain; + } else { + $circBranch = $curRow['branchcode']; + } /** @noinspection SqlResolve */ - $issuingRulesSql = "SELECT * FROM circulation_rules where rule_name = 'waiting_hold_cancellation' AND (categorycode IN ('$patronType', '*') OR categorycode IS NULL) and (itemtype IN('$itemType', '*') OR itemtype is null) and (branchcode IN ('$checkoutBranch', '*') OR branchcode IS NULL) order by branchcode desc, categorycode desc, itemtype desc limit 1"; + $issuingRulesSql = "SELECT * FROM circulation_rules where rule_name = 'waiting_hold_cancellation' AND (categorycode IN ('$patronType', '*') OR categorycode IS NULL) and (itemtype IN('$itemType', '*') OR itemtype is null) and (branchcode IN ('$circBranch', '*') OR branchcode IS NULL) order by branchcode desc, categorycode desc, itemtype desc limit 1"; $issuingRulesRS = mysqli_query($this->dbConnection, $issuingRulesSql); if ($issuingRulesRS !== false) { if ($issuingRulesRow = $issuingRulesRS->fetch_assoc()) { @@ -2964,6 +2985,8 @@ public function renewCheckout($patron, $recordId, $itemId = null, $itemIndex = n $renewResponse = $this->getXMLWebServiceResponse($renewURL); ExternalRequestLogEntry::logRequest('koha.renewCheckout', 'GET', $renewURL, $this->curlWrapper->getHeaders(), '', $this->curlWrapper->getResponseCode(), $renewResponse, []); + $circControl = $this->getKohaSystemPreference('CircControl', 'PatronLibrary'); + //Parse the result if (isset($renewResponse->success) && ($renewResponse->success == 1)) { $renewResults = mysqli_query($this->dbConnection, $renewSql); @@ -2971,14 +2994,20 @@ public function renewCheckout($patron, $recordId, $itemId = null, $itemIndex = n while ($curRow = mysqli_fetch_assoc($renewResults)) { $patronType = $patron->patronType; $itemType = $curRow['itype']; - $checkoutBranch = $curRow['branchcode']; + if ($circControl == 'PatronLibrary') { + $circBranch = $patron->getHomeLocationCode(); + } else if ($circControl == 'PickupLibrary') { + $circBranch = Library::getActiveLibrary()->subdomain; + } else { + $circBranch = $curRow['branchcode']; + } if ($this->getKohaVersion() >= 22.11) { $renewCount = $curRow['renewals_count']; } else { $renewCount = $curRow['renewals']; } /** @noinspection SqlResolve */ - $issuingRulesSql = "SELECT * FROM circulation_rules where rule_name = 'renewalsallowed' AND (categorycode IN ('$patronType', '*') OR categorycode IS NULL) and (itemtype IN('$itemType', '*') OR itemtype is null) and (branchcode IN ('$checkoutBranch', '*') OR branchcode IS NULL) order by branchcode desc, categorycode desc, itemtype desc limit 1"; + $issuingRulesSql = "SELECT * FROM circulation_rules where rule_name = 'renewalsallowed' AND (categorycode IN ('$patronType', '*') OR categorycode IS NULL) and (itemtype IN('$itemType', '*') OR itemtype is null) and (branchcode IN ('$circBranch', '*') OR branchcode IS NULL) order by branchcode desc, categorycode desc, itemtype desc limit 1"; $issuingRulesRS = mysqli_query($this->dbConnection, $issuingRulesSql); if ($issuingRulesRS !== false) { if ($issuingRulesRow = $issuingRulesRS->fetch_assoc()) { @@ -3855,7 +3884,9 @@ function processEmailResetPinForm() : array { $error); } $result['error'] = trim($error); - } elseif (preg_match('%%s', $postResults, $messageInformation)) { + } + elseif (preg_match('%
\s*%s', $postResults, $messageInformation) || + preg_match('%%s', $postResults, $messageInformation)) { $message = $messageInformation[1]; $result['success'] = true; $result['message'] = translate([ @@ -3864,7 +3895,6 @@ function processEmailResetPinForm() : array { ]); } } - return $result; } @@ -4577,7 +4607,6 @@ function getSelfRegistrationFields($type = 'selfReg') { 'maxLength' => $pinValidationRules['maxLength'], 'onlyDigitsAllowed' => $pinValidationRules['onlyDigitsAllowed'], 'showConfirm' => false, - 'required' => true, 'showDescription' => true, 'autocomplete' => false, ], @@ -4590,7 +4619,6 @@ function getSelfRegistrationFields($type = 'selfReg') { 'maxLength' => $pinValidationRules['maxLength'], 'onlyDigitsAllowed' => $pinValidationRules['onlyDigitsAllowed'], 'showConfirm' => false, - 'required' => true, 'showDescription' => false, 'autocomplete' => false, ], @@ -4755,7 +4783,20 @@ function selfRegister(): array { global $library; $result = ['success' => false,]; - if (isset($_REQUEST['borrower_password'])) { + // Check if password is mandatory before attempting validation. + $this->initDatabaseConnection(); + $sql = "SELECT value FROM systempreferences WHERE variable = 'PatronSelfRegistrationBorrowerMandatoryField';"; + $results = mysqli_query($this->dbConnection, $sql); + $mandatoryFieldsValue = ''; + if ($curRow = $results->fetch_assoc()) { + $mandatoryFieldsValue = $curRow['value']; + } + $results->close(); + $requiredFields = explode('|', $mandatoryFieldsValue); + $requiredFields = array_flip($requiredFields); + $passwordIsRequired = array_key_exists('password', $requiredFields); + + if (isset($_REQUEST['borrower_password']) && $passwordIsRequired) { $password = $_REQUEST['borrower_password']; $pinValidationRules = $this->getPasswordPinValidationRules(); @@ -6478,6 +6519,35 @@ public function getMessagingSettingsTemplate(User $patron): ?string { } $borrowerRS->close(); + // Check if Account Expiry notice is mandatory for this patron's category (Koha 25.05+). + $mandatoryExpiryAttributeId = null; + if ($this->getKohaVersion() >= 25.05) { + $patronResponse = $this->kohaApiUserAgent->get("/api/v1/patrons/" . $patron->unique_ils_id, 'koha.getPatron'); + if ($patronResponse && isset($patronResponse['content']['category_id'])) { + $patronCategoryCode = $patronResponse['content']['category_id']; + $categoriesResponse = $this->kohaApiUserAgent->get("/api/v1/patron_categories", 'koha.getPatronCategories'); + if ($categoriesResponse && isset($categoriesResponse['content'])) { + $categoriesData = $categoriesResponse['content']; + if (is_array($categoriesData)) { + foreach ($categoriesData as $category) { + if (isset($category['patron_category_id']) && $category['patron_category_id'] === $patronCategoryCode) { + if (!empty($category['enforce_expiry_notice'])) { + /** @noinspection SqlResolve */ + $expiryAttributeSql = "SELECT message_attribute_id FROM message_attributes WHERE message_name = 'Patron_Expiry'"; + $expiryAttributeRS = mysqli_query($this->dbConnection, $expiryAttributeSql); + if ($expiryAttributeRow = $expiryAttributeRS->fetch_assoc()) { + $mandatoryExpiryAttributeId = $expiryAttributeRow['message_attribute_id']; + } + $expiryAttributeRS->close(); + } + break; + } + } + } + } + } + } + //Lookup which transports are allowed /** @noinspection SqlResolve */ $transportSettingSql = "SELECT message_attribute_id, MAX(is_digest) as allowDigests, message_transport_type FROM message_transports GROUP by message_attribute_id, message_transport_type"; @@ -6509,52 +6579,30 @@ public function getMessagingSettingsTemplate(User $patron): ?string { } $systemPreferencesRS->close(); - - + $messageAttributes = []; $messageAttributesSql = "SELECT * FROM message_attributes"; $messageAttributesRS = mysqli_query($this->dbConnection, $messageAttributesSql); while ($messageType = $messageAttributesRS->fetch_assoc()) { - switch ($messageType['message_name']) { - case "Item_Due": - $messageType['label'] = 'Item due'; - break; - case "Advance_Notice": - $messageType['label'] = 'Advance notice'; - break; - case "Hold_Filled": - $messageType['label'] = 'Hold filled'; - break; - case "Item_Check_in": - $messageType['label'] = 'Item check-in'; - break; - case "Item_Checkout": - $messageType['label'] = 'Item checkout'; - break; - case "Ill_ready": - $messageType['label'] = 'ILL ready'; - break; - case "Ill_unavailable": - $messageType['label'] = 'ILL unavailable'; - break; - case "Auto_Renewals": - $messageType['label'] = 'Auto Renewals'; - break; - case "Ill_update": - $messageType['label'] = 'ILL update'; - break; - case "Hold_Reminder": - $messageType['label'] = 'Hold Reminder'; - break; - default: - $messageType['label'] = $messageType['message_name']; - } + $messageType['label'] = match ($messageType['message_name']) { + "Item_Due" => 'Item Due', + "Advance_Notice" => 'Advance Notice', + "Hold_Filled" => 'Hold Filled', + "Item_Check_in" => 'Item Check-In', + "Item_Checkout" => 'Item Checkout', + "Ill_ready" => 'ILL Ready', + "Ill_unavailable" => 'ILL Unavailable', + "Auto_Renewals" => 'Auto Renewals', + "Ill_update" => 'ILL Update', + "Hold_Reminder" => 'Hold Reminder', + "Patron_Expiry" => 'Patron Expiry', + default => $messageType['message_name'], + }; $messageAttributes[] = $messageType; } $messageAttributesRS->close(); $activeMessagesAttributes = []; foreach($messageAttributes as $messageAttribute){ - # Check if the ILL Module is enabled and if the attribute's name starts with 'ill_. $isDisableILLModule = !$preferences['ILLModule'] && str_starts_with($messageAttribute['message_name'],"Ill_"); @@ -6565,16 +6613,17 @@ public function getMessagingSettingsTemplate(User $patron): ?string { # Check if AutoRenewalNotices preference is set according to patron messaging preferences. # Also checks if the attribute's name is "Auto_Renewals" - # Notify to the user about renewals. $isDisableAutoRenewal = $messageAttribute['message_name'] == "Auto_Renewals" && $preferences['AutoRenewalNotices'] != 'preferences'; # Check if patron use recalls and if the attribute's name starts with 'Recall_. $isDisableUseRecalls = !$preferences['UseRecalls'] && str_starts_with($messageAttribute['message_name'],"Recall_"); - if ($isDisableILLModule || $isDisableExpiryNotice || $isDisableAutoRenewal || $isDisableUseRecalls) { + $isMandatoryExpiryNotice = $messageAttribute['message_name'] == "Patron_Expiry" && $messageAttribute['message_attribute_id'] == $mandatoryExpiryAttributeId; + + if ($isDisableILLModule || $isDisableExpiryNotice || $isDisableAutoRenewal || $isDisableUseRecalls || $isMandatoryExpiryNotice) { continue; } - $activeMessagesAttributes [] = $messageAttribute; + $activeMessagesAttributes[] = $messageAttribute; } $interface->assign('messageAttributes', $activeMessagesAttributes); @@ -8272,6 +8321,10 @@ public function showTimesRenewed(): bool { return true; } + public function showRenewalsRemaining(): bool { + return true; + } + public function showHoldPlacedDate(): bool { return true; } diff --git a/code/web/Drivers/OverDriveDriver.php b/code/web/Drivers/OverDriveDriver.php index 8fcfa8297b..72c5201613 100644 --- a/code/web/Drivers/OverDriveDriver.php +++ b/code/web/Drivers/OverDriveDriver.php @@ -979,7 +979,7 @@ function placeHold($patron, $recordId, $pickupBranch = null, $cancelDate = null) return $holdResult; } - function freezeHold(User $patron, $overDriveId, $reactivationDate): array { + function freezeHold(User $patron, $overDriveId): array { //Figure out which collection the title is on hold in. if (str_contains($overDriveId, '_')){ list ($overDriveId, $settingId) = explode('_', $overDriveId); @@ -992,24 +992,6 @@ function freezeHold(User $patron, $overDriveId, $reactivationDate): array { $params = [ 'emailAddress' => trim($patron->overdriveEmail), ]; - $params['suspensionType'] = 'limited'; - if (empty($reactivationDate)) { - $params['numberOfDays'] = 365; - } else { - try { - $numberOfDaysToSuspend = (new DateTime())->diff(new DateTime($reactivationDate))->days + 1; - if ($numberOfDaysToSuspend > 365) { - $numberOfDaysToSuspend = 365; - } - $params['numberOfDays'] = $numberOfDaysToSuspend; - } /** @noinspection PhpUnusedLocalVariableInspection */ catch (Exception $e) { - return [ - 'success' => false, - 'message' => 'Unable to determine reactivation date', - ]; - } - - } $response = $this->_callPatronUrl($settings, $patron, $url, "freezeHold", $params); $holdResult = []; @@ -1019,7 +1001,7 @@ function freezeHold(User $patron, $overDriveId, $reactivationDate): array { $this->incrementStat('numHoldsFrozen'); $holdResult['success'] = true; $holdResult['message'] = translate([ - 'text' => 'Your hold was frozen successfully.', + 'text' => 'Your hold was frozen successfully. Your hold will be frozen until you thaw the holds. Holds frozen for over a year will be cancelled.', 'isPublicFacing' => true, ]); @@ -1029,7 +1011,7 @@ function freezeHold(User $patron, $overDriveId, $reactivationDate): array { 'isPublicFacing' => true, ]); $holdResult['api']['message'] = translate([ - 'text' => 'Your hold was frozen successfully.', + 'text' => 'Your hold was frozen successfully. Your hold will be frozen until you thaw the holds. Holds frozen for over a year will be cancelled.', 'isPublicFacing' => true, ]); diff --git a/code/web/Drivers/PalaceProjectDriver.php b/code/web/Drivers/PalaceProjectDriver.php index dbb721936a..4819f5c14e 100644 --- a/code/web/Drivers/PalaceProjectDriver.php +++ b/code/web/Drivers/PalaceProjectDriver.php @@ -99,6 +99,7 @@ public function loadCirculationInformation(User $patron) { $checkout->userId = $patron->id; $checkout->sourceId = $publication->metadata->identifier; $checkout->recordId = $publication->metadata->identifier; + $checkout->canRenew = false; $palaceProjectTitle = new PalaceProjectTitle(); $palaceProjectTitle->palaceProjectId = $publication->metadata->identifier; if ($palaceProjectTitle->find(true)) { @@ -427,7 +428,7 @@ function placeHold($patron, $recordId, $pickupBranch = null, $cancelDate = null) $this->incrementStat('numHoldsPlaced'); $this->trackUserUsageOfPalaceProject($patron); - $this->trackRecordCheckout($recordId); + $this->trackRecordHold($recordId); $patron->lastReadingHistoryUpdate = 0; $patron->update(); @@ -542,7 +543,7 @@ function cancelHold($patron, $recordId, $cancelId = null, $isIll = false): array } if (!$cancelWorked) { $result['message'] = translate([ - 'text' => "Could not cancel Palace Project hold, " . (string)$status->statusMessage, + 'text' => 'Could not cancel Palace Project hold.', 'isPublicFacing' => true, ]); @@ -552,7 +553,7 @@ function cancelHold($patron, $recordId, $cancelId = null, $isIll = false): array 'isPublicFacing' => true, ]); $result['api']['message'] = translate([ - 'text' => 'Could not cancel Palace Project hold, ' . (string)$status->statusMessage, + 'text' => 'Could not cancel Palace Project hold.', 'isPublicFacing' => true, ]); @@ -803,16 +804,20 @@ public function trackUserUsageOfPalaceProject($user): void { } /** - * @param string $recordId + * @param string|int $recordId */ - function trackRecordCheckout($recordId): void { - require_once ROOT_DIR . '/sys/PalaceProject/PalaceProjectRecordUsage.php'; + function trackRecordCheckout(string|int $recordId): void { require_once ROOT_DIR . '/sys/PalaceProject/PalaceProjectTitle.php'; - $recordUsage = new PalaceProjectRecordUsage(); $product = new PalaceProjectTitle(); - $product->palaceProjectId = $recordId; + if (is_numeric($recordId)) { + $product->id = $recordId; + } else { + $product->palaceProjectId = $recordId; + } if ($product->find(true)) { - $recordUsage->palaceProjectId = $product->palaceProjectId; + require_once ROOT_DIR . '/sys/PalaceProject/PalaceProjectRecordUsage.php'; + $recordUsage = new PalaceProjectRecordUsage(); + $recordUsage->palaceProjectId = $product->id; global $aspenUsage; $recordUsage->instance = $aspenUsage->getInstance(); $recordUsage->year = date('Y'); @@ -829,18 +834,22 @@ function trackRecordCheckout($recordId): void { } /** - * @param string $recordId + * @param string|int $recordId */ - function trackRecordHold($recordId): void { - require_once ROOT_DIR . '/sys/PalaceProject/PalaceProjectRecordUsage.php'; + function trackRecordHold(string|int $recordId): void { require_once ROOT_DIR . '/sys/PalaceProject/PalaceProjectTitle.php'; - $recordUsage = new PalaceProjectRecordUsage(); $product = new PalaceProjectTitle(); - $product->palaceProjectId = $recordId; + if (is_numeric($recordId)) { + $product->id = $recordId; + } else { + $product->palaceProjectId = $recordId; + } if ($product->find(true)) { + require_once ROOT_DIR . '/sys/PalaceProject/PalaceProjectRecordUsage.php'; + $recordUsage = new PalaceProjectRecordUsage(); global $aspenUsage; $recordUsage->instance = $aspenUsage->getInstance(); - $recordUsage->palaceProjectId = $product->palaceProjectId; + $recordUsage->palaceProjectId = $product->id; $recordUsage->year = date('Y'); $recordUsage->month = date('n'); if ($recordUsage->find(true)) { diff --git a/code/web/Drivers/Polaris.php b/code/web/Drivers/Polaris.php index 58e5b43be3..647dd5b282 100644 --- a/code/web/Drivers/Polaris.php +++ b/code/web/Drivers/Polaris.php @@ -3154,6 +3154,10 @@ public function showTimesRenewed(): bool { return true; } + public function showRenewalsRemaining(): bool { + return true; + } + private function getCarrierList(): array { $staffUserInfo = $this->getStaffUserInfo(); $polarisUrl = "/PAPIService/REST/protected/v1/1033/100/1/{$staffUserInfo['accessToken']}/sysadmin/mobilephonecarriers"; diff --git a/code/web/Drivers/Sierra.php b/code/web/Drivers/Sierra.php index 014b7989c7..e093f48529 100644 --- a/code/web/Drivers/Sierra.php +++ b/code/web/Drivers/Sierra.php @@ -509,6 +509,7 @@ public function getValidPickupLocationsForRecordFromILS($recordId, $patron): arr return [ 'success' => false, 'message' => 'Missing record or patron; unable to retrieve valid pickup locations', + 'useDefaultLocationFiltering' => true, ]; } $patronId = $patron->unique_ils_id; @@ -535,10 +536,11 @@ public function getValidPickupLocationsForRecordFromILS($recordId, $patron): arr } else { $message = 'Unable to retrieve valid pickup locations from Sierra. '; $message .= $pickupLocationsResponse->name ?? ''; - $message .= $pickupLocationsResponse->description ? ': ' . $pickupLocationsResponse->description : ''; + $message .= !empty($pickupLocationsResponse->description) ? ': ' . $pickupLocationsResponse->description : ''; return [ 'success' => false, 'message' => $message, + 'useDefaultLocationFiltering' => true, ]; } } @@ -1400,6 +1402,9 @@ public function getPatronInfoByBarcode($barcode) { public function getPatronInfoByUsername($username) { global $library; + if (empty($library->usernameField)) { + return false; + } $params = [ 'varFieldTag' => $library->usernameField, 'varFieldContent' => $username, @@ -1413,6 +1418,8 @@ public function getPatronInfoByUsername($username) { $response = $this->_callUrl('sierra.findPatronByBarcode', $sierraUrl); if (!$response) { return false; + } else if (isset($response->httpStatus) && ($response->httpStatus != 200)) { + return false; } else { if ($response->deleted || $response->suppressed) { return false; @@ -1698,7 +1705,7 @@ public function updatePatronInfo($patron, $canUpdateContactInfo, $fromMasquerade if (isset($_REQUEST['phone'])) { $patron->phone = $_REQUEST['phone']; $tmpPhone = new stdClass(); - $tmpPhone->type = 'p'; + $tmpPhone->type = 't'; $tmpPhone->number = $_REQUEST['phone']; $params['phones'][] = $tmpPhone; } @@ -1710,7 +1717,7 @@ public function updatePatronInfo($patron, $canUpdateContactInfo, $fromMasquerade if (isset($_REQUEST['workPhone'])) { $patron->_workPhone = $_REQUEST['workPhone']; $tmpPhone = new stdClass(); - $tmpPhone->type = 't'; + $tmpPhone->type = 'p'; $tmpPhone->number = $_REQUEST['workPhone']; $params['phones'][] = $tmpPhone; } @@ -2120,7 +2127,7 @@ public function selfRegister(): array { if ($selfRegistrationForm->selfRegUseAgency) { $params['fixedFields']['158'] = [ 'label' => 'Patron Agency', - 'value' => $selfRegistrationForm->selfRegAgency + 'value' => (string)$selfRegistrationForm->selfRegAgency ]; } if ($selfRegistrationForm->addSelfRegNote) { @@ -3681,7 +3688,9 @@ public function updateAccountNotifications(User $user, ILSNotificationSetting $i $datetime21DaysAgo = new DateTime(); date_sub($datetime21DaysAgo, new DateInterval('P21D')); $loadHoldReadyForPickup = $user->canReceiveILSNotification('hold_ready'); + $cronLogEntry->notes .= "  - Checking Holds Ready For Pickup? $loadHoldReadyForPickup
"; $loadHoldExpiresSoon = $user->canReceiveILSNotification('hold_expire'); + $cronLogEntry->notes .= "  - Checking Holds Expire Soon? $loadHoldExpiresSoon
"; $numMessagesAdded = 0; if ($loadHoldReadyForPickup || $loadHoldExpiresSoon) { //Look for holds for the patron that have been put on the hold shelf in the last 24 hours @@ -3701,23 +3710,32 @@ public function updateAccountNotifications(User $user, ILSNotificationSetting $i $existingMessage->messageId = $curRow['id']; $onHoldshelfTime = strtotime($curRow['on_holdshelf_gmt']); $expireHoldshelfTime = strtotime($curRow['expire_holdshelf_gmt']); - if ($onHoldshelfTime > $dateTime24HoursFromNow->getTimestamp()) { + $cronLogEntry->notes .= "    - Processing hold with onHoldshelfTime of $onHoldshelfTime and expireHoldshelfTime of $expireHoldshelfTime.
"; + if ($onHoldshelfTime > $datetime24HoursAgo->getTimestamp()) { + //We will show that a hold is on the holdshelf if it was moved to the hold shelf in the last 24 hours. if ($loadHoldReadyForPickup) { - $numMessagesAdded += $this->createIlsMessage($user, 'hold_ready', $ilsNotificationSetting, $existingMessage); + $numMessagesAdded += $this->createIlsMessage($user, 'hold_ready', $ilsNotificationSetting, $existingMessage, $cronLogEntry); } - }elseif ($expireHoldshelfTime >= $datetimeNow->getTimestamp() && $expireHoldshelfTime <= $dateTime24HoursFromNow) { + } + if ($expireHoldshelfTime >= $datetimeNow->getTimestamp() && $expireHoldshelfTime <= $dateTime24HoursFromNow) { + //We will show that a hold expires soon if it will expire in the next 24 hours. if ($loadHoldExpiresSoon) { - $numMessagesAdded += $this->createIlsMessage($user, 'hold_expire', $ilsNotificationSetting, $existingMessage); + $numMessagesAdded += $this->createIlsMessage($user, 'hold_expire', $ilsNotificationSetting, $existingMessage, $cronLogEntry); } } } } } $loadCheckoutDueSoon = $user->canReceiveILSNotification('checkout_due_soon'); + $cronLogEntry->notes .= "  - Checking Checkouts Due Soon? $loadHoldReadyForPickup
"; $loadOverdue1 = $user->canReceiveILSNotification('overdue_1'); + $cronLogEntry->notes .= "  - Checking Overdue 1? $loadOverdue1
"; $loadOverdue7 = $user->canReceiveILSNotification('overdue_7'); + $cronLogEntry->notes .= "  - Checking Overdue 7? $loadOverdue7
"; $loadOverdue14 = $user->canReceiveILSNotification('overdue_14'); + $cronLogEntry->notes .= "  - Checking Overdue 14? $loadOverdue14
"; $loadBilled = $user->canReceiveILSNotification('billed'); + $cronLogEntry->notes .= "  - Checking Billed? $loadBilled
"; if ($loadCheckoutDueSoon || $loadOverdue1 || $loadOverdue7 || $loadOverdue14 || $loadBilled) { //Load checkouts for the patron $getCheckoutsNeedingNoticesStmt = "select sierra_view.checkout.*, record_num as patron_record_num from sierra_view.checkout inner join sierra_view.record_metadata on patron_record_id = sierra_view.record_metadata.id where due_gmt < $1 AND record_num = $2"; @@ -3734,25 +3752,26 @@ public function updateAccountNotifications(User $user, ILSNotificationSetting $i //For Sierra, we will use the message id as the hold or checkout $existingMessage->messageId = $curRow['id']; $dueDateTime = strtotime($curRow['due_gmt']); + $cronLogEntry->notes .= "    - Processing checkout with dueDateTime of $dueDateTime.
"; if ($dueDateTime <= $datetime21DaysAgo->getTimestamp()) { if ($loadBilled) { - $numMessagesAdded += $this->createIlsMessage($user, 'billed', $ilsNotificationSetting, $existingMessage); + $numMessagesAdded += $this->createIlsMessage($user, 'billed', $ilsNotificationSetting, $existingMessage, $cronLogEntry); } }elseif ($dueDateTime <= $datetime14DaysAgo->getTimestamp()) { if ($loadOverdue14) { - $numMessagesAdded += $this->createIlsMessage($user, 'overdue_14', $ilsNotificationSetting, $existingMessage); + $numMessagesAdded += $this->createIlsMessage($user, 'overdue_14', $ilsNotificationSetting, $existingMessage, $cronLogEntry); } }elseif ($dueDateTime <= $datetime7DaysAgo->getTimestamp()) { if ($loadOverdue7) { - $numMessagesAdded += $this->createIlsMessage($user, 'overdue_7', $ilsNotificationSetting, $existingMessage); + $numMessagesAdded += $this->createIlsMessage($user, 'overdue_7', $ilsNotificationSetting, $existingMessage, $cronLogEntry); } }elseif ($dueDateTime <= $datetime24HoursAgo->getTimestamp()) { if ($loadOverdue1) { - $numMessagesAdded += $this->createIlsMessage($user, 'overdue_1', $ilsNotificationSetting, $existingMessage); + $numMessagesAdded += $this->createIlsMessage($user, 'overdue_1', $ilsNotificationSetting, $existingMessage, $cronLogEntry); } }elseif ($dueDateTime <= $dateTime3DaysFromNow->getTimestamp()) { if ($loadCheckoutDueSoon) { - $numMessagesAdded += $this->createIlsMessage($user, 'checkout_due_soon', $ilsNotificationSetting, $existingMessage); + $numMessagesAdded += $this->createIlsMessage($user, 'checkout_due_soon', $ilsNotificationSetting, $existingMessage, $cronLogEntry); } } } @@ -3819,11 +3838,11 @@ public function getMessageTypes(): array { * * @param User $user * @param string $messageCode - * @param ILSMessageType|null $ilsMessageType * @param UserILSMessage $existingMessage + * @param ?CronLogEntry $cronLogEntry * @return int */ - private function createIlsMessage(User $user, string $messageCode, ILSNotificationSetting $ilsNotificationSetting, UserILSMessage $existingMessage) : int { + private function createIlsMessage(User $user, string $messageCode, ILSNotificationSetting $ilsNotificationSetting, UserILSMessage $existingMessage, ?CronLogEntry $cronLogEntry) : int { $existingMessage->type = $messageCode; if (!$existingMessage->find(true)) { $ilsMessageType = $ilsNotificationSetting->getMessageTypeByCode($messageCode); @@ -3833,9 +3852,14 @@ private function createIlsMessage(User $user, string $messageCode, ILSNotificati $existingMessage->content = $ilsMessageType->getTextBlockTranslation('messageBody', $user->interfaceLanguage, true); $existingMessage->dateQueued = time(); if ($existingMessage->insert()) { + $cronLogEntry->notes .= "      - ILS Message was created.
"; return 1; + }else{ + $cronLogEntry->notes .= "      - Inserting the message failed.
"; } } + }else{ + $cronLogEntry->notes .= "      - ILS Message has already been created.
"; } return 0; } diff --git a/code/web/Drivers/SirsiDynixROA.php b/code/web/Drivers/SirsiDynixROA.php index f6435a8485..7531063b7c 100644 --- a/code/web/Drivers/SirsiDynixROA.php +++ b/code/web/Drivers/SirsiDynixROA.php @@ -1239,6 +1239,21 @@ public function getHolds($patron): array { $curHold->pickupLocationName = $curPickupBranch->code; } + //Check to see if the hold is coming from in or out of the libary's hold group + if (empty($holdGroups)) { + $inHoldGroup = true; + }else { + $inHoldGroup = false; + foreach ($holdGroups as $holdGroup) { + if (isset($hold->fields->selectedItem)) { + if (in_array($hold->fields->selectedItem->fields->currentLibrary->key, $holdGroup->getLocationCodes())) { + $inHoldGroup = true; + break; + } + } + } + } + $curHold->status = ucfirst(strtolower($hold->fields->status)); $isLocalIllHold = false; $currentLocation = ''; @@ -1249,6 +1264,12 @@ public function getHolds($patron): array { if (in_array(strtoupper($currentLocation), ['ILL', 'ILLSHIPPED', 'ILLPENDING', 'ILL_WYLD'])) { $isLocalIllHold = true; $curHold->status = str_replace('_', ' ', $currentLocation); + }elseif ($currentLocation == 'INTRANSIT') { + //treat the hold as local ILL if the item was shipped from outside the hold group + if (!$inHoldGroup) { + $isLocalIllHold = true; + $curHold->status = str_replace('_', ' ', $currentLocation); + } } } if ($isLocalIllHold){ @@ -1270,6 +1291,9 @@ public function getHolds($patron): array { if (in_array(strtoupper($curHold->status), ['TRANSIT', 'EXPIRED', 'INSHIPPING', 'ILL WYLD', 'ILLPENDING', 'ILLSHIPPED'])) { $curHold->locationUpdateable = false; $curHold->canFreeze = false; + }else if ($isLocalIllHold && !$inHoldGroup && strtoupper($curHold->status) == 'INTRANSIT') { + $curHold->locationUpdateable = false; + $curHold->canFreeze = false; } if (isset($hold->fields->selectedItem->fields->call->fields->volumetric)) { $curHold->volume = $hold->fields->selectedItem->fields->call->fields->volumetric; @@ -1314,18 +1338,6 @@ public function getHolds($patron): array { if (!$library->allowCancellingAvailableHolds) { $curHold->cancelable = false; } - //Do not allow holds that are outside the active hold group from being canceled. - if (empty($holdGroups)) { - $inHoldGroup = true; - }else { - $inHoldGroup = false; - foreach ($holdGroups as $holdGroup) { - if (in_array($hold->fields->selectedItem->fields->currentLibrary->key, $holdGroup->getLocationCodes())) { - $inHoldGroup = true; - break; - } - } - } if (!$inHoldGroup) { $curHold->cancelable = false; } @@ -1388,13 +1400,21 @@ function placeItemHold($patron, $recordId, $itemId, $pickupBranch = null, $type * @access public */ function placeSirsiHold(User $patron, string $recordId, ?string $itemId, ?string $volume = null, ?string $pickupBranch = null, string $type = 'request', ?string $cancelIfNotFilledByDate = null, bool $forceVolumeHold = false, bool $useBooksByMail = false) : array { - //Get the session token for the user $staffSessionToken = $this->getStaffSessionToken(); $sessionToken = $this->getSessionToken($patron); - if (!$staffSessionToken) { + + // If the patron's session token is unavailable, check if staff can place holds on their behalf. + // This handles the Materials Requests case where staff is NOT masquerading. + if (empty($sessionToken) && !empty($staffSessionToken)) { + if (!UserAccount::isUserMasquerading() && UserAccount::userHasPermission('Place Holds For Materials Requests')) { + $sessionToken = $staffSessionToken; + } + } + + if (empty($sessionToken)) { $result['success'] = false; $result['message'] = translate([ - 'text' => "Sorry, it does not look like you are logged in currently. Please login and try again", + 'text' => "Sorry, it does not look like you are logged in currently. Please log in and try again.", 'isPublicFacing' => true, ]); @@ -1403,7 +1423,7 @@ function placeSirsiHold(User $patron, string $recordId, ?string $itemId, ?string 'isPublicFacing' => true, ]); $result['api']['message'] = translate([ - 'text' => 'Sorry, it does not look like you are logged in currently. Please login and try again', + 'text' => 'Sorry, it does not look like you are logged in currently. Please log in and try again.', 'isPublicFacing' => true, ]); return $result; @@ -2636,24 +2656,66 @@ function updatePatronInfo($patron, $canUpdateContactInfo, $fromMasquerade): arra return $result; } - public function showOutstandingFines() { + public function showOutstandingFines(): bool { return true; } - function getForgotPasswordType() { + function getForgotPasswordType(): string { return 'emailResetLink'; } - function getEmailResetPinTemplate() { + function getEmailResetPinTemplate(): string { return 'sirsiROAEmailResetPinLink.tpl'; } - function translateFineMessageType($code) { - switch ($code) { + function updateHomeLibrary(User $patron, string $homeLibraryCode): array { + $result = [ + 'success' => false, + 'messages' => [], + ]; + + $sessionToken = $this->getStaffSessionToken(); + if ($sessionToken) { + $webServiceURL = $this->getWebServiceURL(); + $userID = $patron->unique_ils_id; + if (!empty($userID)) { + $updatePatronInfoParametersClass = $this->getWebServiceResponse('getPatronInfo', $this->getWebServiceURL() . '/user/patron/key/' . $userID, null, $sessionToken); + if ($updatePatronInfoParametersClass) { + $updatePatronInfoParameters = json_decode(json_encode($updatePatronInfoParametersClass), true); + if (isset($updatePatronInfoParameters['resource']) && $updatePatronInfoParameters['resource'] == '/user/patron') { + $homeLibraryLocation = new Location(); + if ($homeLibraryLocation->get('code', $homeLibraryCode)) { + $homeBranchCode = strtoupper($homeLibraryLocation->code); + $updatePatronInfoParameters['fields']['library'] = [ + 'key' => $homeBranchCode, + 'resource' => '/policy/library', + ]; + } + + $updateAccountInfoResponse = $this->getWebServiceResponse('updateHomeLibrary', $webServiceURL . '/user/patron/key/' . $userID, $updatePatronInfoParameters, $sessionToken, 'PUT'); + if (isset($updateAccountInfoResponse->messageList)) { + $result['messages'][] = (string)$updateAccountInfoResponse->messageList[0]->message; + } else { + $result['success'] = true; + $result['messages'][] = '• Your home library was updated successfully.'; - default: - return $code; + $patron->homeLocationId = $homeLibraryLocation->locationId; + $patron->update(); + } + } else { + $result['messages'][] = 'Could not find your account in the system, please contact the library.'; + } + } else { + $result['messages'][] = 'Could not find your account in the system, please contact the library.'; + } + } else { + $result['messages'][] = 'Could not find your account in the system, please contact the library.'; + } + } else { + $result['messages'][] = 'Sorry, we could not connect to Symphony.'; } + + return $result; } public function translateLocation($locationCode) { diff --git a/code/web/RecordDrivers/Axis360RecordDriver.php b/code/web/RecordDrivers/Axis360RecordDriver.php index c6493c8dd7..b3c08b74c2 100644 --- a/code/web/RecordDrivers/Axis360RecordDriver.php +++ b/code/web/RecordDrivers/Axis360RecordDriver.php @@ -207,7 +207,7 @@ public function getISSNs() { return []; } - protected $_actions = null; + protected ?array $_actions = null; public function getRecordActions($relatedRecord, $variationId, $isAvailable, $isHoldable, $volumeData = null) : array { if ($this->_actions === null) { @@ -216,7 +216,7 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is $loadDefaultActions = true; if (UserAccount::isLoggedIn()) { $user = UserAccount::getActiveUserObj(); - $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActions('axis360', $this->id)); + $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActionsWithLazyLoading('axis360', $this->id)); $loadDefaultActions = count($this->_actions) == 0; } @@ -224,26 +224,44 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is global $offlineMode; global $loginAllowedWhileOffline; if ($loadDefaultActions && (!$offlineMode || $loginAllowedWhileOffline)) { + $needsLazyLoading = false; + if (UserAccount::isLoggedIn()) { + $user = UserAccount::getActiveUserObj(); + if (!$user->areCirculationActionsDisabled()) $needsLazyLoading = !$user->isCirculationCacheFresh(); + } + if ($isAvailable) { - $this->_actions[] = [ + $checkoutAction = [ 'title' => translate([ 'text' => 'Check Out Boundless', 'isPublicFacing' => true, ]), - 'onclick' => "return AspenDiscovery.Axis360.checkOutTitle('{$this->id}');", + 'onclick' => "return AspenDiscovery.Axis360.checkOutTitle('{$this->id}', this);", 'requireLogin' => false, 'type' => 'axis360_checkout', ]; + if ($needsLazyLoading) { + $checkoutAction['data-needs-refresh'] = 'true'; + $checkoutAction['data-record-id'] = $this->id; + $checkoutAction['data-record-source'] = 'axis360'; + } + $this->_actions[] = $checkoutAction; } else { - $this->_actions[] = [ + $holdAction = [ 'title' => translate([ 'text' => 'Place Hold Boundless', 'isPublicFacing' => true, ]), - 'onclick' => "return AspenDiscovery.Axis360.placeHold('{$this->id}');", + 'onclick' => "return AspenDiscovery.Axis360.placeHold('{$this->id}', this);", 'requireLogin' => false, 'type' => 'axis360_hold', ]; + if ($needsLazyLoading) { + $holdAction['data-needs-refresh'] = 'true'; + $holdAction['data-record-id'] = $this->id; + $holdAction['data-record-source'] = 'axis360'; + } + $this->_actions[] = $holdAction; } } } @@ -309,7 +327,7 @@ function getFormats() { * * @return array */ - function getFormatCategory() { + function getFormatCategory() : string|array|null { if ($this->axis360RawMetadata->formatType == 'eBook') { return ['eBook']; } elseif ($this->axis360RawMetadata->formatType == 'eAudiobook') { diff --git a/code/web/RecordDrivers/BaseEContentDriver.php b/code/web/RecordDrivers/BaseEContentDriver.php index 7e64ff5345..a4f1086fb0 100644 --- a/code/web/RecordDrivers/BaseEContentDriver.php +++ b/code/web/RecordDrivers/BaseEContentDriver.php @@ -90,6 +90,7 @@ function createActionsFromUrls($relatedUrls, $itemInfo = null, $variationId = 'a } } $actions[] = [ + 'type' => 'access_online', 'url' => $action, 'redirectUrl' => $fileOrUrl, 'title' => $title, @@ -118,6 +119,7 @@ function createActionsFromUrls($relatedUrls, $itemInfo = null, $variationId = 'a } } $actions[] = [ + 'type' => 'access_online', 'url' => $action, 'redirectUrl' => $fileOrUrl, 'title' => $title, diff --git a/code/web/RecordDrivers/CloudLibraryRecordDriver.php b/code/web/RecordDrivers/CloudLibraryRecordDriver.php index 3c2e538544..e856ad77c4 100644 --- a/code/web/RecordDrivers/CloudLibraryRecordDriver.php +++ b/code/web/RecordDrivers/CloudLibraryRecordDriver.php @@ -178,7 +178,7 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is $loadDefaultActions = true; if (UserAccount::isLoggedIn()) { $user = UserAccount::getActiveUserObj(); - $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActions('cloud_library', $this->id)); + $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActionsWithLazyLoading('cloud_library', $this->id)); $loadDefaultActions = count($this->_actions) == 0; } @@ -186,30 +186,48 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is global $offlineMode; global $loginAllowedWhileOffline; if ($loadDefaultActions && (!$offlineMode || $loginAllowedWhileOffline)) { + $needsLazyLoading = false; + if (UserAccount::isLoggedIn()) { + $user = UserAccount::getActiveUserObj(); + if (!$user->areCirculationActionsDisabled()) $needsLazyLoading = !$user->isCirculationCacheFresh(); + } + if ($isAvailable) { $userId = UserAccount::getActiveUserId(); - if ($userId == false) { + if (!$userId) { $userId = 'null'; } - $this->_actions[] = [ + $checkoutAction = [ 'title' => translate([ 'text' => 'Check Out cloudLibrary', 'isPublicFacing' => true, ]), - 'onclick' => "return AspenDiscovery.CloudLibrary.checkOutTitle({$userId}, '{$this->id}');", + 'onclick' => "return AspenDiscovery.CloudLibrary.checkOutTitle({$userId}, '{$this->id}', this);", 'requireLogin' => false, 'type' => 'cloud_library_checkout', ]; + if ($needsLazyLoading) { + $checkoutAction['data-needs-refresh'] = 'true'; + $checkoutAction['data-record-id'] = $this->id; + $checkoutAction['data-record-source'] = 'cloud_library'; + } + $this->_actions[] = $checkoutAction; } else { - $this->_actions[] = [ + $holdAction = [ 'title' => translate([ 'text' => 'Place Hold cloudLibrary', 'isPublicFacing' => true, ]), - 'onclick' => "return AspenDiscovery.CloudLibrary.placeHold('{$this->id}');", + 'onclick' => "return AspenDiscovery.CloudLibrary.placeHold('{$this->id}', this);", 'requireLogin' => false, 'type' => 'cloud_library_hold', ]; + if ($needsLazyLoading) { + $holdAction['data-needs-refresh'] = 'true'; + $holdAction['data-record-id'] = $this->id; + $holdAction['data-record-source'] = 'cloud_library'; + } + $this->_actions[] = $holdAction; } } } @@ -232,7 +250,7 @@ function getFormats() { * * @return array */ - function getFormatCategory() { + function getFormatCategory() : string|array|null { if ($this->cloudLibraryProduct) { if ($this->cloudLibraryProduct->format == "eAudio") { return [ diff --git a/code/web/RecordDrivers/GroupedWorkDriver.php b/code/web/RecordDrivers/GroupedWorkDriver.php index b4fd56a2f7..85fa731f6a 100644 --- a/code/web/RecordDrivers/GroupedWorkDriver.php +++ b/code/web/RecordDrivers/GroupedWorkDriver.php @@ -92,7 +92,7 @@ public function assignBasicTitleDetails() { $summPublisher = null; $summPubDate = null; - $summPlaceOfPublication = null; + $summPlaceOfPublication = null; $summPhysicalDesc = null; $summEdition = null; $summAudience = null; @@ -411,37 +411,40 @@ function compareRelatedRecords($a, $b) { } } - private ?GroupedWorkFormatSortingGroup $_formatSorting = null; + private static ?GroupedWorkFormatSortingGroup $_formatSorting = null; + /** - * @param Grouping_Record $a - * @param Grouping_Record $b + * @param Grouping_Manifestation $a + * @param Grouping_Manifestation $b * @return int */ - function compareRelatedManifestations($a, $b) { - if ($this->_formatSorting == null) { + function compareRelatedManifestations(Grouping_Manifestation $a, Grouping_Manifestation $b): int { + if (self::$_formatSorting == null) { global $library; $groupedWorkDisplaySettings = $library->getGroupedWorkDisplaySettings(); - $this->_formatSorting = $groupedWorkDisplaySettings->getFormatSortingGroup(); + self::$_formatSorting = $groupedWorkDisplaySettings->getFormatSortingGroup(); } + //Format sorting can still be null before the format sorting is fully setup - if ($this->_formatSorting == null) { + if (self::$_formatSorting == null) { $sortMethod = 1; - }else{ + } else { $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; + $sortMethod = self::$_formatSorting->bookSortMethod; + } elseif ($groupedWork->grouping_category == 'comic') { + $sortMethod = self::$_formatSorting->comicSortMethod; + } elseif ($groupedWork->grouping_category == 'movie') { + $sortMethod = self::$_formatSorting->movieSortMethod; + } elseif ($groupedWork->grouping_category == 'music') { + $sortMethod = self::$_formatSorting->musicSortMethod; + } else { + $sortMethod = self::$_formatSorting->otherSortMethod; } } + $formatComparison = 0; if ($sortMethod == 1) { //First sort by format $format1 = trim($a->format); @@ -455,24 +458,24 @@ function compareRelatedManifestations($a, $b) { return 1; } } - }else{ + } else { $weight1 = 999; $weight2 = 999; $format1 = trim($a->format); $format2 = trim($b->format); - $sortFormats = $this->_formatSorting->getSortedFormats($groupedWork->grouping_category); + $sortFormats = self::$_formatSorting->getSortedFormats($groupedWork->grouping_category); foreach ($sortFormats as $format) { if ($format->format == $format1) { $weight1 = $format->weight; - }elseif ($format->format == $format2) { + } elseif ($format->format == $format2) { $weight2 = $format->weight; } } - if ($weight1 < $weight2){ + if ($weight1 < $weight2) { $formatComparison = -1; - }elseif ($weight1 == $weight2){ + } elseif ($weight1 == $weight2) { $format1 = trim($a->format); $format2 = trim($b->format); $formatComparison = strcasecmp($format1, $format2); @@ -484,7 +487,7 @@ function compareRelatedManifestations($a, $b) { $formatComparison = 1; } } - }elseif ($weight1 > $weight2){ + } elseif ($weight1 > $weight2) { $formatComparison = 1; } } @@ -591,11 +594,11 @@ public function getBrowseResult() { $interface->assign('summUrl', $url); - $shortTitle = $this->getShortTitle(); - if (empty($shortTitle)) { - $interface->assign('summTitle', $this->getTitle()); + $title = $this->getTitle(); + if (!empty($title)) { + $interface->assign('summTitle', $title); $interface->assign('summSubTitle', ''); - $interface->assign('summFullTitle', $this->getTitle()); + $interface->assign('summFullTitle', $title); } else { $interface->assign('summTitle', $this->getShortTitle()); $interface->assign('summSubTitle', $this->getSubtitle()); @@ -606,6 +609,11 @@ public function getBrowseResult() { //Get Rating $interface->assign('ratingData', $this->getRatingData()); + // Get user + $user = UserAccount::getLoggedInUser(); + $noPromptForUserReviews = $user ? $user->noPromptForUserReviews : false; + $interface->assign('noPromptForUserReviews', $noPromptForUserReviews); + //Get cover image size global $interface; $appliedTheme = $interface->getAppliedTheme(); @@ -614,7 +622,7 @@ public function getBrowseResult() { $accessibleBrowseCategories = 0; if ($appliedTheme != null) { - if($appliedTheme->browseCategoryImageSize == 1) { + if ($appliedTheme->browseCategoryImageSize == 1) { $interface->assign('bookCoverUrlMedium', $this->getBookcoverUrl('large')); } else { $interface->assign('bookCoverUrlMedium', $this->getBookcoverUrl('medium')); @@ -818,9 +826,9 @@ public function getCombinedResult($view = 'list') { } $interface->assign('summUrl', $linkUrl); - $shortTitle = $this->getShortTitle(); - if (empty($shortTitle)) { - $interface->assign('summTitle', $this->getTitle()); + $title = $this->getTitle(); + if (!empty($title)) { + $interface->assign('summTitle', $title); $interface->assign('summSubTitle', ''); } else { $interface->assign('summTitle', $this->getShortTitle()); @@ -850,6 +858,7 @@ public function getCombinedResult($view = 'list') { $groupedWorkDisplaySettings = $library->getGroupedWorkDisplaySettings(); $alwaysShowMainDetails = $groupedWorkDisplaySettings->alwaysShowSearchResultsMainDetails; $interface->assign('formatDisplayStyle', $groupedWorkDisplaySettings->formatDisplayStyle); + $interface->assign('hideManifestationsInMobileView', $groupedWorkDisplaySettings->hideManifestationsInMobileView); foreach ($relatedRecords as $relatedRecord) { if ($isFirst) { @@ -872,8 +881,8 @@ public function getCombinedResult($view = 'list') { 'isPublicFacing' => true, ]) : null; } - if($summPlaceOfPublication != $relatedRecord->placeOfPublication) { - $summPlaceOfPublication= $alwaysShowMainDetails ? translate([ + if ($summPlaceOfPublication != $relatedRecord->placeOfPublication) { + $summPlaceOfPublication = $alwaysShowMainDetails ? translate([ 'text' => 'Varies, see individual formats and editions', 'isPublicFacing' => true, ]) : null; @@ -942,29 +951,52 @@ public function getCombinedResult($view = 'list') { public function getContributors() { if (!empty($this->fields['author2-role'])) { return $this->fields['author2-role']; //Include the role when displaying contributor - }else{ + } else { return []; } } - function getDescription() { - $description = null; - $cleanIsbn = $this->getCleanISBN(); - /** @var Library $library */ + private ?string $cachedDescription = null; + private ?GroupedWorkDisplayInfo $cachedDisplayInfo = null; + + /** + * Get the description for this grouped work with full enrichment logic. + * + * Priority order: + * 1. Manually set display info description (from grouped_work_display_info table). + * 2. Syndetics summary (if preferSyndeticsSummary setting is enabled). + * 3. Description from Solr index. + * - Respects "Prefer ILS Description" setting to choose between ils_description and display_description. + * 4. Fallback to "Description Not Provided" message if no description is found. + * + * @return string + */ + function getDescription(): string { + if ($this->cachedDescription !== null) { + return $this->cachedDescription; + } + global $library; - if ($description == null) { - $description = $this->getDescriptionFast(); + + $displayInfo = $this->getDisplayInfo(); + if ($displayInfo != null && !empty($displayInfo->description)) { + $this->cachedDescription = $displayInfo->description; + return $this->cachedDescription; } - if ($library->getGroupedWorkDisplaySettings()->preferSyndeticsSummary == 1 || $description == null || strlen($description) == 0) { + + if ($library->getGroupedWorkDisplaySettings()->preferSyndeticsSummary == 1) { + $cleanIsbn = $this->getCleanISBN(); if ($cleanIsbn != null && strlen($cleanIsbn) > 0) { require_once ROOT_DIR . '/Drivers/marmot_inc/GoDeeperData.php'; $summaryInfo = GoDeeperData::getSummary($this->getPermanentId(), $cleanIsbn, $this->getCleanUPC()); if (isset($summaryInfo['summary'])) { - $description = $summaryInfo['summary']; + $this->cachedDescription = $summaryInfo['summary']; + return $this->cachedDescription; } } } + $description = $this->getDescriptionFromSolr(); if ($description == null || strlen($description) == 0) { $description = translate([ @@ -972,14 +1004,36 @@ function getDescription() { 'isPublicFacing' => true, ]); } - return $description; + + $this->cachedDescription = $description; + return $this->cachedDescription; + } + + function getDescriptionFast(bool $useHighlighting = false): string { + $displayInfo = $this->getDisplayInfo(); + if ($displayInfo != null && !empty($displayInfo->description)) { + return $displayInfo->description; + } + + return $this->getDescriptionFromSolr($useHighlighting); } - private $fastDescription = null; + private function getDisplayInfo(): ?GroupedWorkDisplayInfo { + if ($this->cachedDisplayInfo == null) { + $this->cachedDisplayInfo = $this->getSpecifiedDisplayInfo(); + } + return $this->cachedDisplayInfo; + } - function getDescriptionFast($useHighlighting = false) { + /** + * Get description from Solr fields (handles highlighting and ILS preference). + * + * @param bool $useHighlighting + * @return string + */ + private function getDescriptionFromSolr(bool $useHighlighting = false): string { global $library; - // Don't check for highlighted values if highlighting is disabled: + if ($this->highlight && $useHighlighting) { if ($library->getGroupedWorkDisplaySettings()->preferIlsDescription == 1 && isset($this->fields['_highlighting']['ils_description'][0])) { return $this->fields['_highlighting']['ils_description'][0]; @@ -989,20 +1043,13 @@ function getDescriptionFast($useHighlighting = false) { } } - - if ($this->fastDescription != null) { - return $this->fastDescription; - } - if ($library->getGroupedWorkDisplaySettings()->preferIlsDescription == 1 && !empty($this->fields['ils_description'])) { - $this->fastDescription = $this->fields['ils_description']; + return $this->fields['ils_description']; } else if (!empty($this->fields['display_description'])) { - $this->fastDescription = $this->fields['display_description']; - } else { - $this->fastDescription = ""; + return $this->fields['display_description']; } - return $this->fastDescription; + return ""; } private $detailedContributors = null; @@ -1093,7 +1140,11 @@ public function getFormatsArray() { } } - public function getFormatCategory() : string { + /** + * Note this uses a different signature than IndexRecordDriver. + * This expects to return a string or null, but IndexRecordDriver returns an array + */ + public function getFormatCategory(): string|array|null { global $solrScope; require_once ROOT_DIR . '/sys/SystemVariables.php'; $systemVariables = SystemVariables::getSystemVariables(); @@ -1117,9 +1168,9 @@ public function getFormatCategory() : string { return ""; } - protected $_indexedSeries = false; + protected array|null|false $_indexedSeries = false; - public function getIndexedSeries() { + public function getIndexedSeries(): ?array { if ($this->_indexedSeries === false) { global $timer; $this->_indexedSeries = null; @@ -1259,9 +1310,9 @@ public function getListEntry($listId = null, $allowEdit = true) { $linkUrl .= '?searchId=' . $interface->get_template_vars('searchId') . '&recordIndex=' . $interface->get_template_vars('recordIndex') . '&page=' . $interface->get_template_vars('page'); $interface->assign('summUrl', $linkUrl); - $shortTitle = $this->getShortTitle(); - if (empty($shortTitle)) { - $interface->assign('summTitle', $this->getTitle()); + $title = $this->getTitle(); + if (!empty($title)) { + $interface->assign('summTitle', $title); $interface->assign('summSubTitle', ''); } else { $interface->assign('summTitle', $this->getShortTitle()); @@ -1339,9 +1390,9 @@ public function getCourseReserveEntry($listId = null) { $linkUrl .= '?searchId=' . $interface->get_template_vars('searchId') . '&recordIndex=' . $interface->get_template_vars('recordIndex') . '&page=' . $interface->get_template_vars('page'); $interface->assign('summUrl', $linkUrl); - $shortTitle = $this->getShortTitle(); - if (empty($shortTitle)) { - $interface->assign('summTitle', $this->getTitle()); + $title = $this->getTitle(); + if (!empty($title)) { + $interface->assign('summTitle', $title); $interface->assign('summSubTitle', ''); } else { $interface->assign('summTitle', $this->getShortTitle()); @@ -1418,9 +1469,9 @@ public function getSeriesEntry(?int $seriesId = null, ?array $instance = null) { $linkUrl .= '?searchId=' . $interface->get_template_vars('searchId') . '&recordIndex=' . $interface->get_template_vars('recordIndex') . '&page=' . $interface->get_template_vars('page'); $interface->assign('summUrl', $linkUrl); - $shortTitle = $this->getShortTitle(); - if (empty($shortTitle)) { - $interface->assign('summTitle', $this->getTitle()); + $title = $this->getTitle(); + if (!empty($title)) { + $interface->assign('summTitle', $title); $interface->assign('summSubTitle', ''); } else { $interface->assign('summTitle', $this->getShortTitle()); @@ -1539,6 +1590,8 @@ public function getSummaryInformation() { 'ratingData' => $this->getRatingData(), 'format' => $this->getFormats(), 'language' => $this->getLanguage(), + 'primary_isbn' => $this->getPrimaryIsbn(), + 'primary_upc' => $this->getPrimaryUPC(), ]; } @@ -1601,7 +1654,7 @@ function getMoreInfoLinkUrl() { } public function getMpaaRating() { - return isset($this->fields['mpaaRating']) ? $this->fields['mpaaRating'] : null; + return $this->fields['mpaa_rating'] ?? null; } private $numRelatedRecords = -1; @@ -1678,6 +1731,14 @@ public function getPrimaryIsbn() { } } + public function getPrimaryUPC() { + if (isset($this->fields['primary_upc'])) { + return $this->fields['primary_upc']; + } else { + return null; + } + } + function getPublicationDates() { return isset($this->fields['publishDate']) ? $this->fields['publishDate'] : []; } @@ -1691,14 +1752,14 @@ function getEdition() { foreach ($relatedRecords as $relatedRecord) { $relatedRecordDriver = $relatedRecord->getDriver(); $editionsForDriver = $relatedRecordDriver->getEditions(); - if(count($editionsForDriver) > 0) { + if (count($editionsForDriver) > 0) { return reset($editionsForDriver); } } return ''; } - function getPlaceOfPublication () { + function getPlaceOfPublication() { $relatedRecords = $this->getRelatedRecords(); foreach ($relatedRecords as $relatedRecord) { $relatedRecordDriver = $relatedRecord->getDriver(); @@ -1767,7 +1828,7 @@ public function getRecordUrl() { * * @return Grouping_Manifestation[]|null */ - public function getRelatedManifestations() : ?array { + public function getRelatedManifestations(): ?array { if ($this->_relatedManifestations == null) { global $timer; global $memoryWatcher; @@ -1879,6 +1940,13 @@ public function getRelatedManifestations() : ?array { $memoryWatcher->logMemory("Finished loading related manifestations"); } + $activeFilters = $this->getActiveFilters(); + if (!empty($activeFilters) && !empty($activeFilters['format'])) { + return array_filter($this->_relatedManifestations, function ($format) use ($activeFilters) { + return in_array($format, $activeFilters['format']); + }, ARRAY_FILTER_USE_KEY); + } + return $this->_relatedManifestations; } @@ -1886,14 +1954,13 @@ public function getRelatedManifestations() : ?array { /** @noinspection PhpPropertyOnlyWrittenInspection */ private ?array $childRecords = null; - private ?array $relatedItemsByRecordId = null; /** * @param bool $forCovers * @return Grouping_Record[] */ - public function getRelatedRecords($forCovers = false) { - $this->loadRelatedRecords($forCovers); + public function getRelatedRecords(bool $forCovers = false): array { + $this->loadRelatedRecords(); return $this->relatedRecords; } @@ -1902,9 +1969,9 @@ public function getRelatedRecords($forCovers = false) { * Should get the related record based on the selected manifestation (format) * * @param $recordIdentifier - * @return Grouping_Record + * @return ?Grouping_Record */ - public function getRelatedRecord($recordIdentifier) : ?Grouping_Record { + public function getRelatedRecord($recordIdentifier): ?Grouping_Record { $this->loadRelatedRecords(); if (isset($this->relatedRecords[$recordIdentifier])) { return $this->relatedRecords[$recordIdentifier]; @@ -1914,14 +1981,15 @@ public function getRelatedRecord($recordIdentifier) : ?Grouping_Record { return null; } } + public function getRelatedRecordForVariation($recordIdentifier, $variationId = '') { $this->loadRelatedRecords(); $recordToLoad = $this->relatedRecords[$recordIdentifier]; $recordToLoadNotCS = $this->relatedRecords[strtolower($recordIdentifier)]; if (isset($recordToLoad)) { - if (($recordToLoad->variationId != $variationId) && $variationId != ''){ - foreach ($recordToLoad->recordVariations as $variation){ - if ($variation->databaseId == $variationId){ + if (($recordToLoad->variationId != $variationId) && $variationId != '') { + foreach ($recordToLoad->recordVariations as $variation) { + if ($variation->databaseId == $variationId) { $records = $variation->getRecords(); return $records[0]; } @@ -1929,9 +1997,9 @@ public function getRelatedRecordForVariation($recordIdentifier, $variationId = ' } return $recordToLoad; } elseif (isset($recordToLoadNotCS)) { - if (($recordToLoadNotCS->variationId != $variationId) && $variationId != ''){ - foreach ($recordToLoadNotCS->recordVariations as $variation){ - if ($variation->databaseId == $variationId){ + if (($recordToLoadNotCS->variationId != $variationId) && $variationId != '') { + foreach ($recordToLoadNotCS->recordVariations as $variation) { + if ($variation->databaseId == $variationId) { $records = $variation->getRecords(); return $records[0]; } @@ -1942,6 +2010,7 @@ public function getRelatedRecordForVariation($recordIdentifier, $variationId = ' return null; } } + public function getScrollerTitle($index, $scrollerName) { global $interface; $interface->assign('index', $index); @@ -1973,7 +2042,7 @@ public function getScrollerTitle($index, $scrollerName) { * * @return string Name of Smarty template file to display. */ - public function getSearchResult($view = 'list') : string { + public function getSearchResult($view = 'list'): string { if ($view == 'covers') { // Displaying Results as bookcover tiles return $this->getBrowseResult(); } @@ -2022,9 +2091,9 @@ public function getSearchResult($view = 'list') : string { } $interface->assign('summUrl', $linkUrl); - $shortTitle = $this->getShortTitle(); - if (empty($shortTitle)) { - $interface->assign('summTitle', $this->getTitle()); + $title = $this->getTitle(); + if (!empty($title)) { + $interface->assign('summTitle', $title); $interface->assign('summSubTitle', ''); } else { $interface->assign('summTitle', $this->getShortTitle()); @@ -2064,7 +2133,7 @@ public function getSearchResult($view = 'list') : string { if ($isFirst) { $summPublisher = $relatedRecord->publisher; $summPubDate = $relatedRecord->publicationDate; - $summPlaceOfPublication= $relatedRecord->placeOfPublication; + $summPlaceOfPublication = $relatedRecord->placeOfPublication; $summPhysicalDesc = $relatedRecord->physical; $summEdition = $relatedRecord->edition; $summAudience = $relatedRecord->audience; @@ -2130,7 +2199,9 @@ public function getSearchResult($view = 'list') : string { } $timer->logTime("Finished assignment of data based on solr debug info"); - $interface->assign('formatDisplayStyle', $library->getGroupedWorkDisplaySettings()->formatDisplayStyle); + $groupedWorkDisplaySettings = $library->getGroupedWorkDisplaySettings(); + $interface->assign('formatDisplayStyle', $groupedWorkDisplaySettings->formatDisplayStyle); + $interface->assign('hideManifestationsInMobileView', $groupedWorkDisplaySettings->hideManifestationsInMobileView); //Get Rating $interface->assign('summRating', $this->getRatingData()); @@ -2160,7 +2231,8 @@ public function getSearchResult($view = 'list') : string { } private bool $_requiredDataForActionsPreloaded = false; - private function preloadRequiredDataForActions(array $allRecordIdsBySource, array $allRecordIdsWithSource) : void { + + private function preloadRequiredDataForActions(array $allRecordIdsBySource, array $allRecordIdsWithSource): void { if (!$this->_requiredDataForActionsPreloaded) { $this->_requiredDataForActionsPreloaded = true; @@ -2170,13 +2242,13 @@ private function preloadRequiredDataForActions(array $allRecordIdsBySource, arra OverDriveAPIProduct::preloadProducts($recordIds); require_once ROOT_DIR . '/sys/OverDrive/OverDriveAPIProductAvailability.php'; OverDriveAPIProductAvailability::preloadAvailability($recordIds); - }else if ($source == 'axis360') { + } else if ($source == 'axis360') { require_once ROOT_DIR . '/sys/Axis360/Axis360Title.php'; Axis360Title::preloadTitles($recordIds); - }else if ($source == 'hoopla') { + } else if ($source == 'hoopla') { require_once ROOT_DIR . '/sys/Hoopla/HooplaExtract.php'; HooplaExtract::preloadTitles($recordIds); - }else{ + } else { require_once ROOT_DIR . '/sys/ILS/RecordFile.php'; require_once ROOT_DIR . '/sys/ILS/IlsHoldSummary.php'; require_once ROOT_DIR . '/sys/ILS/IlsVolumeInfo.php'; @@ -2262,7 +2334,7 @@ private function getSemanticWorkExamples() { /** * @return SeriesMember[] */ - private function getSeriesMembers() : array { + private function getSeriesMembers(): array { if ($this->_seriesMembers == null) { require_once ROOT_DIR . '/sys/Series/SeriesMember.php'; $seriesMember = new SeriesMember(); @@ -2279,7 +2351,7 @@ private function getSeriesMembers() : array { private $seriesData; - public function getSeries($allowReload = true, ?int $seriesId = null) : ?array { + public function getSeries($allowReload = true, ?int $seriesId = null): ?array { require_once ROOT_DIR . '/sys/Grouping/GroupedWorkDisplayInfo.php'; if (empty($this->seriesData)) { @@ -2347,16 +2419,16 @@ public function getSeries($allowReload = true, ?int $seriesId = null) : ?array { $existingDisplayInfo = new GroupedWorkDisplayInfo(); $existingDisplayInfo->permanent_id = $this->getPermanentId(); //prefer use of grouped work series display info if any - if ($existingDisplayInfo->find(true) && ((!empty($existingDisplayInfo->seriesDisplayOrder) && $existingDisplayInfo->seriesDisplayOrder != 0)|| !empty($existingDisplayInfo->seriesName)) ) { - if ($novelistData != null && !empty($novelistData->seriesTitle)){ - if (strtolower($novelistData->seriesTitle) == strtolower($existingDisplayInfo->seriesName)){ + if ($existingDisplayInfo->find(true) && ((!empty($existingDisplayInfo->seriesDisplayOrder) && $existingDisplayInfo->seriesDisplayOrder != 0) || !empty($existingDisplayInfo->seriesName))) { + if ($novelistData != null && !empty($novelistData->seriesTitle)) { + if (strtolower($novelistData->seriesTitle) == strtolower($existingDisplayInfo->seriesName)) { $this->seriesData = [ 'seriesTitle' => $existingDisplayInfo->seriesName, 'volume' => $existingDisplayInfo->seriesDisplayOrder, 'fromNovelist' => true, 'fromSeriesIndex' => false ]; - } else{ + } else { $this->seriesData = [ 'seriesTitle' => $existingDisplayInfo->seriesName, 'volume' => $existingDisplayInfo->seriesDisplayOrder, @@ -2364,7 +2436,7 @@ public function getSeries($allowReload = true, ?int $seriesId = null) : ?array { 'fromSeriesIndex' => false ]; } - } else{ + } else { $this->seriesData = [ 'seriesTitle' => $existingDisplayInfo->seriesName, 'volume' => $existingDisplayInfo->seriesDisplayOrder, @@ -2382,6 +2454,47 @@ public function getSeries($allowReload = true, ?int $seriesId = null) : ?array { } else { $seriesFromIndex = $this->getIndexedSeries(); if ($seriesFromIndex != null && count($seriesFromIndex) > 0) { + // Sort series entries by series name first, then by volume. + usort($seriesFromIndex, function($a, $b) { + $seriesA = $a['seriesTitle'] ?? ''; + $seriesB = $b['seriesTitle'] ?? ''; + + $seriesCompare = strcmp($seriesA, $seriesB); + if ($seriesCompare !== 0) { + return $seriesCompare; + } + // Within the same series, sort by volume. + $volA = $a['volume'] ?? ''; + $volB = $b['volume'] ?? ''; + + $hasVolA = !empty($volA); + $hasVolB = !empty($volB); + // If one has volume and one doesn't, the one with volume comes first. + if ($hasVolA && !$hasVolB) { + return -1; + } + if (!$hasVolA && $hasVolB) { + return 1; + } + // If neither has volume, they're equal. + if (!$hasVolA && !$hasVolB) { + return 0; + } + + // Both have volumes: extract numeric portion for comparison. + preg_match('/(\d+)/', $volA, $matchesA); + preg_match('/(\d+)/', $volB, $matchesB); + $numA = isset($matchesA[1]) ? intval($matchesA[1]) : 0; + $numB = isset($matchesB[1]) ? intval($matchesB[1]) : 0; + + // If numeric portions differ, sort by number. + if ($numA !== $numB) { + return $numA - $numB; + } + // If numeric portions are the same, do string comparison of full volume. + return strcmp($volA, $volB); + }); + $firstSeries = $seriesFromIndex[0]; $this->seriesData = [ 'seriesTitle' => $firstSeries['seriesTitle'], @@ -2389,6 +2502,17 @@ public function getSeries($allowReload = true, ?int $seriesId = null) : ?array { 'fromNovelist' => false, 'fromSeriesIndex' => false ]; + if (count($seriesFromIndex) > 1) { + $this->seriesData['additionalSeries'] = []; + for ($i = 1; $i < count($seriesFromIndex); $i++) { + $this->seriesData['additionalSeries'][] = [ + 'seriesTitle' => $seriesFromIndex[$i]['seriesTitle'], + 'volume' => $seriesFromIndex[$i]['volume'] ?? '', + 'fromNovelist' => false, + 'fromSeriesIndex' => false + ]; + } + } } else { return null; } @@ -2409,11 +2533,11 @@ private function isSeriesHidden(string $seriesTitle): bool { if (empty($seriesTitle)) { return false; } - + require_once ROOT_DIR . '/sys/Grouping/HideSeries.php'; $hideSeries = new HideSeries(); $normalizedSeriesTitle = $hideSeries->normalizeSeries($seriesTitle); - + $hideSeries = new HideSeries(); $hideSeries->seriesNormalized = $normalizedSeriesTitle; return $hideSeries->find(true); @@ -2449,11 +2573,12 @@ public function getShortTitle($useHighlighting = false) { } private GroupedWork|null|false $_groupedWork = false; - public function getGroupedWorkObject() : ?GroupedWork { + + public function getGroupedWorkObject(): ?GroupedWork { if ($this->_groupedWork === false) { if (empty($this->getUniqueID())) { $this->_groupedWork = null; - }else{ + } else { require_once ROOT_DIR . '/sys/Grouping/GroupedWork.php'; $this->_groupedWork = new GroupedWork(); $this->_groupedWork->permanent_id = $this->getUniqueID(); @@ -2487,7 +2612,7 @@ public function getStaffView() { if (IPAddress::showDebuggingInformation()) { require_once ROOT_DIR . '/sys/Grouping/GroupedWork.php'; $groupedWork = $this->getGroupedWorkObject(); - if ( $groupedWork != null) { + if ($groupedWork != null) { global $aspen_db; //Get the scopeId for the active scope global $solrScope; @@ -2525,20 +2650,24 @@ public function assignGroupedWorkStaffView() { $interface->assign('alternateTitles', $this->getAlternateTitles()); + $interface->assign('recordGroupingOverrides', $this->getRecordGroupingOverrides()); + $interface->assign('primaryIdentifiers', $this->getPrimaryIdentifiers()); $interface->assign('specifiedDisplayInfo', $this->getSpecifiedDisplayInfo()); + + $interface->assign('manualGroupingInfo', $this->getManualGroupingInfo()); } - public function getSpecifiedDisplayInfo() { + public function getSpecifiedDisplayInfo(): ?GroupedWorkDisplayInfo { require_once ROOT_DIR . '/sys/Grouping/GroupedWorkDisplayInfo.php'; $existingDisplayInfo = new GroupedWorkDisplayInfo(); $existingDisplayInfo->permanent_id = $this->getPermanentId(); if ($existingDisplayInfo->find(true)) { return $existingDisplayInfo; - } else { - return null; } + + return null; } public function getAlternateTitles() { @@ -2572,6 +2701,18 @@ public function getAlternateTitles() { return null; } + public function getManualGroupingInfo(): ?ManualGroupedWork { + if (UserAccount::userHasPermission('Manually Group and Ungroup Works')) { + require_once ROOT_DIR . '/sys/Grouping/ManualGroupedWork.php'; + $manualGroupedWork = new ManualGroupedWork(); + $manualGroupedWork->grouped_work_permanent_id = $this->getPermanentId(); + if ($manualGroupedWork->find(true)) { + return $manualGroupedWork; + } + } + return null; + } + public function getPrimaryIdentifiers() { $primaryIdentifiers = []; if (UserAccount::userHasPermission('Manually Group and Ungroup Works')) { @@ -2596,8 +2737,7 @@ public function getSolrField($fieldName) { } public function loadSubjects() { - /** @var Library $library */ - global $library; + /** @var Library $library */ global $library; global $interface; $subjects = []; @@ -2744,7 +2884,7 @@ public function getTableOfContents() { public function getUniqueID() { if (is_null($this->fields)) { return $this->permanentId; - }else { + } else { return $this->fields['id']; } } @@ -2816,7 +2956,7 @@ public function getUserReviews() { return $reviews; } - public function hasCachedSeries() : bool { + public function hasCachedSeries(): bool { //First check to see if we have series data cached in the series module global $enabledModules; global $library; @@ -2884,388 +3024,285 @@ public function loadEnrichment() { return $enrichment; } - /** - * @param $validItemIds - * @return array - */ - protected function loadItemDetailsFromIndex($validItemIds) { - $relatedItemsFieldName = 'item_details'; - $itemsFromIndex = []; - if (isset($this->fields[$relatedItemsFieldName])) { - $itemsFromIndexRaw = $this->fields[$relatedItemsFieldName]; - if (!is_array($itemsFromIndexRaw)) { - $itemsFromIndexRaw = [$itemsFromIndexRaw]; - } - foreach ($itemsFromIndexRaw as $tmpItem) { - $itemDetails = explode('|', $tmpItem); - $itemIdentifier = $itemDetails[0] . ':' . $itemDetails[1]; - if (in_array($itemIdentifier, $validItemIds)) { - $itemsFromIndex[] = $itemDetails; - if (!array_key_exists($itemDetails[0], $this->relatedItemsByRecordId)) { - $this->relatedItemsByRecordId[$itemDetails[0]] = []; - } - $this->relatedItemsByRecordId[$itemDetails[0]][] = $itemDetails; - } - } - return $itemsFromIndex; - } - return $itemsFromIndex; - } + static bool $scopesLoaded = false; + static false|int|string $activeLocationScopeId = false; + static false|int|string $mainLocationScopeId = false; + static false|int|string $userNearbyLocation1ScopeId = false; + static false|int|string $userNearbyLocation2ScopeId = false; + static false|int|string $atNearbyLocation1 = false; + static false|int|string $atNearbyLocation2 = false; + static false|int|string $homeLocationScopeId = false; - /** - * Get related records from the index filtered according to the current scope - * - * @param $validRecordIds - * @return array - */ - protected function loadRecordDetailsFromIndex($validRecordIds) { - $relatedRecordFieldName = "record_details"; - $recordsFromIndex = []; - if (isset($this->fields[$relatedRecordFieldName])) { - $relatedRecordIdsRaw = $this->fields[$relatedRecordFieldName]; - if (!is_array($relatedRecordIdsRaw)) { - $relatedRecordIdsRaw = [$relatedRecordIdsRaw]; - } - foreach ($relatedRecordIdsRaw as $tmpItem) { - $recordDetails = explode('|', $tmpItem); - //Check to see if the record is valid - if (in_array($recordDetails[0], $validRecordIds)) { - $recordsFromIndex[$recordDetails[0]] = $recordDetails; - } - } - } - return $recordsFromIndex; - } - - static $scopesLoaded = false; - static $activeLocationScopeId = false; - static $mainLocationScopeId = false; - static $userNearbyLocation1ScopeId = false; - static $userNearbyLocation2ScopeId = false; - static $atNearbyLocation1 = false; - static $atNearbyLocation2 = false; - static $homeLocationScopeId = false; - - private function loadRelatedRecords($forCovers = false, $forceLoadFromDB = true) { + private function loadRelatedRecords(): void { global $timer; - global $memoryWatcher; if ($this->relatedRecords == null || isset($_REQUEST['reload'])) { $timer->logTime("Starting to load related records for {$this->getUniqueID()}"); - $this->relatedItemsByRecordId = []; - global $solrScope; global $library; - $scopingInfoFieldName = 'scoping_details_' . $solrScope; $relatedRecords = []; $childRecords = []; - if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'Aspen LiDA') === 0) { - $forceLoadFromDB = true; - } - if (!$forceLoadFromDB && isset($this->fields[$scopingInfoFieldName])) { - $user = UserAccount::getActiveUserObj(); + $searchLocation = Location::getSearchLocation(); - $searchLocation = Location::getSearchLocation(); - [ - $scopingInfo, - $validRecordIds, - $validItemIds, - ] = $this->loadScopingDetails($solrScope); - $timer->logTime("Loaded Scoping Details from the index"); - $memoryWatcher->logMemory("Loaded scoping details from the index"); - - $recordsFromIndex = $this->loadRecordDetailsFromIndex($validRecordIds); - $timer->logTime("Loaded Record Details from the index"); - $memoryWatcher->logMemory("Loaded Record Details from the index"); - - //Get a list of related items filtered according to scoping - $this->loadItemDetailsFromIndex($validItemIds); - $timer->logTime("Loaded Item Details from the index"); - $memoryWatcher->logMemory("Loaded Item Details from the index"); - - //Load the work from the database so we can use it in each record diver - require_once ROOT_DIR . '/sys/Grouping/GroupedWork.php'; - $groupedWork = new GroupedWork(); - $groupedWork->permanent_id = $this->getUniqueID(); - //This will be false if the record is old - //Protect against loading every record in the database! - if (!empty($groupedWork->permanent_id)) { - if ($groupedWork->find(true)) { - //Generate record information based on the information we have in the index - foreach ($recordsFromIndex as $recordDetails) { - $relatedRecord = $this->setupRelatedRecordDetails($recordDetails, $groupedWork, $timer, $scopingInfo, $searchLocation, $library, $forCovers); - if ($relatedRecord != null) { - $relatedRecords[$relatedRecord->id] = $relatedRecord; - $memoryWatcher->logMemory("Setup related record details for " . $relatedRecord->id); - } else { - global $logger; - $logger->log("Error setting up related record " . $recordDetails, LOG_NOTICE); - } + if (!GroupedWorkDriver::$scopesLoaded) { + GroupedWorkDriver::$scopesLoaded = true; + + //Check for the main location for the library + require_once ROOT_DIR . '/sys/Grouping/Scope.php'; + //Get the scope for the main location for the library + foreach ($library->getLocations() as $mainLocation) { + if ($mainLocation->isMainBranch) { + $scope = new Grouping_Scope(); + $mainLibraryScopeName = str_replace('-', '', !empty($mainLocation->subdomain) ? $mainLocation->subdomain : $mainLocation->code); + $scope->name = $mainLibraryScopeName; + $scope->isLocationScope = 1; + if ($scope->find(true)) { + GroupedWorkDriver::$mainLocationScopeId = $scope->id; } } } - } else { - $searchLocation = Location::getSearchLocation(); - - if (!GroupedWorkDriver::$scopesLoaded) { - GroupedWorkDriver::$scopesLoaded = true; + global $locationSingleton; + $activeLocation = $locationSingleton->getActiveLocation(); + if ($activeLocation != null) { + $scope = new Grouping_Scope(); + $activeLocationScopeName = str_replace('-', '', !empty($activeLocation->subdomain) ? $activeLocation->subdomain : $activeLocation->code); + $scope->name = $activeLocationScopeName; + $scope->isLocationScope = 1; + if ($scope->find(true)) { + GroupedWorkDriver::$activeLocationScopeId = $scope->id; + } - //Check for the main location for the library - require_once ROOT_DIR . '/sys/Grouping/Scope.php'; - //Get the scope for the main location for the library - foreach ($library->getLocations() as $mainLocation) { - if ($mainLocation->isMainBranch) { + if ($activeLocation->nearbyLocation1 > 0) { + $altLocation1 = new Location(); + $altLocation1->locationId = $activeLocation->nearbyLocation1; + if ($altLocation1->find(true)) { $scope = new Grouping_Scope(); - $mainLibraryScopeName = str_replace('-', '', strtolower(!empty($mainLocation->subdomain) ? $mainLocation->subdomain : $mainLocation->code)); - $scope->name = $mainLibraryScopeName; + $altLocation1ScopeName = str_replace('-', '', !empty($altLocation1->subdomain) ? $altLocation1->subdomain : $altLocation1->code); + $scope->name = $altLocation1ScopeName; $scope->isLocationScope = 1; if ($scope->find(true)) { - GroupedWorkDriver::$mainLocationScopeId = $scope->id; + GroupedWorkDriver::$atNearbyLocation1 = $scope->id; } } } - global $locationSingleton; - $activeLocation = $locationSingleton->getActiveLocation(); - if ($activeLocation != null) { + if ($activeLocation->nearbyLocation2 > 0) { + $altLocation2 = new Location(); + $altLocation2->locationId = $activeLocation->nearbyLocation2; + if ($altLocation2->find(true)) { + $scope = new Grouping_Scope(); + $altLocation2ScopeName = str_replace('-', '', !empty($altLocation2->subdomain) ? $altLocation2->subdomain : $altLocation2->code); + $scope->name = $altLocation2ScopeName; + $scope->isLocationScope = 1; + if ($scope->find(true)) { + GroupedWorkDriver::$atNearbyLocation2 = $scope->id; + } + } + } + } + if (UserAccount::isLoggedIn()) { + $user = UserAccount::getActiveUserObj(); + $userHomeLocation = $user->getPickupLocation(); + if ($userHomeLocation != null) { $scope = new Grouping_Scope(); - $activeLocationScopeName = str_replace('-', '', strtolower(!empty($activeLocation->subdomain) ? $activeLocation->subdomain : $activeLocation->code)); - $scope->name = $activeLocationScopeName; + $mainLibraryScopeName = str_replace('-', '', !empty($userHomeLocation->subdomain) ? $userHomeLocation->subdomain : $userHomeLocation->code); + $scope->name = $mainLibraryScopeName; $scope->isLocationScope = 1; if ($scope->find(true)) { - GroupedWorkDriver::$activeLocationScopeId = $scope->id; - } - - if ($activeLocation->nearbyLocation1 > 0) { - $altLocation1 = new Location(); - $altLocation1->locationId = $activeLocation->nearbyLocation1; - if ($altLocation1->find(true)) { - $scope = new Grouping_Scope(); - $altLocation1ScopeName = str_replace('-', '', strtolower(!empty($altLocation1->subdomain) ? $altLocation1->subdomain : $altLocation1->code)); - $scope->name = $altLocation1ScopeName; - $scope->isLocationScope = 1; - if ($scope->find(true)) { - GroupedWorkDriver::$atNearbyLocation1 = $scope->id; - } - } - } - if ($activeLocation->nearbyLocation2 > 0) { - $altLocation2 = new Location(); - $altLocation2->locationId = $activeLocation->nearbyLocation2; - if ($altLocation2->find(true)) { - $scope = new Grouping_Scope(); - $altLocation2ScopeName = str_replace('-', '', strtolower(!empty($altLocation2->subdomain) ? $altLocation2->subdomain : $altLocation2->code)); - $scope->name = $altLocation2ScopeName; - $scope->isLocationScope = 1; - if ($scope->find(true)) { - GroupedWorkDriver::$atNearbyLocation2 = $scope->id; - } - } + GroupedWorkDriver::$homeLocationScopeId = $scope->id; } } - if (UserAccount::isLoggedIn()) { - $user = UserAccount::getActiveUserObj(); - $userHomeLocation = $user->getPickupLocation(); - if ($userHomeLocation != null) { + if ($user->myLocation1Id > 0) { + $myLocation1 = new Location(); + $myLocation1->locationId = $user->myLocation1Id; + if ($myLocation1->find(true)) { + $mainLibraryScopeName = str_replace('-', '', !empty($myLocation1->subdomain) ? $myLocation1->subdomain : $myLocation1->code); $scope = new Grouping_Scope(); - $mainLibraryScopeName = str_replace('-', '', strtolower(!empty($userHomeLocation->subdomain) ? $userHomeLocation->subdomain : $userHomeLocation->code)); $scope->name = $mainLibraryScopeName; $scope->isLocationScope = 1; if ($scope->find(true)) { - GroupedWorkDriver::$homeLocationScopeId = $scope->id; - } - } - if ($user->myLocation1Id > 0) { - $myLocation1 = new Location(); - $myLocation1->locationId = $user->myLocation1Id; - if ($myLocation1->find(true)) { - $mainLibraryScopeName = str_replace('-', '', strtolower(!empty($myLocation1->subdomain) ? $myLocation1->subdomain : $myLocation1->code)); - $scope = new Grouping_Scope(); - $scope->name = $mainLibraryScopeName; - $scope->isLocationScope = 1; - if ($scope->find(true)) { - GroupedWorkDriver::$userNearbyLocation1ScopeId = $scope->id; - } + GroupedWorkDriver::$userNearbyLocation1ScopeId = $scope->id; } } - if ($user->myLocation2Id > 0) { - $myLocation2 = new Location(); - $myLocation2->locationId = $user->myLocation2Id; - if ($myLocation2->find(true)) { - $mainLibraryScopeName = str_replace('-', '', strtolower(!empty($myLocation2->subdomain) ? $myLocation2->subdomain : $myLocation2->code)); - $scope = new Grouping_Scope(); - $scope->name = $mainLibraryScopeName; - $scope->isLocationScope = 1; - if ($scope->find(true)) { - GroupedWorkDriver::$userNearbyLocation2ScopeId = $scope->id; - } + } + if ($user->myLocation2Id > 0) { + $myLocation2 = new Location(); + $myLocation2->locationId = $user->myLocation2Id; + if ($myLocation2->find(true)) { + $mainLibraryScopeName = str_replace('-', '', !empty($myLocation2->subdomain) ? $myLocation2->subdomain : $myLocation2->code); + $scope = new Grouping_Scope(); + $scope->name = $mainLibraryScopeName; + $scope->isLocationScope = 1; + if ($scope->find(true)) { + GroupedWorkDriver::$userNearbyLocation2ScopeId = $scope->id; } } } } + } - require_once ROOT_DIR . '/sys/Grouping/GroupedWork.php'; - require_once ROOT_DIR . '/sys/Grouping/Manifestation.php'; - require_once ROOT_DIR . '/sys/Grouping/Variation.php'; - require_once ROOT_DIR . '/sys/Grouping/Record.php'; - require_once ROOT_DIR . '/sys/Grouping/Item.php'; - $groupedWork = new GroupedWork(); - $groupedWork->permanent_id = $this->getUniqueID(); - if (!empty($groupedWork->permanent_id) && $groupedWork->find(true)) { - global $aspen_db; - //Get the scopeId for the active scope - $scopeIdQuery = "SELECT id from scope where name = '$solrScope'"; - $scopeId = -1; - $results = $aspen_db->query($scopeIdQuery, PDO::FETCH_ASSOC); - if ($scopeResults = $results->fetch()) { - $scopeId = $scopeResults['id']; - } + require_once ROOT_DIR . '/sys/Grouping/GroupedWork.php'; + require_once ROOT_DIR . '/sys/Grouping/Manifestation.php'; + require_once ROOT_DIR . '/sys/Grouping/Variation.php'; + require_once ROOT_DIR . '/sys/Grouping/Record.php'; + require_once ROOT_DIR . '/sys/Grouping/Item.php'; + $groupedWork = new GroupedWork(); + $groupedWork->permanent_id = $this->getUniqueID(); + if (!empty($groupedWork->permanent_id) && $groupedWork->find(true)) { + global $aspen_db; + //Get the scopeId for the active scope + $scopeIdQuery = "SELECT id from scope where name = '$solrScope'"; + $scopeId = -1; + $results = $aspen_db->query($scopeIdQuery, PDO::FETCH_ASSOC); + if ($scopeResults = $results->fetch()) { + $scopeId = $scopeResults['id']; + } - //Get the ids of all the variations, records, and items attached to the work - $databaseIds = $this->getVariationRecordAndItemIdsFromDB($scopeId, $groupedWork->id, true); + //Get the ids of all the variations, records, and items attached to the work + $databaseIds = $this->getVariationRecordAndItemIdsFromDB($scopeId, $groupedWork->id, true); - $variations = $this->getRawVariationsDataFromDB($databaseIds['uniqueVariationIds']); - $this->_relatedManifestations = []; + $variations = $this->getRawVariationsDataFromDB($databaseIds['uniqueVariationIds']); + $this->_relatedManifestations = []; - //Get the variations from the database and add to the appropriate manifestation - /** @var $allVariations Grouping_Variation[] */ - $allVariations = []; - foreach ($variations as $variation) { - if (!array_key_exists($variation['format'], $this->_relatedManifestations)) { - $this->_relatedManifestations[$variation['format']] = new Grouping_Manifestation($variation); - } - $variationObj = new Grouping_Variation($variation); - //Add to the correct manifestation - $this->_relatedManifestations[$variation['format']]->addVariation($variationObj); - $allVariations[$variationObj->databaseId] = $variationObj; + //Get the variations from the database and add to the appropriate manifestation + /** @var $allVariations Grouping_Variation[] */ + $allVariations = []; + foreach ($variations as $variation) { + if (!array_key_exists($variation['format'], $this->_relatedManifestations)) { + $this->_relatedManifestations[$variation['format']] = new Grouping_Manifestation($variation); } - - $records = $this->getRawRecordDataFromDB($databaseIds['uniqueRecordIds']); - $allRecordIdsBySource = []; - $allRecordIdsWithSource = []; - foreach ($records as $record) { - if (!isset($allRecordIdsBySource[$record['source']])) { - $allRecordIdsBySource[$record['source']] = []; - $allRecordIdsWithSource[$record['source']] = []; - } - $allRecordIdsBySource[$record['source']][] = $record['recordIdentifier']; - $allRecordIdsWithSource[$record['source']][] = $record['source'] . ':' . $record['recordIdentifier']; + $variationObj = new Grouping_Variation($variation); + //Add to the correct manifestation + $this->_relatedManifestations[$variation['format']]->addVariation($variationObj); + $allVariations[$variationObj->databaseId] = $variationObj; + } + + $records = $this->getRawRecordDataFromDB($databaseIds['uniqueRecordIds']); + $allRecordIdsBySource = []; + $allRecordIdsWithSource = []; + foreach ($records as $record) { + if (!isset($allRecordIdsBySource[$record['source']])) { + $allRecordIdsBySource[$record['source']] = []; + $allRecordIdsWithSource[$record['source']] = []; } + $allRecordIdsBySource[$record['source']][] = $record['recordIdentifier']; + $allRecordIdsWithSource[$record['source']][] = $record['source'] . ':' . $record['recordIdentifier']; + } - $this->preloadRequiredDataForActions($allRecordIdsBySource, $allRecordIdsWithSource); - - //Load all records - /** @var Grouping_Record[] $allRecords */ - $allRecords = []; - foreach ($records as $record) { - - //Get all the variations that the record should be attached to - $itemQuery = "SELECT groupedWorkVariationId from grouped_work_record_items WHERE groupedWorkRecordId = {$record['id']}"; - $res = $aspen_db->query($itemQuery, PDO::FETCH_ASSOC); - $allItems = $res->fetchAll(); - $res->closeCursor(); - - $recordVariations = []; - foreach ($allItems as $item) { - $thisVariation = $item['groupedWorkVariationId']; - foreach ($allVariations as $variation) { - if ($thisVariation == $variation->databaseId) { - $recordVariations[$variation->manifestation->format] = $variation; - } + $this->preloadRequiredDataForActions($allRecordIdsBySource, $allRecordIdsWithSource); + + //Load all records + /** @var Grouping_Record[] $allRecords */ + $allRecords = []; + foreach ($records as $record) { + + //Get all the variations that the record should be attached to + $itemQuery = "SELECT groupedWorkVariationId from grouped_work_record_items WHERE groupedWorkRecordId = {$record['id']}"; + $res = $aspen_db->query($itemQuery, PDO::FETCH_ASSOC); + $allItems = $res->fetchAll(); + $res->closeCursor(); + + $recordVariations = []; + foreach ($allItems as $item) { + $thisVariation = $item['groupedWorkVariationId']; + foreach ($allVariations as $variation) { + if ($thisVariation == $variation->databaseId) { + $recordVariations[$variation->manifestation->format] = $variation; } } - //Create different Grouping_Record objects for each variation - foreach ($recordVariations as $variation) { - /** GroupedWorkSubDriver $recordDriver */ - require_once ROOT_DIR . '/RecordDrivers/RecordDriverFactory.php'; - $recordId = $record['source']; - $recordId .= ($record['subSource'] != null ? ':' . $record['subSource'] : ''); - $recordId .= ':' . $record['recordIdentifier']; - $recordDriver = RecordDriverFactory::initRecordDriverById($recordId, $groupedWork); - - //Do not add invalid records - if ($recordDriver != null) { - $volumeData = $this->getVolumeDataForRecord($record['source'], $recordId); - $relatedRecord = new Grouping_Record($recordId, $record, $recordDriver, $volumeData, $record['source'], true, $variation); - $relatedRecord->recordVariations = $recordVariations; - - $relatedRecords[$relatedRecord->id] = $relatedRecord; - $allRecords[$relatedRecord->databaseId . ':' . $variation->manifestation->format] = $relatedRecord; - } + } + //Create different Grouping_Record objects for each variation + foreach ($recordVariations as $variation) { + /** GroupedWorkSubDriver $recordDriver */ + require_once ROOT_DIR . '/RecordDrivers/RecordDriverFactory.php'; + $recordId = $record['source']; + $recordId .= ($record['subSource'] != null ? ':' . $record['subSource'] : ''); + $recordId .= ':' . $record['recordIdentifier']; + $recordDriver = RecordDriverFactory::initRecordDriverById($recordId, $groupedWork); + + //Do not add invalid records + if ($recordDriver != null) { + $volumeData = $this->getVolumeDataForRecord($record['source'], $recordId); + $relatedRecord = new Grouping_Record($recordId, $record, $recordDriver, $volumeData, $record['source'], true, $variation); + $relatedRecord->recordVariations = $recordVariations; + + $relatedRecords[$relatedRecord->id] = $relatedRecord; + $allRecords[$relatedRecord->databaseId . ':' . $variation->manifestation->format] = $relatedRecord; } } + } - $scopedItems = $this->getRawItemDataFromDB($databaseIds['uniqueItemIds']); - - foreach ($scopedItems as $scopedItem) { - //Get the variation for the item - $relatedVariation = $allVariations[$scopedItem['groupedWorkVariationId']]; - //Load the correct record based on the variation since the same record can exist in multiple variations - if (isset($allRecords[$scopedItem['groupedWorkRecordId'] . ':' . $relatedVariation->manifestation->format])) { - $relatedRecord = $allRecords[$scopedItem['groupedWorkRecordId'] . ':' . $relatedVariation->manifestation->format]; - $scopedItem['isEContent'] = $relatedVariation->isEContent; - $scopedItem['eContentSource'] = $relatedVariation->econtentSource; - $scopedItem['scopeId'] = $scopeId; - //Look for urls for the item - $itemUrlQuery = "SELECT url from grouped_work_record_item_url where groupedWorkItemId = {$scopedItem['groupedWorkItemId']} AND (scopeId = -1 OR scopeId = $scopeId) ORDER BY scopeId desc limit 1"; - $results = $aspen_db->query($itemUrlQuery, PDO::FETCH_ASSOC); - $itemUrls = $results->fetchAll(); - if (count($itemUrls) > 0) { - $scopedItem['localUrl'] = $itemUrls[0]['url']; - } - $results->closeCursor(); - $itemData = new Grouping_Item($scopedItem, null, $searchLocation, $library, GroupedWorkDriver::$activeLocationScopeId, GroupedWorkDriver::$mainLocationScopeId, GroupedWorkDriver::$homeLocationScopeId, GroupedWorkDriver::$userNearbyLocation1ScopeId, GroupedWorkDriver::$userNearbyLocation2ScopeId, GroupedWorkDriver::$atNearbyLocation1, GroupedWorkDriver::$atNearbyLocation2); - $relatedRecord->addItem($itemData); + $scopedItems = $this->getRawItemDataFromDB($databaseIds['uniqueItemIds']); + + foreach ($scopedItems as $scopedItem) { + //Get the variation for the item + $relatedVariation = $allVariations[$scopedItem['groupedWorkVariationId']]; + //Load the correct record based on the variation since the same record can exist in multiple variations + if (isset($allRecords[$scopedItem['groupedWorkRecordId'] . ':' . $relatedVariation->manifestation->format])) { + $relatedRecord = $allRecords[$scopedItem['groupedWorkRecordId'] . ':' . $relatedVariation->manifestation->format]; + $scopedItem['isEContent'] = $relatedVariation->isEContent; + $scopedItem['eContentSource'] = $relatedVariation->econtentSource; + $scopedItem['scopeId'] = $scopeId; + //Look for urls for the item + $itemUrlQuery = "SELECT url from grouped_work_record_item_url where groupedWorkItemId = {$scopedItem['groupedWorkItemId']} AND (scopeId = -1 OR scopeId = $scopeId) ORDER BY scopeId desc limit 1"; + $results = $aspen_db->query($itemUrlQuery, PDO::FETCH_ASSOC); + $itemUrls = $results->fetchAll(); + if (count($itemUrls) > 0) { + $scopedItem['localUrl'] = $itemUrls[0]['url']; } + $results->closeCursor(); + $itemData = new Grouping_Item($scopedItem, $searchLocation, GroupedWorkDriver::$activeLocationScopeId, GroupedWorkDriver::$mainLocationScopeId, GroupedWorkDriver::$homeLocationScopeId, GroupedWorkDriver::$userNearbyLocation1ScopeId, GroupedWorkDriver::$userNearbyLocation2ScopeId, GroupedWorkDriver::$atNearbyLocation1, GroupedWorkDriver::$atNearbyLocation2); + $relatedRecord->addItem($itemData); } + } - //Finally, add records to the correct manifestation (so status updates properly) - foreach ($allRecords as $record) { - if ($record->hasParentRecord) { - continue; - } - //Add to the correct manifestation - if (isset($this->_relatedManifestations[$record->variationFormat])) { - $this->_relatedManifestations[$record->variationFormat]->addRecord($record); - } else { - //This should not happen - $manifestation = new Grouping_Manifestation($record); - $this->_relatedManifestations[$record->variationFormat] = $manifestation; - global $logger; - $logger->log("Manifestation not found for record {$record->id} {$record->variationFormat}", Logger::LOG_ERROR); - } + //Finally, add records to the correct manifestation (so status updates properly) + foreach ($allRecords as $record) { + if ($record->hasParentRecord) { + continue; } + //Add to the correct manifestation + if (isset($this->_relatedManifestations[$record->variationFormat])) { + $this->_relatedManifestations[$record->variationFormat]->addRecord($record); + } else { + //This should not happen + $manifestation = new Grouping_Manifestation($record); + $this->_relatedManifestations[$record->variationFormat] = $manifestation; + global $logger; + $logger->log("Manifestation not found for record {$record->id} {$record->variationFormat}", Logger::LOG_ERROR); + } + } - //Sort Records within each manifestation and variation - foreach ($this->_relatedManifestations as $manifestationKey => $manifestation) { - $relatedRecordsForManifestation = $manifestation->getRelatedRecords(); - if (count($relatedRecordsForManifestation) >= 1) { - uasort($relatedRecordsForManifestation, [ - $this, - "compareRelatedRecords", - ]); - $manifestation->setSortedRelatedRecords($relatedRecordsForManifestation); - foreach ($manifestation->getVariations() as $variationKey => $variation) { - $relatedRecordsForVariation = $variation->getRelatedRecords($variation->databaseId); - if (count($relatedRecordsForVariation) > 1) { - uasort($relatedRecordsForVariation, [ - $this, - "compareRelatedRecords", - ]); - $variation->setSortedRelatedRecords($relatedRecordsForVariation); - } elseif (count($relatedRecordsForVariation) == 0) { - $manifestation->removeVariation($variationKey); - } + //Sort Records within each manifestation and variation + foreach ($this->_relatedManifestations as $manifestationKey => $manifestation) { + $relatedRecordsForManifestation = $manifestation->getRelatedRecords(); + $manifestation->sortVariations(); + if (count($relatedRecordsForManifestation) >= 1) { + uasort($relatedRecordsForManifestation, [ + $this, + "compareRelatedRecords", + ]); + $manifestation->setSortedRelatedRecords($relatedRecordsForManifestation); + foreach ($manifestation->getVariations() as $variationKey => $variation) { + $relatedRecordsForVariation = $variation->getRelatedRecords($variation->databaseId); + if (count($relatedRecordsForVariation) > 1) { + uasort($relatedRecordsForVariation, [ + $this, + "compareRelatedRecords", + ]); + $variation->setSortedRelatedRecords($relatedRecordsForVariation); + } elseif (count($relatedRecordsForVariation) == 0) { + $manifestation->removeVariation($variationKey); } - } elseif (count($relatedRecordsForManifestation) == 0) { - unset($this->_relatedManifestations[$manifestationKey]); } + } elseif (count($relatedRecordsForManifestation) == 0) { + unset($this->_relatedManifestations[$manifestationKey]); } - - uasort($this->_relatedManifestations, [ - $this, - "compareRelatedManifestations", - ]); } + + uasort($this->_relatedManifestations, [ + $this, + "compareRelatedManifestations", + ]); } //Sort the records based on format and then edition @@ -3280,38 +3317,6 @@ private function loadRelatedRecords($forCovers = false, $forceLoadFromDB = true) } } - /** - * @param $solrScope - * @return array - */ - protected function loadScopingDetails($solrScope) { - //First load scoping information from the index. This is stored as multiple values - //within the scoping details field for the scope. - //Each field is - $scopingInfoFieldName = 'scoping_details_' . $solrScope; - $scopingInfo = []; - $validRecordIds = []; - $validItemIds = []; - if (isset($this->fields[$scopingInfoFieldName])) { - $scopingInfoRaw = $this->fields[$scopingInfoFieldName]; - if (!is_array($scopingInfoRaw)) { - $scopingInfoRaw = [$scopingInfoRaw]; - } - foreach ($scopingInfoRaw as $tmpItem) { - $scopingDetails = explode('|', $tmpItem); - $scopeKey = $scopingDetails[0] . ':' . ($scopingDetails[1] == 'null' ? '' : $scopingDetails[1]); - $scopingInfo[$scopeKey] = $scopingDetails; - $validRecordIds[] = $scopingDetails[0]; - $validItemIds[] = $scopeKey; - } - } - return [ - $scopingInfo, - $validRecordIds, - $validItemIds, - ]; - } - private function getVariationRecordAndItemIdsFromDB($scopeId, $groupedWorkId) { global $aspen_db; $getIdsQuery = "select groupedWorkId, groupedWorkVariationId, groupedWorkRecordId, grouped_work_record_items.id as groupedRecordItemId, hasParentRecord FROM @@ -3429,200 +3434,6 @@ private static function normalizeEdition($edition) { return $edition; } - /** - * @param $recordDetails - * @param GroupedWork $groupedWork - * @param Timer $timer - * @param $scopingInfo - * @param Location $searchLocation - * @param Library $library - * @param bool $forCovers Optimization if we are only loading info for the covers - * @return Grouping_Record - */ - protected function setupRelatedRecordDetails($recordDetails, $groupedWork, $timer, $scopingInfo, $searchLocation, $library, $forCovers = false) { - global $memoryWatcher; - - // list($source) = explode(':', $recordDetails[0], 1); // this does not work for 'overdrive:27770ba9-9e68-410c-902b-de2de8e2b7fe', returns 'overdrive:27770ba9-9e68-410c-902b-de2de8e2b7fe' - // when loading book covers. - [$source] = explode(':', $recordDetails[0], 2); - - //Check to see if we have any volume data for the record - $volumeData = $this->getVolumeDataForRecord($source, $recordDetails[0]); - - /** GroupedWorkSubDriver $recordDriver */ - require_once ROOT_DIR . '/RecordDrivers/RecordDriverFactory.php'; - $recordDriver = RecordDriverFactory::initRecordDriverById($recordDetails[0], $groupedWork); - $timer->logTime("Loaded Record Driver for $recordDetails[0]"); - $memoryWatcher->logMemory("Loaded Record Driver for $recordDetails[0]"); - - require_once ROOT_DIR . '/sys/Grouping/Record.php'; - $relatedRecord = new Grouping_Record($recordDetails[0], $recordDetails, $recordDriver, $volumeData, $source, false, null); - - $timer->logTime("Setup base related record"); - $memoryWatcher->logMemory("Setup base related record"); - - //Process the items for the record and add additional information as needed - $localShelfLocation = null; - $libraryShelfLocation = null; - $localCallNumber = null; - $libraryCallNumber = null; - - global $locationSingleton; - $physicalLocation = $locationSingleton->getPhysicalLocation(); - - $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, false); - $relatedRecord->addItem($item); - - $description = $item->shelfLocation . ':' . $item->callNumber; - - $volume = null; - $volumeId = null; - $volumeOrder = null; - if (count($volumeData) > 0) { - /** @var IlsVolumeInfo $volumeDataPoint */ - foreach ($volumeData as $volumeDataPoint) { - if ((strlen($volumeDataPoint->relatedItems) == 0) || (strpos($volumeDataPoint->relatedItems, $curItem[1]) !== false)) { - if ($item->holdable) { - $volumeDataPoint->holdable = true; - } - if (strlen($volumeDataPoint->relatedItems) > 0) { - $volume = $volumeDataPoint->displayLabel; - $volumeId = $volumeDataPoint->volumeId; - $volumeOrder = $volumeDataPoint->displayOrder; - break; - } - } - } - } - $key = str_pad($volumeOrder, 10, '0', STR_PAD_LEFT) . $description; - - $section = 'Other Locations'; - if ($item->locallyOwned) { - if ($localShelfLocation == null) { - $localShelfLocation = $item->shelfLocation; - } - if ($localCallNumber == null) { - $localCallNumber = $item->callNumber; - } - if ($item->available && !$item->isEContent) { - //Set available here only if we're in the library - if (!empty($physicalLocation)) { - $relatedRecord->getStatusInformation()->setAvailableHere(true); - } - $relatedRecord->getStatusInformation()->setAvailableLocally(true); - $relatedRecord->setClass('here'); - } - $relatedRecord->addLocalCopies($item->numCopies); - $relatedRecord->setHasLocalItem(true); - $key = '1 ' . $key; - $sectionId = 1; - $section = 'In this library'; - } elseif ($item->libraryOwned) { - if ($libraryShelfLocation == null) { - $libraryShelfLocation = $item->shelfLocation; - } - if ($libraryCallNumber == null) { - $libraryCallNumber = $item->callNumber; - } - //We don't want to set this if we are in a location scope and it isn't available at that branch - //if it was, location owned would be set - if ($item->available && !$item->isEContent && empty($physicalLocation)) { - $relatedRecord->getStatusInformation()->setAvailableLocally(true); - } - $relatedRecord->addLocalCopies($item->numCopies); - //If we are inside a branch, we only set that it has a local item if locationOwned is true - if (empty($physicalLocation) && ($searchLocation == null || $item->isEContent)) { - $relatedRecord->setHasLocalItem(true); - } - //If the item is owned by the main location of the library, move it above the other locations for that library - if ($item->atLibraryMainBranch) { - $key = '4 ' . $key; - } else { - $key = '5 ' . $key; - } - $sectionId = 5; - $section = $library->displayName; - } elseif ($item->isOrderItem) { - $key = '7 ' . $key; - $sectionId = 7; - $section = 'On Order'; - } else { - $key = '6 ' . $key; - $sectionId = 6; - } - - $callNumber = $item->callNumber; - if ((strlen($volume) > 0) && !substr($item->callNumber, -strlen($volume)) == $volume) { - $callNumber = trim($item->callNumber . ' ' . $volume); - } - //Add the item to the item summary - $itemSummaryInfo = [ - 'description' => $description, - 'shelfLocation' => $item->shelfLocation, - 'callNumber' => $callNumber, - 'totalCopies' => $item->numCopies, - 'availableCopies' => ($item->available && !$item->isOrderItem) ? $item->numCopies : 0, - 'isLocalItem' => $item->locallyOwned, - 'isLibraryItem' => $item->libraryOwned, - 'inLibraryUseOnly' => $item->inLibraryUseOnly, - 'allLibraryUseOnly' => $item->inLibraryUseOnly, - 'displayByDefault' => $item->isDisplayByDefault(), - 'onOrderCopies' => $item->isOrderItem ? $item->numCopies : 0, - 'status' => $item->groupedStatus, - 'statusFull' => $item->status, - 'available' => $item->available, - 'holdable' => $item->holdable, - 'sectionId' => $sectionId, - 'section' => $section, - 'relatedUrls' => $item->getRelatedUrls(), - 'lastCheckinDate' => isset($curItem[14]) ? $curItem[14] : '', - 'volume' => $volume, - 'volumeId' => $volumeId, - 'isEContent' => $item->isEContent, - 'locationCode' => $item->locationCode, - 'subLocation' => $item->subLocation, - 'itemId' => $item->itemId, - ]; - if (!$forCovers) { - $item->setActions($recordDriver != null ? $recordDriver->getItemActions($itemSummaryInfo) : []); - $itemSummaryInfo['actions'] = $item->getActions(); - } - - //Group the item based on location and call number for display in the summary - $relatedRecord->addItemSummary($item->variationId, $key, $itemSummaryInfo, $item->groupedStatus); - //Also add to the details for display in the full list - $relatedRecord->addItemDetails($item->variationId, $key . $i++, $itemSummaryInfo); - } - if ($localShelfLocation != null) { - $relatedRecord->setShelfLocation($localShelfLocation); - } elseif ($libraryShelfLocation != null) { - $relatedRecord->setShelfLocation($libraryShelfLocation); - } - if ($localCallNumber != null) { - $relatedRecord->setCallNumber($localCallNumber); - } elseif ($libraryCallNumber != null) { - $relatedRecord->setCallNumber($libraryCallNumber); - } - $timer->logTime("Setup record items " . count($this->relatedItemsByRecordId[$relatedRecord->id])); - $relatedRecord->sortItemSummary($relatedRecord->variationId); - $timer->logTime("Sorted Item Summary"); - $relatedRecord->sortItemDetails($relatedRecord->variationId); - $timer->logTime("Sorted Item Details"); - $memoryWatcher->logMemory("Setup record items"); - - if (!$forCovers) { - $relatedRecord->setActions($relatedRecord->variationId,$recordDriver != null ? $recordDriver->getRecordActions($relatedRecord, null, $relatedRecord->getStatusInformation()->isAvailableLocally() || $relatedRecord->getStatusInformation()->isAvailableOnline(), $relatedRecord->isHoldable(), $volumeData) : []); - $timer->logTime("Loaded actions"); - $memoryWatcher->logMemory("Loaded actions"); - } - - $recordDriver = null; - return $relatedRecord; - } - /** * @return array */ @@ -3659,12 +3470,12 @@ public function getBookcoverInfo() { } } - function getWhileYouWait($selectedFormat = null) : array { + function getWhileYouWait($selectedFormat = null): array { global $library; if (!$library->showWhileYouWait) { return []; } - if ($selectedFormat == null && !empty($_REQUEST['activeFormat'])){ + if ($selectedFormat == null && !empty($_REQUEST['activeFormat'])) { $selectedFormat = $_REQUEST['activeFormat']; } //Load Similar titles (from Solr) @@ -3679,7 +3490,7 @@ function getWhileYouWait($selectedFormat = null) : array { if ($library->showWhileYouWait == 2 && !empty($selectedFormat)) { $similar = $searchObject->getMoreLikeThis($this->getPermanentId(), $selectedAvailabilityToggle, true, true, 3, $selectedFormat); $interface->assign('activeFormat', $selectedFormat); - } else{ + } else { $similar = $searchObject->getMoreLikeThis($this->getPermanentId(), $selectedAvailabilityToggle, true, false, 3); } @@ -3789,8 +3600,8 @@ public function formatGroupedWorkCitation() { $risFields = array(); // RIS TY - Format - $format = $this->getFormat() ; - if(is_array($format) && count($format) > 0) { + $format = $this->getFormat(); + if (is_array($format) && count($format) > 0) { $format = implode(', ', $format); switch ($format) { @@ -3883,13 +3694,13 @@ public function formatGroupedWorkCitation() { break; case 'Electronic Database': $format = 'EBOOK'; - break; + break; case 'Reference': $format = 'BOOK'; break; } - $risFields[] = "TY - ".$format; + $risFields[] = "TY - " . $format; } //RIS Tag: AU - Author $authors = array(); @@ -3899,12 +3710,12 @@ public function formatGroupedWorkCitation() { } $contributors = $this->getContributors(); - if(is_array($contributors) && count($contributors) > 0) { + if (is_array($contributors) && count($contributors) > 0) { $authors = array_merge($authors, $contributors); } if (!empty($authors)) { - foreach ($authors as $author){ + foreach ($authors as $author) { $risFields[] = "AU - " . $author; } } @@ -3934,65 +3745,71 @@ public function formatGroupedWorkCitation() { } $placesOfPublication = $this->getPlaceOfPublication(); - if(is_array($placesOfPublication) && count($placesOfPublication) > 0) { + if (is_array($placesOfPublication) && count($placesOfPublication) > 0) { $placesOfPublicationClean = implode(', ', $placesOfPublication); - $placesOfPublicationClean = str_replace([':', '; '], ' ', $placesOfPublication); - $risFields[] = "CY - ".$placesOfPublicationClean; + $placesOfPublicationClean = str_replace([ + ':', + '; ' + ], ' ', $placesOfPublication); + $risFields[] = "CY - " . $placesOfPublicationClean; } else { - if(!empty($placesOfPublication)) { - $placesOfPublicationClean = str_replace([':', '; '], ' ', $placesOfPublication); - $risFields[] = "CY - ".$placesOfPublicationClean; + if (!empty($placesOfPublication)) { + $placesOfPublicationClean = str_replace([ + ':', + '; ' + ], ' ', $placesOfPublication); + $risFields[] = "CY - " . $placesOfPublicationClean; } } // //RIS Tag: ET - Editions $editions = $this->getEdition(); - if(is_array($editions) && count($editions) > 0) { + if (is_array($editions) && count($editions) > 0) { $editions = implode(', ', $editions); - $risFields[] = "ET - ".$editions; + $risFields[] = "ET - " . $editions; } else { - if(!empty($editions)) { - $risFields[] = "ET - ".$editions; + if (!empty($editions)) { + $risFields[] = "ET - " . $editions; } } //RIS UR - URL $url = $this->getRecordUrl(); - if(is_array($url) && count($url) > 0) { + if (is_array($url) && count($url) > 0) { $url = implode(', ', $url); - $risFields[] = "UR - ".$url; + $risFields[] = "UR - " . $url; } //RIS Tag: N1 - Info $notes = $this->getTableOfContentsNotes(); - if(is_array($notes) && count($notes) > 0) { + if (is_array($notes) && count($notes) > 0) { $notes = implode(', ', $notes); - $risFields[] = "N1 - ".$notes; + $risFields[] = "N1 - " . $notes; } //RIS Tag: N2 - Notes $description = $this->getDescription(); - if(!empty($description)) { - $risFields[] = "N2 - ".$description; + if (!empty($description)) { + $risFields[] = "N2 - " . $description; } //RIS T2 - Series $series = $this->getSeries(); - if(is_array($series) && count($series) >0){ + if (is_array($series) && count($series) > 0) { $series = implode(', ', $series); - $risFields[] = "T2 - ".$series; + $risFields[] = "T2 - " . $series; } //RIS ST - Short Title $shortTilte = $this->getShortTitle(); - if(!empty($shortTilte)) { - $risFields[] = "ST - ".$shortTilte; + if (!empty($shortTilte)) { + $risFields[] = "ST - " . $shortTilte; } // RIS Tag: SN - ISBN $ISBN = $this->getPrimaryIsbn(); - if(!empty($ISBN)){ - $risFields[] = "SN - ".$ISBN; + if (!empty($ISBN)) { + $risFields[] = "SN - " . $ISBN; } //RIS Tag: AV @@ -4003,4 +3820,39 @@ public function formatGroupedWorkCitation() { return ''; } } + + public function getRecordGroupingOverrides(): ?array { + if (UserAccount::userHasPermission('Manually Group and Ungroup Works')) { + require_once ROOT_DIR . '/sys/Grouping/RecordGroupingOverride.php'; + $override = new RecordGroupingOverride(); + $permanentId = $this->getPermanentId(); + $overrides = []; + if (!empty($permanentId)) { + $override->grouped_work_permanent_id = $permanentId; + if ($override->find()) { + while ($override->fetch()) { + $overrides[$override->id] = clone $override; + } + } + } + return $overrides; + } + return null; + } + + /** + * Check if this grouped work is manually grouped. + * + * @return bool + */ + public function isManuallyGrouped(): bool { + if (empty($this->permanentId)) { + return false; + } + + require_once ROOT_DIR . '/sys/Grouping/ManualGroupedWork.php'; + $manualGroupedWork = new ManualGroupedWork(); + $manualGroupedWork->grouped_work_permanent_id = $this->permanentId; + return $manualGroupedWork->find(true) !== false; + } } diff --git a/code/web/RecordDrivers/GroupedWorkSubDriver.php b/code/web/RecordDrivers/GroupedWorkSubDriver.php index 37402f95d9..7617d016d3 100644 --- a/code/web/RecordDrivers/GroupedWorkSubDriver.php +++ b/code/web/RecordDrivers/GroupedWorkSubDriver.php @@ -305,7 +305,7 @@ public function getPrimaryFormat() { * * @return array */ - abstract function getFormatCategory(); + abstract function getFormatCategory() : string|array|null ; public function getFountasPinnellLevel() { return $this->getGroupedWorkDriver()->getFountasPinnellLevel(); @@ -332,6 +332,20 @@ public function hasValidGroupedWorkDriver() { return $groupedWorkDriver != null && $groupedWorkDriver->isValid(); } + /** + * Check if this record is part of a manually grouped work. + * + * @return bool + * @noinspection PhpUnused + */ + public function isInManuallyGroupedWork(): bool { + $groupedWorkDriver = $this->getGroupedWorkDriver(); + if ($groupedWorkDriver && $groupedWorkDriver->isValid()) { + return $groupedWorkDriver->isManuallyGrouped(); + } + return false; + } + /** * Get an array of all ISBNs associated with the record (may be empty). * @@ -529,10 +543,10 @@ public function getUPC() { /** * @param IlsVolumeInfo[] $volumeData - * @return int + * @return array */ - function getVolumeHolds(/** @noinspection PhpUnusedParameterInspection */ $volumeData) { - return 0; + function getVolumeHolds(/** @noinspection PhpUnusedParameterInspection */ array $volumeData) : array { + return []; } static $groupedWorks = []; diff --git a/code/web/RecordDrivers/HooplaRecordDriver.php b/code/web/RecordDrivers/HooplaRecordDriver.php index 9b6df49d04..c855c12221 100644 --- a/code/web/RecordDrivers/HooplaRecordDriver.php +++ b/code/web/RecordDrivers/HooplaRecordDriver.php @@ -240,7 +240,7 @@ public function getISSNs() { return []; } - protected $_actions = null; + protected ?array $_actions = null; function getRecordActions($relatedRecord, $variationId, $isAvailable, $isHoldable, $volumeData = null) : array { if ($this->_actions === null) { @@ -249,7 +249,7 @@ function getRecordActions($relatedRecord, $variationId, $isAvailable, $isHoldabl $loadDefaultActions = true; if (UserAccount::isLoggedIn()) { $user = UserAccount::getActiveUserObj(); - $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActions('hoopla', $this->id)); + $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActionsWithLazyLoading('hoopla', $this->id)); $loadDefaultActions = count($this->_actions) == 0; } @@ -257,6 +257,12 @@ function getRecordActions($relatedRecord, $variationId, $isAvailable, $isHoldabl global $offlineMode; global $loginAllowedWhileOffline; if ($loadDefaultActions && (!$offlineMode || $loginAllowedWhileOffline)) { + $needsLazyLoading = false; + if (UserAccount::isLoggedIn()) { + $user = UserAccount::getActiveUserObj(); + if (!$user->areCirculationActionsDisabled()) $needsLazyLoading = !$user->isCirculationCacheFresh(); + } + /** @var Library $searchLibrary */ $searchLibrary = Library::getSearchLibrary(); if ($searchLibrary->hooplaLibraryID > 0) { // Library is enabled for Hoopla patron action integration @@ -267,21 +273,33 @@ function getRecordActions($relatedRecord, $variationId, $isAvailable, $isHoldabl 'text' => 'Place Hold Hoopla', 'isPublicFacing' => true, ]); - $this->_actions[] = [ - 'onclick' => "return AspenDiscovery.Hoopla.placeHold('$id')", + $holdAction = [ + 'onclick' => "return AspenDiscovery.Hoopla.placeHold('$id', this)", 'title' => $title, 'type' => 'hoopla_hold', ]; + if ($needsLazyLoading) { + $holdAction['data-needs-refresh'] = 'true'; + $holdAction['data-record-id'] = $this->id; + $holdAction['data-record-source'] = 'hoopla'; + } + $this->_actions[] = $holdAction; } else { $title = translate([ 'text' => 'Check Out Hoopla', 'isPublicFacing' => true, ]); - $this->_actions[] = [ - 'onclick' => "return AspenDiscovery.Hoopla.getCheckOutPrompts('$id', '$hooplaType')", + $checkoutAction = [ + 'onclick' => "return AspenDiscovery.Hoopla.getCheckOutPrompts('$id', '$hooplaType', this)", 'title' => $title, 'type' => 'hoopla_checkout', ]; + if ($needsLazyLoading) { + $checkoutAction['data-needs-refresh'] = 'true'; + $checkoutAction['data-record-id'] = $this->id; + $checkoutAction['data-record-source'] = 'hoopla'; + } + $this->_actions[] = $checkoutAction; } } else { $this->_actions[] = $this->getAccessLink(); @@ -343,7 +361,7 @@ function getFormats() { * * @return array */ - function getFormatCategory() { + function getFormatCategory() : string|array|null { if ($this->hooplaExtract->kind == "AUDIOBOOK") { return [ 'eBook', @@ -489,13 +507,15 @@ function getActions() { /** @var Library $searchLibrary */ $searchLibrary = Library::getSearchLibrary(); if ($searchLibrary->hooplaLibraryID > 0) { // Library is enabled for Hoopla patron action integration + $hooplaType = $this->getHooplaType(); $title = translate([ 'text' => 'Check Out Hoopla', 'isPublicFacing' => true, ]); $actions[] = [ - 'onclick' => "return AspenDiscovery.Hoopla.getCheckOutPrompts('{$this->id}')", + 'onclick' => "return AspenDiscovery.Hoopla.getCheckOutPrompts('$this->id', '$hooplaType', this)", 'title' => $title, + 'type' => 'hoopla_checkout' ]; } else { diff --git a/code/web/RecordDrivers/IndexRecordDriver.php b/code/web/RecordDrivers/IndexRecordDriver.php index c82433ca3c..36cbec1412 100644 --- a/code/web/RecordDrivers/IndexRecordDriver.php +++ b/code/web/RecordDrivers/IndexRecordDriver.php @@ -26,7 +26,6 @@ abstract class IndexRecordDriver extends RecordInterface { */ protected $forbiddenSnippetFields = [ 'author', - 'author-letter', 'auth_author2', 'title', 'title_short', @@ -122,17 +121,17 @@ public function getFormat() { * @access public * @return array */ - public function getFormats() { - return isset($this->fields['format']) ? $this->fields['format'] : []; + public function getFormats() : array { + return $this->fields['format'] ?? []; } /** * Get an array of all the format categories associated with the record. * - * @return array + * @return array|null|string */ - public function getFormatCategory() { - return isset($this->fields['format_category']) ? $this->fields['format_category'] : []; + public function getFormatCategory() : array|null|string { + return $this->fields['format_category'] ?? []; } /** diff --git a/code/web/RecordDrivers/MarcRecordDriver.php b/code/web/RecordDrivers/MarcRecordDriver.php index f349544441..00af21f26e 100644 --- a/code/web/RecordDrivers/MarcRecordDriver.php +++ b/code/web/RecordDrivers/MarcRecordDriver.php @@ -125,10 +125,10 @@ public function isValid() { /** * overriding getLinkUrl from RecordInterface to check * the conditions which cause us to load the invalidRecord.tpl - * template when viewing a record. - * + * template when viewing a record. + * * @param bool $absolutePath if true prepend site url from config to the result - * @return string url for the record or an empty string + * @return string url for the record or an empty string */ public function getLinkUrl($absolutePath = false) { if(!$this->isValid()) @@ -691,7 +691,7 @@ public function getTitle() { /** @noinspection PhpUnused */ public function get880Title() : string { $this->loadAlternateGraphicRepresentations(); - return $this->_alternateGraphicRepresentations['title']; + return $this->_alternateGraphicRepresentations['title'] ?? ''; } /** @noinspection PhpUnused */ @@ -1273,7 +1273,7 @@ function getRecordVariations() { return $records; } - function getFormatCategory() { + function getFormatCategory() : string|array|null { return $this->getGroupedWorkDriver()->getFormatCategory(); } @@ -1292,7 +1292,7 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is if (UserAccount::isLoggedIn()) { $user = UserAccount::getActiveUserObj(); - $this->_actions[$variationId] = array_merge($this->_actions[$variationId], $user->getCirculatedRecordActions($this->getIndexingProfile()->name, $this->id)); + $this->_actions[$variationId] = array_merge($this->_actions[$variationId], $user->getCirculatedRecordActionsWithLazyLoading($this->getIndexingProfile()->name, $this->id)); } $treatVolumeHoldsAsItemHolds = $this->getCatalogDriver()->treatVolumeHoldsAsItemHolds(); @@ -1398,14 +1398,14 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is //VDX does not support volumes, we'll just prompt for a regular VDX $this->_actions[$variationId][] = getVdxRequestAction($this->getModule(), $source, $id); } elseif ($interLibraryLoanType == 'localIll') { - $this->_actions[$variationId][] = getMultiVolumeRequestAction($this->getModule(), $source, $id, $this); + $this->_actions[$variationId][] = getMultiVolumeRequestAction($this->getModule(), $source, $id, $this, count($itemsWithoutVolumes)); } }else{ $this->_actions[$variationId][] = getUntitledVolumeHoldAction($this->getModule(), $source, $id, $variationId); } } else { //The button will show a message to the patron no volumes can be requested - $this->_actions[$variationId][] = getNoVolumesCanBeRequestedAction($this->getModule(), $source, $id); + $this->_actions[$variationId][] = getNoVolumesCanBeRequestedAction($id); } }else{ //We will need to show a popup to select the volume @@ -1572,6 +1572,7 @@ function createActionsFromUrls($relatedUrls, $itemInfo = null, $variationId = 'a 'requireLogin' => false, 'alt' => $alt, 'target' => '_blank', + 'type' => 'access_online' ]; } } @@ -1669,7 +1670,7 @@ public function getPhysicalDescriptions() { $this->_physicalDescriptions = []; $physicalDescriptionFields = $this->getFields('300|530', true); foreach ($physicalDescriptionFields as $field) { - if ($field == '300') { + if ($field->getTag() == '300') { $info = $this->getSubfieldArray($field, ['a', 'b', 'c', 'e', 'f', 'g'], true); }else{ $info = $this->getSubfieldArray($field, ['a', 'b', 'c', 'd'], true); @@ -1946,6 +1947,7 @@ public function getMoreDetailsOptions() { $interface->assign('show856LinksAsTab', $library->getGroupedWorkDisplaySettings()->show856LinksAsTab); $interface->assign('showItemDueDates', $library->getGroupedWorkDisplaySettings()->showItemDueDates); $interface->assign('showItemNotes', $library->getGroupedWorkDisplaySettings()->showItemNotes); + $interface->assign('showItemBarcodes', $library->getGroupedWorkDisplaySettings()->showItemBarcodes); if ($library->getGroupedWorkDisplaySettings()->show856LinksAsTab && count($links) > 0) { $moreDetailsOptions['links'] = [ @@ -1973,8 +1975,8 @@ public function getMoreDetailsOptions() { } } - // Only add copies section if there are non-eContent holdings to display. - if ($hasNonEContentHoldings) { + // Only add copies section if there are non-eContent holdings to display or if it's a periodical (with setting enabled). + if ($hasNonEContentHoldings || ($this->isPeriodical() && $library->getGroupedWorkDisplaySettings()->showCopiesForPeriodicalsWithNoItems)) { $moreDetailsOptions['copies'] = [ 'label' => 'Copies', 'body' => $interface->fetch('Record/view-holdings.tpl'), @@ -2445,10 +2447,9 @@ function getNumHolds(): int { * @param IlsVolumeInfo[] $volumeData * @return array */ - function getVolumeHolds($volumeData) { - $holdInfo = null; + function getVolumeHolds(array $volumeData) : array { + $holdInfo = []; if (count($volumeData) > 0) { - $holdInfo = []; foreach ($volumeData as $volumeInfo) { $ilsHoldInfo = new IlsHoldSummary(); $ilsHoldInfo->ilsId = $volumeInfo->volumeId; @@ -2554,13 +2555,14 @@ function getNotes() { return $notes; } - private $holdings; - private $copiesInfoLoaded = false; - private $holdingSections; - private $statusSummary; - private $holdingsHaveUrls = false; + private ?array $holdings; + private bool $copiesInfoLoaded = false; + private ?array $holdingSections; + //Looks like this can be null, a Grouping_Record or an empty Array? + private null|Grouping_Record|array $statusSummary; + private bool $holdingsHaveUrls = false; - private function loadCopies() { + private function loadCopies() : void { if (!$this->copiesInfoLoaded) { $this->copiesInfoLoaded = true; $indexingProfile = $this->getIndexingProfile(); @@ -2604,7 +2606,7 @@ private function loadCopies() { //Divide the items into sections and create the status summary $this->holdingSections = []; $itemsFromMarc = []; - if (!empty($indexingProfile->noteSubfield) || !empty($indexingProfile->dueDate) || !empty($indexingProfile->itemUrl)) { + if (!empty($indexingProfile->noteSubfield) || !empty($indexingProfile->dueDate) || !empty($indexingProfile->itemUrl) || !empty($indexingProfile->barcode)) { //Get items from the marc record $itemFields = $this->getMarcRecord()->getFields($indexingProfile->itemTag); /** @var File_MARC_Data_Field $field */ @@ -2695,10 +2697,32 @@ private function loadCopies() { } } } - //if ($copyInfo['shelfLocation'] != '') { + if (!empty($indexingProfile->barcode)) { + if (array_key_exists($copyInfo['itemId'], $itemsFromMarc)) { + $itemField = $itemsFromMarc[$copyInfo['itemId']]; + $copyInfo['barcode'] = ''; + if (!empty($itemField)) { + $barcodeSubfield = $itemField->getSubfield($indexingProfile->barcode); + if ($barcodeSubfield != null && !empty($barcodeSubfield->getData())) { + $copyInfo['barcode'] = $barcodeSubfield->getData(); + } + } + } + } $this->holdingSections[$sectionName]['holdings'][] = $copyInfo; - //} + } + //Sort each holding section + $isPeriodical = $this->isPeriodical(); + foreach ($this->holdingSections as $sectionName => $sectionInfo) { + $holdings = $sectionInfo['holdings']; + require_once ROOT_DIR . '/sys/Utils/GroupingUtils.php'; + if ($isPeriodical) { + $holdings = sortPeriodicalItemsByShelfLocationAndCallNumber($holdings); + }else{ + $holdings = sortItemsByShelfLocationAndCallNumber($holdings); + } + $this->holdingSections[$sectionName]['holdings'] = $holdings; } $this->statusSummary = $recordFromIndex; @@ -2713,7 +2737,6 @@ private function loadCopies() { } } else { //This will happen for linked records where we are not indexing the grouped work - $this->holdings = []; $this->holdingSections = []; $this->statusSummary = []; @@ -2721,13 +2744,14 @@ private function loadCopies() { } } - public function assignCopiesInformation() { + public function assignCopiesInformation() : void { $this->loadCopies(); global $interface; $hasLastCheckinData = false; $hasVolume = false; $hasNote = false; $hasDueDate = false; + $hasBarcode = false; foreach ($this->holdings as $holding) { if ($holding['lastCheckinDate']) { $hasLastCheckinData = true; @@ -2741,50 +2765,59 @@ public function assignCopiesInformation() { if (!empty($holding['dueDate'])) { $hasDueDate = true; } + if (!empty($holding['barcode'])) { + $hasBarcode = true; + } } $interface->assign('hasLastCheckinData', $hasLastCheckinData); $interface->assign('hasVolume', $hasVolume); $interface->assign('hasNote', $hasNote); $interface->assign('hasDueDate', $hasDueDate); + $interface->assign('hasBarcode', $hasBarcode); $interface->assign('holdings', $this->holdings); $interface->assign('sections', $this->holdingSections); - $interface->assign('statusSummary', $this->statusSummary); global $timer; $timer->logTime("Assigned copy information"); } - public function getCopies() { + public function getCopies() : array { $this->loadCopies(); return $this->holdings; } - public function isPeriodical() { - $ils = 'Unknown'; - if ($this->getIndexingProfile()->getAccountProfile() != null) { - $ils = $this->getIndexingProfile()->getAccountProfile()->ils; - - } - //If this is a periodical we may have additional information - $isPeriodical = false; - require_once ROOT_DIR . '/sys/Indexing/FormatMapValue.php'; - foreach ($this->getFormats() as $format) { - if ($ils == 'sierra' || $ils == 'millennium') { - $formatValue = new FormatMapValue(); - $formatValue->format = $format; - $formatValue->displaySierraCheckoutGrid = 1; - if ($formatValue->find(true)) { - $isPeriodical = true; - break; + private ?bool $_isPeriodical = null; + public function isPeriodical() : bool { + if ($this->_isPeriodical === null) { + $ils = 'Unknown'; + if ($this->getIndexingProfile() instanceof IndexingProfile) { + if ($this->getIndexingProfile()->getAccountProfile() != null) { + $ils = $this->getIndexingProfile()->getAccountProfile()->ils; } - }else{ - if ($format == 'Journal' || $format == 'Newspaper' || $format == 'Print Periodical' || $format == 'Magazine') { - $isPeriodical = true; - break; + } + + //If this is a periodical we may have additional information + $isPeriodical = false; + require_once ROOT_DIR . '/sys/Indexing/FormatMapValue.php'; + foreach ($this->getFormats() as $format) { + if ($ils == 'sierra' || $ils == 'millennium') { + $formatValue = new FormatMapValue(); + $formatValue->format = $format; + $formatValue->displaySierraCheckoutGrid = 1; + if ($formatValue->find(true)) { + $isPeriodical = true; + break; + } + } else { + if ($format == 'Journal' || $format == 'Newspaper' || $format == 'Print Periodical' || $format == 'Magazine') { + $isPeriodical = true; + break; + } } } + $this->_isPeriodical = $isPeriodical; } - return $isPeriodical; + return $this->_isPeriodical; } public function loadPeriodicalInformation() { @@ -3150,7 +3183,7 @@ private function getMarcHoldings() { if (array_key_exists($tmpOwningLibraryCode, $libraryCodeToDisplayName)) { $owningLibrary = $libraryCodeToDisplayName[$tmpOwningLibraryCode]; break; - //Handle sierra quirks where the actual location code is specified with a z at the end + //Handle sierra quirks where the actual location code is specified with a z at the end } elseif (array_key_exists($tmpOwningLibraryCode . 'z', $libraryCodeToDisplayName)) { $owningLibrary = $libraryCodeToDisplayName[$tmpOwningLibraryCode . 'z']; break; @@ -3409,6 +3442,4 @@ public function getAudience(): string|null { } return $descriptionField->getSubfield('a')->getData(); } -} - - +} \ No newline at end of file diff --git a/code/web/RecordDrivers/OverDriveRecordDriver.php b/code/web/RecordDrivers/OverDriveRecordDriver.php index 99813632a6..62c5ffe6af 100644 --- a/code/web/RecordDrivers/OverDriveRecordDriver.php +++ b/code/web/RecordDrivers/OverDriveRecordDriver.php @@ -124,7 +124,7 @@ function getStatusSummary() : array { foreach ($availabilityInfo as $availability) { $availableCopies += $availability->copiesAvailable; $totalCopies += $availability->copiesOwned; - $numHolds = $availability->numberOfHolds; + $numHolds += $availability->numberOfHolds; } //Load status summary @@ -471,10 +471,8 @@ public function getFormats() : array { /** * Get an array of all the format categories associated with the record. - * - * @return string[] */ - public function getFormatCategory() : array { + public function getFormatCategory() : string|array|null { return [$this->getGroupedWorkDriver()->getFormatCategory()]; } @@ -670,6 +668,15 @@ public function getMoreDetailsOptions() : array { ]; } + $accessibilityStatements = $this->getAccessibilityStatements(); + if (!empty($accessibilityStatements)) { + $interface->assign('overdriveAccessibilityStatements', $accessibilityStatements); + $moreDetailsOptions['accessibilityStatements'] = [ + 'label' => 'Accessibility Statements', + 'body' => $interface->fetch('OverDrive/view-accessibility.tpl'), + ]; + } + return $this->filterAndSortMoreDetailsOptions($moreDetailsOptions); } @@ -730,7 +737,7 @@ function getPublicationDetails() : array { $i = 0; $returnVal = []; while (isset($places[$i]) || isset($placesOfPublication[$i]) || isset($names[$i]) || isset($dates[$i])) { - // while (isset($places[$i]) || isset($names[$i]) || isset($dates[$i])) { + // while (isset($places[$i]) || isset($names[$i]) || isset($dates[$i])) { // Put all the pieces together, and do a little processing to clean up // unwanted whitespace. $publicationInfo = (isset($places[$i]) ? $places[$i] . ' ' : '') . (isset($placesOfPublication[$i]) ? $placesOfPublication[$i] . ' ': '') . (isset($names[$i]) ? $names[$i] . ' ' : '') . (isset($dates[$i]) ? (', ' . $dates[$i] . '.') : ''); @@ -774,8 +781,6 @@ public function getGroupedWorkDriver() : ?GroupedWorkDriver { } } - protected ?array $_actions = null; - /** * Determines which item should be used for circulation actions including * - the collection (setting) @@ -953,6 +958,8 @@ public function getValidCollectionsForRecord(null|false|User $patron) : array { return $validCollections; } + protected ?array $_actions = null; + public function getRecordActions($relatedRecord, $variationId, $isAvailable, $isHoldable, $volumeData = null) : array { if ($this->_actions === null) { if ($relatedRecord == null) { @@ -969,14 +976,18 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is if (UserAccount::isLoggedIn()) { $activeUser = UserAccount::getActiveUserObj(); if ($activeUser->isValidForEContentSource('overdrive')) { - $this->_actions = array_merge($this->_actions, $activeUser->getCirculatedRecordActions('overdrive', $this->id)); + $this->_actions = array_merge($this->_actions, $activeUser->getCirculatedRecordActionsWithLazyLoading('overdrive', $this->id)); } $loadDefaultActions = count($this->_actions) == 0; }else{ $activeUser = null; } - if ($loadDefaultActions) { + $needsLazyLoading = false; + if ($activeUser) { + if (!$activeUser->areCirculationActionsDisabled()) $needsLazyLoading = !$activeUser->isCirculationCacheFresh(); + } + require_once ROOT_DIR . '/Drivers/OverDriveDriver.php'; $overDriveDriver = new OverDriveDriver(); $availableReaders = $overDriveDriver->getReaderNames(); @@ -1020,27 +1031,39 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is if ($loadDefaultActions && (!$offlineMode || $loginAllowedWhileOffline)) { if ($isAvailable) { //Only one setting with a checkout link so far using this reader name - $actionsByReader[$readerName]['checkout'] = [ + $checkoutAction = [ 'title' => translate([ 'text' => "Borrow with %1%", 1 => $readerName, "isPublicFacing" => true, ]), - 'onclick' => "return AspenDiscovery.OverDrive.checkOutTitle('$this->id', '$readerName');", + 'onclick' => "return AspenDiscovery.OverDrive.checkOutTitle('$this->id', this);", 'requireLogin' => false, 'type' => 'overdrive_checkout', ]; + if ($needsLazyLoading) { + $checkoutAction['data-needs-refresh'] = 'true'; + $checkoutAction['data-record-id'] = $this->id; + $checkoutAction['data-record-source'] = 'overdrive'; + } + $actionsByReader[$readerName]['checkout'] = $checkoutAction; } else { - $actionsByReader[$readerName]['placeHold'] = [ + $holdAction = [ 'title' => translate([ 'text' => 'Place Hold with %1%', 1 => $readerName, 'isPublicFacing' => true, ]), - 'onclick' => "return AspenDiscovery.OverDrive.placeHold('$this->id', '$readerName');", + 'onclick' => "return AspenDiscovery.OverDrive.placeHold('$this->id', this);", 'requireLogin' => false, 'type' => 'overdrive_hold', ]; + if ($needsLazyLoading) { + $holdAction['data-needs-refresh'] = 'true'; + $holdAction['data-record-id'] = $this->id; + $holdAction['data-record-source'] = 'overdrive'; + } + $actionsByReader[$readerName]['placeHold'] = $holdAction; } } } //End checking if circulation is enabled @@ -1176,4 +1199,98 @@ public function setNumHoldsForItem(Grouping_Item $item) : void { } } + public function getAccessibilityStatements(): array { + $accessibilityStatements = []; + $rawData = $this->getOverDriveMetaData()->getDecodedRawData(); + if (isset($rawData->accessibilityStatements) && is_array($rawData->accessibilityStatements)) { + foreach ($rawData->accessibilityStatements as $statement) { + $accessibilityStatements[] = $this->formatAccessibilityStatement($statement); + } + } + return $accessibilityStatements; + } + + private function getAccessibilityMappings(): array { + static $accessibilityMappings = null; + if ($accessibilityMappings !== null) { + return $accessibilityMappings; + } + + $accessibilityMappings = $this->loadAccessibilityMappingsFromFiles(); + return $accessibilityMappings; + } + + private function loadAccessibilityMappingsFromFiles(): array { + $mappingFile = ROOT_DIR . '/sys/OverDrive/accessibilityMappings.json'; + if (file_exists($mappingFile)) { + $contents = file_get_contents($mappingFile); + if ($contents !== false) { + $decodedMappings = json_decode($contents, true); + if (is_array($decodedMappings)) { + return $decodedMappings; + } + } + } + + return []; + } + + private function formatAccessibilityStatement($statement): array { + $formattedStatements = [ + 'summaryStatement' => $statement->summaryStatement ?? '', + 'conformance' => $this->formatConformance($statement), + ]; + $accessibilitySections = [ + 'waysOfReading', + 'navigation', + 'richContent', + 'hazards', + 'legalConsiderations', + 'additionalInformation', + ]; + foreach ($accessibilitySections as $sectionName) { + $sectionItems = isset($statement->$sectionName) ? $statement->$sectionName : []; + $formattedStatements[$sectionName] = $this->formatAccessibilitySection($sectionItems, $sectionName); + } + + return $formattedStatements; + } + + private function formatAccessibilitySection($items, $section): array { + if (!is_array($items) || empty($items)) { + return []; + } + $sectionMappings = $this->getAccessibilityMappings()[$section] ?? []; + $formattedDescriptions = []; + foreach ($items as $item) { + if (isset($sectionMappings[$item])) { + $formattedDescriptions[] = $sectionMappings[$item]; + } + } + return $formattedDescriptions; + } + + private function formatConformance($statement): array { + $conformance = []; + $mappings = $this->getAccessibilityMappings()['conformance'] ?? []; + + if (isset($statement->conformance)) { + $conformance = $this->formatAccessibilitySection($statement->conformance, 'conformance'); + if (isset($statement->wcagLevel)) { + foreach ($conformance as &$wcagLevel) { + $wcagLevel = str_replace('{wcagLevel}', $statement->wcagLevel, $wcagLevel); + } + unset($wcagLevel); + } + } + + if (isset($statement->certifiedBy) && isset($mappings['CertifiedBy'])) { + $conformance[] = str_replace('{certifiedBy}', $statement->certifiedBy, $mappings['CertifiedBy']); + } + + if (isset($statement->certifierCredential) && isset($mappings['CertifierCredential'])) { + $conformance[] = str_replace('{certifierCredential}', $statement->certifierCredential, $mappings['CertifierCredential']); + } + return $conformance; + } } diff --git a/code/web/RecordDrivers/PalaceProjectRecordDriver.php b/code/web/RecordDrivers/PalaceProjectRecordDriver.php index dea62f8660..1853496d0c 100644 --- a/code/web/RecordDrivers/PalaceProjectRecordDriver.php +++ b/code/web/RecordDrivers/PalaceProjectRecordDriver.php @@ -224,7 +224,7 @@ public function getISSNs() { return []; } - protected $_actions = null; + protected ?array $_actions = null; public function getRecordActions($relatedRecord, $variationId, $isAvailable, $isHoldable, $volumeData = null) : array { if ($this->_actions === null) { @@ -234,35 +234,53 @@ public function getRecordActions($relatedRecord, $variationId, $isAvailable, $is $loadDefaultActions = true; if (UserAccount::isLoggedIn()) { $user = UserAccount::getActiveUserObj(); - $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActions('palace_project', $this->id)); + $this->_actions = array_merge($this->_actions, $user->getCirculatedRecordActionsWithLazyLoading('palace_project', $this->id)); $loadDefaultActions = count($this->_actions) == 0; } //Check if catalog is offline and login for eResources should be allowed for offline global $offlineMode; global $loginAllowedWhileOffline; if ($loadDefaultActions && (!$offlineMode || $loginAllowedWhileOffline)) { + $needsLazyLoading = false; + if (UserAccount::isLoggedIn()) { + $user = UserAccount::getActiveUserObj(); + if (!$user->areCirculationActionsDisabled()) $needsLazyLoading = !$user->isCirculationCacheFresh(); + } + $titleAvailability = $this->getTitleAvailability(); if ($titleAvailability != null) { if (!$titleAvailability->needsHold) { - $this->_actions[] = [ + $checkoutAction = [ 'title' => translate([ 'text' => 'Check Out Palace Project', 'isPublicFacing' => true, ]), - 'onclick' => "return AspenDiscovery.PalaceProject.checkOutTitle('{$this->id}');", + 'onclick' => "return AspenDiscovery.PalaceProject.checkOutTitle('{$this->id}', this);", 'requireLogin' => false, 'type' => 'palace_project_checkout', ]; + if ($needsLazyLoading) { + $checkoutAction['data-needs-refresh'] = 'true'; + $checkoutAction['data-record-id'] = $this->id; + $checkoutAction['data-record-source'] = 'palace_project'; + } + $this->_actions[] = $checkoutAction; }else{ - $this->_actions[] = [ + $holdAction = [ 'title' => translate([ 'text' => 'Place Hold Palace Project', 'isPublicFacing' => true, ]), - 'onclick' => "return AspenDiscovery.PalaceProject.placeHold('{$this->id}');", + 'onclick' => "return AspenDiscovery.PalaceProject.placeHold('{$this->id}', this);", 'requireLogin' => false, 'type' => 'palace_project_hold', ]; + if ($needsLazyLoading) { + $holdAction['data-needs-refresh'] = 'true'; + $holdAction['data-record-id'] = $this->id; + $holdAction['data-record-source'] = 'palace_project'; + } + $this->_actions[] = $holdAction; } } } @@ -383,7 +401,7 @@ function getFormats() { * * @return array */ - function getFormatCategory() { + function getFormatCategory() : string|array|null { switch ($this->getType()) { case 'http://schema.org/EBook': return ['eBook']; diff --git a/code/web/RecordDrivers/RecordActionGenerator.php b/code/web/RecordDrivers/RecordActionGenerator.php index 42c863be97..08d8a4777e 100644 --- a/code/web/RecordDrivers/RecordActionGenerator.php +++ b/code/web/RecordDrivers/RecordActionGenerator.php @@ -8,9 +8,10 @@ function getUntitledVolumeHoldAction($module, $source, $id, $variationId) : arra ]), 'url' => '', 'id' => "actionButton$id", - 'onclick' => "return AspenDiscovery.Record.showPlaceHold('$module', '$source', '$id', '~untitled~', '$variationId');", + 'onclick' => "return AspenDiscovery.Record.showPlaceHold('$module', '$source', '$id', '~untitled~', '$variationId', this);", 'requireLogin' => false, 'type' => 'ils_hold', + 'subtype' => 'untitled_volume_ils_hold', ]; } //Regular ILS holds @@ -22,9 +23,10 @@ function getHoldRequestAction($module, $source, $id, $variationId) : array { ]), 'url' => '', 'id' => "actionButton$id", - 'onclick' => "return AspenDiscovery.Record.showPlaceHold('$module', '$source', '$id', '', '$variationId');", + 'onclick' => "return AspenDiscovery.Record.showPlaceHold('$module', '$source', '$id', '', '$variationId', this);", 'requireLogin' => false, 'type' => 'ils_hold', + 'subtype' => 'standard_ils_hold', ]; } @@ -37,9 +39,10 @@ function getSpecificVolumeHoldAction($module, $source, $id, $volumeInfo) : array ]), 'url' => '', 'id' => "actionButton$id", - 'onclick' => "return AspenDiscovery.Record.showPlaceHold('$module', '$source', '$id', '{$volumeInfo['volumeId']}');", + 'onclick' => "return AspenDiscovery.Record.showPlaceHold('$module', '$source', '$id', '{$volumeInfo['volumeId']}', '', this);", 'requireLogin' => false, 'type' => 'ils_hold', + 'subtype' => 'single_volume_ils_hold', 'volumeId' => $volumeInfo['volumeId'], 'volumeName' => $volumeInfo['volumeName'], ]; @@ -53,13 +56,14 @@ function getMultiVolumeHoldAction($module, $source, $id) : array { ]), 'url' => '', 'id' => "actionButton$id", - 'onclick' => "return AspenDiscovery.Record.showPlaceHoldVolumes('$module', '$source', '$id');", + 'onclick' => "return AspenDiscovery.Record.showPlaceHoldVolumes('$module', '$source', '$id', this);", 'requireLogin' => false, 'type' => 'ils_hold', + 'subtype' => 'multi_volume_ils_hold', ]; } -function getMultiVolumeRequestAction($module, $source, $id, $recordDriver) : array { +function getMultiVolumeRequestAction($module, $source, $id, $recordDriver, $hasUntitledVolumes) : array { global $library; $activeLibrary = $library; if (UserAccount::isLoggedIn()) { @@ -67,10 +71,10 @@ function getMultiVolumeRequestAction($module, $source, $id, $recordDriver) : arr $activeLibrary = $user->getHomeLibrary(); } - if ($activeLibrary->enableMaterialsRequest != 0) { + if ($activeLibrary->enableMaterialsRequest != 0 && !$hasUntitledVolumes) { return getRedirectToMaterialsRequestAction($activeLibrary, $id, null, $recordDriver); }else { - if (!empty($activeLibrary->localIllEmail)) { + if (!empty($activeLibrary->localIllEmail) && !$hasUntitledVolumes) { $redirectParams =[ 'title' => $recordDriver->getTitle(), 'author' => $recordDriver->getPrimaryAuthor() ?? '', @@ -280,7 +284,7 @@ function getLocalIllRequestAction($module, $source, $id) : array { ]; } -function getNoVolumesCanBeRequestedAction($module, $source, $id) : array { +function getNoVolumesCanBeRequestedAction($id) : array { //Check to see if the user can do local ILL by PType if (UserAccount::isLoggedIn()) { $user = UserAccount::getActiveUserObj(); @@ -416,4 +420,4 @@ function getDownloadMultiSupplementalFileAction($id) : array { 'requireLogin' => false, 'type' => 'download_supplemental_file', ]; -} +} \ No newline at end of file diff --git a/code/web/RecordDrivers/RecordDriverFactory.php b/code/web/RecordDrivers/RecordDriverFactory.php index 47bb2383b0..6d1fc7095c 100644 --- a/code/web/RecordDrivers/RecordDriverFactory.php +++ b/code/web/RecordDrivers/RecordDriverFactory.php @@ -57,15 +57,16 @@ static function initRecordDriver($record) { /** * @param $id - * @param GroupedWork $groupedWork ; - * @return ExternalEContentDriver|MarcRecordDriver|null|OverDriveRecordDriver + * @param null $groupedWork ; + * @return PalaceProjectRecordDriver|CourseReservesRecordDriver|OpenArchivesRecordDriver|TalpaRecordDriver|HooplaRecordDriver|ExternalEContentDriver|OverDriveRecordDriver|MarcRecordDriver|CloudLibraryRecordDriver|Axis360RecordDriver|AspenError|SideLoadedRecord|null */ - static function initRecordDriverById($id, $groupedWork = null) { + static function initRecordDriverById($id, $groupedWork = null): PalaceProjectRecordDriver|CourseReservesRecordDriver|OpenArchivesRecordDriver|TalpaRecordDriver|HooplaRecordDriver|ExternalEContentDriver|OverDriveRecordDriver|MarcRecordDriver|CloudLibraryRecordDriver|Axis360RecordDriver|AspenError|SideLoadedRecord|null + { global $configArray; if (isset(RecordDriverFactory::$recordDrivers[$id])) { return RecordDriverFactory::$recordDrivers[$id]; } - if (strpos($id, ':') !== false) { + if (str_contains($id, ':')) { $recordInfo = explode(':', $id, 2); $recordType = $recordInfo[0]; $recordId = $recordInfo[1]; @@ -190,6 +191,4 @@ public static function initAndReturnDriver($record, $driver, $path) { // If we got here, something went very wrong: return new AspenError("Problem loading record driver: {$driver}"); } - - } \ No newline at end of file diff --git a/code/web/RecordDrivers/RecordInterface.php b/code/web/RecordDrivers/RecordInterface.php index 09b8e68839..af4d669503 100644 --- a/code/web/RecordDrivers/RecordInterface.php +++ b/code/web/RecordDrivers/RecordInterface.php @@ -11,6 +11,7 @@ abstract class RecordInterface { private $listNotes; private $listEntryId; private $listEntryWeight; + private array $activeFilters = []; /** * Constructor. We build the object using all the data retrieved @@ -344,6 +345,7 @@ public static function getValidMoreDetailsSources() { 'citations' => 'Citations', 'copyDetails' => 'Copy Details (OverDrive)', 'staff' => 'Staff View', + 'accessibilityStatements' => 'Accessibility Statements (OverDrive)', ]; } @@ -378,6 +380,7 @@ public static function getDefaultMoreDetailsOptions() { 'citations' => 'closed', 'copyDetails' => 'closed', 'staff' => 'closed', + 'accessibilityStatements' => 'closed', ]; } @@ -441,6 +444,15 @@ function setListEntryWeight($listEntryWeight) { $this->listEntryWeight = $listEntryWeight; } + + function setActiveFilters($activeFilters): void { + $this->activeFilters = $activeFilters; + } + + function getActiveFilters(): array { + return $this->activeFilters; + } + function getListEntryWeight() { return $this->listEntryWeight; } diff --git a/code/web/RecordDrivers/SeriesRecordDriver.php b/code/web/RecordDrivers/SeriesRecordDriver.php index 93e1006c28..041322def8 100644 --- a/code/web/RecordDrivers/SeriesRecordDriver.php +++ b/code/web/RecordDrivers/SeriesRecordDriver.php @@ -80,11 +80,7 @@ public function getSearchResult($view = 'list', $showListsAppearingOn = true) { } else { $interface->assign('summAudience', ''); } - if (isset($this->fields['num_titles'])) { - $interface->assign('summNumTitles', $this->fields['num_titles']); - } else { - $interface->assign('summNumTitles', 0); - } + $interface->assign('summUrl', $this->getAbsoluteUrl()); global $solrScope; @@ -94,7 +90,15 @@ public function getSearchResult($view = 'list', $showListsAppearingOn = true) { $interface->assign('isNew', false); } - $listObject = $this->getSeriesObject(); + $seriesObject = $this->getSeriesObject(); + if ($seriesObject) { + $seriesTitles = $seriesObject->getTitles(); + $interface->assign('summNumTitles', count($seriesTitles['seriesMembers'])); + }else{ + $seriesTitles = $seriesObject->getTitles(); + $interface->assign('summNumTitles', 0); + } + if ($showListsAppearingOn) { //Check to see if there are lists the record is on @@ -240,7 +244,7 @@ public function getAudience() { return !empty($this->fields['audience']) ? $this->fields['audience'] : ''; } - private function getSeriesObject() { + private function getSeriesObject() : Series|false { if ($this->seriesObject == null) { require_once ROOT_DIR . '/sys/Series/Series.php'; $this->seriesObject = new Series(); diff --git a/code/web/RecordDrivers/SideLoadedRecord.php b/code/web/RecordDrivers/SideLoadedRecord.php index 66f6d1c337..a4faf02233 100644 --- a/code/web/RecordDrivers/SideLoadedRecord.php +++ b/code/web/RecordDrivers/SideLoadedRecord.php @@ -152,6 +152,7 @@ function createActionsFromUrls($relatedUrls, $itemInfo = null, $variationId = 'a $fileOrUrl = isset($urlInfo['url']) ? $urlInfo['url'] : $urlInfo['file']; if (strlen($fileOrUrl) > 0) { $actions[] = [ + 'type' => 'sideload_access_online', 'url' => $action, 'redirectUrl' => $fileOrUrl, 'title' => $title, diff --git a/code/web/RecordDrivers/TalpaRecordDriver.php b/code/web/RecordDrivers/TalpaRecordDriver.php index 95bb69f9a2..4d58501942 100644 --- a/code/web/RecordDrivers/TalpaRecordDriver.php +++ b/code/web/RecordDrivers/TalpaRecordDriver.php @@ -183,7 +183,6 @@ public function getSearchResult($inLibrary = false) { $interface->assign('summDescription', $groupedWorkDriver->getDescription()); $interface->assign('bookCoverUrl', $groupedWorkDriver->getBookcoverUrl('small')); $interface->assign('bookCoverUrlMedium', $groupedWorkDriver->getBookcoverUrl('medium')); - // $interface->assign('summDescription', $this->getDescriptionFast(true)); if ($groupedWorkDriver->hasCachedSeries()) { $interface->assign('ajaxSeries', false); $interface->assign('summSeries', $groupedWorkDriver->getSeries(false)); diff --git a/code/web/RecordDrivers/WebResourceRecordDriver.php b/code/web/RecordDrivers/WebResourceRecordDriver.php index f74376e7b1..912745b969 100644 --- a/code/web/RecordDrivers/WebResourceRecordDriver.php +++ b/code/web/RecordDrivers/WebResourceRecordDriver.php @@ -103,7 +103,24 @@ public function getUniqueID() { return $this->fields['id']; } + public function getNumericId(): string { + return str_replace('WebResource:', '', $this->getUniqueID()); + } + public function getLinkUrl($absolutePath = false) { + require_once ROOT_DIR . '/sys/WebBuilder/WebResource.php'; + $webResource = new WebResource(); + $webResource->id = $this->getNumericId(); + if ($webResource->find(true)) { + $libraryId = null; + $activeLibrary = Library::getActiveLibrary(); + if ($activeLibrary != null) { + $libraryId = $activeLibrary->libraryId; + } + + return $webResource->getUrlForLibrary($libraryId); + } + return $this->fields['source_url']; } } \ No newline at end of file diff --git a/code/web/bootstrap.php b/code/web/bootstrap.php index 84c05f71e9..5ec4c6bc6c 100644 --- a/code/web/bootstrap.php +++ b/code/web/bootstrap.php @@ -93,7 +93,7 @@ if (!$isValidServerName) { http_response_code(404); if (IPAddress::showDebuggingInformation()) { - echo("Invalid RequestInvalid Host $aspenUsage->getInstance(), valid instances are " . implode(', ', $validServerNames) . ""); + echo("Invalid RequestInvalid Host {$aspenUsage->getInstance()}, valid instances are " . implode(', ', $validServerNames) . ""); } else { echo("Invalid RequestInvalid Host"); } @@ -386,6 +386,7 @@ function getValidServerNames(): array { $validServerNames[] = "{$subdomain}t.$mainServer"; if ($mainServerBase != null) { $validServerNames[] = "{$subdomain}t.$mainServerBase"; + $validServerNames[] = "{$subdomain}x.$mainServerBase"; } } } @@ -402,6 +403,7 @@ function getValidServerNames(): array { $validServerNames[] = "{$code}x.$mainServer"; if ($mainServerBase != null) { $validServerNames[] = "{$code}t.$mainServerBase"; + $validServerNames[] = "{$code}x.$mainServerBase"; } } } @@ -416,8 +418,10 @@ function getValidServerNames(): array { } if ($isTestServer) { $validServerNames[] = "{$subdomain}t.$mainServer"; + $validServerNames[] = "{$subdomain}x.$mainServer"; if ($mainServerBase != null) { $validServerNames[] = "{$subdomain}t.$mainServerBase"; + $validServerNames[] = "{$subdomain}x.$mainServerBase"; } } } diff --git a/code/web/cron/backupAspen.php b/code/web/cron/backupAspen.php index 1d8abefa5a..097960dd53 100644 --- a/code/web/cron/backupAspen.php +++ b/code/web/cron/backupAspen.php @@ -8,6 +8,12 @@ global $aspen_db; +require_once ROOT_DIR . '/sys/CronLogEntry.php'; +$cronLogEntry = new CronLogEntry(); +$cronLogEntry->startTime = time(); +$cronLogEntry->name = 'Backup Aspen'; +$cronLogEntry->insert(); + $debug = false; $dbUser = $configArray['Database']['database_user']; @@ -23,6 +29,7 @@ } //Remove any backups older than 2 days +$cronLogEntry->notes .= date('g:i:s A') . " Removing old backups.
"; $currentFilesInBackup = scandir($backupDir); $earliestTimeToKeep = time() - (2 * 24 * 60 * 60); foreach ($currentFilesInBackup as $file) { @@ -50,10 +57,18 @@ //Create the tar file $curDateTime = date('ymdHis'); -$backupFile = "$backupDir/aspen.$serverName.$curDateTime.tar"; -//exec("tar -cf $backupFile"); +$todaysBackupDir = $backupDir . '/' . $curDateTime; +if (!file_exists($todaysBackupDir)) { + mkdir($todaysBackupDir, 700, true); +} + +$backupFile = "$backupDir/aspen.$serverName.$curDateTime.tar.gz"; +$compressCommand = ''; +$dumpScript = 'mysqldump'; if ($configArray['System']['operatingSystem'] != 'windows') { exec_advanced("cd $backupDir", $debug); + $compressCommand = '| pigz'; + $dumpScript = 'mariadb-dump'; } //Create the export files @@ -67,28 +82,44 @@ } $exportFile = "$serverName.$curDateTime.$table.sql"; + + $tableExtension = '.sql'; + if ($configArray['System']['operatingSystem'] != 'windows') { + $exportFile .= '.gz'; + } $fullExportFilePath = "$backupDir/$exportFile"; + $cronLogEntry->notes .= date('g:i:s A') . " Exporting $table.
"; + $cronLogEntry->update(); if ($exportData) { - $dumpCommand = "mariadb-dump -u$dbUser -p$dbPassword -h$dbHost -P$dbPort $dbName $table > $fullExportFilePath"; + $dumpCommand = "$dumpScript -u$dbUser -p$dbPassword -h$dbHost -P$dbPort --single-transaction --quick $dbName $table $compressCommand > $todaysBackupDir/$exportFile"; }else{ - $dumpCommand = "mariadb-dump -u$dbUser -p$dbPassword -h$dbHost -P$dbPort --no-data $dbName $table > $fullExportFilePath"; + $dumpCommand = "$dumpScript -u$dbUser -p$dbPassword -h$dbHost -P$dbPort --no-data $dbName $table $compressCommand > $todaysBackupDir/$exportFile"; } exec_advanced($dumpCommand, $debug); - //Add the file to the archive - if (file_exists($fullExportFilePath)) { - if ($configArray['System']['operatingSystem'] != 'windows') { - exec_advanced("cd $backupDir; tar -rf $backupFile $exportFile", $debug); - - unlink($fullExportFilePath); - } - } + //Do not add the file to the archive now, we will compress them all later } $listTablesStmt->closeCursor(); +$cronLogEntry->notes .= date('g:i:s A') . " All tables have been exported
"; +$cronLogEntry->update(); //zip up the archive +$cronLogEntry->notes .= date('g:i:s A') . " Creating tarball.
"; +$cronLogEntry->update(); if ($configArray['System']['operatingSystem'] != 'windows') { - exec_advanced("gzip $backupFile", $debug); + exec_advanced("tar -cf - -C $todaysBackupDir . $compressCommand > $backupFile", $debug); +}else{ + exec_advanced("tar -czf $backupFile -C $todaysBackupDir .", $debug); +} +$cronLogEntry->notes .= date('g:i:s A') . " Finished creating tarball.
"; +$cronLogEntry->update(); + +//Clean up exported files +if (file_exists($todaysBackupDir)) { + require_once ROOT_DIR . '/sys/Utils/SystemUtils.php'; + SystemUtils::recursive_rmdir($todaysBackupDir); + $cronLogEntry->notes .= date('g:i:s A') . " Cleaned up individual backup files.
"; + $cronLogEntry->update(); } //Optionally, move the file to the Google backup bucket @@ -99,10 +130,15 @@ // See if we have a bucket to back up to if ($systemVariables->find(true) && !empty($systemVariables->googleBucket)) { //Perform the backup + $cronLogEntry->notes .= date('g:i:s A') . " Sending backup to Google bucket.
"; $bucketName = $systemVariables->googleBucket; - exec_advanced("gsutil cp $backupFile.gz gs://$bucketName/", $debug); + exec_advanced("gsutil cp $backupFile gs://$bucketName/", $debug); } +$cronLogEntry->notes .= date('g:i:s A') . " Finished backup."; +$cronLogEntry->endTime = time(); +$cronLogEntry->update(); + $aspen_db = null; $configArray = null; die(); diff --git a/code/web/cron/checkSolrForDeletedWorks.php b/code/web/cron/checkSolrForDeletedWorks.php index e65fee6c28..e4e7768aa1 100644 --- a/code/web/cron/checkSolrForDeletedWorks.php +++ b/code/web/cron/checkSolrForDeletedWorks.php @@ -27,16 +27,16 @@ $numRecordsDeleted = 0; if (!$result instanceof AspenError && empty($result['error'])) { $numResults = $searchObject->getResultTotal(); - $cronLogEntry->notes .= date('h:i:s') . " There are $numResults records in Solr.
"; + $cronLogEntry->notes .= date('g:i:s A') . " There are $numResults records in Solr.
"; $solrBatchSize = 250; $searchObject->setTimeout(60); $searchObject->setLimit($solrBatchSize); $searchObject->clearFacets(); $numBatches = (int)ceil($numResults / $solrBatchSize); - $cronLogEntry->notes .= date('h:i:s') . " Processing in $numBatches batches.
"; + $cronLogEntry->notes .= date('g:i:s A') . " Processing in $numBatches batches.
"; for ($batchIndex = 1; $batchIndex <= $numBatches; $batchIndex++) { if ($batchIndex % 100 == 0) { - $cronLogEntry->notes .= date('h:i:s') . " Processing batch $batchIndex.
"; + $cronLogEntry->notes .= date('g:i:s A') . " Processing batch $batchIndex.
"; } $cronLogEntry->lastUpdate = time(); $cronLogEntry->update(); @@ -58,7 +58,7 @@ //Loop through to figure out which record(s) are missing foreach ($recordsInBatch as $groupedWorkId) { if (!isset($allResultsFromDB[$groupedWorkId])) { - $cronLogEntry->notes .= date('h:i:s') . " $groupedWorkId does not exist in the database and needs to be deleted.
"; + $cronLogEntry->notes .= date('g:i:s A') . " $groupedWorkId does not exist in the database and needs to be deleted.
"; $recordsToDeleteFromSolr[] = $groupedWorkId; $cronLogEntry->update(); } @@ -69,19 +69,19 @@ foreach ($recordsToDeleteFromSolr as $groupedWorkId) { if (!$solrConnection->deleteRecord($groupedWorkId)) { - $cronLogEntry->notes .= date('h:i:s') . " ERROR $groupedWorkId could not be deleted.
"; + $cronLogEntry->notes .= date('g:i:s A') . " ERROR $groupedWorkId could not be deleted.
"; $cronLogEntry->numErrors++; }else{ $numRecordsDeleted++; } } }else{ - $cronLogEntry->notes .= date('h:i:s') . " Could not connect to Solr.
"; + $cronLogEntry->notes .= date('g:i:s A') . " Could not connect to Solr.
"; } if ($numRecordsDeleted > 0) { $solrConnection->commit(); } -$cronLogEntry->notes .= date('h:i:s') . " Deleted $numRecordsDeleted records."; +$cronLogEntry->notes .= date('g:i:s A') . " Deleted $numRecordsDeleted records."; $cronLogEntry->endTime = time(); $cronLogEntry->update(); diff --git a/code/web/cron/importAspenBackup.php b/code/web/cron/importAspenBackup.php index 2af6fe88aa..8b50e2d437 100644 --- a/code/web/cron/importAspenBackup.php +++ b/code/web/cron/importAspenBackup.php @@ -30,7 +30,7 @@ fclose($fhnd); echo("Importing $exportFile\n"); - if (strpos($line, "/*!999999\- enable the sandbox mode") === 0){ + if (str_contains($line, "- enable the sandbox mode")){ $importCommand = "mysql -u$dbUser -p$dbPassword -h$dbHost -P$dbPort -D $dbName --force < $sqlBackupDir$exportFile"; }else{ $importCommand = "mysql -u$dbUser -p$dbPassword -h$dbHost -P$dbPort $dbName < $sqlBackupDir$exportFile "; diff --git a/code/web/cron/purgeSoftDeleted.php b/code/web/cron/purgeSoftDeleted.php index 2f9670d00b..e737068760 100644 --- a/code/web/cron/purgeSoftDeleted.php +++ b/code/web/cron/purgeSoftDeleted.php @@ -10,16 +10,15 @@ * so administrators still have the entire "Final Day" to * restore objects before automatic removal. */ + require_once ROOT_DIR . '/sys/CronLogEntry.php'; $cronLogEntry = new CronLogEntry(); $cronLogEntry->startTime = time(); $cronLogEntry->name = 'Purge Soft Deleted'; $cronLogEntry->insert(); -global $logger; require_once ROOT_DIR . '/services/Admin/ObjectRestorations.php'; $softDeleteClasses = Admin_ObjectRestorations::getManagedClasses(); - $totalPurged = 0; foreach ($softDeleteClasses as $className) { if (class_exists($className) && method_exists($className, 'purgeExpired')) { diff --git a/code/web/cron/updateCommunityEngagementMilestones.php b/code/web/cron/updateCommunityEngagementMilestones.php new file mode 100644 index 0000000000..90125c962b --- /dev/null +++ b/code/web/cron/updateCommunityEngagementMilestones.php @@ -0,0 +1,35 @@ +startTime = time(); + $cronLogEntry->name = 'Update Community Engagement Users'; + $cronLogEntry->insert(); + $userCampaigns = new UserCampaign(); + //adding 1 day to catch things that were enrolled today + $userCampaigns->whereAdd("enrollmentDate < '" . date("Y-m-d", strtotime("+1 day")) . "'"); + $userCampaigns->unenrollmentDate = null; + $userCampaigns->completed = 0; + $userIds = $userCampaigns->fetchAll("userId"); + $totalUpdated = 0; + foreach($userIds as $userID) + { + $user = new User(); + $user->id = $userID; + if($user->find(true)) + { + $cronLogEntry->notes .= "updating checkouts/holds for user: $userID
"; + $user->getCheckouts(); + $user->getHolds(); + $totalUpdated++; + $cronLogEntry->update(); + } + // TODO do we need to do a similar check for any other milestone criteria? + } + $cronLogEntry->notes .= "

Updating users complete: $totalUpdated users had checkouts and holds automatically checked."; + $cronLogEntry->endTime = time(); + $cronLogEntry->update(); + } +?> \ No newline at end of file diff --git a/code/web/cron/updateSavedSearches.php b/code/web/cron/updateSavedSearches.php index 7ab59647d2..7b29fdbba3 100644 --- a/code/web/cron/updateSavedSearches.php +++ b/code/web/cron/updateSavedSearches.php @@ -7,6 +7,11 @@ require_once ROOT_DIR . '/sys/Account/UserNotificationToken.php'; require_once ROOT_DIR . '/sys/Notifications/ExpoNotification.php'; +require_once ROOT_DIR . '/sys/CronLogEntry.php'; +$cronLogEntry = new CronLogEntry(); +$cronLogEntry->startTime = time(); +$cronLogEntry->name = 'Updating Saved Searches'; +$cronLogEntry->insert(); //Create a log entry $searchUpdateLogEntry = new SearchUpdateLogEntry(); @@ -133,6 +138,10 @@ $searchUpdateLogEntry->endTime = time(); $searchUpdateLogEntry->update(); +$cronLogEntry->notes .= "
Updated a total of " . $searchUpdateLogEntry->numUpdated. " searches"; +$cronLogEntry->endTime = time(); +$cronLogEntry->update(); + $search->__destruct(); $search = null; diff --git a/code/web/cron/updateTicketStats.php b/code/web/cron/updateTicketStats.php deleted file mode 100644 index 6a4553e3d9..0000000000 --- a/code/web/cron/updateTicketStats.php +++ /dev/null @@ -1,421 +0,0 @@ -count(); -$loadHistoricDataForTicketsByQueue = $numExistingStats == 0; -$ticketStat->__destruct(); -$ticketStat = null; - -$ticketStat = new TicketTrendBugsBySeverity(); -$numExistingStats = $ticketStat->count(); -$loadHistoricDataForBugsBySeverity = $numExistingStats == 0; -$ticketStat->__destruct(); -$ticketStat = null; - -$ticketStat = new TicketTrendByPartner(); -$numExistingStats = $ticketStat->count(); -$loadHistoricDataForTicketsByPartner = $numExistingStats == 0; -$ticketStat->__destruct(); -$ticketStat = null; - -//Tickets by Queue -if ($loadHistoricDataForTicketsByQueue) { - //Clear old data - $ticketStat = new TicketTrendByQueue(); - $ticketStat->delete(true); - $ticketStat->__destruct(); - $ticketStat = null; - - //Start with Mark's first Day at ByWater when tickets started getting entered - $startDate = strtotime('2020-10-29'); - $endDate = time(); - for ($tmpDate = $startDate; $tmpDate < $endDate; $tmpDate += 24 * 60 * 60) { - $nextDay = $tmpDate + 24 * 60 * 60; - $ticketQueues = new TicketQueueFeed(); - $ticketQueues->find(); - while ($ticketQueues->fetch()) { - //Open tickets - $ticketQuery = new Ticket(); - $ticketQuery->queue = $ticketQueues->name; - $ticketQuery->whereAdd("status <> 'Closed'"); - $ticketQuery->whereAdd("dateCreated <= $tmpDate"); - $numTickets = $ticketQuery->count(); - $ticketStat = new TicketTrendByQueue(); - $ticketStat->year = date('Y', $tmpDate); - $ticketStat->month = date('n', $tmpDate); - $ticketStat->day = date('j', $tmpDate); - $ticketStat->queue = $ticketQueues->name; - - if ($ticketStat->find(true)) { - $ticketStat->count = $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $ticketStat = null; - $ticketQuery = null; - - //Closed tickets - $ticketQuery = new Ticket(); - $ticketQuery->queue = $ticketQueues->name; - $ticketQuery->whereAdd("status = 'Closed'"); - $ticketQuery->whereAdd("dateCreated <= $tmpDate"); - $ticketQuery->whereAdd("dateClosed >= $nextDay"); - $numTickets = $ticketQuery->count(); - $ticketStat = new TicketTrendByQueue(); - $ticketStat->year = date('Y', $tmpDate); - $ticketStat->month = date('n', $tmpDate); - $ticketStat->day = date('j', $tmpDate); - $ticketStat->queue = $ticketQueues->name; - - if ($ticketStat->find(true)) { - $ticketStat->count = $ticketStat->count + $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $ticketStat = null; - $ticketQuery = null; - } - } -} else { - $ticketQueues = new TicketQueueFeed(); - $ticketQueues->find(); - while ($ticketQueues->fetch()) { - //Only updating current values from today - // Query tickets for today to generate stats - $ticketQuery = new Ticket(); - $ticketQuery->whereAdd("status <> 'Closed'"); - $ticketQuery->queue = $ticketQueues->name; - $numTickets = $ticketQuery->count(); - - $ticketStat = new TicketTrendByQueue(); - $ticketStat->year = date('Y'); - $ticketStat->month = date('n'); - $ticketStat->day = date('j'); - $ticketStat->queue = $ticketQueues->name; - - if ($ticketStat->find(true)) { - $ticketStat->count = $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $ticketStat = null; - $ticketQuery = null; - } - $ticketQueues = null; -} - -// Bugs by severity -if ($loadHistoricDataForBugsBySeverity) { - //Clear old data - $ticketStat = new TicketTrendBugsBySeverity(); - $ticketStat->delete(true); - $ticketStat = null; - - $severitiesToLoad = []; - $severitiesToLoad[] = null; - $ticketSeverity = new TicketSeverityFeed(); - $ticketSeverity->find(); - while ($ticketSeverity->fetch()) { - $severitiesToLoad[] = $ticketSeverity->name; - } - $ticketSeverity = null; - - //Start with Mark's first Day at ByWater when tickets started getting entered - $startDate = strtotime('2020-10-29'); - $endDate = time(); - for ($tmpDate = $startDate; $tmpDate < $endDate; $tmpDate += 24 * 60 * 60) { - $nextDay = $tmpDate + 24 * 60 * 60; - foreach ($severitiesToLoad as $severity) { - //Open tickets - $ticketQuery = new Ticket(); - $ticketQuery->queue = 'Bugs'; - if ($severity == null) { - $ticketQuery->whereAdd('severity IS NULL'); - } else { - $ticketQuery->severity = $severity; - } - $ticketQuery->whereAdd("status <> 'Closed'"); - $ticketQuery->whereAdd("dateCreated <= $tmpDate"); - $numTickets = $ticketQuery->count(); - $ticketStat = new TicketTrendBugsBySeverity(); - $ticketStat->year = date('Y', $tmpDate); - $ticketStat->month = date('n', $tmpDate); - $ticketStat->day = date('j', $tmpDate); - if ($severity == null) { - $ticketStat->severity = 'Not Set'; - } else { - $ticketStat->severity = $severity; - } - - if ($ticketStat->find(true)) { - $ticketStat->count = $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $ticketStat = null; - $ticketQuery = null; - - //Closed tickets - $ticketQuery = new Ticket(); - $ticketQuery->queue = 'Bugs'; - if ($severity == null) { - $ticketQuery->whereAdd('severity IS NULL'); - } else { - $ticketQuery->severity = $severity; - } - $ticketQuery->whereAdd("status = 'Closed'"); - $ticketQuery->whereAdd("dateCreated <= $tmpDate"); - $ticketQuery->whereAdd("dateClosed >= $nextDay"); - $numTickets = $ticketQuery->count(); - $ticketStat = new TicketTrendBugsBySeverity(); - $ticketStat->year = date('Y', $tmpDate); - $ticketStat->month = date('n', $tmpDate); - $ticketStat->day = date('j', $tmpDate); - if ($severity == null) { - $ticketStat->severity = 'Not Set'; - } else { - $ticketStat->severity = $severity; - } - - if ($ticketStat->find(true)) { - $ticketStat->count = $ticketStat->count + $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $ticketStat = null; - $ticketQuery = null; - } - } -}else { - $ticketSeverity = new TicketSeverityFeed(); - $ticketSeverity->find(); - while ($ticketSeverity->fetch()) { - // Query tickets for today to generate stats - $ticketQuery = new Ticket(); - $ticketQuery->queue = 'Bugs'; - $ticketQuery->whereAdd("status <> 'Closed'"); - $ticketQuery->severity = $ticketSeverity->name; - $numTickets = $ticketQuery->count(); - - $ticketStat = new TicketTrendBugsBySeverity(); - $ticketStat->year = date('Y'); - $ticketStat->month = date('n'); - $ticketStat->day = date('j'); - $ticketStat->severity = $ticketSeverity->name; - - if ($ticketStat->find(true)) { - $ticketStat->count = $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $ticketQuery = null; - $ticketStat = null; - } - $ticketSeverity = null; -} -//Tickets by Partner -if ($loadHistoricDataForTicketsByPartner) { - //Clear old data - $ticketStat = new TicketTrendByPartner(); - $ticketStat->delete(true); - $ticketStat = null; - - $aspenSite = new AspenSite(); - $aspenSite->siteType = "0"; - $aspenSite->whereAdd('implementationStatus <> 0 AND implementationStatus <> 4'); - $aspenSite->find(); - $partners = []; - $partners[] = null; - while ($aspenSite->fetch()){ - $partners[] = $aspenSite->id; - } - $aspenSite = null; - - //Start with Mark's first Day at ByWater when tickets started getting entered - $startDate = strtotime('2020-10-29'); - $endDate = time(); - for ($tmpDate = $startDate; $tmpDate < $endDate; $tmpDate += 24 * 60 * 60) { - $nextDay = $tmpDate + 24 * 60 * 60; - - //Open tickets - $ticketQuery = new Ticket(); - $ticketQuery->whereAdd("status <> 'Closed'"); - $ticketQuery->whereAdd("dateCreated <= $tmpDate"); - $ticketQuery->groupBy('requestingPartner'); - $ticketQuery->selectAdd(); - $ticketQuery->selectAdd('count(*) as numTickets'); - $ticketQuery->selectAdd('requestingPartner'); - $ticketQuery->find(); - $partnersFound = []; - while ($ticketQuery->fetch()) { - /** @noinspection PhpUndefinedFieldInspection */ - $numTickets = $ticketQuery->numTickets; - $ticketStat = new TicketTrendByPartner(); - $ticketStat->year = date('Y', $tmpDate); - $ticketStat->month = date('n', $tmpDate); - $ticketStat->day = date('j', $tmpDate); - $ticketStat->requestingPartner = $ticketQuery->requestingPartner; - if ($ticketStat->find(true)) { - $ticketStat->count = $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $partnersFound[$ticketQuery->requestingPartner] = $ticketQuery->requestingPartner; - } - $ticketStat = null; - $ticketQuery = null; - - //Closed tickets - $ticketQuery = new Ticket(); - $ticketQuery->whereAdd("status = 'Closed'"); - $ticketQuery->whereAdd("dateCreated <= $tmpDate"); - $ticketQuery->whereAdd("dateClosed >= $nextDay"); - $ticketQuery->groupBy('requestingPartner'); - $ticketQuery->selectAdd(); - $ticketQuery->selectAdd('count(*) as numTickets'); - $ticketQuery->selectAdd('requestingPartner'); - $ticketQuery->find(); - while ($ticketQuery->fetch()) { - /** @noinspection PhpUndefinedFieldInspection */ - $numTickets = $ticketQuery->numTickets; - $ticketStat = new TicketTrendByPartner(); - $ticketStat->year = date('Y', $tmpDate); - $ticketStat->month = date('n', $tmpDate); - $ticketStat->day = date('j', $tmpDate); - $ticketStat->requestingPartner = $ticketQuery->requestingPartner; - if ($ticketStat->find(true)) { - $ticketStat->count = $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $partnersFound[$ticketQuery->requestingPartner] = $ticketQuery->requestingPartner; - } - $ticketStat = null; - $ticketQuery = null; - - //Set 0's for this day for any partners that were not found: - foreach ($partners as $partnerId) { - if (!in_array($partnerId, $partnersFound)) { - $ticketStat = new TicketTrendByPartner(); - $ticketStat->year = date('Y', $tmpDate); - $ticketStat->month = date('n', $tmpDate); - $ticketStat->day = date('j', $tmpDate); - $ticketStat->requestingPartner = $partnerId; - if (!$ticketStat->find(true)) { - $ticketStat->count = 0; - $ticketStat->insert(); - } - $ticketStat = null; - } - } - } -} else { - require_once ROOT_DIR . '/sys/Greenhouse/AspenSite.php'; - $aspenSite = new AspenSite(); - $aspenSite->siteType = "0"; - $aspenSite->whereAdd('implementationStatus <> 0 AND implementationStatus <> 4'); - $aspenSite->find(); - while ($aspenSite->fetch()) { - $ticketQuery = new Ticket(); - $ticketQuery->requestingPartner = $aspenSite->id; - $ticketQuery->whereAdd("status <> 'Closed'"); - $numTickets = $ticketQuery->count(); - - $ticketStat = new TicketTrendByPartner(); - $ticketStat->year = date('Y'); - $ticketStat->month = date('n'); - $ticketStat->day = date('j'); - $ticketStat->requestingPartner = $aspenSite->id; - - if ($ticketStat->find(true)) { - $ticketStat->count = $numTickets; - $ticketStat->update(); - } else { - $ticketStat->count = $numTickets; - $ticketStat->insert(); - } - $ticketStat = null; - $ticketQuery = null; - } - $aspenSite = null; -} - - -//$ticketQueues = new TicketQueueFeed(); -//$ticketQueues->find(); -//while ($ticketQueues->fetch()) { -// // Loop through status -// $ticketStatus = new TicketStatusFeed(); -// $ticketStatus->find(); -// while ($ticketStatus->fetch()) { -// // Loop through severity -// $ticketSeverity = new TicketSeverityFeed(); -// $ticketSeverity->find(); -// while ($ticketSeverity->fetch()) { -// // Query tickets for today to generate stats -// $ticketQuery = new Ticket(); -// $ticketQuery->queue = $ticketQueues->name; -// $ticketQuery->status = $ticketStatus->name; -// $ticketQuery->severity = $ticketSeverity->name; -// $numTickets = $ticketQuery->count(); -// -// $ticketStat = new TicketStats(); -// $ticketStat->year = date('Y'); -// $ticketStat->month = date('n'); -// $ticketStat->day = date('j'); -// $ticketStat->queue = $ticketQueues->name; -// $ticketStat->status = $ticketStatus->name; -// $ticketStat->severity = $ticketSeverity->name; -// -// if ($ticketStat->find(true)) { -// $ticketStat->count = $ticketQuery->count(); -// $ticketStat->update(); -// } else { -// $ticketStat->count = $ticketQuery->count(); -// $ticketStat->insert(); -// } -// -// // Loop through Components -// -// -// } -// } -//} - -global $aspen_db; -$aspen_db = null; - -die(); - -/////// END OF PROCESS /////// \ No newline at end of file diff --git a/code/web/cron/updateTickets.php b/code/web/cron/updateTickets.php deleted file mode 100644 index 1218a2fcc9..0000000000 --- a/code/web/cron/updateTickets.php +++ /dev/null @@ -1,348 +0,0 @@ -find(true)) { - $rtAuthToken = $greenhouseSettings->requestTrackerAuthToken; - $baseRtUrl = $greenhouseSettings->requestTrackerBaseUrl; -} -$greenhouseSettings->__destruct(); -$greenhouseSettings = null; - -$openTicketsFound = []; -$ticketStatusFeeds = new TicketStatusFeed(); -$ticketStatusFeeds->find(); -while ($ticketStatusFeeds->fetch()) { - $ticketsInFeed = getTicketInfoFromFeed('Status ' . $ticketStatusFeeds->name, $ticketStatusFeeds->rssFeed); - foreach ($ticketsInFeed as $ticketInfo) { - $ticket = getTicket($ticketInfo); - $ticket->status = $ticketStatusFeeds->name; - try { - $ticket->update(); - $openTicketsFound[$ticket->ticketId] = $ticket->ticketId; - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - $ticket = null; - } -} -$ticketStatusFeeds->__destruct(); -$ticketStatusFeeds = null; - -//There are too many closed tickets to get an RSS feed, we need to just mark anything closed we don't see. -$ticket = new Ticket(); -$ticket->whereAdd("status <> 'Closed'"); -$ticket->find(); -while ($ticket->fetch()) { - if (!in_array($ticket->ticketId, $openTicketsFound)) { - $ticket->status = 'Closed'; - $ticket->dateClosed = time(); - try { - $ticket->update(); - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - } -} -$ticket->__destruct(); -$ticket = null; - -//Update all tickets based on their queues -require_once ROOT_DIR . '/sys/Support/TicketQueueFeed.php'; -$ticketQueueFeeds = new TicketQueueFeed(); -$ticketQueueFeeds->find(); -while ($ticketQueueFeeds->fetch()) { - $ticketsInFeed = getTicketInfoFromFeed('Queue ' . $ticketQueueFeeds->name, $ticketQueueFeeds->rssFeed); - foreach ($ticketsInFeed as $ticketInfo) { - $ticket = getTicket($ticketInfo); - $ticket->queue = $ticketQueueFeeds->name; - try { - $ticket->update(); - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - $ticket = null; - } -} -$ticketQueueFeeds->__destruct(); -$ticketQueueFeeds = null; - -//Update all tickets based on their severity -require_once ROOT_DIR . '/sys/Support/TicketSeverityFeed.php'; -$ticketSeverityFeeds = new TicketSeverityFeed(); -$ticketSeverityFeeds->find(); -while ($ticketSeverityFeeds->fetch()) { - $ticketsInFeed = getTicketInfoFromFeed('Severity ' . $ticketSeverityFeeds->name, $ticketSeverityFeeds->rssFeed); - foreach ($ticketsInFeed as $ticketInfo) { - $ticket = getTicket($ticketInfo); - $ticket->severity = $ticketSeverityFeeds->name; - try { - $ticket->update(); - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - $ticket = null; - } -} -$ticketSeverityFeeds->__destruct(); -$ticketSeverityFeeds = null; - -//Update all tickets based on assigned component -$tmpTicket = new Ticket(); -$tmpTicket->whereAdd("status <> 'Closed'"); -/** @var Ticket[] $allOpenTickets */ -$allOpenTickets = $tmpTicket->fetchAll(); - -//Get a list of all components -$allComponents = new TicketComponentFeed(); -$allComponents->find(); -$allComponentsByName = []; -while ($allComponents->fetch()) { - $allComponentsByName[$allComponents->name] = clone $allComponents; -} -$allComponents->__destruct(); -$allComponents = null; - -$curlConnection = new CurlWrapper(); -foreach ($allOpenTickets as $openTicket) { - $ticketInfoUrl = $baseRtUrl . '/REST/2.0/ticket/' . $openTicket->ticketId . "?token=$rtAuthToken"; - $response = $curlConnection->curlGetPage($ticketInfoUrl); - $json = json_decode($response); - $customFields = $json->CustomFields; - /** @var ComponentTicketLink $relatedComponents */ - $relatedComponents = []; - $existingComponents = $openTicket->getRelatedComponents(); - foreach ($customFields as $customField) { - if ($customField->name == 'Aspen Discovery Components') { - foreach ($customField->values as $value) { - //Get the right component object - if (array_key_exists($value, $allComponentsByName)) { - $relatedComponent = $allComponentsByName[$value]; - } else { - //This is a new component we haven't seen, add it - $ticketComponent = new TicketComponentFeed(); - $ticketComponent->name = $value; - $ticketComponent->insert(); - $allComponentsByName[$ticketComponent->name] = clone $ticketComponent; - $relatedComponent = $ticketComponent; - } - //Check to see if we are already linked to that component - $foundExistingLink = false; - foreach ($existingComponents as $existingComponent) { - if ($existingComponent->componentId == $relatedComponent->id) { - $relatedComponents[] = $existingComponent; - $foundExistingLink = true; - } - } - if (!$foundExistingLink) { - $componentLink = new ComponentTicketLink(); - $componentLink->ticketId = $openTicket->id; - $componentLink->componentId = $relatedComponent->id; - $relatedComponents[] = $componentLink; - } - } - break; - } - } - $openTicket->setRelatedComponents($relatedComponents); - $openTicket->update(); -} -$curlConnection = null; - -//Update all tickets from partner feeds - -//Update partner priorities -require_once ROOT_DIR . '/sys/Greenhouse/AspenSite.php'; -$aspenSite = new AspenSite(); -$aspenSite->siteType = "0"; -$aspenSite->whereAdd('implementationStatus <> 0 AND implementationStatus <> 4'); -$aspenSite->find(); -while ($aspenSite->fetch()) { - if (!empty($aspenSite->getSiteBaseUrl())) { - $priority1Ticket = -1; - $priority2Ticket = -1; - $priority3Ticket = -1; - $prioritiesUrl = $aspenSite->getSiteBaseUrl() . '/API/SystemAPI?method=getDevelopmentPriorities'; - $prioritiesData = file_get_contents($prioritiesUrl); - if ($prioritiesData) { - $prioritiesData = json_decode($prioritiesData); - echo(print_r($prioritiesData, true)); - //Get existing priorities for the partner - if ($prioritiesData->result->success) { - $priority1Ticket = $prioritiesData->result->priorities->priority1->id; - $priority2Ticket = $prioritiesData->result->priorities->priority2->id; - $priority3Ticket = $prioritiesData->result->priorities->priority3->id; - } - } - //Get a list of all tickets for the partner - if (!empty($aspenSite->getActiveTicketFeed())) { - $ticketsInFeed = getTicketInfoFromFeed($aspenSite->getSiteName(), $aspenSite->getActiveTicketFeed()); - foreach ($ticketsInFeed as $ticketInfo) { - $ticket = getTicket($ticketInfo); - $ticket->requestingPartner = $aspenSite->getSiteId(); - $newPriority = -1; - if ($ticket->ticketId == $priority1Ticket) { - $newPriority = 1; - } elseif ($ticket->ticketId == $priority2Ticket) { - $newPriority = 2; - } elseif ($ticket->ticketId == $priority3Ticket) { - $newPriority = 3; - } - if ($newPriority != $ticket->partnerPriority) { - $ticket->partnerPriority = $newPriority; - $ticket->partnerPriorityChangeDate = time(); - } - try { - $ticket->update(); - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - $ticket->__destruct(); - $ticket = null; - } - } else { - if ($priority1Ticket != -1) { - $ticket = new Ticket(); - $ticket->ticketId = $priority1Ticket; - if ($ticket->find(true)) { - $ticket->requestingPartner = $aspenSite->getSiteId(); - if ($ticket->partnerPriority != 1) { - $ticket->partnerPriority = 1; - $ticket->partnerPriorityChangeDate = time(); - } - try { - $ticket->update(); - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - } - $ticket->__destruct(); - $ticket = null; - } - if ($priority2Ticket != -1) { - $ticket = new Ticket(); - $ticket->ticketId = $priority2Ticket; - if ($ticket->find(true)) { - $ticket->requestingPartner = $aspenSite->getSiteId(); - if ($ticket->partnerPriority != 2) { - $ticket->partnerPriority = 2; - $ticket->partnerPriorityChangeDate = time(); - } - try { - $ticket->update(); - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - } - $ticket->__destruct(); - $ticket = null; - } - if ($priority3Ticket != -1) { - $ticket = new Ticket(); - $ticket->ticketId = $priority3Ticket; - if ($ticket->find(true)) { - $ticket->requestingPartner = $aspenSite->getSiteId(); - if ($ticket->partnerPriority != 3) { - $ticket->partnerPriority = 3; - $ticket->partnerPriorityChangeDate = time(); - } - try { - $ticket->update(); - } catch (PDOException $e) { - echo("Could not update ticket $ticket->ticketId " . $ticket->getLastError()); - } - } - $ticket->__destruct(); - $ticket = null; - } - } - } -} -$aspenSite = null; - -//Update Ticket Components, we will loop through all open tickets - - -//Update stats for today -//require_once ROOT_DIR . '/sys/Support/TicketStats.php'; -//$ticketStats = new TicketStats(); -//$ticketStats->year = date('Y'); -//$ticketStats->month = date('n'); -//$ticketStats->day = date('d'); - -global $aspen_db; -$aspen_db = null; - -die(); - -/////// END OF PROCESS /////// - -function getTicketInfoFromFeed($name, $feedUrl): array { - $rssDataRaw = @file_get_contents($feedUrl); - fwrite(STDOUT, "Loading $name - $feedUrl \n"); - if ($rssDataRaw == false) { - echo("Could not load data from $feedUrl \r\n"); - fwrite(STDOUT, " No data found \n"); - return []; - } else { - $activeTickets = []; - try { - $rssData = new SimpleXMLElement($rssDataRaw); - $ns = $rssData->getNamespaces(true); - if (!empty($rssData->item)) { - foreach ($rssData->item as $item) { - $matches = []; - preg_match('/.*id=(\d+)/', $item->link, $matches); - $dcData = $item->children($ns['dc']); - $activeTickets[$matches[1]] = [ - 'id' => $matches[1], - 'title' => (string)$item->title, - 'description' => (string)$item->description, - 'link' => (string)$item->link, - 'dateCreated' => (string)$dcData->date, - ]; - } - fwrite(STDOUT, " Found " . count($rssData->item) . " \n"); - } else { - fwrite(STDOUT, " Found 0 \n"); - } - } /** @noinspection PhpUnusedLocalVariableInspection */ catch (Exception $e) { - fwrite(STDOUT, " Could not parse data \n"); - } - return $activeTickets; - } -} - -function getTicket($ticketInfo): Ticket { - $ticket = new Ticket(); - $ticket->ticketId = $ticketInfo['id']; - if (!$ticket->find(true)) { - $ticket = new Ticket(); - $ticket->ticketId = $ticketInfo['id']; - $ticket->title = $ticketInfo['title']; - $ticket->description = $ticketInfo['description']; - $ticket->displayUrl = $ticketInfo['link']; - $ticket->dateCreated = strtotime($ticketInfo['dateCreated']); - try { - if (!$ticket->insert()) { - echo("Could not create ticket $ticket->ticketId " . $ticket->getLastError()); - fwrite(STDOUT, "Could not create ticket $ticket->ticketId " . $ticket->getLastError() . "\n"); - } - } catch (PDOException $e) { - echo("Could not create ticket $ticket->ticketId " . $e); - fwrite(STDOUT, "Could not create ticket $ticket->ticketId " . $e . "\n"); - } - } - return $ticket; -} \ No newline at end of file diff --git a/code/web/fonts/NotoSansBengali-Bold.ttf b/code/web/fonts/NotoSansBengali-Bold.ttf new file mode 100644 index 0000000000..fcdacfc125 Binary files /dev/null and b/code/web/fonts/NotoSansBengali-Bold.ttf differ diff --git a/code/web/fonts/NotoSansBengali-Regular.ttf b/code/web/fonts/NotoSansBengali-Regular.ttf new file mode 100644 index 0000000000..fe0ae042c2 Binary files /dev/null and b/code/web/fonts/NotoSansBengali-Regular.ttf differ diff --git a/code/web/index.php b/code/web/index.php index ab9f818fdb..c6c0142d44 100644 --- a/code/web/index.php +++ b/code/web/index.php @@ -241,13 +241,6 @@ $systemVariables = false; } -//Check to see if we should show the submit ticket option -$interface->assign('showSubmitTicket', false); -if (UserAccount::isLoggedIn() && UserAccount::userHasPermission('Submit Ticket')) { - if (!empty($systemVariables) && !empty($systemVariables->ticketEmail)) { - $interface->assign('showSubmitTicket', true); - } -} //Check to see if we should show the cookieConsent banner $interface->assign('cookieStorageConsent', false); $interface->assign('cookieStorageConsentHTML', ''); @@ -810,7 +803,7 @@ $librarySystemMessage->setPreFormattedMessage($library->systemMessage); $systemMessages[] = $librarySystemMessage; } - $systemMessages += SystemMessage::getActiveSystemMessages(); + $systemMessages = array_merge($systemMessages, SystemMessage::getActiveSystemMessages()); $interface->assign('systemMessages', $systemMessages); } catch (Exception $e) { @@ -1371,39 +1364,37 @@ function isSpammySearchTerm($lookfor): bool { } $lookfor = $decoded; - if (strpos($lookfor, 'DBMS_PIPE.RECEIVE_MESSAGE') !== false) { - return true; - } elseif (strpos($lookfor, 'PG_SLEEP') !== false) { + if (str_contains($lookfor, 'DBMS_PIPE.RECEIVE_MESSAGE')) { return true; - } elseif (strpos($lookfor, 'SELECT') !== false) { + } elseif (str_contains($lookfor, 'PG_SLEEP')) { return true; - } elseif (strpos($lookfor, 'SLEEP') !== false) { + } elseif (str_contains($lookfor, 'SELECT')) { return true; - } elseif (strpos($lookfor, 'ORDER BY') !== false) { + } elseif (str_contains($lookfor, 'ORDER BY')) { return true; - } elseif (strpos($lookfor, 'WAITFOR') !== false) { + } elseif (str_contains($lookfor, 'WAITFOR')) { return true; - } elseif (strpos($lookfor, 'nvOpzp') !== false) { + } elseif (str_contains($lookfor, 'nvOpzp')) { return true; - } elseif (strpos($lookfor, 'window.location') !== false) { + } elseif (str_contains($lookfor, 'window.location')) { return true; - } elseif (strpos($lookfor, 'window.top') !== false) { + } elseif (str_contains($lookfor, 'window.top')) { return true; - } elseif (strpos($lookfor, 'nslookup') !== false) { + } elseif (str_contains($lookfor, 'nslookup')) { return true; - } elseif (strpos($lookfor, 'if(') !== false) { + } elseif (str_contains($lookfor, 'if(')) { return true; - } elseif (strpos($lookfor, 'now(') !== false) { + } elseif (str_contains($lookfor, 'now(')) { return true; - } elseif (strpos($lookfor, 'sysdate()') !== false) { + } elseif (str_contains($lookfor, 'sysdate()')) { return true; - } elseif (strpos($lookfor, 'sleep(') !== false) { + } elseif (str_contains($lookfor, 'sleep(')) { return true; - } elseif (strpos($lookfor, 'cast(') !== false) { + } elseif (str_contains($lookfor, 'cast(')) { return true; - } elseif (strpos($lookfor, 'current_database') !== false) { + } elseif (str_contains($lookfor, 'current_database')) { return true; - } elseif (strpos($lookfor, 'response.write') !== false) { + } elseif (str_contains($lookfor, 'response.write')) { return true; } diff --git a/code/web/interface/themes/responsive/AJAX/libraryHoursAndLocations.tpl b/code/web/interface/themes/responsive/AJAX/libraryHoursAndLocations.tpl index f618da555f..db2e6dc75f 100644 --- a/code/web/interface/themes/responsive/AJAX/libraryHoursAndLocations.tpl +++ b/code/web/interface/themes/responsive/AJAX/libraryHoursAndLocations.tpl @@ -107,7 +107,7 @@ {/foreach}
{/if} - {if !empty($curLocation.latitude) && $curLocation.latitude !== 0} + {if !empty($curLocation.latitude) && $curLocation.latitude !== 0 || $curLocation.useLocationNameForMaps == 1}
{/if} diff --git a/code/web/interface/themes/responsive/API/apiDocumentation.tpl b/code/web/interface/themes/responsive/API/apiDocumentation.tpl index 2cbb4ef02b..fb9394fdc7 100644 --- a/code/web/interface/themes/responsive/API/apiDocumentation.tpl +++ b/code/web/interface/themes/responsive/API/apiDocumentation.tpl @@ -8,14 +8,14 @@ default-schema-tab="schema" allow-try="false" allow-authentication="false" - theme="light" bg-color="{$bodyBackgroundColor}" header-color="{$bodyBackgroundColor}" regular-font="{$bodyFont}" mono-font="'Consolas', monospace" text-color="{$bodyTextColor}" primary-color="{$linkColor}" - nav-bg-color="{$secondaryBackgroundColor}"> + nav-bg-color="{$secondaryBackgroundColor}" + {if $isDarkColorScheme}theme="dark"{else}theme="light"{/if}> {strip} diff --git a/code/web/interface/themes/responsive/Admin/collectionSpotlight.tpl b/code/web/interface/themes/responsive/Admin/collectionSpotlight.tpl index 76f3c8fa1a..d53bfe8402 100644 --- a/code/web/interface/themes/responsive/Admin/collectionSpotlight.tpl +++ b/code/web/interface/themes/responsive/Admin/collectionSpotlight.tpl @@ -1,13 +1,17 @@

{translate text="View Collection Spotlight" isAdminFacing=true}

+ + {if !empty($canDelete)} + + {/if} {* Show details for the selected spotlight *}

{$object->name}


@@ -154,3 +158,11 @@ window.setSpotlightSizing = setSpotlightSizing; {/literal} + + diff --git a/code/web/interface/themes/responsive/Admin/collectionSpotlightEdit.tpl b/code/web/interface/themes/responsive/Admin/collectionSpotlightEdit.tpl index c5d78b4b73..9cd989e6a6 100644 --- a/code/web/interface/themes/responsive/Admin/collectionSpotlightEdit.tpl +++ b/code/web/interface/themes/responsive/Admin/collectionSpotlightEdit.tpl @@ -3,14 +3,26 @@

{translate text="Edit Collection Spotlight" isAdminFacing=true}

+ + {if !empty($canDelete)} + + {/if} {$editForm}
-{/strip} \ No newline at end of file +{/strip} + + \ No newline at end of file diff --git a/code/web/interface/themes/responsive/Admin/compareObjects.tpl b/code/web/interface/themes/responsive/Admin/compareObjects.tpl index 7a0160585d..85b4b76c02 100644 --- a/code/web/interface/themes/responsive/Admin/compareObjects.tpl +++ b/code/web/interface/themes/responsive/Admin/compareObjects.tpl @@ -7,7 +7,9 @@
- {if !empty($showReturnToList)} + {if !empty($returnToListUrl)} + {translate text="Return to List" isAdminFacing=true} + {else} {translate text="Return to List" isAdminFacing=true} {/if}
diff --git a/code/web/interface/themes/responsive/Admin/objectEditor.tpl b/code/web/interface/themes/responsive/Admin/objectEditor.tpl index 1819383869..fb32c4f8d4 100644 --- a/code/web/interface/themes/responsive/Admin/objectEditor.tpl +++ b/code/web/interface/themes/responsive/Admin/objectEditor.tpl @@ -22,12 +22,16 @@
- {if !empty($showReturnToList) && !empty($object) && !empty($object->formId)} - {translate text="Return to List" isAdminFacing=true} - {elseif !empty($showReturnToList) && !empty($object) && !empty($object->pollId)} - {translate text="Return to List" isAdminFacing=true} - {elseif !empty($showReturnToList)} - {translate text="Return to List" isAdminFacing=true} + {if !empty($showReturnToList)} + {if !empty($returnToListUrl)} + {translate text="Return to List" isAdminFacing=true} + {elseif !empty($object) && !empty($object->formId)} + {translate text="Return to List" isAdminFacing=true} + {elseif !empty($object) && !empty($object->pollId)} + {translate text="Return to List" isAdminFacing=true} + {else} + {translate text="Return to List" isAdminFacing=true} + {/if} {/if} {if !empty($id)} {translate text="History" isAdminFacing=true} diff --git a/code/web/interface/themes/responsive/Admin/objectHistory.tpl b/code/web/interface/themes/responsive/Admin/objectHistory.tpl index 1e4d8917de..759851ac77 100644 --- a/code/web/interface/themes/responsive/Admin/objectHistory.tpl +++ b/code/web/interface/themes/responsive/Admin/objectHistory.tpl @@ -4,10 +4,14 @@ {translate text="Only 90 days of history is included" isAdminFacing=true}
{if !empty($showReturnToList)} - {translate text="Return to List" isAdminFacing=true} + {if !empty($returnToListUrl)} + {translate text="Return to List" isAdminFacing=true} + {else} + {translate text="Return to List" isAdminFacing=true} + {/if} {/if} {if !empty($showEditButtonsInCompareAndHistoryViews)} -{translate text="Edit" isAdminFacing=true} +{translate text="Edit" isAdminFacing=true} {/if} {if count($objectHistory) > 0} diff --git a/code/web/interface/themes/responsive/Admin/permissionsByRole.tpl b/code/web/interface/themes/responsive/Admin/permissionsByRole.tpl new file mode 100644 index 0000000000..71075db7c0 --- /dev/null +++ b/code/web/interface/themes/responsive/Admin/permissionsByRole.tpl @@ -0,0 +1,35 @@ +{strip} +

{translate text="Permissions by Role" isAdminFacing=true}

+ +
+ + + + + {foreach from=$roles item=$role} + + {/foreach} + + + + {foreach from=$permissionsBySection item=sectionPermissions key=sectionName} + + + + {foreach from=$sectionPermissions item=permission} + + + {foreach from=$roles item=$role} + + {/foreach} + + {/foreach} + {/foreach} + +
{$role->name}
{$sectionName}
{$permission}{if $role->hasPermission($permission)}X{/if}
+
+ +
+ {translate text="Export To CSV" isAdminFacing=true} + +{/strip} diff --git a/code/web/interface/themes/responsive/Admin/propertiesList.tpl b/code/web/interface/themes/responsive/Admin/propertiesList.tpl index 220cb28cb1..9540e0e837 100644 --- a/code/web/interface/themes/responsive/Admin/propertiesList.tpl +++ b/code/web/interface/themes/responsive/Admin/propertiesList.tpl @@ -85,7 +85,7 @@ - {if $canCompare || $canBatchUpdate || $canExportToCSV} + {if $canCompare || $canBatchUpdate || $canExportToCSV || $canBatchDelete} {/if} {foreach from=$structure item=property key=id} @@ -101,7 +101,7 @@ {foreach from=$dataList item=dataItem key=id} {assign var=canEdit value=$dataItem->canActiveUserEdit()} - {if $canCompare || $canBatchUpdate || $canExportToCSV} + {if $canCompare || $canBatchUpdate || $canExportToCSV || $canBatchDelete} {/if} {foreach from=$structure item=property} @@ -112,7 +112,7 @@ {if $property.type == 'label'} {if empty($dataItem->class) || $dataItem->class != 'objectDeleted'} {if $dataItem->canActiveUserEdit()} - {if $propName == $dataItem->getPrimaryKey()} + {if $propName == $dataItem->getPrimaryKey()} {/if} {if empty($propValue)} {translate text="Not Set" isAdminFacing=true} @@ -169,9 +169,20 @@ {/if} {elseif $property.type == 'multiSelect'} {if is_array($propValue) && count($propValue) > 0} - {foreach from=$property.values item=propertyName key=propertyValue} - {if array_key_exists($propertyValue, $propValue)}{$propertyName|escape}
{/if} + {assign var=numShown value=0} + {assign var=numSelected value=0} + {foreach from=$property.values item=propertyName key=propertyValue name="multiSelectValues"} + {if array_key_exists($propertyValue, $propValue)} + {assign var=numSelected value=$numSelected+1} + {if $numShown < 4} + {$propertyName|escape}
+ {assign var=numShown value=$numShown+1} + {/if} + {/if} {/foreach} + {if $numSelected >= 4} + {translate text="and %1% more values" 1=$numSelected-3 isAdminFacing='true'} + {/if} {else} {translate text="No values selected" isAdminFacing='true'} {/if} @@ -199,7 +210,7 @@
{foreach from=$propValue item=subObject} {assign var=subObjectId value=$subObject->getPrimaryKeyValue()} + {assign var=instanceStructure value=$subObject->_instanceStructure} {if !empty($property.sortable)} @@ -33,21 +34,30 @@ {if !empty($showVolume)} @@ -23,6 +17,11 @@ + {if !empty($hasBarcode) && $showItemBarcodes} + + {/if} {if !empty($hasNote) && $showItemNotes} {if !empty($showVolume)} + {if !empty($hasBarcode) && $showItemBarcodes} + + {/if} {if !empty($hasNote) && $showItemNotes} @@ -70,4 +69,4 @@ {/if} -{/strip} \ No newline at end of file +{/strip} diff --git a/code/web/interface/themes/responsive/Record/full-record.tpl b/code/web/interface/themes/responsive/Record/full-record.tpl index 3a02690018..83d0936869 100644 --- a/code/web/interface/themes/responsive/Record/full-record.tpl +++ b/code/web/interface/themes/responsive/Record/full-record.tpl @@ -20,7 +20,14 @@ {if $recordDriver->getFormats()}
- ({implode subject=$recordDriver->getFormats() glue=", " translate=true isPublicFacing=true}) + {assign var=formats value=$recordDriver->getFormats()} + {assign var=hasEmptyFormat value=false} + {foreach from=$formats item=format} + {if empty($format)} + {assign var=hasEmptyFormat value=true} + {/if} + {/foreach} + ({if $hasEmptyFormat}{translate text="Missing Format" isPublicFacing=true}{else}{implode subject=$formats glue=", " translate=true isPublicFacing=true}{/if}) {if $recordDriver->isClosedCaptioned()}   {/if} @@ -32,7 +39,7 @@
{if $disableCoverArt != 1}
- {translate text='Book Cover' isPublicFacing=true inAttribute=true} + {translate text='Book Cover' isPublicFacing=true inAttribute=true}
{/if} {if !empty($showRatings)} diff --git a/code/web/interface/themes/responsive/Record/hold-popup.tpl b/code/web/interface/themes/responsive/Record/hold-popup.tpl index 12eb66d229..e6372a3ebf 100644 --- a/code/web/interface/themes/responsive/Record/hold-popup.tpl +++ b/code/web/interface/themes/responsive/Record/hold-popup.tpl @@ -79,7 +79,7 @@ {if is_string($location)} {else} - + {/if} {/foreach} {else} @@ -185,6 +185,66 @@
{/if} + + {if $holdPromptForEditions > 0 && count($editionOptions) > 0 && $promptForEdition} +
+ + +
+
+
+
+
+
+
+ {assign var=firstEdition value=""} + {foreach from=$editionOptions item=edition name=editions} + {if $smarty.foreach.editions.index ==0} + {assign var=firstEdition value=$edition->databaseId} + {/if} + {assign var=current value=$smarty.foreach.editions.index + 1} +
+ +
+ {/foreach} +
+
+
+ +
+
+
+
+ +
+ {/if} {if !empty($promptForHoldNotifications)}
{include file=$holdNotificationTemplate} @@ -200,9 +260,9 @@ {/if}
{if $showLogMeOut == 1} -
- -
+
+ +
{/if}
diff --git a/code/web/interface/themes/responsive/Record/hold-select-volume-popup.tpl b/code/web/interface/themes/responsive/Record/hold-select-volume-popup.tpl index 41e57bd6f6..9166a6cade 100644 --- a/code/web/interface/themes/responsive/Record/hold-select-volume-popup.tpl +++ b/code/web/interface/themes/responsive/Record/hold-select-volume-popup.tpl @@ -1,5 +1,5 @@ {strip} -
+
@@ -118,14 +118,40 @@ {/if}
- {foreach from=$volumes item=volume} - + {/foreach}
+ {if $holdPromptForEditions > 0 && $promptForEdition} +
+ + + +
+ {/if} + {if $showHoldCancelDate == 1}
diff --git a/code/web/interface/themes/responsive/Record/result-tools.tpl b/code/web/interface/themes/responsive/Record/result-tools.tpl index bb80202892..87634e2640 100644 --- a/code/web/interface/themes/responsive/Record/result-tools.tpl +++ b/code/web/interface/themes/responsive/Record/result-tools.tpl @@ -3,7 +3,7 @@
diff --git a/code/web/interface/themes/responsive/Record/view-continued-by-records.tpl b/code/web/interface/themes/responsive/Record/view-continued-by-records.tpl index a305b8181c..993eae4938 100644 --- a/code/web/interface/themes/responsive/Record/view-continued-by-records.tpl +++ b/code/web/interface/themes/responsive/Record/view-continued-by-records.tpl @@ -13,9 +13,9 @@ diff --git a/code/web/interface/themes/responsive/Record/view-more-details.tpl b/code/web/interface/themes/responsive/Record/view-more-details.tpl index fdfb1175bb..f707bcac44 100644 --- a/code/web/interface/themes/responsive/Record/view-more-details.tpl +++ b/code/web/interface/themes/responsive/Record/view-more-details.tpl @@ -119,7 +119,9 @@ {/if} {if !empty($notes)} -

{translate text='Notes' isPublicFacing=true}

+
+ {translate text='Notes' isPublicFacing=true} +
{foreach from=$notes item=note name=loop}
{translate text=$note.label isPublicFacing=true isMetadata=true}
diff --git a/code/web/interface/themes/responsive/Record/view-title-details.tpl b/code/web/interface/themes/responsive/Record/view-title-details.tpl index a50786100f..47e3a42399 100644 --- a/code/web/interface/themes/responsive/Record/view-title-details.tpl +++ b/code/web/interface/themes/responsive/Record/view-title-details.tpl @@ -158,8 +158,8 @@ {if !empty($mpaaRating)}
-
{translate text='Rating' isPublicFacing=true}
-
{$mpaaRating|escape}
+
{translate text='MPAA Rating' isPublicFacing=true}
+
{implode subject=$mpaaRating glue=", " translate=true isPublicFacing=true}
{/if} diff --git a/code/web/interface/themes/responsive/RecordDrivers/Axis360/staff-view.tpl b/code/web/interface/themes/responsive/RecordDrivers/Axis360/staff-view.tpl index 757a5275f0..c547e1bcf1 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Axis360/staff-view.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Axis360/staff-view.tpl @@ -18,10 +18,24 @@ {/if} {if !empty($loggedIn) && in_array('Set Grouped Work Display Information', $userPermissions)} - {/if} - {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} - + {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions) && !$recordDriver->isInManuallyGroupedWork()} + {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/CloudLibrary/staff.tpl b/code/web/interface/themes/responsive/RecordDrivers/CloudLibrary/staff.tpl index 1c37d6255c..afd5569a01 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/CloudLibrary/staff.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/CloudLibrary/staff.tpl @@ -24,8 +24,22 @@ {/if} {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} - - + {/if} {if !empty($loggedIn) && in_array('Download MARC Records', $userPermissions)} {translate text="Download Marc" isAdminFacing=true} diff --git a/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/listEntry.tpl index 61c4808178..93f45568bb 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/listEntry.tpl @@ -10,7 +10,7 @@
{if $disableCoverArt != 1} - {translate text='Cover Image' inAttribute=true isPublicFacing=true} + {translate text='Cover Image' inAttribute=true isPublicFacing=true} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/result.tpl index 3d30a229b4..f3f2ed5fbc 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/CourseReserve/result.tpl @@ -4,7 +4,7 @@ diff --git a/code/web/interface/themes/responsive/RecordDrivers/EBSCO/combinedResult.tpl b/code/web/interface/themes/responsive/RecordDrivers/EBSCO/combinedResult.tpl index baffb1ea4f..6ba72f2af9 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/EBSCO/combinedResult.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/EBSCO/combinedResult.tpl @@ -4,7 +4,7 @@
{if $disableCoverArt != 1} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/EBSCO/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/EBSCO/listEntry.tpl index 99a0b787be..8195e1fec4 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/EBSCO/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/EBSCO/listEntry.tpl @@ -10,7 +10,7 @@
{if $disableCoverArt != 1 && !empty($bookCoverUrlMedium)} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/EBSCO/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/EBSCO/result.tpl index 6545d557e4..e41eb5379d 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/EBSCO/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/EBSCO/result.tpl @@ -5,9 +5,9 @@ {if $disableCoverArt != 1 && !empty($bookCoverUrlMedium)} {/if} diff --git a/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/combinedResult.tpl b/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/combinedResult.tpl index c37982a233..4ab2afb3c3 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/combinedResult.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/combinedResult.tpl @@ -4,7 +4,7 @@
{if $disableCoverArt != 1} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/listEntry.tpl index 12ec4520a4..78b774e6ef 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/listEntry.tpl @@ -10,7 +10,7 @@
{if $disableCoverArt != 1 && !empty($bookCoverUrlMedium)} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/result.tpl index 032b7a684e..a0bff95bb3 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/EBSCOhost/result.tpl @@ -5,9 +5,9 @@ {if $disableCoverArt != 1 && !empty($bookCoverUrlMedium)} {/if} diff --git a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/browse_result.tpl b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/browse_result.tpl index 3097bccbcb..51af3d6863 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/browse_result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/browse_result.tpl @@ -1,4 +1,5 @@ {strip} + {assign var="vSummAuthor" value=""} {if !empty($summAuthor)} {assign var="vSummAuthor" value="by $summAuthor"} @@ -7,7 +8,7 @@ {if $accessibleBrowseCategories == '1' && $action != 'Results' && !$isForSearchResults} @@ -15,23 +16,24 @@ {if $browseMode == '1'} {else}{*Default Browse Mode (covers) *} +
{if !empty($isNew)}{translate text="New!" isPublicFacing=true} {/if}
- {$summTitle|escape}{if !empty($summSubTitle)} {$summSubTitle|escape}{/if} {$vSummAuthor|escape} + {$summTitle|escape}{if !empty($summSubTitle)} {$summSubTitle|escape}{/if} {$vSummAuthor|escape}
{if !empty($showRatings) && $browseCategoryRatingsMode != 0} -
{if $disableCoverArt != 1} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/courseReserveEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/courseReserveEntry.tpl index f9a8869e92..806cf9d8ae 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/courseReserveEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/courseReserveEntry.tpl @@ -4,7 +4,7 @@ {if !empty($showCovers)}
{if !empty($showRatings)} {include file="GroupedWork/title-rating.tpl" id=$summId ratingData=$summRating showNotInterested=false} @@ -55,9 +55,7 @@
{/if} - {* Short Mobile Entry for Formats when there aren't hidden formats *}
- {* Determine if there were hidden Formats for this entry *} {assign var=hasHiddenFormats value=false} {foreach from=$relatedManifestations item=relatedManifestation} @@ -66,9 +64,8 @@ {/if} {/foreach} - {* If there weren't hidden formats, show this short Entry (mobile view only). The exception is single format manifestations, they - won't have any hidden formats and will be displayed *} - {if empty($hasHiddenFormats) && count($relatedManifestations) != 1} + {assign var=hideInMobile value=$hideManifestationsInMobileView|default:1} + {if empty($hasHiddenFormats) && $hideInMobile && count($relatedManifestations) > 1}
Formats:
@@ -83,12 +80,10 @@ {* Formats Section *}
-
- {* Hide Formats section on mobile view, unless there is a single format or a format has been selected by the user *} - {* relatedManifestationsValue ID is used by the Formats button *} - - {include file="GroupedWork/relatedManifestations.tpl" id=$summId workId=$summId} - +
+ {* Hide Formats section on mobile view, unless there is a single format or a format has been selected by the user *} + {* relatedManifestationsValue ID is used by the Formats button *} + {include file="GroupedWork/relatedManifestations.tpl" id=$summId workId=$summId}
@@ -117,4 +112,4 @@
-{/strip} \ No newline at end of file +{/strip} diff --git a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/grouping-information.tpl b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/grouping-information.tpl index 14b47aa3ad..7158432a53 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/grouping-information.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/grouping-information.tpl @@ -26,6 +26,7 @@
+ {if !empty($specifiedDisplayInfo->description)}{/if}
{translate text='Select' isAdminFacing=true}
{if $dataItem->canActiveUserEdit()} - {translate text="Edit" isAdminFacing=true} + {translate text="Edit" isAdminFacing=true} {/if} {if $dataItem->getAdditionalListActions()} {foreach from=$dataItem->getAdditionalListActions() item=action} @@ -226,7 +237,7 @@ {if !empty($pageLinks.all)}
{$pageLinks.all}
{/if} - {if $canCompare || $canBatchUpdate || $canExportToCSV} + {if $canCompare || $canBatchUpdate || $canExportToCSV || $canBatchDelete}
@@ -316,3 +327,11 @@ {/literal} {/if} + + diff --git a/code/web/interface/themes/responsive/Admin/shareForm.tpl b/code/web/interface/themes/responsive/Admin/shareForm.tpl index 9c48db4034..bd8d6825e7 100644 --- a/code/web/interface/themes/responsive/Admin/shareForm.tpl +++ b/code/web/interface/themes/responsive/Admin/shareForm.tpl @@ -10,10 +10,14 @@ diff --git a/code/web/interface/themes/responsive/Admin/submitTicket.tpl b/code/web/interface/themes/responsive/Admin/submitTicket.tpl deleted file mode 100644 index 1759101ce4..0000000000 --- a/code/web/interface/themes/responsive/Admin/submitTicket.tpl +++ /dev/null @@ -1,77 +0,0 @@ -{strip} -
-

{translate text="Submit Support Ticket" isAdminFacing=true}

-
- {if !empty($error)} -
- {$error} -
- {/if} -
-
- - -
-
- - -
-
- - {translate text="Please use descriptive keywords, i.e. Creating lists in LiDA" isAdminFacing=true} - -
-
- - -
-
- - -
-
- - {translate text="Include detailed steps to replicate the problem, account number for impacted user(s), device information, etc. Please do not include sensitive information like login credentials." isAdminFacing=true} - -
- {if $supportingCompany == 'ByWater Solutions'} -
- - {translate text="Use Sharepass to send sensitive information to us such as login credentials we can use to replicate the behavior reported." isAdminFacing=true} {translate text="Learn how to use Sharepass." isAdminFacing=true} - -
- {/if} -
- -
- - - -
-
-
- - {translate text="Attach any screenshots or recordings that display the reported behavior or to help us replicate it. To select multiple files, hold down the CTRL or SHIFT key while selecting." isAdminFacing=true} - - {translate text="10MB size limit per attachment." isAdminFacing=true} -
-
- -
-
- -
-{/strip} diff --git a/code/web/interface/themes/responsive/Admin/submitTicketSuccess.tpl b/code/web/interface/themes/responsive/Admin/submitTicketSuccess.tpl deleted file mode 100644 index e91a54d5ab..0000000000 --- a/code/web/interface/themes/responsive/Admin/submitTicketSuccess.tpl +++ /dev/null @@ -1,15 +0,0 @@ -{strip} -
-

{translate text="Submit Support Ticket" isAdminFacing=true}

-
- {if $error} -
- {translate text="There was an error submitting your ticket." isAdminFacing=true} -
- {else} -
- {translate text="Your ticket was submitted successfully." isAdminFacing=true} -
- {/if} -
-{/strip} diff --git a/code/web/interface/themes/responsive/Axis360/full-record.tpl b/code/web/interface/themes/responsive/Axis360/full-record.tpl index ca924a6a7d..ba47d05a8c 100644 --- a/code/web/interface/themes/responsive/Axis360/full-record.tpl +++ b/code/web/interface/themes/responsive/Axis360/full-record.tpl @@ -17,7 +17,7 @@
{if $disableCoverArt != 1}
- {translate text='Book Cover' inAttribute=true isPublicFacing=true} + {translate text='Book Cover' inAttribute=true isPublicFacing=true}
{/if} {if !empty($showRatings)} @@ -45,9 +45,9 @@ diff --git a/code/web/interface/themes/responsive/CloudLibrary/full-record.tpl b/code/web/interface/themes/responsive/CloudLibrary/full-record.tpl index 7f6e0a855a..f8abab3ce4 100644 --- a/code/web/interface/themes/responsive/CloudLibrary/full-record.tpl +++ b/code/web/interface/themes/responsive/CloudLibrary/full-record.tpl @@ -45,9 +45,9 @@ diff --git a/code/web/interface/themes/responsive/DataObjectUtil/duration.tpl b/code/web/interface/themes/responsive/DataObjectUtil/duration.tpl index 801208ac2c..1ab1b641dc 100644 --- a/code/web/interface/themes/responsive/DataObjectUtil/duration.tpl +++ b/code/web/interface/themes/responsive/DataObjectUtil/duration.tpl @@ -1,5 +1,5 @@ {strip} -{assign var=hours value=($propValue/60)|intval} +{assign var=hours value=intval($propValue/60)} {assign var=minutes value=$propValue % 60}
diff --git a/code/web/interface/themes/responsive/DataObjectUtil/multiSelect.tpl b/code/web/interface/themes/responsive/DataObjectUtil/multiSelect.tpl index 3d8813ca4a..7b80fd0436 100644 --- a/code/web/interface/themes/responsive/DataObjectUtil/multiSelect.tpl +++ b/code/web/interface/themes/responsive/DataObjectUtil/multiSelect.tpl @@ -31,6 +31,58 @@ {if !empty($property.translateValues)}{translate text=$propertyName|escape inAttribute=true isPublicFacing=$property.isPublicFacing isAdminFacing=$property.isAdminFacing }{else}{$propertyName|escape}{/if}
{/foreach}
+ {elseif $property.listStyle == 'checkboxWithOptions'} + {if empty($property.readOnly)} +
+ +
+ {/if} +
+ {foreach from=$property.values item=propertyName key=propertyValue} + {assign var="isChecked" value=false} + {assign var="optionValues" value=[]} + {if is_array($propValue) && array_key_exists($propertyValue, $propValue)} + {assign var="isChecked" value=true} + {assign var="optionValues" value=$propValue[$propertyValue]} + {/if} +
+
+
+
+ +
+
+
+ {if !empty($property.optionsStructure)} + {foreach from=$property.optionsStructure key=optionKey item=optionProperty} +
+ + {if $optionProperty.type == 'text' || $optionProperty.type == 'url'} + + {elseif $optionProperty.type == 'textarea'} + + {/if} +
+ {/foreach} + {/if} +
+
+
+ {/foreach} +
{/if} {else}
diff --git a/code/web/interface/themes/responsive/DataObjectUtil/objectEditForm.tpl b/code/web/interface/themes/responsive/DataObjectUtil/objectEditForm.tpl index b54474dae6..49aadb84da 100644 --- a/code/web/interface/themes/responsive/DataObjectUtil/objectEditForm.tpl +++ b/code/web/interface/themes/responsive/DataObjectUtil/objectEditForm.tpl @@ -17,6 +17,10 @@
{$linkedObjectNotifications}
{/if} +{if !empty($editFormInstructions)} +
{$editFormInstructions}
+{/if} + {* Create the base form *}
@@ -39,6 +43,31 @@ {if !empty($activeIls)} {/if} + {* Preserve all context parameters for list state *} + {if !empty($page)} + + {/if} + {if !empty($pageSize)} + + {/if} + {if !empty($sort)} + + {/if} + {if !empty($filterType)} + {foreach from=$filterType item=filterTypeValue key=filterKey} + + {/foreach} + {/if} + {if !empty($filterValue)} + {foreach from=$filterValue item=filterValueValue key=filterKey} + + {/foreach} + {/if} + {if !empty($filterValue2)} + {foreach from=$filterValue2 item=filterValue2Value key=filterKey} + + {/foreach} + {/if} {foreach from=$structure item=property} {if is_array($property) && isset($property.property) && isset($property.type)} diff --git a/code/web/interface/themes/responsive/DataObjectUtil/oneToMany.tpl b/code/web/interface/themes/responsive/DataObjectUtil/oneToMany.tpl index bc6ab05966..4f42d808e6 100644 --- a/code/web/interface/themes/responsive/DataObjectUtil/oneToMany.tpl +++ b/code/web/interface/themes/responsive/DataObjectUtil/oneToMany.tpl @@ -20,6 +20,7 @@
{assign var=subPropName value=$subProperty.property} {assign var=subPropValue value=$subObject->$subPropName} + {* Check for instance-specific readonly conditions *} + {assign var=instanceReadOnly value=false} + {assign var=instanceReadOnlyReason value=null} + {if !empty($instanceStructure[$subPropName].readOnly)} + {assign var=instanceReadOnly value=true} + {if !empty($instanceStructure[$subPropName].readOnlyReason)} + {assign var=instanceReadOnlyReason value=$instanceStructure[$subPropName].readOnlyReason} + {/if} + {/if} {if $subProperty.type=='text' || $subProperty.type=='regularExpression' || $subProperty.type=='integer' || $subProperty.type=='html'} - + {elseif $subProperty.type=='date'} - + {elseif $subProperty.type=='time'} - + {elseif $subProperty.type=='dynamic_label'} - {$subPropValue|escape} + {$subPropValue|escape} {elseif $subProperty.type=='textarea' || $subProperty.type=='multilineRegularExpression'} - + {elseif $subProperty.type=='checkbox'} - {if !empty($subProperty.readOnly) || !empty($property.readOnly)} + {if !empty($subProperty.readOnly) || !empty($property.readOnly) || $instanceReadOnly} {if $subPropValue == 1}{translate text='Yes' isAdminFacing=true}{else}{translate text='No' isAdminFacing=true}{/if} {/if} - + {elseif $subProperty.type=='multiSelect'} {if is_array($subPropValue) && count($subPropValue) > 0} @@ -72,7 +82,7 @@ {else} {if $subObject->canActiveUserChangeSelection()} - {foreach from=$subProperty.values item=propertyName key=propertyValue} {/foreach} @@ -100,7 +110,7 @@ {assign var=subPropName value=$subProperty.property} {assign var=subPropValue value=$subObject->$subPropName} {foreach from=$subProperty.values item=propertyName} - + {$propertyName|escape}
{/foreach} @@ -231,6 +241,10 @@ "value='{if !empty($subProperty.default)}{$subProperty.default}{/if}' " + "class='form-control{if $subProperty.type=="integer"} integer{/if}{if !empty($subProperty.required)} required{/if}'" + '{if !empty($subProperty.maxLength)} maxlength="{$subProperty.maxLength}"{/if}' + + '{if $subProperty.type=="integer" && !empty($subProperty.max)} max="{$subProperty.max}"{/if}' + + '{if $subProperty.type=="integer" && !empty($subProperty.min)} min="{$subProperty.min}"{/if}' + + '{if !empty($subProperty.placeholder)}placeholder="{$subProperty.placeholder}"{/if}' + + '{if !empty($subProperty.readOnlyWhenNew )}readonly{/if}' + " data-id='" + numAdditional{$propName} + "'>"; {/if} @@ -241,7 +255,11 @@ {elseif $subProperty.type=='dynamic_label'} newRow += "" {elseif $subProperty.type=='checkbox'} - newRow += ""; + {if !empty($subProperty.readOnlyWhenNew)} + newRow += "{if !empty($subProperty.default) && $subProperty.default == 1}{translate text='Yes' isAdminFacing=true}{else}{translate text='No' isAdminFacing=true}{/if}"; + {else} + newRow += ""; + {/if} {else} newRow += " @@ -165,7 +165,7 @@ {elseif ($formField->textId == 'address' || $formField->textId == 'address2' || $formField->textId == 'city' || $formField->textId == 'state' || $formField->textId == 'zip')} -
+
diff --git a/code/web/interface/themes/responsive/Donations/squarePayments.tpl b/code/web/interface/themes/responsive/Donations/squarePayments.tpl index 6edaae6d46..e8898797a1 100644 --- a/code/web/interface/themes/responsive/Donations/squarePayments.tpl +++ b/code/web/interface/themes/responsive/Donations/squarePayments.tpl @@ -20,7 +20,10 @@ async function tokenize(paymentMethod) {ldelim} const tokenResult = await paymentMethod.tokenize(); if (tokenResult.status === 'OK') {ldelim} - AspenDiscovery.Account.createSquareOrder('#donation{$userId}', 'donation', tokenResult.token); + const orderResult = AspenDiscovery.Account.createSquareOrder('#donation{$userId}', 'donation', tokenResult.token); + if (orderResult === false) {ldelim} + return null; + {rdelim} return tokenResult.token; {rdelim} else {ldelim} let errorMessage = `Tokenization failed.`; @@ -69,6 +72,10 @@ try {ldelim} cardButton.disabled = true; const token = await tokenize(paymentMethod); + if (token === null) {ldelim} + cardButton.disabled = false; + return; + {rdelim} const paymentResults = await createPayment(token); displayPaymentResults('SUCCESS'); {rdelim} catch (e) {ldelim} diff --git a/code/web/interface/themes/responsive/EBSCO/researchStarter.tpl b/code/web/interface/themes/responsive/EBSCO/researchStarter.tpl index 23a005e541..3e138c787d 100644 --- a/code/web/interface/themes/responsive/EBSCO/researchStarter.tpl +++ b/code/web/interface/themes/responsive/EBSCO/researchStarter.tpl @@ -12,7 +12,7 @@
{if !empty($image)} - {$title} + {$title} {/if}
diff --git a/code/web/interface/themes/responsive/Events/calendar.tpl b/code/web/interface/themes/responsive/Events/calendar.tpl index af39248e69..ba11b7fda0 100644 --- a/code/web/interface/themes/responsive/Events/calendar.tpl +++ b/code/web/interface/themes/responsive/Events/calendar.tpl @@ -6,6 +6,9 @@
{/if}

{translate text='Events Calendar' isPublicFacing=true}

+
@@ -54,15 +57,29 @@ -
+
{$event.formattedTime}
+ {if !empty($event.eventFields)} + {foreach from=$event.eventFields key=eventFieldName item=eventField} +
+ {foreach from=$eventField item=value} + {str_replace(',',', ',$value)}  + {/foreach} +
+ {/foreach} + {/if} {if !empty($event.isCancelled)}
{translate text="Cancelled" isPublicFacing=true}
{/if}
+ {if !empty($printDescriptionAgenda) && !empty($event.description)} +
+ {$event.description} +
+ {/if} {/foreach}
diff --git a/code/web/interface/themes/responsive/Events/event-graph.tpl b/code/web/interface/themes/responsive/Events/event-graph.tpl index a221772d21..60d5425ab2 100644 --- a/code/web/interface/themes/responsive/Events/event-graph.tpl +++ b/code/web/interface/themes/responsive/Events/event-graph.tpl @@ -2,94 +2,148 @@

{translate text=$graphTitle isAdminFacing=true}

-
- -
- -

{translate text="Filter Options" isAdminFacing=true}

-
+
- +
+ + {* Date Range *}
- +
- +
-
- - -
-
- - -
-
- - + + + + + {translate text="Groupings with no events will not show" isAdminFacing=true}
-

{translate text="Custom Fields" isAdminFacing=true}

-
- {foreach $checkboxFields as $id => $checkbox} -
- + + {* Other Filters *} +
+
+ +
+
+ {* Filter by standard fields (event type, location) *} +
+ + +
+
+ + +
+
+ + +
+ + {* Filtering by custom fields *} +
+
+ +
+
+ {translate text="Filter By Event Fields" isAdminFacing=true} +
+
+
+
+
+ {* Checkbox fields *} +
+ {foreach $checkboxFields as $id => $checkbox} +
+ +
+ {/foreach} +
+ {* Select Fields *} + {foreach $selectFields as $id => $select} +
+ + +
+ {/foreach} +
+
+
+
+ + {* Event Search *} +
+ + {translate text="Searches all text fields (title, description, custom text fields)" isAdminFacing=true} +
+
+
+
- {/foreach}
-
- {foreach $selectFields as $id => $select} -
- - -
- {/foreach} -
-
- - {translate text="Searches all text fields (title, description, custom text fields)" isAdminFacing=true} -
-
+ +
+
- +

+ {if $graphOption != '3'} +
+ +
+ {/if} +

{translate text="Raw Data" isAdminFacing=true}

@@ -106,7 +160,7 @@ {foreach from=$dataSeries item=seriesData} - + {/foreach} {/foreach} @@ -115,7 +169,7 @@ {if !empty($showCSVExportButton)}
- {translate text='Export To CSV' isAdminFacing=true} + {translate text='Export To CSV' isAdminFacing=true}
{translate text="Exporting will retrieve the latest data. To see it on screen, refresh this page." isAdminFacing=true}
{/if} @@ -123,6 +177,9 @@ {/strip} {literal} -{/literal} diff --git a/code/web/interface/themes/responsive/Greenhouse/ticketsByComponent.tpl b/code/web/interface/themes/responsive/Greenhouse/ticketsByComponent.tpl deleted file mode 100644 index 2cefa5cfe6..0000000000 --- a/code/web/interface/themes/responsive/Greenhouse/ticketsByComponent.tpl +++ /dev/null @@ -1,45 +0,0 @@ -{strip} -
-

{translate text="Active Tickets By Component" isAdminFacing=true}

- -
{if !empty($translateColumnLabels)}{translate text=$label isAdminFacing=true}{else}{$label}{/if}{if (empty($seriesData.data.$label))}0{else}{$seriesData.data.$label|number_format}{/if}{if (empty($seriesData.data.$label))}0{else}{$seriesData.data.$label|number_format:2}{/if}
- - - - - - - - - - - - - - - - - {foreach from=$ticketsByComponent item=componentTicketInfo} - - - - - - - - - - - - - - {/foreach} - -
{translate text="Component" isAdminFacing=true}{translate text="Implementation" isAdminFacing=true}{translate text="Support" isAdminFacing=true}{translate text="Bugs" isAdminFacing=true}{translate text="Development" isAdminFacing=true}{translate text="Priority 1" isAdminFacing=true}{translate text="Priority 2" isAdminFacing=true}{translate text="Priority 3" isAdminFacing=true}{translate text="Priority Tickets" isAdminFacing=true}{translate text="Priority Score" isAdminFacing=true}{translate text="Total" isAdminFacing=true}
{$componentTicketInfo.component}{$componentTicketInfo.Implementation}{$componentTicketInfo.Support}{$componentTicketInfo.Bugs}{$componentTicketInfo.Development}{$componentTicketInfo.Priority1}{$componentTicketInfo.Priority2}{$componentTicketInfo.Priority3}{$componentTicketInfo.PriorityTickets}{$componentTicketInfo.PriorityScore}{$componentTicketInfo.Total}
-
-{/strip} - \ No newline at end of file diff --git a/code/web/interface/themes/responsive/Greenhouse/ticketsByPartner.tpl b/code/web/interface/themes/responsive/Greenhouse/ticketsByPartner.tpl deleted file mode 100644 index 7d479da011..0000000000 --- a/code/web/interface/themes/responsive/Greenhouse/ticketsByPartner.tpl +++ /dev/null @@ -1,35 +0,0 @@ -{strip} -
-

{translate text="Active Tickets By Partner" isAdminFacing=true}

- - - - - - - - - - - - - - {foreach from=$ticketsByPartner item=partnerTicketInfo} - - - - - - - - - {/foreach} - -
{translate text="Site Name" isAdminFacing=true}{translate text="Implementation" isAdminFacing=true}{translate text="Support" isAdminFacing=true}{translate text="Bugs" isAdminFacing=true}{translate text="Development" isAdminFacing=true}{translate text="Total" isAdminFacing=true}
{$partnerTicketInfo.siteName}{if $partnerTicketInfo.Implementation > 0}{/if}{$partnerTicketInfo.Implementation}{if $partnerTicketInfo.Implementation > 0}{/if}{if $partnerTicketInfo.Support > 0}{/if}{$partnerTicketInfo.Support}{if $partnerTicketInfo.Support > 0}{/if}{if $partnerTicketInfo.Bugs > 0}{/if}{$partnerTicketInfo.Bugs}{if $partnerTicketInfo.Bugs > 0}{/if}{if $partnerTicketInfo.Development > 0}{/if}{$partnerTicketInfo.Development}{if $partnerTicketInfo.Development > 0}{/if}{$partnerTicketInfo.Total}
-
-{/strip} - \ No newline at end of file diff --git a/code/web/interface/themes/responsive/GroupedWork/ajax-innreach.tpl b/code/web/interface/themes/responsive/GroupedWork/ajax-innreach.tpl index 414b1e4990..f5aaa70bcc 100644 --- a/code/web/interface/themes/responsive/GroupedWork/ajax-innreach.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/ajax-innreach.tpl @@ -6,7 +6,7 @@ {translate text="Author" isPublicFacing=true}
- {translate text="Pub. Date" isPublicFacing=true} + {translate text="Publication Date" isPublicFacing=true} {translate text="Format" isPublicFacing=true} diff --git a/code/web/interface/themes/responsive/GroupedWork/ajax-shareit.tpl b/code/web/interface/themes/responsive/GroupedWork/ajax-shareit.tpl index 1bd792d003..b1b99d570e 100644 --- a/code/web/interface/themes/responsive/GroupedWork/ajax-shareit.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/ajax-shareit.tpl @@ -6,7 +6,7 @@ {translate text="Author" isPublicFacing=true} - {translate text="Pub. Date" isPublicFacing=true} + {translate text="Publication. Date" isPublicFacing=true} {translate text="Format" isPublicFacing=true} diff --git a/code/web/interface/themes/responsive/GroupedWork/allManifestations.tpl b/code/web/interface/themes/responsive/GroupedWork/allManifestations.tpl index d860fa2a8a..ee5bf40d3c 100644 --- a/code/web/interface/themes/responsive/GroupedWork/allManifestations.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/allManifestations.tpl @@ -1,5 +1,5 @@ {if $formatDisplayStyle == 1} - {* Short Mobile Entry for Formats when there aren't hidden formats *} + {* Short Mobile Entry adapts based on manifestation count *}
{* Determine if there were hidden Formats for this entry *} {assign var=hasHiddenFormats value=false} @@ -9,9 +9,9 @@ {/if} {/foreach} - {* If there weren't hidden formats, show this short Entry (mobile view only). The exception is single format manifestations, they - won't have any hidden formats and will be displayed *} - {if empty($hasHiddenFormats) && count($relatedManifestations) != 1} + + {assign var=hideInMobile value=$hideManifestationsInMobileView|default:1} + {if empty($hasHiddenFormats) && $hideInMobile && count($relatedManifestations) > 1}
{translate text="Formats" isPublicFacing=true}
@@ -24,7 +24,7 @@
{* Formats Section *} -
+
{* Hide Formats section on mobile view, unless there is a single format or a format has been selected by the user *} {* relatedManifestationsValue ID is used by the Formats button *} {include file="GroupedWork/relatedManifestations.tpl" id=$summId workId=$summId} @@ -53,7 +53,7 @@
{$manifestation->format}
- {include file='GroupedWork/statusIndicator.tpl' statusInformation=$manifestation->getStatusInformation() viewingIndividualRecord=0 applyColors=false} + {include file='GroupedWork/statusIndicator.tpl' statusInformation=$manifestation->getStatusInformation() viewingIndividualRecord=0 applyColors=false hideCopiesLine=true}
{/foreach}
diff --git a/code/web/interface/themes/responsive/GroupedWork/full-record.tpl b/code/web/interface/themes/responsive/GroupedWork/full-record.tpl index f5081a0aa1..abb56594b7 100644 --- a/code/web/interface/themes/responsive/GroupedWork/full-record.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/full-record.tpl @@ -6,12 +6,14 @@ {* Display Title *}

- {if empty($recordDriver->getShortTitle())} + {if !empty($recordDriver->getTitle())} {$recordDriver->getTitle()|removeTrailingPunctuation|escape} - {else} + {elseif !empty($recordDriver->getShortTitle())} {$recordDriver->getShortTitle()|removeTrailingPunctuation|escape}{if $recordDriver->getSubtitle()} : {$recordDriver->getSubtitle()|removeTrailingPunctuation|escape} {/if} + {else} + {translate text="Title Unavailable" isPublicFacing=true} {/if}

@@ -20,7 +22,7 @@
{if $disableCoverArt != 1}
- {translate text='Book Cover' isPublicFacing=true inAttribute=true} + {translate text='Book Cover' isPublicFacing=true inAttribute=true}
{/if} {if !empty($showRatings)} diff --git a/code/web/interface/themes/responsive/GroupedWork/groupedWorkDisplayInfoForm.tpl b/code/web/interface/themes/responsive/GroupedWork/groupedWorkDisplayInfoForm.tpl index 5b13692bff..d9e1b5ef9f 100644 --- a/code/web/interface/themes/responsive/GroupedWork/groupedWorkDisplayInfoForm.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/groupedWorkDisplayInfoForm.tpl @@ -35,6 +35,14 @@
{/if} +
+
+ +
+
+ +
+
diff --git a/code/web/interface/themes/responsive/GroupedWork/horizDisplayEdition.tpl b/code/web/interface/themes/responsive/GroupedWork/horizDisplayEdition.tpl index 77b3fcd0ae..4388a92714 100644 --- a/code/web/interface/themes/responsive/GroupedWork/horizDisplayEdition.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/horizDisplayEdition.tpl @@ -2,7 +2,7 @@
- {capture assign=statusIndicator}{include file='GroupedWork/statusIndicator.tpl' statusInformation=$firstRecord->getStatusInformation() viewingIndividualRecord=0 applyColors=false}{/capture} + {capture assign=statusIndicator}{include file='GroupedWork/statusIndicator.tpl' statusInformation=$firstRecord->getStatusInformation() viewingIndividualRecord=0 applyColors=false hideCopiesLine=false}{/capture} {capture assign=formatWithLink}{translate text=$firstRecord->getFormat() isPublicFacing=true inAttribute=true}{/capture}
{translate text="This %1% is currently %2%" 1=$formatWithLink 2=$statusIndicator isPublicFacing=true}
@@ -19,7 +19,7 @@
{foreach from=$firstRecord->getActions($variationId) item=curAction} - {$curAction.title} + {$curAction.title} {/foreach}
@@ -27,16 +27,25 @@ {* Show Shelf Locations *} {if !$isEContent}
+ {assign var=numDisplayed value=0} + {assign var=totalSummariesToDisplay value=0} {foreach from=$itemSummary item=$curItemSummary name=itemSummary} - {if $smarty.foreach.itemSummary.index < 2} -
-
{$curItemSummary.shelfLocation}
-
{$curItemSummary.callNumber}
-
{$curItemSummary.availableCopies} of {$curItemSummary.totalCopies} available
-
+ {if $curItemSummary.displayByDefault} + {assign var=totalSummariesToDisplay value=$totalSummariesToDisplay+1} + {/if} + {/foreach} + {foreach from=$itemSummary item=$curItemSummary name=itemSummary} + {*If we only have 3 or fewe summaries to show, show all 3. If we have more than 3, display 2 and a button to see the rest *} + {if ($numDisplayed < 2 || ($totalSummariesToDisplay == 3 && count($itemSummary) == 3)) && $curItemSummary.displayByDefault} + {assign var=numDisplayed value=$numDisplayed+1} +
+
{$curItemSummary.shelfLocation}
+
{$curItemSummary.callNumber}
+
{$curItemSummary.availableCopies} of {$curItemSummary.totalCopies} available
+
{/if} {/foreach} - {if count($itemSummary) > 2} + {if count($itemSummary) > 2 && $totalSummariesToDisplay != count($itemSummary)}
@@ -49,8 +58,8 @@
- - + +
diff --git a/code/web/interface/themes/responsive/GroupedWork/moveRecordForm.tpl b/code/web/interface/themes/responsive/GroupedWork/moveRecordForm.tpl new file mode 100644 index 0000000000..78a2d520e3 --- /dev/null +++ b/code/web/interface/themes/responsive/GroupedWork/moveRecordForm.tpl @@ -0,0 +1,38 @@ +{strip} +
+
+ {translate text="This will move only this specific record to another grouped work. The record will persist within the target work after reindexing." isAdminFacing=true} +
+
+
+
+ {translate text="You are moving %1%:%2%" 1=$source 2=$identifier isAdminFacing=true} +
+
+
+
+ {translate text="Currently in" isAdminFacing=true} +
+
+ {$currentWork->full_title} {translate text="by" isPublicFacing=true} {$currentWork->author} +
+
+
+ + {if !empty($existingOverride)} +
+ {translate text="This record already has a grouping override. Moving it will update the existing override." isAdminFacing=true} +
+ {/if} + + +
+ +
+ +
+
+
+
+
+{/strip} diff --git a/code/web/interface/themes/responsive/GroupedWork/multipleVariationManifestion.tpl b/code/web/interface/themes/responsive/GroupedWork/multipleVariationManifestion.tpl index 7466327f80..5a5ec2db44 100644 --- a/code/web/interface/themes/responsive/GroupedWork/multipleVariationManifestion.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/multipleVariationManifestion.tpl @@ -2,7 +2,13 @@
- {translate text=$relatedManifestation->format isPublicFacing=true} + {if $relatedManifestation->hasInvalidFormat()} + + {translate text='Unknown Format' isPublicFacing=true} + + {else} + {translate text=$relatedManifestation->format isPublicFacing=true} + {/if}
{foreach from=$relatedManifestation->getVariations() item=variation} @@ -38,9 +44,9 @@ @@ -54,4 +60,4 @@
{/foreach}
-{/strip} \ No newline at end of file +{/strip} diff --git a/code/web/interface/themes/responsive/GroupedWork/previewRelatedCover.tpl b/code/web/interface/themes/responsive/GroupedWork/previewRelatedCover.tpl index a2b88dd4f2..a22d38f788 100644 --- a/code/web/interface/themes/responsive/GroupedWork/previewRelatedCover.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/previewRelatedCover.tpl @@ -6,7 +6,7 @@
- {translate text='Book Cover' inAttribute=true isPublicFacing=true} + {translate text='Book Cover' inAttribute=true isPublicFacing=true}
diff --git a/code/web/interface/themes/responsive/GroupedWork/relatedRecords.tpl b/code/web/interface/themes/responsive/GroupedWork/relatedRecords.tpl index 39de6228f0..5bdd360769 100644 --- a/code/web/interface/themes/responsive/GroupedWork/relatedRecords.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/relatedRecords.tpl @@ -3,7 +3,7 @@
{if !empty($showEditionCovers) && $showEditionCovers == 1}
- {translate text='Book Cover' inAttribute=true isPublicFacing=true} + {translate text='Book Cover' inAttribute=true isPublicFacing=true}
{/if} @@ -52,7 +52,7 @@
{translate text="More Info" isPublicFacing=true} {foreach from=$relatedRecord->getActions($variationId) item=curAction} - {$curAction.title} + {$curAction.title} {/foreach}
diff --git a/code/web/interface/themes/responsive/GroupedWork/result-tools-horizontal.tpl b/code/web/interface/themes/responsive/GroupedWork/result-tools-horizontal.tpl index 30fb3aa68c..759dfc4ab8 100644 --- a/code/web/interface/themes/responsive/GroupedWork/result-tools-horizontal.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/result-tools-horizontal.tpl @@ -28,7 +28,7 @@ {/if} {/if} - {if !empty($loggedIn) && ($module == 'Search' || $module == 'Author') && in_array('Manually Group and Ungroup Works', $userPermissions)} + {if !empty($loggedIn) && ($module == 'Search' || $module == 'Author') && in_array('Manually Group and Ungroup Works', $userPermissions) && !$recordDriver->isManuallyGrouped()} {/if} {/if} diff --git a/code/web/interface/themes/responsive/GroupedWork/series-summary.tpl b/code/web/interface/themes/responsive/GroupedWork/series-summary.tpl index 41b2b44d8a..e082ba95ef 100644 --- a/code/web/interface/themes/responsive/GroupedWork/series-summary.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/series-summary.tpl @@ -25,7 +25,21 @@ {/if} {/if} {elseif !empty($summSeries.seriesTitle)} - {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if} + {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if}
+ {if !empty($summSeries.additionalSeries)} + {assign var=numSeriesShown value=1} + {foreach from=$summSeries.additionalSeries item=additional} + {assign var=numSeriesShown value=$numSeriesShown+1} + {if $numSeriesShown == 4} + {translate text='More Series...' isPublicFacing=true} + + {/if} + {/if} {/if} {if !empty($indexedSeries) && empty($summSeries.fromSeriesIndex)} {if !empty($summSeries)} diff --git a/code/web/interface/themes/responsive/GroupedWork/similarTitlesNovelist.tpl b/code/web/interface/themes/responsive/GroupedWork/similarTitlesNovelist.tpl index f8aa9787ce..37e6307603 100644 --- a/code/web/interface/themes/responsive/GroupedWork/similarTitlesNovelist.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/similarTitlesNovelist.tpl @@ -5,7 +5,7 @@
diff --git a/code/web/interface/themes/responsive/GroupedWork/singleVariationManifestion.tpl b/code/web/interface/themes/responsive/GroupedWork/singleVariationManifestion.tpl index 37135d994d..57cf15ade0 100644 --- a/code/web/interface/themes/responsive/GroupedWork/singleVariationManifestion.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/singleVariationManifestion.tpl @@ -4,7 +4,11 @@ {if $printInterface === false || ($printInterface === true && $printEntryFormats === true)}
-{/strip} \ No newline at end of file +{/strip} diff --git a/code/web/interface/themes/responsive/GroupedWork/statusIndicator.tpl b/code/web/interface/themes/responsive/GroupedWork/statusIndicator.tpl index c88a56b7a2..fcc6b0d7b6 100644 --- a/code/web/interface/themes/responsive/GroupedWork/statusIndicator.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/statusIndicator.tpl @@ -1,5 +1,6 @@ {strip} {if !isset($applyColors)}{assign var=applyColors value=true}{/if} +{if !isset($hideCopiesLine)}{assign var=hideCopiesLine value=false}{/if} {if $statusInformation->isEContent()} {if $statusInformation->isShowStatus()} {* eContent, easy to handle *} @@ -66,7 +67,9 @@
{/if} {/if} -{if ((($statusInformation->getHoldableCopies() > 0 && $statusInformation->getNumHolds() > 0) || $statusInformation->getOnOrderCopies() > 0) && ($showGroupedHoldCopiesCount || $viewingIndividualRecord == 1) || $showGroupedHoldCopiesCount == 3)} - +{if !$hideCopiesLine} + {if ((($statusInformation->getHoldableCopies() > 0 && $statusInformation->getNumHolds() > 0) || $statusInformation->getOnOrderCopies() > 0) && ($showGroupedHoldCopiesCount || $viewingIndividualRecord == 1) || $showGroupedHoldCopiesCount == 3 || $showGroupedHoldCopiesCount == 4)} + + {/if} {/if} {/strip} diff --git a/code/web/interface/themes/responsive/GroupedWork/view-title-details.tpl b/code/web/interface/themes/responsive/GroupedWork/view-title-details.tpl index 86c18d98fb..7a635b84d3 100644 --- a/code/web/interface/themes/responsive/GroupedWork/view-title-details.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/view-title-details.tpl @@ -32,8 +32,8 @@ {if !empty($recordDriver->getMpaaRating())}
-
{translate text='Rating' isPublicFacing=true}
-
{translate text=$recordDriver->getMpaaRating() isPublicFacing=true isMetadata=true}
+
{translate text='MPAA Rating' isPublicFacing=true}
+
{implode subject=$recordDriver->getMpaaRating() glue=", " translate=true isPublicFacing=true isMetadata=true}
{/if} @@ -100,4 +100,4 @@ {include file="GroupedWork/relatedLists.tpl" isSearchResults=false} {include file="GroupedWork/readingHistoryIndicator.tpl" isSearchResults=false} -{/strip} \ No newline at end of file +{/strip} diff --git a/code/web/interface/themes/responsive/GroupedWork/whileYouWait.tpl b/code/web/interface/themes/responsive/GroupedWork/whileYouWait.tpl index 45e0ffa629..743b96f3d0 100644 --- a/code/web/interface/themes/responsive/GroupedWork/whileYouWait.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/whileYouWait.tpl @@ -6,7 +6,7 @@ {foreach from=$whileYouWaitTitles item=whileYouWaitTitle}
- {$whileYouWaitTitle.title|escape} + {$whileYouWaitTitle.title|escape} {if !empty($showRatings)}
diff --git a/code/web/interface/themes/responsive/GroupedWork/work-details.tpl b/code/web/interface/themes/responsive/GroupedWork/work-details.tpl index 7f67fd167c..ec820aad26 100644 --- a/code/web/interface/themes/responsive/GroupedWork/work-details.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/work-details.tpl @@ -37,7 +37,21 @@ {/if} {/if} {elseif !empty($summSeries.seriesTitle)} - {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if} + {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if}
+ {if !empty($summSeries.additionalSeries)} + {assign var=numSeriesShown value=1} + {foreach from=$summSeries.additionalSeries item=additional} + {assign var=numSeriesShown value=$numSeriesShown+1} + {if $numSeriesShown == 4} + {translate text='More Series...' isPublicFacing=true} + + {/if} + {/if} {/if}
diff --git a/code/web/interface/themes/responsive/GroupedWork/youMightAlsoLike.tpl b/code/web/interface/themes/responsive/GroupedWork/youMightAlsoLike.tpl index 63dc63a19b..b2b1b2855a 100644 --- a/code/web/interface/themes/responsive/GroupedWork/youMightAlsoLike.tpl +++ b/code/web/interface/themes/responsive/GroupedWork/youMightAlsoLike.tpl @@ -8,7 +8,7 @@ diff --git a/code/web/interface/themes/responsive/Hoopla/full-record.tpl b/code/web/interface/themes/responsive/Hoopla/full-record.tpl index dd4a12d2d6..1bb075da66 100644 --- a/code/web/interface/themes/responsive/Hoopla/full-record.tpl +++ b/code/web/interface/themes/responsive/Hoopla/full-record.tpl @@ -18,7 +18,7 @@
{if $disableCoverArt != 1}
- {translate text='Book Cover' isPublicFacing=true inAttribute=true} + {translate text='Book Cover' isPublicFacing=true inAttribute=true}
{/if} {if !empty($showRatings)} @@ -47,9 +47,9 @@ diff --git a/code/web/interface/themes/responsive/MaterialsRequest/request-form-fields.tpl b/code/web/interface/themes/responsive/MaterialsRequest/request-form-fields.tpl index 49c00ddd71..edefc61e12 100644 --- a/code/web/interface/themes/responsive/MaterialsRequest/request-form-fields.tpl +++ b/code/web/interface/themes/responsive/MaterialsRequest/request-form-fields.tpl @@ -372,7 +372,7 @@
-
Book Cover
+
Book Cover
diff --git a/code/web/interface/themes/responsive/MyAccount/addAccountLink.tpl b/code/web/interface/themes/responsive/MyAccount/addAccountLink.tpl index 7d1673ba53..ab83b96e3a 100644 --- a/code/web/interface/themes/responsive/MyAccount/addAccountLink.tpl +++ b/code/web/interface/themes/responsive/MyAccount/addAccountLink.tpl @@ -18,7 +18,7 @@
diff --git a/code/web/interface/themes/responsive/MyAccount/ajax-login.tpl b/code/web/interface/themes/responsive/MyAccount/ajax-login.tpl index 242f376849..dd4c736997 100644 --- a/code/web/interface/themes/responsive/MyAccount/ajax-login.tpl +++ b/code/web/interface/themes/responsive/MyAccount/ajax-login.tpl @@ -75,7 +75,7 @@
{if empty($isOpac)} diff --git a/code/web/interface/themes/responsive/MyAccount/axis360CheckedOutTitle.tpl b/code/web/interface/themes/responsive/MyAccount/axis360CheckedOutTitle.tpl index 7083bbeb5e..e3f1090474 100644 --- a/code/web/interface/themes/responsive/MyAccount/axis360CheckedOutTitle.tpl +++ b/code/web/interface/themes/responsive/MyAccount/axis360CheckedOutTitle.tpl @@ -16,10 +16,10 @@ {if $record->getCoverUrl()} {if $record->recordId && $record->getLinkUrl()} {else} {* Cover Image but no Record-View link *} - + {/if} {/if} {/if} diff --git a/code/web/interface/themes/responsive/MyAccount/axis360Hold.tpl b/code/web/interface/themes/responsive/MyAccount/axis360Hold.tpl index 04aa40e62d..4233eff020 100644 --- a/code/web/interface/themes/responsive/MyAccount/axis360Hold.tpl +++ b/code/web/interface/themes/responsive/MyAccount/axis360Hold.tpl @@ -11,10 +11,10 @@ {if $record->getCoverUrl()} {if $record->recordId && $record->getLinkUrl()} {else} {* Cover Image but no Record-View link *} - + {/if} {/if}
diff --git a/code/web/interface/themes/responsive/MyAccount/cloudLibraryCheckedOutTitle.tpl b/code/web/interface/themes/responsive/MyAccount/cloudLibraryCheckedOutTitle.tpl index 1d7db613e2..7c16ecf5d7 100644 --- a/code/web/interface/themes/responsive/MyAccount/cloudLibraryCheckedOutTitle.tpl +++ b/code/web/interface/themes/responsive/MyAccount/cloudLibraryCheckedOutTitle.tpl @@ -16,10 +16,10 @@ {if $record->getCoverUrl()} {if $record->recordId && $record->getLinkUrl()} {else} {* Cover Image but no Record-View link *} - + {/if} {/if} {/if} @@ -96,7 +96,7 @@ {if !empty($accessOnlineLink)} {translate text='Open in cloudLibrary' isPublicFacing=true} {/if} - {if array_key_exists('Palace Project', $enabledModules) && !empty($asccessOnlineLink)} + {if array_key_exists('Palace Project', $enabledModules) && !empty($accessOnlineLink)} {translate text='Access In Palace Project' isPublicFacing=true} {/if} {if $record->canRenew} diff --git a/code/web/interface/themes/responsive/MyAccount/cloudLibraryHold.tpl b/code/web/interface/themes/responsive/MyAccount/cloudLibraryHold.tpl index ea81e30b76..dece2a0fef 100644 --- a/code/web/interface/themes/responsive/MyAccount/cloudLibraryHold.tpl +++ b/code/web/interface/themes/responsive/MyAccount/cloudLibraryHold.tpl @@ -11,10 +11,10 @@ {if $record->getCoverUrl()} {if $record->sourceId && $record->getLinkUrl()} {else} {* Cover Image but no Record-View link *} - + {/if} {/if}
@@ -90,7 +90,7 @@
{if $section == 'available'} - + {/if}
diff --git a/code/web/interface/themes/responsive/MyAccount/contactInformation.tpl b/code/web/interface/themes/responsive/MyAccount/contactInformation.tpl index 44a1856e22..74f17a47a4 100644 --- a/code/web/interface/themes/responsive/MyAccount/contactInformation.tpl +++ b/code/web/interface/themes/responsive/MyAccount/contactInformation.tpl @@ -17,9 +17,6 @@ {if !empty($offline)}
{translate text=$offlineMessage isPublicFacing=true}
{else} -{* MDN 7/26/2019 Do not allow access to contact information for linked users *} -{* {include file="MyAccount/switch-linked-user-form.tpl" label="View Contact Information for" actionPath="/MyAccount/ContactInformation"}*} - {if !empty($profileUpdateErrors)}
{$profileUpdateErrors}
{/if} @@ -209,6 +206,19 @@
+ {if $enableThirdPartySMS} +
+
+
+ {if $edit == true} + _thirdPartySMSOptIn==1}checked='checked'{/if} data-switch=""> + {else} + {if $profile->_thirdPartySMSOptIn==0}{translate text="No" isPublicFacing=true}{else}{translate text="Yes" isPublicFacing=true}{/if} + {/if} +
+
+ {/if} +
@@ -229,8 +239,9 @@
+ {if !$enableThirdPartySMS}
-
+
{if $edit == true} _availableHoldNotice==1}checked='checked'{/if} data-switch=""> @@ -241,7 +252,7 @@
-
+
{if $edit == true} _comingDueNotice==1}checked='checked'{/if} data-switch=""> @@ -250,6 +261,7 @@ {/if}
+ {/if} {/if} diff --git a/code/web/interface/themes/responsive/MyAccount/createListForm.tpl b/code/web/interface/themes/responsive/MyAccount/createListForm.tpl index 85dff33386..f34b269ad6 100644 --- a/code/web/interface/themes/responsive/MyAccount/createListForm.tpl +++ b/code/web/interface/themes/responsive/MyAccount/createListForm.tpl @@ -58,6 +58,95 @@
{/if} {/if} + + {*Options for adding to a list group*} +
+ +
+ + +
+
+ + {*Show the new group name and nesting options if "new" is selected*} + + + {*Show the existing group selection if "existing" is selected*} + {if !empty($userListGroups)} + + {/if} diff --git a/code/web/interface/themes/responsive/MyAccount/createListGroupForm.tpl b/code/web/interface/themes/responsive/MyAccount/createListGroupForm.tpl new file mode 100644 index 0000000000..902b634570 --- /dev/null +++ b/code/web/interface/themes/responsive/MyAccount/createListGroupForm.tpl @@ -0,0 +1,26 @@ +{strip} + {if !empty($listError)}

{translate text=$listError isPublicFacing=true}

{/if} +
+
+ +
+ +
+
+ + {if !empty($userListGroups)} +
+ +
+ +
+
+ {/if} +
+ +{/strip} \ No newline at end of file diff --git a/code/web/interface/themes/responsive/MyAccount/curbsidePickupsHoldsReady.tpl b/code/web/interface/themes/responsive/MyAccount/curbsidePickupsHoldsReady.tpl index ad6c9c440e..d7ae9f0e7e 100644 --- a/code/web/interface/themes/responsive/MyAccount/curbsidePickupsHoldsReady.tpl +++ b/code/web/interface/themes/responsive/MyAccount/curbsidePickupsHoldsReady.tpl @@ -9,12 +9,12 @@ {if !empty($record->getLinkUrl())} {else} {* Cover Image but no Record-View link *} {/if} diff --git a/code/web/interface/themes/responsive/MyAccount/editListGroupName.tpl b/code/web/interface/themes/responsive/MyAccount/editListGroupName.tpl new file mode 100644 index 0000000000..a6086085be --- /dev/null +++ b/code/web/interface/themes/responsive/MyAccount/editListGroupName.tpl @@ -0,0 +1,22 @@ +{strip} +
+ +
+
+ +
+ +
+
+
+
+ +{/strip} \ No newline at end of file diff --git a/code/web/interface/themes/responsive/MyAccount/editListGroupParent.tpl b/code/web/interface/themes/responsive/MyAccount/editListGroupParent.tpl new file mode 100644 index 0000000000..63b8079dbc --- /dev/null +++ b/code/web/interface/themes/responsive/MyAccount/editListGroupParent.tpl @@ -0,0 +1,29 @@ +{strip} +
+ +
+
+ +
+ +
+
+
+
+ +{/strip} \ No newline at end of file diff --git a/code/web/interface/themes/responsive/MyAccount/hooplaCheckedOutTitle.tpl b/code/web/interface/themes/responsive/MyAccount/hooplaCheckedOutTitle.tpl index cbd131696d..90f0e6820a 100644 --- a/code/web/interface/themes/responsive/MyAccount/hooplaCheckedOutTitle.tpl +++ b/code/web/interface/themes/responsive/MyAccount/hooplaCheckedOutTitle.tpl @@ -14,10 +14,10 @@ {if $record->getCoverUrl()} {if $record->recordId && $record->getLinkUrl()} {else} {* Cover Image but no Record-View link *} - + {/if} {/if} {/if} diff --git a/code/web/interface/themes/responsive/MyAccount/hooplaHold.tpl b/code/web/interface/themes/responsive/MyAccount/hooplaHold.tpl index 53a0e47ec5..b02a100ee6 100644 --- a/code/web/interface/themes/responsive/MyAccount/hooplaHold.tpl +++ b/code/web/interface/themes/responsive/MyAccount/hooplaHold.tpl @@ -10,10 +10,10 @@ {if $record->getCoverUrl()} {if $record->recordId && $record->getLinkUrl()} {else} - + {/if} {/if}
diff --git a/code/web/interface/themes/responsive/MyAccount/ilsCheckedOutTitle.tpl b/code/web/interface/themes/responsive/MyAccount/ilsCheckedOutTitle.tpl index f07a2298a3..a17f309c34 100644 --- a/code/web/interface/themes/responsive/MyAccount/ilsCheckedOutTitle.tpl +++ b/code/web/interface/themes/responsive/MyAccount/ilsCheckedOutTitle.tpl @@ -16,10 +16,10 @@ {if $record->getCoverUrl()} {if $record->recordId && !empty($record->getLinkUrl())} {else} {* Cover Image but no Record-View link *} - + {/if} {/if} {/if} @@ -208,7 +208,7 @@ {if empty($record->maxRenewals)} 0 {else} - {$record->maxRenewals} + {$record->maxRenewals-$record->renewCount} {/if} diff --git a/code/web/interface/themes/responsive/MyAccount/ilsHold.tpl b/code/web/interface/themes/responsive/MyAccount/ilsHold.tpl index c797a9cd8f..37ec41c3c6 100644 --- a/code/web/interface/themes/responsive/MyAccount/ilsHold.tpl +++ b/code/web/interface/themes/responsive/MyAccount/ilsHold.tpl @@ -18,12 +18,12 @@ {else} {* Cover Image but no Record-View link *} {/if} diff --git a/code/web/interface/themes/responsive/MyAccount/list.tpl b/code/web/interface/themes/responsive/MyAccount/list.tpl index 30caddd59f..37bf202296 100644 --- a/code/web/interface/themes/responsive/MyAccount/list.tpl +++ b/code/web/interface/themes/responsive/MyAccount/list.tpl @@ -23,6 +23,13 @@

{$userList->title|escape:"html"}

+ {if $inListGroup} +
+

+ {translate text='Part of the list group: %1%' 1=$listGroupInfo->title isPublicFacing=true} +

+
+ {/if} {if !empty($notes)}
{foreach from=$notes item="note"} @@ -83,6 +90,22 @@
+ {if !empty($userListGroups)} +
+ +
+ +
+ {translate text="Select a group to associate this list with. List groups can be used to organize multiple lists." isPublicFacing=true} +
+
+
+ {/if} {if in_array('Include Lists In Search Results', $userPermissions)}
public == 0}style="display: none"{/if}> @@ -161,6 +184,36 @@ {translate text='Export List to RIS' isPublicFacing=true} + {if !empty($availableFilters)} +
+ + +
+ {/if} +
{translate text="Call Number" isPublicFacing=true} + {translate text="Barcode" isPublicFacing=true} + {translate text="Note" isPublicFacing=true} diff --git a/code/web/interface/themes/responsive/Record/copiesTableRow.tpl b/code/web/interface/themes/responsive/Record/copiesTableRow.tpl index 3a4a4f7ecd..6ad4c94d6d 100644 --- a/code/web/interface/themes/responsive/Record/copiesTableRow.tpl +++ b/code/web/interface/themes/responsive/Record/copiesTableRow.tpl @@ -1,10 +1,4 @@ {strip} - {* resize the columns when including the lastcheckin box - xs-5 : 41.6667% - xs-4 : 33.3333% (1/3) - xs-3 : 25% (1/4) - xs-2 : 16.6667% (1/6) - *}
{translate text="Author" isPublicFacing=true}{$specifiedDisplayInfo->author}
{translate text="Series Name" isPublicFacing=true}{$specifiedDisplayInfo->seriesName}
{translate text="Series Display Order" isPublicFacing=true}{if $specifiedDisplayInfo->seriesDisplayOrder != 0}{$specifiedDisplayInfo->seriesDisplayOrder|format_float_with_min_decimals}{/if}
{translate text="Description" isPublicFacing=true}{$specifiedDisplayInfo->description|nl2br}
{if !empty($loggedIn) && in_array('Set Grouped Work Display Information', $userPermissions)} {translate text="Delete" isPublicFacing=true} @@ -33,6 +34,19 @@
{/if} +{if !empty($manualGroupingInfo)} +
+

{translate text="Manual Grouping Information" isPublicFacing=true}

+ + + + {if !empty($manualGroupingInfo->description)}{/if} + + +
{translate text="Manual Group ID" isPublicFacing=true} {$manualGroupingInfo->id}
{translate text="Manual Group Title" isPublicFacing=true}{$manualGroupingInfo->title}
{translate text="Description" isPublicFacing=true}{$manualGroupingInfo->description|nl2br}
{translate text="Date Created" isPublicFacing=true}{$manualGroupingInfo->date_created|date_format:"%Y-%m-%d %H:%M"}
{translate text="Last Updated" isPublicFacing=true}{$manualGroupingInfo->last_updated|date_format:"%Y-%m-%d %H:%M"}
+
+{/if} + {if (!empty($alternateTitles))}

{translate text="Alternate Titles and Authors" isPublicFacing=true}

@@ -59,6 +73,32 @@
{/if} +{if (!empty($recordGroupingOverrides))} +

{translate text="Record Grouping Overrides" isPublicFacing=true}

+ + + + + + + {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} + + {/if} + + + {foreach from=$recordGroupingOverrides item="override"} + + + + + {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} + + {/if} + + {/foreach} +
{translate text="Source" isPublicFacing=true}{translate text="Record ID" isPublicFacing=true}{translate text="Date Added" isPublicFacing=true}{translate text="Actions" isPublicFacing=true}
{$override->source}{$override->record_id}{$override->date_added|date_format:"%Y-%m-%d %H:%M"}{translate text="Delete" isPublicFacing=true}
+{/if} + {if !empty($isUngrouped) && !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)}

{translate text="Record Ungrouped" isPublicFacing=true}

diff --git a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/listEntry.tpl index 7ff4f6c360..04bd8186d9 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/listEntry.tpl @@ -9,7 +9,7 @@ {if (!empty($showCovers) && $printInterface === false) || ($printInterface === true && $printEntryCovers === true)}
{if (!empty($showRatings) && $printInterface === false) || ($printInterface === true && $printEntryRating === true)} {include file="GroupedWork/title-rating.tpl" id=$summId ratingData=$summRating showNotInterested=false} @@ -46,14 +46,59 @@
{/if} - {if (!empty($summSeries) && !empty($summSeries.seriesTitle)) && ($printInterface === false || ($printInterface === true && $printEntrySeries === true))} + {if !empty($showSeries) && (!empty($summSeries) && !empty($summSeries.seriesTitle)) && ($printInterface === false || ($printInterface === true && $printEntrySeries === true))} {* If the series has an ISBN, use it to make the class unique to this series *} -
-
{translate text="Series" isPublicFacing=true}
-
- {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if} + {assign var=indexedSeries value=$recordDriver->getIndexedSeries()} + {if ($summSeries && empty($summSeries.allHidden)) || ($indexedSeries && empty($summSeries.fromSeriesIndex))} +
+
{translate text="Series" isPublicFacing=true}
+
+ {if !empty($summSeries)} + {if !empty($summSeries.fromNovelist)} + {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text=volume isPublicFacing=true} {$summSeries.volume|format_float_with_min_decimals}{/if}
+ {elseif !empty($summSeries.fromSeriesIndex)} + {if !$summSeries.hidden} + {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if}
+ {/if} + {if !empty($summSeries.additionalSeries)} + {assign var=numSeriesShown value=1} + {foreach from=$summSeries.additionalSeries item=additional} + {if !$additional.hidden} + {assign var=numSeriesShown value=$numSeriesShown+1} + {if $numSeriesShown == 4} + {translate text='More Series...' isPublicFacing=true} + + {/if} + {/if} + {elseif !empty($summSeries.seriesTitle)} + {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if}
+ {/if} + {/if} + {if !empty($indexedSeries) && empty($summSeries.fromSeriesIndex)} + {assign var=numSeriesShown value=0} + {foreach from=$indexedSeries item=seriesItem name=loop} + {if !isset($summSeries.seriesTitle) || ((strpos(strtolower($seriesItem.seriesTitle), strtolower($summSeries.seriesTitle)) === false) && (strpos(strtolower($summSeries.seriesTitle), strtolower($seriesItem.seriesTitle)) === false))} + {assign var=numSeriesShown value=$numSeriesShown+1} + {if $numSeriesShown == 4} + {translate text='More Series...' isPublicFacing=true} + + {/if} + {/if} +
-
+ {/if} {/if} {if (!empty($listEntryNotes) && $printInterface === false) || (!empty($listEntryNotes) && $printInterface === true && $printEntryNotes === true)} @@ -89,10 +134,10 @@ {/if} {if $printInterface === false} -
- {include file='GroupedWork/result-tools-horizontal.tpl' ratingData=$summRating recordUrl=$summUrl showMoreInfo=true showNotInterested=false} -
- {/if} +
+ {include file='GroupedWork/result-tools-horizontal.tpl' ratingData=$summRating recordUrl=$summUrl showMoreInfo=true showNotInterested=false} +
+ {/if}
{if !empty($listEditAllowed) && $printInterface === false} diff --git a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/result.tpl index cbb27dbaec..3a5f9ee80b 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/result.tpl @@ -12,7 +12,7 @@ {/if} @@ -28,7 +28,7 @@ {* Title Row *} @@ -84,6 +84,20 @@ {/if} {elseif !empty($summSeries.seriesTitle)} {$summSeries.seriesTitle}{if !empty($summSeries.volume)} {translate text="volume %1%" 1=$summSeries.volume|format_float_with_min_decimals isPublicFacing=true}{/if}
+ {if !empty($summSeries.additionalSeries)} + {assign var=numSeriesShown value=1} + {foreach from=$summSeries.additionalSeries item=additional} + {assign var=numSeriesShown value=$numSeriesShown+1} + {if $numSeriesShown == 4} + {translate text='More Series...' isPublicFacing=true} + + {/if} + {/if} {/if} {/if} {if !empty($indexedSeries) && empty($summSeries.fromSeriesIndex)} @@ -125,7 +139,7 @@ {if !empty($showPublicationDate) && $showPublicationDate} {if $alwaysShowSearchResultsMainDetails || $summPubDate} -
{translate text="Pub. Date" isPublicFacing=true}
+
{translate text="Publication Date" isPublicFacing=true}
{if !empty($summPubDate)} {$summPubDate|escape} @@ -139,7 +153,7 @@ {if !empty($showPlaceOfPublication) && $showPlaceOfPublication} {if $alwaysShowSearchResultsMainDetails || $summPlaceOfPublication} -
{translate text="Pub. Places" isPublicFacing=true}
+
{translate text="Publication Places" isPublicFacing=true}
{if !empty($summPlaceOfPublication)} {$summPlaceOfPublication|escape} diff --git a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/seriesEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/seriesEntry.tpl index 55abf83fc9..8864d26d90 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/seriesEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/seriesEntry.tpl @@ -4,7 +4,7 @@ {if !empty($showCovers)} {/if} diff --git a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/staff-view.tpl b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/staff-view.tpl index 852f0a2a15..c72efd175d 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/staff-view.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/GroupedWork/staff-view.tpl @@ -20,7 +20,7 @@ {if !empty($loggedIn) && in_array('Set Grouped Work Display Information', $userPermissions)} {/if} -{if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} +{if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions) && !$recordDriver->isManuallyGrouped()} {/if} diff --git a/code/web/interface/themes/responsive/RecordDrivers/Hoopla/staff-view.tpl b/code/web/interface/themes/responsive/RecordDrivers/Hoopla/staff-view.tpl index 4851175910..549604af8a 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Hoopla/staff-view.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Hoopla/staff-view.tpl @@ -19,9 +19,23 @@ {if !empty($loggedIn) && in_array('Set Grouped Work Display Information', $userPermissions)} {/if} - {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} - - + {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions) && !$recordDriver->isInManuallyGroupedWork()} + {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/Index/browse_result.tpl b/code/web/interface/themes/responsive/RecordDrivers/Index/browse_result.tpl index 271b0c895a..5af995ddcc 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Index/browse_result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Index/browse_result.tpl @@ -2,7 +2,7 @@ {if $accessibleBrowseCategories == '1' && $action != 'Results' && !$isForSearchResults} @@ -10,7 +10,7 @@ {if $browseMode == '1'} @@ -19,7 +19,7 @@
- {$summTitle} + {$summTitle}
{if !empty($showRatings) && $browseCategoryRatingsMode != 0} diff --git a/code/web/interface/themes/responsive/RecordDrivers/Index/combinedResult.tpl b/code/web/interface/themes/responsive/RecordDrivers/Index/combinedResult.tpl index ec08cba182..9197eed4b3 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Index/combinedResult.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Index/combinedResult.tpl @@ -10,7 +10,7 @@
{if $disableCoverArt != 1} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/List/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/List/listEntry.tpl index 25b0dd6e7a..20e3703f51 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/List/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/List/listEntry.tpl @@ -10,7 +10,7 @@
{if $disableCoverArt != 1} - {translate text='Cover Image' inAttribute=true isPublicFacing=true} + {translate text='Cover Image' inAttribute=true isPublicFacing=true} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/List/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/List/result.tpl index 67d86fa677..62d631be44 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/List/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/List/result.tpl @@ -4,7 +4,7 @@ diff --git a/code/web/interface/themes/responsive/RecordDrivers/Marc/staff.tpl b/code/web/interface/themes/responsive/RecordDrivers/Marc/staff.tpl index 45d2c2de0a..ef90d6d33b 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Marc/staff.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Marc/staff.tpl @@ -52,8 +52,22 @@ {/if} {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} - - + {/if} {/if} {if !empty($loggedIn) && in_array('Download MARC Records', $userPermissions)} diff --git a/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/listEntry.tpl index 80c865ba74..c76861d521 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/listEntry.tpl @@ -10,7 +10,7 @@
{if $disableCoverArt != 1} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/result.tpl index 22a645fc29..3305ad6edb 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/OpenArchives/result.tpl @@ -4,7 +4,7 @@ diff --git a/code/web/interface/themes/responsive/RecordDrivers/OverDrive/staff.tpl b/code/web/interface/themes/responsive/RecordDrivers/OverDrive/staff.tpl index 58ae3691c9..a003198bdb 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/OverDrive/staff.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/OverDrive/staff.tpl @@ -19,8 +19,22 @@ {/if} {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} - - + {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/PalaceProject/staff-view.tpl b/code/web/interface/themes/responsive/RecordDrivers/PalaceProject/staff-view.tpl index 0e3d2fab05..4e26e424b4 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/PalaceProject/staff-view.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/PalaceProject/staff-view.tpl @@ -18,10 +18,24 @@ {/if} {if !empty($loggedIn) && in_array('Set Grouped Work Display Information', $userPermissions)} - {/if} - {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions)} - + {if !empty($loggedIn) && in_array('Manually Group and Ungroup Works', $userPermissions) && !$recordDriver->isInManuallyGroupedWork()} + {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/Series/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/Series/listEntry.tpl index 6f74e324a1..1dabf90a2b 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Series/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Series/listEntry.tpl @@ -11,7 +11,7 @@ {if $disableCoverArt != 1} {if !empty($isNew)}{translate text="New!" isPublicFacing=true} {/if} - {translate text='Cover Image' inAttribute=true isPublicFacing=true} + {translate text='Cover Image' inAttribute=true isPublicFacing=true} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/Series/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/Series/result.tpl index ffd12fc526..e203432858 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Series/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Series/result.tpl @@ -6,7 +6,7 @@
{if !empty($isNew)}{translate text="New!" isPublicFacing=true} {/if} - {$summTitle|removeTrailingPunctuation|highlight|escapeCSS|truncate:180: + {$summTitle|removeTrailingPunctuation|highlight|escapeCSS|truncate:180:
{/if} diff --git a/code/web/interface/themes/responsive/RecordDrivers/Summon/combinedResult.tpl b/code/web/interface/themes/responsive/RecordDrivers/Summon/combinedResult.tpl index c37982a233..4ab2afb3c3 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Summon/combinedResult.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Summon/combinedResult.tpl @@ -4,7 +4,7 @@
{if $disableCoverArt != 1} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/Summon/listEntry.tpl b/code/web/interface/themes/responsive/RecordDrivers/Summon/listEntry.tpl index 7853ed5899..8f4d08eeb9 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Summon/listEntry.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Summon/listEntry.tpl @@ -10,7 +10,7 @@
{if $disableCoverArt != 1 && !empty($bookCoverUrlMedium)} {/if}
diff --git a/code/web/interface/themes/responsive/RecordDrivers/Summon/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/Summon/result.tpl index dfd9cfcdb0..7366af4f72 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Summon/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Summon/result.tpl @@ -4,7 +4,7 @@ diff --git a/code/web/interface/themes/responsive/RecordDrivers/Talpa/result.tpl b/code/web/interface/themes/responsive/RecordDrivers/Talpa/result.tpl index a333f306f4..60ad5ae4a4 100644 --- a/code/web/interface/themes/responsive/RecordDrivers/Talpa/result.tpl +++ b/code/web/interface/themes/responsive/RecordDrivers/Talpa/result.tpl @@ -11,7 +11,7 @@ {if !empty($showCovers)}