diff --git a/config/vufind/reserves.ini b/config/vufind/reserves.ini
index cddeb3a9238..d5540a6dffa 100644
--- a/config/vufind/reserves.ini
+++ b/config/vufind/reserves.ini
@@ -42,6 +42,11 @@ formatting_rule[*] = "phrase"
[Autocomplete_Types]
Reserves = "SolrReserves:AllFields:course,instructor,department"
+[Advanced_Settings]
+translated_facets[] = course_str:Reserves
+translated_facets[] = department_str:Reserves
+translated_facets[] = instructor_str:Reserves
+
[SearchCache]
;adapter = Memcached
;options[servers] = "localhost:11211,otherhost:11211"
diff --git a/languages/Reserves/en.ini b/languages/Reserves/en.ini
new file mode 100644
index 00000000000..453f273b194
--- /dev/null
+++ b/languages/Reserves/en.ini
@@ -0,0 +1,3 @@
+no_course_listed = "No Course Listed"
+no_department_listed = "No Department Listed"
+no_instructor_listed = "No Instructor Listed"
diff --git a/languages/en.ini b/languages/en.ini
index c58ac9c99b7..e82696e28f5 100644
--- a/languages/en.ini
+++ b/languages/en.ini
@@ -889,8 +889,11 @@ No Preference = "No Preference"
No reviews were found for this record = "No reviews were found for this record"
No saved logins = "No saved logins"
No Tags = "No Tags"
+no_course_listed = "No Course Listed"
+no_department_listed = "No Department Listed"
no_description = "Description not available."
no_email_address = "Email address missing."
+no_instructor_listed = "No Instructor Listed"
no_items_selected = "No Items were Selected"
no_proxied_user = "No proxied user (request for yourself)"
nohit_active_filters = "One or more facet filters have been applied to this search. If you remove filters, you may retrieve more results."
diff --git a/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php b/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php
index 8a58f86c9b1..f8795cd5433 100644
--- a/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php
+++ b/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php
@@ -54,4 +54,37 @@ public function __construct(\VuFind\Search\Results\PluginManager $results)
$this->defaultDisplayField = 'course';
$this->searchClassId = 'SolrReserves';
}
+
+ /**
+ * Try to turn an array of record drivers into an array of suggestions.
+ * Excluding `no_*_listed` matches since those are the translation values
+ * when there is no data in that field.
+ *
+ * @param array $searchResults An array of record drivers
+ * @param string $query User search query
+ * @param bool $exact Ignore non-exact matches?
+ *
+ * @return array
+ */
+ protected function getSuggestionsFromSearch($searchResults, $query, $exact)
+ {
+ $results = [];
+ foreach ($searchResults as $object) {
+ $current = $object->getRawData();
+ foreach ($this->displayField as $field) {
+ if (isset($current[$field]) && !preg_match('/no_.*_listed/', $current[$field])) {
+ $bestMatch = $this->pickBestMatch(
+ $current[$field],
+ $query,
+ $exact
+ );
+ if ($bestMatch) {
+ $results[] = $bestMatch;
+ break;
+ }
+ }
+ }
+ }
+ return $results;
+ }
}
diff --git a/module/VuFindConsole/src/VuFindConsole/Command/Util/IndexReservesCommand.php b/module/VuFindConsole/src/VuFindConsole/Command/Util/IndexReservesCommand.php
index e542b0ce14b..c15e095e86d 100644
--- a/module/VuFindConsole/src/VuFindConsole/Command/Util/IndexReservesCommand.php
+++ b/module/VuFindConsole/src/VuFindConsole/Command/Util/IndexReservesCommand.php
@@ -40,6 +40,7 @@
use function count;
use function in_array;
use function ini_get;
+use function sprintf;
/**
* Console command: index course reserves into Solr.
@@ -56,6 +57,13 @@
)]
class IndexReservesCommand extends AbstractSolrAndIlsCommand
{
+ /**
+ * Output interface
+ *
+ * @var OutputInterface
+ */
+ protected $output;
+
/**
* Default delimiter for reading files
*
@@ -154,16 +162,57 @@ protected function buildReservesIndex(
'id' => $id,
'bib_id' => [],
'instructor_id' => $instructorId,
- 'instructor' => $instructors[$instructorId] ?? '',
+ 'instructor' => $instructors[$instructorId] ?? 'no_instructor_listed',
'course_id' => $courseId,
- 'course' => $courses[$courseId] ?? '',
+ 'course' => $courses[$courseId] ?? 'no_course_listed',
'department_id' => $departmentId,
- 'department' => $departments[$departmentId] ?? '',
+ 'department' => $departments[$departmentId] ?? 'no_department_listed',
];
}
if (!in_array($record['BIB_ID'], $index[$id]['bib_id'])) {
$index[$id]['bib_id'][] = $record['BIB_ID'];
}
+
+ // Show a warning if the any of the IDs were set, but was not found in the resulting data
+ if (!empty($instructorId) && !isset($instructors[$instructorId])) {
+ $this->showTimestampedMessage(
+ sprintf(
+ 'WARNING! The instructor (ID: %s) for the course: %s (ID: %s) ' .
+ 'and department: %s (ID: %s) did not match any found instructors.',
+ $index[$id]['instructor_id'],
+ $index[$id]['course'],
+ $index[$id]['course_id'],
+ $index[$id]['department'],
+ $index[$id]['department_id']
+ )
+ );
+ }
+ if (!empty($departmentId) && !isset($departments[$departmentId])) {
+ $this->showTimestampedMessage(
+ sprintf(
+ 'WARNING! The department (ID: %s) for the course: %s (ID: %s) ' .
+ 'and instructor: %s (ID: %s) did not match any found departments.',
+ $index[$id]['department_id'],
+ $index[$id]['course'],
+ $index[$id]['course_id'],
+ $index[$id]['instructor'],
+ $index[$id]['instructor_id']
+ )
+ );
+ }
+ if (!empty($courseId) && !isset($courses[$courseId])) {
+ $this->showTimestampedMessage(
+ sprintf(
+ 'WARNING! The course (ID: %s) for the instructor: %s (ID: %s) ' .
+ 'and department: %s (ID: %s) did not match any found courses.',
+ $index[$id]['course_id'],
+ $index[$id]['instructor'],
+ $index[$id]['instructor_id'],
+ $index[$id]['department'],
+ $index[$id]['department_id']
+ )
+ );
+ }
}
$updates = new UpdateDocument();
@@ -194,6 +243,18 @@ protected function getCsvReader(
return new CsvReader($files, $delimiter, $template);
}
+ /**
+ * Print the message to the provided output stream prefixed with a timestamp.
+ *
+ * @param string $message Message to display
+ *
+ * @return null
+ */
+ protected function showTimestampedMessage(string $message)
+ {
+ $this->output->writeln(date('Y-m-d H:i:s') . ' ' . $message);
+ }
+
/**
* Run the command.
*
@@ -204,6 +265,8 @@ protected function getCsvReader(
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
+ $this->output = $output;
+ $startTime = date('Y-m-d H:i:s');
// Check time limit; increase if necessary:
if (ini_get('max_execution_time') < 3600) {
ini_set('max_execution_time', '3600');
@@ -214,30 +277,50 @@ protected function execute(InputInterface $input, OutputInterface $output)
if ($file = $input->getOption('filename')) {
try {
+ $this->showTimestampedMessage('Starting reserves processing from file');
+
$reader = $this->getCsvReader($file, $delimiter, $template);
+ $this->showTimestampedMessage('Retrieving instructors');
$instructors = $reader->getInstructors();
+ $this->showTimestampedMessage('Found instructor count: ' . count($instructors));
+ $this->showTimestampedMessage('Retrieving courses');
$courses = $reader->getCourses();
+ $this->showTimestampedMessage('Found course count: ' . count($courses));
+ $this->showTimestampedMessage('Retrieving departments');
$departments = $reader->getDepartments();
+ $this->showTimestampedMessage('Found department count: ' . count($departments));
+ $this->showTimestampedMessage('Retrieving reserves');
$reserves = $reader->getReserves();
+ $this->showTimestampedMessage('Found reserve count: ' . count($reserves));
} catch (\Exception $e) {
- $output->writeln($e->getMessage());
+ $this->showTimestampedMessage($e->getMessage());
return 1;
}
} elseif ($delimiter !== $this->defaultDelimiter) {
- $output->writeln('-d (delimiter) is meaningless without -f (filename)');
+ $this->output->writeln('-d (delimiter) is meaningless without -f (filename)');
return 1;
} elseif ($template !== $this->defaultTemplate) {
- $output->writeln('-t (template) is meaningless without -f (filename)');
+ $this->output->writeln('-t (template) is meaningless without -f (filename)');
return 1;
} else {
try {
+ $this->showTimestampedMessage('Starting reserves processing from ILS');
+
// Connect to ILS and load data:
+ $this->showTimestampedMessage('Retrieving instructors');
$instructors = $this->catalog->getInstructors();
+ $this->showTimestampedMessage('Found instructor count: ' . count($instructors ?? []));
+ $this->showTimestampedMessage('Retrieving courses');
$courses = $this->catalog->getCourses();
+ $this->showTimestampedMessage('Found course count: ' . count($courses ?? []));
+ $this->showTimestampedMessage('Retrieving departments');
$departments = $this->catalog->getDepartments();
+ $this->showTimestampedMessage('Found department count: ' . count($departments ?? []));
+ $this->showTimestampedMessage('Retrieving reserves');
$reserves = $this->catalog->findReserves('', '', '');
+ $this->showTimestampedMessage('Found reserve count: ' . count($reserves ?? []));
} catch (\Exception $e) {
- $output->writeln($e->getMessage());
+ $this->showTimestampedMessage($e->getMessage());
return 1;
}
}
@@ -249,22 +332,27 @@ protected function execute(InputInterface $input, OutputInterface $output)
&& !empty($reserves)
) {
// Delete existing records
+ $this->showTimestampedMessage('Clearing existing reserves');
$this->solr->deleteAll('SolrReserves');
// Build and Save the index
+ $this->showTimestampedMessage('Building new reserves index');
$index = $this->buildReservesIndex(
$instructors,
$courses,
$departments,
$reserves
);
+ $this->showTimestampedMessage('Writing new reserves index');
$this->solr->save('SolrReserves', $index);
// Commit and Optimize the Solr Index
$this->solr->commit('SolrReserves');
$this->solr->optimize('SolrReserves');
- $output->writeln('Successfully loaded ' . count($reserves) . ' rows.');
+ $this->showTimestampedMessage('Successfully loaded ' . count($reserves) . ' rows.');
+ $endTime = date('Y-m-d H:i:s');
+ $this->showTimestampedMessage('Started at: ' . $startTime . ' Completed at: ' . $endTime);
return 0;
}
$missing = array_merge(
@@ -273,9 +361,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
empty($departments) ? ['departments'] : [],
empty($reserves) ? ['reserves'] : []
);
- $output->writeln(
- 'Unable to load data. No data found for: ' . implode(', ', $missing)
- );
+ $this->showTimestampedMessage('Unable to load data. No data found for: ' . implode(', ', $missing));
return 1;
}
}
diff --git a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/IndexReservesCommandTest.php b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/IndexReservesCommandTest.php
index a5cb139e080..739d0dca83c 100644
--- a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/IndexReservesCommandTest.php
+++ b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/IndexReservesCommandTest.php
@@ -34,6 +34,8 @@
use VuFind\Solr\Writer;
use VuFindConsole\Command\Util\IndexReservesCommand;
+use function ini_get;
+
/**
* IndexReservesCommand test.
*
@@ -60,6 +62,64 @@ protected function getMockIlsConnection()
->getMock();
}
+ /**
+ * Get command tester object given the course data provided.
+ *
+ * @param array $instructors Instructors returned from getInstructors
+ * @param array $courses Courses returned from getCourses
+ * @param array $departments Departments returned from getDepartments
+ * @param array $reserves Reserves returned from findReserves
+ * @param string $expectedXml Data to expect for the Solr document
+ *
+ * @return Symfony\Component\Console\Tester\CommandTester
+ */
+ protected function getMockIlsCommandTesterWithCourseData(
+ $instructors = [],
+ $courses = [],
+ $departments = [],
+ $reserves = [],
+ $expectedXml = null
+ ) {
+ $ils = $this->getMockIlsConnection();
+ $this->expectConsecutiveCalls(
+ $ils,
+ '__call',
+ [
+ ['getInstructors'],
+ ['getCourses'],
+ ['getDepartments'],
+ ['findReserves'],
+ ],
+ [
+ $instructors,
+ $courses,
+ $departments,
+ $reserves,
+ ]
+ );
+ $writer = $this->getMockSolrWriter();
+ if ($expectedXml) {
+ $updateValidator = function ($update) use ($expectedXml) {
+ $this->assertEquals($expectedXml, trim($update->getContent()));
+ return true;
+ };
+ $writer->expects($this->once())->method('save')
+ ->with(
+ $this->equalTo('SolrReserves'),
+ $this->callback($updateValidator)
+ );
+ $writer->expects($this->once())->method('deleteAll')
+ ->with($this->equalTo('SolrReserves'));
+ $writer->expects($this->once())->method('commit')
+ ->with($this->equalTo('SolrReserves'));
+ $writer->expects($this->once())->method('optimize')
+ ->with($this->equalTo('SolrReserves'));
+ }
+ $command = $this->getCommand($writer, $ils);
+ $commandTester = new CommandTester($command);
+ return $commandTester;
+ }
+
/**
* Get mock Solr writer.
*
@@ -89,18 +149,35 @@ protected function getCommand(Writer $solr = null, Connection $ils = null)
}
/**
- * Test bad parameter combination.
+ * Test bad parameter combination with template.
+ *
+ * @return void
+ */
+ public function testBadParameterCombinationTemplate()
+ {
+ $command = $this->getCommand();
+ $commandTester = new CommandTester($command);
+ $commandTester->execute(['--template' => '|']);
+ $this->assertEquals(1, $commandTester->getStatusCode());
+ $this->assertStringContainsString(
+ '-t (template) is meaningless without -f (filename)',
+ $commandTester->getDisplay()
+ );
+ }
+
+ /**
+ * Test bad parameter combination with delimiter.
*
* @return void
*/
- public function testBadParameterCombination()
+ public function testBadParameterCombinationDelimiter()
{
$command = $this->getCommand();
$commandTester = new CommandTester($command);
$commandTester->execute(['--delimiter' => '|']);
$this->assertEquals(1, $commandTester->getStatusCode());
- $this->assertEquals(
- "-d (delimiter) is meaningless without -f (filename)\n",
+ $this->assertStringContainsString(
+ '-d (delimiter) is meaningless without -f (filename)',
$commandTester->getDisplay()
);
}
@@ -116,8 +193,8 @@ public function testBadFilename()
$commandTester = new CommandTester($command);
$commandTester->execute(['--filename' => '/does/not/exist']);
$this->assertEquals(1, $commandTester->getStatusCode());
- $this->assertEquals(
- "Could not open /does/not/exist!\n",
+ $this->assertStringContainsString(
+ 'Could not open /does/not/exist!',
$commandTester->getDisplay()
);
}
@@ -191,8 +268,8 @@ public function testSuccessWithMultipleFiles()
]
);
$this->assertEquals(0, $commandTester->getStatusCode());
- $this->assertEquals(
- "Successfully loaded 3 rows.\n",
+ $this->assertStringContainsString(
+ 'Successfully loaded 3 rows.',
$commandTester->getDisplay()
);
}
@@ -204,25 +281,12 @@ public function testSuccessWithMultipleFiles()
*/
public function testMissingData()
{
- $ils = $this->getMockIlsConnection();
- $this->expectConsecutiveCalls(
- $ils,
- '__call',
- [
- ['getInstructors'],
- ['getCourses'],
- ['getDepartments'],
- ['findReserves'],
- ],
- []
- );
- $command = $this->getCommand($this->getMockSolrWriter(), $ils);
- $commandTester = new CommandTester($command);
+ $commandTester = $this->getMockIlsCommandTesterWithCourseData();
$commandTester->execute([]);
$this->assertEquals(1, $commandTester->getStatusCode());
- $this->assertEquals(
+ $this->assertStringContainsString(
'Unable to load data. No data found for: '
- . "instructors, courses, departments, reserves\n",
+ . 'instructors, courses, departments, reserves',
$commandTester->getDisplay()
);
}
@@ -234,7 +298,6 @@ public function testMissingData()
*/
public function testSuccessWithILS()
{
- $ils = $this->getMockIlsConnection();
$instructors = ['inst1' => 'inst1', 'inst2' => 'inst2', 'inst3' => 'inst3'];
$courses = [
'course1' => 'course1', 'course2' => 'course2', 'course3' => 'course3',
@@ -260,79 +323,236 @@ public function testSuccessWithILS()
'INSTRUCTOR_ID' => 'inst3',
],
];
+ $expectedXml = "\n"
+ . '