From 872a35e4e15aa9d199683aa03e9c36a2347b4d2c Mon Sep 17 00:00:00 2001 From: Demian Katz <demian.katz@villanova.edu> Date: Thu, 25 Oct 2012 11:26:39 -0400 Subject: [PATCH] Resolving VUFIND-582 (allow indexing reserves from CSV); fixed some comments. --- .../VuFind/src/VuFind/Reserves/CsvReader.php | 264 ++++++++++++++++++ .../Controller/UtilController.php | 139 +++++++-- 2 files changed, 376 insertions(+), 27 deletions(-) create mode 100644 module/VuFind/src/VuFind/Reserves/CsvReader.php diff --git a/module/VuFind/src/VuFind/Reserves/CsvReader.php b/module/VuFind/src/VuFind/Reserves/CsvReader.php new file mode 100644 index 00000000000..07514597626 --- /dev/null +++ b/module/VuFind/src/VuFind/Reserves/CsvReader.php @@ -0,0 +1,264 @@ +<?php +/** + * CLI Controller Module + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Reserves + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/building_a_recommendations_module Wiki + */ +namespace VuFind\Reserves; + + /** + * Support class to build reserves data from CSV file(s). + * + * @category VuFind2 + * @package Reserves + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki Wiki + */ +class CsvReader +{ + /** + * Files to load + * + * @var array + */ + protected $files; + + /** + * CSV delimiter + * + * @var string + */ + protected $delimiter; + + /** + * Field template (value => index) + * + * @var array + */ + protected $template; + + /** + * Instructor data loaded from files + * + * @var array + */ + protected $instructors = array(); + + /** + * Course data loaded from files + * + * @var array + */ + protected $courses = array(); + + /** + * Department data loaded from files + * + * @var array + */ + protected $departments = array(); + + /** + * Reserves data loaded from files + * + * @var array + */ + protected $reserves = array(); + + /** + * Flag indicating whether or not we have processed data yet. + * + * @var bool + */ + protected $loaded = false; + + /** + * Constructor + * + * @param array|string $files Array of files to load (or single filename). + * @param string $delimiter Delimiter used by file(s). + * @param string $template Template showing field positions within + * file(s). Comma-separated list containing BIB_ID, INSTRUCTOR, COURSE, + * DEPARTMENT and/or SKIP. Default = BIB_ID,COURSE,INSTRUCTOR,DEPARTMENT + * + * @throws \Exception + */ + public function __construct($files, $delimiter = ',', $template = null) + { + $this->files = is_array($files) ? $files : array($files); + $this->delimiter = $delimiter; + + // Provide default template if none passed in: + if (null === $template) { + $template = 'BIB_ID,COURSE,INSTRUCTOR,DEPARTMENT'; + } + + // Convert template from comma-delimited list to map of name => index: + $this->template = array_flip(array_map('trim', explode(',', $template))); + + if (!isset($this->template['BIB_ID'])) { + throw new \Exception('Template must include BIB_ID field.'); + } + } + + /** + * Load the appropriate data field from the line using our template. + * + * @param array $line CSV row + * @param string $key Value to load + * + * @return string + */ + protected function getValueFromLine($line, $key) + { + return isset($this->template[$key]) ? $line[$this->template[$key]] : ''; + } + + /** + * Load data from a single file. + * + * @param string $fn Filename + * + * @return void + * @throws \Exception + */ + protected function loadFile($fn) + { + if (!file_exists($fn) || !($fh = fopen($fn, 'r'))) { + throw new \Exception("Could not open $fn!"); + } + $lineNo = $goodLines = 0; + $errors = ''; + while ($line = fgetcsv($fh, 0, $this->delimiter)) { + $lineNo++; + + if (count($line) < count($this->template)) { + $errors .= "Skipping incomplete row: $fn, line $lineNo\n"; + continue; + } + + $instructor = $this->getValueFromLine($line, 'INSTRUCTOR'); + if (!empty($instructor)) { + $this->instructors[$instructor] = $instructor; + } + + $course = $this->getValueFromLine($line, 'COURSE'); + if (!empty($course)) { + $this->courses[$course] = $course; + } + + $department = $this->getValueFromLine($line, 'DEPARTMENT'); + if (!empty($department)) { + $this->departments[$department] = $department; + } + + $bibId = trim($line[$this->template['BIB_ID']]); + if ($bibId == '') { + $errors .= "Skipping empty/missing Bib ID: $fn, line $lineNo\n"; + continue; + } + + $goodLines++; + $this->reserves[] = array( + 'BIB_ID' => $bibId, + 'INSTRUCTOR_ID' => $instructor, + 'COURSE_ID' => $course, + 'DEPARTMENT_ID' => $department, + ); + } + fclose($fh); + if ($goodLines == 0) { + throw new \Exception( + "Could not find valid data. Details:\n" . trim($errors) + ); + } + } + + /** + * Load data if it is not already loaded. + * + * @return void + * @throws \Exception + */ + protected function load() + { + // Only load data if we haven't already retrieved it. + if (!$this->loaded) { + foreach ($this->files as $fn) { + $this->loadFile($fn); + } + + $this->loaded = true; + } + } + + /** + * Get instructor data + * + * @return array + * @throws \Exception + */ + public function getInstructors() + { + $this->load(); + return $this->instructors; + } + + /** + * Get course data + * + * @return array + * @throws \Exception + */ + public function getCourses() + { + $this->load(); + return $this->courses; + } + + /** + * Get department data + * + * @return array + * @throws \Exception + */ + public function getDepartments() + { + $this->load(); + return $this->departments; + } + + /** + * Get reserves data + * + * @return array + * @throws \Exception + */ + public function getReserves() + { + $this->load(); + return $this->reserves; + } +} diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php index 2967cbaefbf..ddee7c9adf4 100644 --- a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php +++ b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php @@ -40,40 +40,122 @@ use File_MARC, File_MARCXML, VuFind\Connection\Manager as ConnectionManager, */ class UtilController extends AbstractBase { + /** + * Display help for the index reserves action. + * + * @param string $msg Extra message to display + * + * @return \Zend\Console\Response + */ + protected function indexReservesHelp($msg = '') + { + if (!empty($msg)) { + foreach (explode("\n", $msg) as $line) { + Console::writeLine($line); + } + Console::writeLine(''); + } + + Console::writeLine('Course reserves index builder'); + Console::writeLine(''); + Console::writeLine( + 'If run with no options, this will attempt to load data from your ILS.' + ); + Console::writeLine(''); + Console::writeLine( + 'Switches may be used to index from delimited files instead:' + ); + Console::writeLine(''); + Console::writeLine( + ' -f [filename] loads a file (may be repeated for multiple files)' + ); + Console::writeLine( + ' -d [delimiter] specifies a delimiter (comma is default)' + ); + Console::writeLine( + ' -t [template] provides a template showing where important values' + ); + Console::writeLine( + ' can be found within the file. The template is a comma-' + ); + Console::writeLine(' separated list of values. Choose from:'); + Console::writeLine(' BIB_ID - bibliographic ID'); + Console::writeLine(' COURSE - course name'); + Console::writeLine(' DEPARTMENT - department name'); + Console::writeLine(' INSTRUCTOR - instructor name'); + Console::writeLine(' SKIP - ignore data in this position'); + Console::writeLine( + ' Default template is BIB_ID,COURSE,INSTRUCTOR,DEPARTMENT' + ); + Console::writeLine(' -h or -? display this help information.'); + + return $this->getFailureResponse(); + } + /** * Build the Reserves index. * - * @return void + * @return \Zend\Console\Response */ public function indexreservesAction() { ini_set('memory_limit', '50M'); ini_set('max_execution_time', '3600'); - // Setup Solr Connection - $solr = ConnectionManager::connectToIndex('SolrReserves'); - - // Connect to ILS - $catalog = $this->getILS(); - - // Records to index - $index = array(); - - // Get instructors - $instructors = $catalog->getInstructors(); - - // Get Courses - $courses = $catalog->getCourses(); - - // Get Departments - $departments = $catalog->getDepartments(); + $this->consoleOpts->setOptions( + array(\Zend\Console\Getopt::CONFIG_CUMULATIVE_PARAMETERS => true) + ); + $this->consoleOpts->addRules( + array( + 'h|help' => 'Get help', + 'd-s' => 'Delimiter', + 't-s' => 'Template', + 'f-s' => 'File', + ) + ); - // Get all reserve records - $reserves = $catalog->findReserves('', '', ''); + if ($this->consoleOpts->getOption('h') + || $this->consoleOpts->getOption('help') + ) { + return $this->indexReservesHelp(); + } elseif ($file = $this->consoleOpts->getOption('f')) { + try { + $delimiter = ($d = $this->consoleOpts->getOption('d')) ? $d : ','; + $template = ($t = $this->consoleOpts->getOption('t')) ? $t : null; + $reader = new \VuFind\Reserves\CsvReader( + $file, $delimiter, $template + ); + $instructors = $reader->getInstructors(); + $courses = $reader->getCourses(); + $departments = $reader->getDepartments(); + $reserves = $reader->getReserves(); + } catch (\Exception $e) { + return $this->indexReservesHelp($e->getMessage()); + } + } elseif ($this->consoleOpts->getOption('d')) { + return $this->indexReservesHelp('-d is meaningless without -f'); + } elseif ($this->consoleOpts->getOption('t')) { + return $this->indexReservesHelp('-t is meaningless without -f'); + } else { + try { + // Connect to ILS and load data: + $catalog = $this->getILS(); + $instructors = $catalog->getInstructors(); + $courses = $catalog->getCourses(); + $departments = $catalog->getDepartments(); + $reserves = $catalog->findReserves('', '', ''); + } catch (\Exception $e) { + return $this->indexReservesHelp($e->getMessage()); + } + } - if (!empty($instructors) && !empty($courses) && !empty($departments) + // Make sure we have reserves and at least one of: instructors, courses, departments: + if ((!empty($instructors) || !empty($courses) || !empty($departments)) && !empty($reserves) ) { + // Setup Solr Connection + $solr = ConnectionManager::connectToIndex('SolrReserves'); + // Delete existing records $solr->deleteAll(); @@ -83,14 +165,17 @@ class UtilController extends AbstractBase // Commit and Optimize the Solr Index $solr->commit(); $solr->optimize(); + + Console::writeLine('Successfully loaded ' . count($reserves) . ' rows.'); + return $this->getSuccessResponse(); } - return $this->getSuccessResponse(); + return $this->indexReservesHelp('Unable to load data.'); } /** * Optimize the Solr index. * - * @return void + * @return \Zend\Console\Response */ public function optimizeAction() { @@ -113,7 +198,7 @@ class UtilController extends AbstractBase /** * Generate a Sitemap * - * @return void + * @return \Zend\Console\Response */ public function sitemapAction() { @@ -129,7 +214,7 @@ class UtilController extends AbstractBase /** * Command-line tool to batch-delete records from the Solr index. * - * @return void + * @return \Zend\Console\Response */ public function deletesAction() { @@ -217,7 +302,7 @@ class UtilController extends AbstractBase * Command-line tool to clear unwanted entries * from search history database table. * - * @return void + * @return \Zend\Console\Response */ public function expiresearchesAction() { @@ -250,7 +335,7 @@ class UtilController extends AbstractBase /** * Command-line tool to delete suppressed records from the index. * - * @return void + * @return \Zend\Console\Response */ public function suppressedAction() { -- GitLab