diff --git a/config/vufind/channels.ini b/config/vufind/channels.ini index 522236fbaa3346b172cd16fd75f9241d0a643273..ec6ffc605fb19257b51218ef57688a22de1b3abb 100644 --- a/config/vufind/channels.ini +++ b/config/vufind/channels.ini @@ -13,6 +13,10 @@ default_home_source = "Solr" ; ; Available providers: ; +; alphabrowse - Find nearby records in the alphabetical browse index; only +; supported for Solr; requires you to generate the index as described at +; https://vufind.org/wiki/indexing:alphabetical_heading_browse +; ; facets - Find records matching facet values from search results (or try to find ; other records matching the facet values of a specific record). ; @@ -32,9 +36,11 @@ home[] = "facets:provider.facets.home" ; Providers to use for record-based channels (order matters!) record[] = "similaritems" record[] = "facets" +;record[] = "alphabrowse" ; Providers to use for search-based channels (order matters!) search[] = "facets" search[] = "similaritems" +;search[] = "alphabrowse" ; This section controls which providers are used for Summon searches/records. ; See [source.Solr] above for more details. @@ -47,6 +53,20 @@ record[] = "facets:provider.facets.Summon" ; Providers to use for search-based channels (order matters!) search[] = "facets:provider.facets.Summon" +; This section contains default settings for the AlphaBrowse channel provider +[provider.alphabrowse] +; Which index to use (recommended: lcc or dewey for call number browsing) +browseIndex = lcc +; Which Solr field should we use to seed the index search? (recommended: +; callnumber-raw for lcc, dewey-raw for dewey). +solrField = callnumber-raw +; How many rows before the current record to display +rows_before = 10 +; Maximum number of records to examine for similar results. +maxRecordsToExamine = 2 +; Number of results to include in each channel. +channelSize = 20 + ; This section contains home-page-specific settings for the Facets channel provider [provider.facets.home] ; Facet fields to use (field name => description). diff --git a/languages/en.ini b/languages/en.ini index c7d9c55e5cdfa80e309a3fcfbe30faa83c44d3b1..5274d0bf6acc10774e25a8c983289abd0909cbe1 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -172,6 +172,7 @@ catalog_login_desc = "Enter your library catalog credentials." CD = "CD" Change Password = "Change Password" channel_add_more = "Add more channels like this" +channel_browse = "Browse more records" channel_expand = "Explore related channels" channel_explore = "Explore Channels" channel_search = "Show items as search results" @@ -627,6 +628,7 @@ My Holds = "Holds" My Profile = "Profile" Narrow Search = "Narrow Search" navigate_back = "Back" +nearby_items = "Items Near "%%title%%"" Need Help? = "Need Help?" New Item Feed = "New Item Feed" New Item Search = "New Item Search" diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index e19a4297cfaee409ff4e218c968f846ae0f55a86..3391b5d612994655c09cca6ef9a14d9f7c26c2ef 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -339,6 +339,7 @@ $config = [ ], 'channelprovider' => [ 'factories' => [ + 'alphabrowse' => 'VuFind\ChannelProvider\Factory::getAlphaBrowse', 'facets' => 'VuFind\ChannelProvider\Factory::getFacets', 'listitems' => 'VuFind\ChannelProvider\Factory::getListItems', 'random' => 'VuFind\ChannelProvider\Factory::getRandom', diff --git a/module/VuFind/src/VuFind/ChannelProvider/AlphaBrowse.php b/module/VuFind/src/VuFind/ChannelProvider/AlphaBrowse.php new file mode 100644 index 0000000000000000000000000000000000000000..980aa4b145a1b563b870c0df9a0b7952ce6ec151 --- /dev/null +++ b/module/VuFind/src/VuFind/ChannelProvider/AlphaBrowse.php @@ -0,0 +1,316 @@ +<?php +/** + * Alphabrowse channel provider. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2016. + * + * 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; +use VuFind\RecordDriver\AbstractBase as RecordDriver; +use VuFind\Record\Router as RecordRouter; +use VuFind\Search\Base\Params, VuFind\Search\Base\Results; +use VuFind\I18n\Translator\TranslatorAwareInterface; +use VuFindSearch\Backend\Solr\Backend; +use VuFindSearch\ParamBag; +use Zend\Mvc\Controller\Plugin\Url; + +/** + * Alphabrowse 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 AlphaBrowse extends AbstractChannelProvider + implements TranslatorAwareInterface +{ + use \VuFind\I18n\Translator\TranslatorAwareTrait; + + /** + * Number of results to include in each channel. + * + * @var int + */ + protected $channelSize; + + /** + * Maximum number of records to examine for similar results. + * + * @var int + */ + protected $maxRecordsToExamine; + + /** + * Search service + * + * @var \VuFindSearch\Service + */ + protected $searchService; + + /** + * Solr backend + * + * @var Backend + */ + protected $solr; + + /** + * URL helper + * + * @var Url + */ + protected $url; + + /** + * Record router + * + * @var RecordRouter + */ + protected $recordRouter; + + /** + * Browse index to search + * + * @var string + */ + protected $browseIndex; + + /** + * Solr field to use for search seed + * + * @var string + */ + protected $solrField; + + /** + * How many rows to show before the selected value + * + * @var int + */ + protected $rowsBefore; + + /** + * Constructor + * + * @param \VuFindSearch\Service $search Search service + * @param Backend $solr Solr backend + * @param Url $url URL helper + * @param RecordRouter $router Record router + * @param array $options Settings (optional) + */ + public function __construct(\VuFindSearch\Service $search, Backend $solr, + Url $url, RecordRouter $router, array $options = [] + ) { + $this->searchService = $search; + $this->solr = $solr; + $this->url = $url; + $this->recordRouter = $router; + $this->setOptions($options); + } + + /** + * Set the options for the provider. + * + * @param array $options Options + * + * @return void + */ + public function setOptions(array $options) + { + $this->channelSize = isset($options['channelSize']) + ? $options['channelSize'] : 20; + $this->maxRecordsToExamine = isset($options['maxRecordsToExamine']) + ? $options['maxRecordsToExamine'] : 2; + $this->browseIndex = isset($options['browseIndex']) ? + $options['browseIndex'] : 'lcc'; + $this->solrField = isset($options['solrField']) ? + $options['solrField'] : 'callnumber-raw'; + $this->rowsBefore = isset($options['rows_before']) ? + $options['rows_before'] : 10; + } + + /** + * Return channel information derived from a record driver object. + * + * @param RecordDriver $driver Record driver + * @param string $channelToken Token identifying a single specific channel + * to load (if omitted, all channels will be loaded) + * + * @return array + */ + public function getFromRecord(RecordDriver $driver, $channelToken = null) + { + // If we have a token and it doesn't match the record driver, we can't + // fetch any results! + if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) { + return []; + } + $channel = $this->buildChannelFromRecord($driver); + return (count($channel['contents']) > 0) ? [$channel] : []; + } + + /** + * Return channel information derived from a search results object. + * + * @param Results $results Search results + * @param string $channelToken Token identifying a single specific channel + * to load (if omitted, all channels will be loaded) + * + * @return array + */ + public function getFromSearch(Results $results, $channelToken = null) + { + $channels = []; + foreach ($results->getResults() as $driver) { + // If we have a token and it doesn't match the current driver, skip + // that driver. + if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) { + continue; + } + $channel = (count($channels) < $this->maxRecordsToExamine) + ? $this->buildChannelFromRecord($driver) + : $this->buildChannelFromRecord($driver, true); + if (isset($channel['token']) || count($channel['contents']) > 0) { + $channels[] = $channel; + } + } + // If the search results did not include the object we were looking for, + // we need to fetch it from the search service: + if (empty($channels) && is_object($driver) && $channelToken !== null) { + $driver = $this->searchService->retrieve( + $driver->getSourceIdentifier(), $channelToken + )->first(); + if ($driver) { + $channels[] = $this->buildChannelFromRecord($driver); + } + } + return $channels; + } + + /** + * Given details from alphabeticBrowse(), create channel contents. + * + * @param array $details Details from alphabetic browse index + * + * @return array + */ + protected function summarizeBrowseDetails($details) + { + $ids = $results = []; + if (isset($details['Browse']['items'])) { + foreach ($details['Browse']['items'] as $item) { + if (!isset($item['extras']['title'][0][0])) { + continue; + } + // Collect a list of IDs in the result set while we create it: + $ids[] = $id = $item['extras']['id'][0][0]; + $results[] = [ + 'title' => $item['extras']['title'][0][0], + 'source' => 'Solr', + 'thumbnail' => false, // TODO: better thumbnails! + 'id' => $id + ]; + } + } + // If we have a cover router and a non-empty ID list, look up thumbnails: + if ($this->coverRouter && !empty($ids)) { + $records = $this->searchService->retrieveBatch('Solr', $ids); + $thumbs = []; + // First map record drivers to an ID => thumb array... + foreach ($records as $record) { + $thumbs[$record->getUniqueId()] = $this->coverRouter + ->getUrl($record, 'medium'); + } + // Now apply the thumbnails to the existing result set... + foreach ($results as $i => $current) { + if (isset($thumbs[$current['id']])) { + $results[$i]['thumbnail'] = $thumbs[$current['id']]; + } + } + } + return $results; + } + + /** + * Add a new filter to an existing search results object to populate a + * channel. + * + * @param RecordDriver $driver Record driver + * @param bool $tokenOnly Create full channel (false) or return a + * token for future loading (true)? + * + * @return array + */ + protected function buildChannelFromRecord(RecordDriver $driver, + $tokenOnly = false + ) { + $retVal = [ + 'title' => $this->translate( + 'nearby_items', ['%%title%%' => $driver->getBreadcrumb()] + ), + 'providerId' => $this->providerId, + 'links' => [] + ]; + $raw = $driver->getRawData(); + $from = isset($raw[$this->solrField]) ? (array)$raw[$this->solrField] : null; + if (empty($from[0])) { + // If there is no "from" value to look up, skip this so we don't + //generate a token that retrieves nothing later! + $retVal['contents'] = []; + } elseif ($tokenOnly) { + $retVal['token'] = $driver->getUniqueID(); + } else { + // If we got this far, we can safely assume that $from[0] is set + $details = $this->solr->alphabeticBrowse( + $this->browseIndex, $from[0], 0, $this->channelSize, + new ParamBag(['extras' => 'title:author:isbn:id']), + -$this->rowsBefore + ); + $retVal['contents'] = $this->summarizeBrowseDetails($details); + $route = $this->recordRouter->getRouteDetails($driver); + $retVal['links'][] = [ + 'label' => 'View Record', + 'icon' => 'fa-file-text-o', + 'url' => $this->url + ->fromRoute($route['route'], $route['params']) + ]; + $retVal['links'][] = [ + 'label' => 'channel_expand', + 'icon' => 'fa-search-plus', + 'url' => $this->url->fromRoute('channels-record') + . '?id=' . urlencode($driver->getUniqueID()) + . '&source=' . urlencode($driver->getSourceIdentifier()) + ]; + $retVal['links'][] = [ + 'label' => 'channel_browse', + 'icon' => 'fa-list', + 'url' => $this->url->fromRoute('alphabrowse-home') + . '?source=' . urlencode($this->browseIndex) + . '&from=' . $from[0] + ]; + } + return $retVal; + } +} diff --git a/module/VuFind/src/VuFind/ChannelProvider/Factory.php b/module/VuFind/src/VuFind/ChannelProvider/Factory.php index 413803f34c6184486e5aa1bf344eb710b85652fc..25a1b96172b158a006e64828ad2506054e04a025 100644 --- a/module/VuFind/src/VuFind/ChannelProvider/Factory.php +++ b/module/VuFind/src/VuFind/ChannelProvider/Factory.php @@ -41,6 +41,28 @@ use Zend\ServiceManager\ServiceManager; */ class Factory { + /** + * Construct the AlphaBrowse channel provider. + * + * @param ServiceManager $sm Service manager. + * + * @return AlphaBrowse + */ + public static function getAlphaBrowse(ServiceManager $sm) + { + $helper = new AlphaBrowse( + $sm->getServiceLocator()->get('VuFind\Search'), + $sm->getServiceLocator()->get('VuFind\Search\BackendManager') + ->get('Solr'), + $sm->getServiceLocator()->get('ControllerPluginManager')->get('url'), + $sm->getServiceLocator()->get('VuFind\RecordRouter') + ); + $helper->setCoverRouter( + $sm->getServiceLocator()->get('VuFind\Cover\Router') + ); + return $helper; + } + /** * Construct the Facets channel provider. * diff --git a/themes/bootstrap3/templates/channels/channelList.phtml b/themes/bootstrap3/templates/channels/channelList.phtml index b2e9f6008073325e497f8b84a9edf0b320cfce43..788851f5d47e76fc1b33ccd20d657b1064d86bc0 100644 --- a/themes/bootstrap3/templates/channels/channelList.phtml +++ b/themes/bootstrap3/templates/channels/channelList.phtml @@ -4,6 +4,7 @@ <? $this->headScript()->appendFile('vendor/jquery.dotdotdot.min.js'); ?> <? $this->jsTranslations()->addStrings([ + 'channel_browse' => 'channel_browse', 'channel_expand' => 'channel_expand', 'channel_search' => 'channel_search', 'nohit_heading' => 'nohit_heading',