From 17b4f8bd3fc4cfad88a3fe1ef7e10c76d6cdd52d Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Wed, 18 Apr 2018 09:03:30 -0400
Subject: [PATCH] Add "recently returned" / "trending items" channels (#1075)

- Includes underlying Voyager and Demo ILS driver support.
---
 config/vufind/channels.ini                    |  22 ++++
 languages/en.ini                              |   2 +
 .../VuFind/ChannelProvider/PluginManager.php  |   6 ++
 .../ChannelProvider/RecentlyReturned.php      |  72 +++++++++++++
 .../ChannelProvider/TrendingILSItems.php      |  88 +++++++++++++++
 module/VuFind/src/VuFind/ILS/Driver/Demo.php  |  35 ++++++
 .../VuFind/src/VuFind/ILS/Driver/Voyager.php  | 101 ++++++++++++++++++
 7 files changed, 326 insertions(+)
 create mode 100644 module/VuFind/src/VuFind/ChannelProvider/RecentlyReturned.php
 create mode 100644 module/VuFind/src/VuFind/ChannelProvider/TrendingILSItems.php

diff --git a/config/vufind/channels.ini b/config/vufind/channels.ini
index 221efba4404..3270e332bbf 100644
--- a/config/vufind/channels.ini
+++ b/config/vufind/channels.ini
@@ -31,14 +31,22 @@ cache_home_channels = true
 ; random - Pick random results from the result set (in search results) or from
 ; the search backend from which the record was retrieved (in record results).
 ;
+; recentlyreturned - Display items that were recently returned by patrons;
+; requires an ILS driver that supports this feature.
+;
 ; similaritems - Find records similar to a specific record, or to top hits in a
 ; set of search results.
+;
+; trendingilsitems - Display items that have displayed a lot of recent activity in
+; the ILS. (Exact definition of "trending" may vary from system to system).
 [source.Solr]
 ; Providers to use on the home page (these will be populated with a blank search
 ; by default; order matters!)
 home[] = "facets:provider.facets.home"
 ;home[] = "random"
 ;home[] = "newilsitems"
+;home[] = "recentlyreturned"
+;home[] = "trendingilsitems"
 ;home[] = "listitems"
 ; Providers to use for record-based channels (order matters!)
 record[] = "similaritems"
@@ -145,9 +153,23 @@ channelSize = 20
 ; entire backend.
 mode = "retain"
 
+; This section contains default settings for the RecentlyReturned channel provider
+[provider.recentlyreturned]
+; Number of results to include in the channel.
+channelSize = 20
+; Maximum age of results to return (in days)
+maxAge = 30
+
 ; This section contains default settings for the SimilarItems channel provider
 [provider.similaritems]
 ; Number of results to include in each channel.
 channelSize = 20
 ; Maximum number of records to examine for similar results.
 maxRecordsToExamine = 2
+
+; This section contains default settings for the TrendingILSItems channel provider
+[provider.trendingilsitems]
+; Number of results to include in the channel.
+channelSize = 20
+; Maximum age of results to return (in days)
+maxAge = 90
diff --git a/languages/en.ini b/languages/en.ini
index 88bf2f16919..bd9f5bb6450 100644
--- a/languages/en.ini
+++ b/languages/en.ini
@@ -799,6 +799,7 @@ Range slider = "Range slider"
 Read the full review online... = "Read the full review online..."
 Recall This = "Recall This"
 recaptcha_not_passed = "CAPTCHA not passed"
+recently_returned_channel_title = "Recently Returned"
 Record Citations = "Record Citations"
 Record Count = "Record Count"
 Record Type = "Record Type"
@@ -1047,6 +1048,7 @@ total_tags = "Total Tags"
 total_users = "Total Users"
 Transliterated Title = "Transliterated Title"
 tree_search_limit_reached_html = "Your search returned too many results to display in the tree. Showing only the first <b>%%limit%%</b> items. For a full search click <a id="fullSearchLink" href="%%url%%" target="_blank">here.</a>"
+trending_items_channel_title = "Trending Items"
 unique_tags = "Unique Tags"
 University Library = "University Library"
 Unknown = "Unknown"
diff --git a/module/VuFind/src/VuFind/ChannelProvider/PluginManager.php b/module/VuFind/src/VuFind/ChannelProvider/PluginManager.php
index e9c5e523fcd..603b7c1254d 100644
--- a/module/VuFind/src/VuFind/ChannelProvider/PluginManager.php
+++ b/module/VuFind/src/VuFind/ChannelProvider/PluginManager.php
@@ -49,7 +49,9 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
         'listitems' => 'VuFind\ChannelProvider\ListItems',
         'newilsitems' => 'VuFind\ChannelProvider\NewILSItems',
         'random' => 'VuFind\ChannelProvider\Random',
+        'recentlyreturned' => 'VuFind\ChannelProvider\RecentlyReturned',
         'similaritems' => 'VuFind\ChannelProvider\SimilarItems',
+        'trendingilsitems' => 'VuFind\ChannelProvider\TrendingILSItems',
     ];
 
     /**
@@ -68,8 +70,12 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
             'VuFind\ChannelProvider\AbstractILSChannelProviderFactory',
         'VuFind\ChannelProvider\Random' =>
             'VuFind\ChannelProvider\Factory::getRandom',
+        'VuFind\ChannelProvider\RecentlyReturned' =>
+            'VuFind\ChannelProvider\AbstractILSChannelProviderFactory',
         'VuFind\ChannelProvider\SimilarItems' =>
             'VuFind\ChannelProvider\Factory::getSimilarItems',
+        'VuFind\ChannelProvider\TrendingILSItems' =>
+            'VuFind\ChannelProvider\AbstractILSChannelProviderFactory',
     ];
 
     /**
diff --git a/module/VuFind/src/VuFind/ChannelProvider/RecentlyReturned.php b/module/VuFind/src/VuFind/ChannelProvider/RecentlyReturned.php
new file mode 100644
index 00000000000..82d7eebb557
--- /dev/null
+++ b/module/VuFind/src/VuFind/ChannelProvider/RecentlyReturned.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * "Recently returned" channel provider.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2017.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Channels
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\ChannelProvider;
+
+/**
+ * "Recently returned" channel provider.
+ *
+ * @category VuFind
+ * @package  Channels
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class RecentlyReturned extends AbstractILSChannelProvider
+{
+    /**
+     * Channel title (will be run through translator).
+     *
+     * @var string
+     */
+    protected $channelTitle = 'recently_returned_channel_title';
+
+    /**
+     * Retrieve data from the ILS.
+     *
+     * @return array
+     */
+    protected function getIlsResponse()
+    {
+        return $this->ils->checkCapability('getRecentlyReturnedBibs')
+            ? $this->ils->getRecentlyReturnedBibs($this->channelSize, $this->maxAge)
+            : [];
+    }
+
+    /**
+     * Given one element from the ILS function's response array, extract the
+     * ID value.
+     *
+     * @param array $response Response array
+     *
+     * @return string
+     */
+    protected function extractIdsFromResponse($response)
+    {
+        return $response['id'];
+    }
+}
diff --git a/module/VuFind/src/VuFind/ChannelProvider/TrendingILSItems.php b/module/VuFind/src/VuFind/ChannelProvider/TrendingILSItems.php
new file mode 100644
index 00000000000..ee1f4795ddb
--- /dev/null
+++ b/module/VuFind/src/VuFind/ChannelProvider/TrendingILSItems.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * "Trending ILS items" channel provider.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Channels
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\ChannelProvider;
+
+/**
+ * "Trending ILS items" channel provider.
+ *
+ * @category VuFind
+ * @package  Channels
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class TrendingILSItems extends AbstractILSChannelProvider
+{
+    /**
+     * Channel title (will be run through translator).
+     *
+     * @var string
+     */
+    protected $channelTitle = 'trending_items_channel_title';
+
+    /**
+     * Set the options for the provider.
+     *
+     * @param array $options Options
+     *
+     * @return void
+     */
+    public function setOptions(array $options)
+    {
+        // Use higher default age for trending.
+        if (!isset($options['maxAge'])) {
+            $options['maxAge'] = 90;
+        }
+        return parent::setOptions($options);
+    }
+
+    /**
+     * Retrieve data from the ILS.
+     *
+     * @return array
+     */
+    protected function getIlsResponse()
+    {
+        return $this->ils->checkCapability('getTrendingBibs')
+            ? $this->ils->getTrendingBibs($this->channelSize, $this->maxAge)
+            : [];
+    }
+
+    /**
+     * Given one element from the ILS function's response array, extract the
+     * ID value.
+     *
+     * @param array $response Response array
+     *
+     * @return string
+     */
+    protected function extractIdsFromResponse($response)
+    {
+        return $response['id'];
+    }
+}
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Demo.php b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
index 3ef066c1be9..ded059e02b7 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Demo.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
@@ -2194,4 +2194,39 @@ class Demo extends AbstractBase
         }
         return [];
     }
+
+    /**
+     * Get bib records for recently returned items.
+     *
+     * @param int   $limit  Maximum number of records to retrieve (default = 30)
+     * @param int   $maxage The maximum number of days to consider "recently
+     * returned."
+     * @param array $patron Patron Data
+     *
+     * @return array
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getRecentlyReturnedBibs($limit = 30, $maxage = 30,
+        $patron = null
+    ) {
+        // This is similar to getNewItems for demo purposes.
+        $results = $this->getNewItems(1, $limit, $maxage);
+        return $results['results'];
+    }
+
+    /**
+     * Get bib records for "trending" items (recently returned with high usage).
+     *
+     * @param int   $limit  Maximum number of records to retrieve (default = 30)
+     * @param int   $maxage The maximum number of days' worth of data to examine.
+     * @param array $patron Patron Data
+     *
+     * @return array
+     */
+    public function getTrendingBibs($limit = 30, $maxage = 30, $patron = null)
+    {
+        // This is similar to getRecentlyReturnedBibs for demo purposes.
+        return $this->getRecentlyReturnedBibs($limit, $maxage, $patron);
+    }
 }
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
index 68d8a62a434..34a48256a1e 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
@@ -2454,6 +2454,107 @@ EOT;
         return $recordList;
     }
 
+    /**
+     * Get bib records for recently returned items.
+     *
+     * @param int   $limit  Maximum number of records to retrieve (default = 30)
+     * @param int   $maxage The maximum number of days to consider "recently
+     * returned."
+     * @param array $patron Patron Data
+     *
+     * @return array
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getRecentlyReturnedBibs($limit = 30, $maxage = 30,
+        $patron = null
+    ) {
+        $recordList = [];
+
+        // Oracle does not support the SQL LIMIT clause before version 12, so
+        // instead we need to provide an optimizer hint, which requires us to
+        // ensure that $limit is a valid integer.
+        $intLimit = intval($limit);
+        $safeLimit = $intLimit < 1 ? 30 : $intLimit;
+
+        $sql = "select /*+ FIRST_ROWS($safeLimit) */ BIB_MFHD.BIB_ID, "
+            . "max(CIRC_TRANS_ARCHIVE.DISCHARGE_DATE) as RETURNED "
+            . "from $this->dbName.CIRC_TRANS_ARCHIVE "
+            . "join $this->dbName.MFHD_ITEM "
+            . "on CIRC_TRANS_ARCHIVE.ITEM_ID = MFHD_ITEM.ITEM_ID "
+            . "join $this->dbName.BIB_MFHD "
+            . "on BIB_MFHD.MFHD_ID = MFHD_ITEM.MFHD_ID "
+            . "join $this->dbName.BIB_MASTER "
+            . "on BIB_MASTER.BIB_ID = BIB_MFHD.BIB_ID "
+            . "where CIRC_TRANS_ARCHIVE.DISCHARGE_DATE is not null "
+            . "and CIRC_TRANS_ARCHIVE.DISCHARGE_DATE > SYSDATE - :maxage "
+            . "and BIB_MASTER.SUPPRESS_IN_OPAC='N' "
+            . "group by BIB_MFHD.BIB_ID "
+            . "order by RETURNED desc";
+        try {
+            $sqlStmt = $this->executeSQL($sql, [':maxage' => $maxage]);
+            while (count($recordList) < $limit
+                && $row = $sqlStmt->fetch(PDO::FETCH_ASSOC)
+            ) {
+                $recordList[] = ['id' => $row['BIB_ID']];
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $recordList;
+    }
+
+    /**
+     * Get bib records for "trending" items (recently returned with high usage).
+     *
+     * @param int   $limit  Maximum number of records to retrieve (default = 30)
+     * @param int   $maxage The maximum number of days' worth of data to examine.
+     * @param array $patron Patron Data
+     *
+     * @return array
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getTrendingBibs($limit = 30, $maxage = 30, $patron = null)
+    {
+        $recordList = [];
+
+        // Oracle does not support the SQL LIMIT clause before version 12, so
+        // instead we need to provide an optimizer hint, which requires us to
+        // ensure that $limit is a valid integer.
+        $intLimit = intval($limit);
+        $safeLimit = $intLimit < 1 ? 30 : $intLimit;
+
+        $sql = "select /*+ FIRST_ROWS($safeLimit) */ BIB_MFHD.BIB_ID, "
+            . "count(CIRC_TRANS_ARCHIVE.DISCHARGE_DATE) as RECENT, "
+            . "sum(ITEM.HISTORICAL_CHARGES) as OVERALL "
+            . "from $this->dbName.CIRC_TRANS_ARCHIVE "
+            . "join $this->dbName.MFHD_ITEM "
+            . "on CIRC_TRANS_ARCHIVE.ITEM_ID = MFHD_ITEM.ITEM_ID "
+            . "join $this->dbName.BIB_MFHD "
+            . "on BIB_MFHD.MFHD_ID = MFHD_ITEM.MFHD_ID "
+            . "join $this->dbName.ITEM "
+            . "on CIRC_TRANS_ARCHIVE.ITEM_ID = ITEM.ITEM_ID "
+            . "join $this->dbName.BIB_MASTER "
+            . "on BIB_MASTER.BIB_ID = BIB_MFHD.BIB_ID "
+            . "where CIRC_TRANS_ARCHIVE.DISCHARGE_DATE is not null "
+            . "and CIRC_TRANS_ARCHIVE.DISCHARGE_DATE > SYSDATE - :maxage "
+            . "and BIB_MASTER.SUPPRESS_IN_OPAC='N' "
+            . "group by BIB_MFHD.BIB_ID "
+            . "order by RECENT desc, OVERALL desc";
+        try {
+            $sqlStmt = $this->executeSQL($sql, [':maxage' => $maxage]);
+            while (count($recordList) < $limit
+                && $row = $sqlStmt->fetch(PDO::FETCH_ASSOC)
+            ) {
+                $recordList[] = ['id' => $row['BIB_ID']];
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $recordList;
+    }
+
     /**
      * Get suppressed records.
      *
-- 
GitLab