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