Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.07% |
102 / 112 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
IndexReservesCommand | |
91.07% |
102 / 112 |
|
50.00% |
2 / 4 |
24.41 | |
0.00% |
0 / 1 |
configure | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
1 | |||
buildReservesIndex | |
87.10% |
27 / 31 |
|
0.00% |
0 / 1 |
7.11 | |||
getCsvReader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
88.00% |
44 / 50 |
|
0.00% |
0 / 1 |
15.39 |
1 | <?php |
2 | |
3 | /** |
4 | * Console command: index course reserves into Solr. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2020. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Console |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFindConsole\Command\Util; |
31 | |
32 | use Symfony\Component\Console\Attribute\AsCommand; |
33 | use Symfony\Component\Console\Input\InputInterface; |
34 | use Symfony\Component\Console\Input\InputOption; |
35 | use Symfony\Component\Console\Output\OutputInterface; |
36 | use VuFind\Reserves\CsvReader; |
37 | use VuFindSearch\Backend\Solr\Document\UpdateDocument; |
38 | use VuFindSearch\Backend\Solr\Record\SerializableRecord; |
39 | |
40 | use function count; |
41 | use function in_array; |
42 | use function ini_get; |
43 | |
44 | /** |
45 | * Console command: index course reserves into Solr. |
46 | * |
47 | * @category VuFind |
48 | * @package Console |
49 | * @author Demian Katz <demian.katz@villanova.edu> |
50 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
51 | * @link https://vufind.org/wiki/development Wiki |
52 | */ |
53 | #[AsCommand( |
54 | name: 'util/index_reserves', |
55 | description: 'Course reserves index builder' |
56 | )] |
57 | class IndexReservesCommand extends AbstractSolrAndIlsCommand |
58 | { |
59 | /** |
60 | * Default delimiter for reading files |
61 | * |
62 | * @var string |
63 | */ |
64 | protected $defaultDelimiter = ','; |
65 | |
66 | /** |
67 | * Default template for reading files |
68 | * |
69 | * @var string |
70 | */ |
71 | protected $defaultTemplate = 'BIB_ID,COURSE,INSTRUCTOR,DEPARTMENT'; |
72 | |
73 | /** |
74 | * Keys required in the data to create a valid reserves index. |
75 | * |
76 | * @var string[] |
77 | */ |
78 | protected $requiredKeys = ['INSTRUCTOR_ID', 'COURSE_ID', 'DEPARTMENT_ID']; |
79 | |
80 | /** |
81 | * Configure the command. |
82 | * |
83 | * @return void |
84 | */ |
85 | protected function configure() |
86 | { |
87 | $this |
88 | ->setHelp( |
89 | 'This tool populates your course reserves Solr index. If run with' |
90 | . ' no options, it will attempt to load data from your ILS.' |
91 | . ' Switches may be used to index from delimited files instead.' |
92 | )->addOption( |
93 | 'filename', |
94 | 'f', |
95 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
96 | 'file(s) containing delimited values' |
97 | )->addOption( |
98 | 'delimiter', |
99 | 'd', |
100 | InputOption::VALUE_REQUIRED, |
101 | 'specifies the delimiter used in file(s)', |
102 | $this->defaultDelimiter |
103 | )->addOption( |
104 | 'template', |
105 | 't', |
106 | InputOption::VALUE_REQUIRED, |
107 | 'provides a template showing where important values can be found ' |
108 | . "within the file.\nThe template is a comma-separated list of " |
109 | . "values. Choose from:\n" |
110 | . "BIB_ID - bibliographic ID\n" |
111 | . "COURSE - course name\n" |
112 | . "DEPARTMENT - department name\n" |
113 | . "INSTRUCTOR - instructor name\n" |
114 | . "SKIP - ignore data in this position\n", |
115 | $this->defaultTemplate |
116 | ); |
117 | } |
118 | |
119 | /** |
120 | * Build the reserves index from date returned by the ILS driver, |
121 | * specifically: getInstructors, getDepartments, getCourses, findReserves |
122 | * |
123 | * @param array $instructors Array of instructors $instructor_id => $instructor |
124 | * @param array $courses Array of courses $course_id => $course |
125 | * @param array $departments Array of department $dept_id => $department |
126 | * @param array $reserves Array of reserves records from driver's |
127 | * findReserves. |
128 | * |
129 | * @return UpdateDocument |
130 | */ |
131 | protected function buildReservesIndex( |
132 | $instructors, |
133 | $courses, |
134 | $departments, |
135 | $reserves |
136 | ) { |
137 | $index = []; |
138 | foreach ($reserves as $record) { |
139 | $requiredKeysFound |
140 | = count(array_intersect(array_keys($record), $this->requiredKeys)); |
141 | if ($requiredKeysFound < count($this->requiredKeys)) { |
142 | throw new \Exception( |
143 | implode(' and/or ', $this->requiredKeys) . ' fields ' . |
144 | 'not present in reserve records. Please update ILS driver.' |
145 | ); |
146 | } |
147 | $instructorId = $record['INSTRUCTOR_ID']; |
148 | $courseId = $record['COURSE_ID']; |
149 | $departmentId = $record['DEPARTMENT_ID']; |
150 | $id = $courseId . '|' . $instructorId . '|' . $departmentId; |
151 | |
152 | if (!isset($index[$id])) { |
153 | $index[$id] = [ |
154 | 'id' => $id, |
155 | 'bib_id' => [], |
156 | 'instructor_id' => $instructorId, |
157 | 'instructor' => $instructors[$instructorId] ?? '', |
158 | 'course_id' => $courseId, |
159 | 'course' => $courses[$courseId] ?? '', |
160 | 'department_id' => $departmentId, |
161 | 'department' => $departments[$departmentId] ?? '', |
162 | ]; |
163 | } |
164 | if (!in_array($record['BIB_ID'], $index[$id]['bib_id'])) { |
165 | $index[$id]['bib_id'][] = $record['BIB_ID']; |
166 | } |
167 | } |
168 | |
169 | $updates = new UpdateDocument(); |
170 | foreach ($index as $id => $data) { |
171 | if (!empty($data['bib_id'])) { |
172 | $updates->addRecord(new SerializableRecord($data)); |
173 | } |
174 | } |
175 | return $updates; |
176 | } |
177 | |
178 | /** |
179 | * Construct a CSV reader. |
180 | * |
181 | * @param array|string $files Array of files to load (or single filename). |
182 | * @param string $delimiter Delimiter used by file(s). |
183 | * @param string $template Template showing field positions within |
184 | * file(s). Comma-separated list containing BIB_ID, INSTRUCTOR, COURSE, |
185 | * DEPARTMENT and/or SKIP. Default = BIB_ID,COURSE,INSTRUCTOR,DEPARTMENT |
186 | * |
187 | * @return CsvReader |
188 | */ |
189 | protected function getCsvReader( |
190 | $files, |
191 | string $delimiter, |
192 | string $template |
193 | ): CsvReader { |
194 | return new CsvReader($files, $delimiter, $template); |
195 | } |
196 | |
197 | /** |
198 | * Run the command. |
199 | * |
200 | * @param InputInterface $input Input object |
201 | * @param OutputInterface $output Output object |
202 | * |
203 | * @return int 0 for success |
204 | */ |
205 | protected function execute(InputInterface $input, OutputInterface $output) |
206 | { |
207 | // Check time limit; increase if necessary: |
208 | if (ini_get('max_execution_time') < 3600) { |
209 | ini_set('max_execution_time', '3600'); |
210 | } |
211 | |
212 | $delimiter = $input->getOption('delimiter'); |
213 | $template = $input->getOption('template'); |
214 | |
215 | if ($file = $input->getOption('filename')) { |
216 | try { |
217 | $reader = $this->getCsvReader($file, $delimiter, $template); |
218 | $instructors = $reader->getInstructors(); |
219 | $courses = $reader->getCourses(); |
220 | $departments = $reader->getDepartments(); |
221 | $reserves = $reader->getReserves(); |
222 | } catch (\Exception $e) { |
223 | $output->writeln($e->getMessage()); |
224 | return 1; |
225 | } |
226 | } elseif ($delimiter !== $this->defaultDelimiter) { |
227 | $output->writeln('-d (delimiter) is meaningless without -f (filename)'); |
228 | return 1; |
229 | } elseif ($template !== $this->defaultTemplate) { |
230 | $output->writeln('-t (template) is meaningless without -f (filename)'); |
231 | return 1; |
232 | } else { |
233 | try { |
234 | // Connect to ILS and load data: |
235 | $instructors = $this->catalog->getInstructors(); |
236 | $courses = $this->catalog->getCourses(); |
237 | $departments = $this->catalog->getDepartments(); |
238 | $reserves = $this->catalog->findReserves('', '', ''); |
239 | } catch (\Exception $e) { |
240 | $output->writeln($e->getMessage()); |
241 | return 1; |
242 | } |
243 | } |
244 | |
245 | // Make sure we have reserves and at least one of: instructors, courses, |
246 | // departments: |
247 | if ( |
248 | (!empty($instructors) || !empty($courses) || !empty($departments)) |
249 | && !empty($reserves) |
250 | ) { |
251 | // Delete existing records |
252 | $this->solr->deleteAll('SolrReserves'); |
253 | |
254 | // Build and Save the index |
255 | $index = $this->buildReservesIndex( |
256 | $instructors, |
257 | $courses, |
258 | $departments, |
259 | $reserves |
260 | ); |
261 | $this->solr->save('SolrReserves', $index); |
262 | |
263 | // Commit and Optimize the Solr Index |
264 | $this->solr->commit('SolrReserves'); |
265 | $this->solr->optimize('SolrReserves'); |
266 | |
267 | $output->writeln('Successfully loaded ' . count($reserves) . ' rows.'); |
268 | return 0; |
269 | } |
270 | $missing = array_merge( |
271 | empty($instructors) ? ['instructors'] : [], |
272 | empty($courses) ? ['courses'] : [], |
273 | empty($departments) ? ['departments'] : [], |
274 | empty($reserves) ? ['reserves'] : [] |
275 | ); |
276 | $output->writeln( |
277 | 'Unable to load data. No data found for: ' . implode(', ', $missing) |
278 | ); |
279 | return 1; |
280 | } |
281 | } |