Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 115 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
CollectionsController | |
0.00% |
0 / 115 |
|
0.00% |
0 / 12 |
812 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
bytitleAction | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
homeAction | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getBrowseDelimiter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showBrowseAlphabetic | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
30 | |||
showBrowseIndex | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
72 | |||
sortFindKeyLocation | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
normalizeAndSortFacets | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
normalizeForBrowse | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getAlphabetList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getBrowseLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCollectionsFromTitle | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Collections Controller |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010, 2022. |
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 Controller |
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 Main Site |
28 | */ |
29 | |
30 | namespace VuFind\Controller; |
31 | |
32 | use Laminas\Config\Config; |
33 | use Laminas\ServiceManager\ServiceLocatorInterface; |
34 | use VuFindSearch\Command\SearchCommand; |
35 | use VuFindSearch\Query\Query; |
36 | |
37 | use function array_slice; |
38 | use function count; |
39 | |
40 | /** |
41 | * Collections Controller |
42 | * |
43 | * @category VuFind |
44 | * @package Controller |
45 | * @author Demian Katz <demian.katz@villanova.edu> |
46 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
47 | * @link https://vufind.org Main Site |
48 | */ |
49 | class CollectionsController extends AbstractBase implements |
50 | \VuFind\I18n\HasSorterInterface |
51 | { |
52 | use Feature\AlphaBrowseTrait; |
53 | use \VuFind\I18n\HasSorterTrait; |
54 | |
55 | /** |
56 | * VuFind configuration |
57 | * |
58 | * @var \Laminas\Config\Config |
59 | */ |
60 | protected $config; |
61 | |
62 | /** |
63 | * Constructor |
64 | * |
65 | * @param ServiceLocatorInterface $sm Service manager |
66 | * @param Config $config VuFind configuration |
67 | */ |
68 | public function __construct(ServiceLocatorInterface $sm, Config $config) |
69 | { |
70 | $this->config = $config; |
71 | $this->setSorter($sm->get(\VuFind\I18n\Sorter::class)); |
72 | parent::__construct($sm); |
73 | } |
74 | |
75 | /** |
76 | * Search by title action |
77 | * |
78 | * @return mixed |
79 | */ |
80 | public function bytitleAction() |
81 | { |
82 | $collections = $this->getCollectionsFromTitle( |
83 | $this->params()->fromQuery('title') |
84 | ); |
85 | if (count($collections) != 1) { |
86 | return $this->createViewModel(['collections' => $collections]); |
87 | } |
88 | return $this->redirect() |
89 | ->toRoute('collection', ['id' => $collections[0]->getUniqueId()]); |
90 | } |
91 | |
92 | /** |
93 | * Browse action |
94 | * |
95 | * @return mixed |
96 | */ |
97 | public function homeAction() |
98 | { |
99 | $browseType = $this->config->Collections->browseType ?? 'Index'; |
100 | return ($browseType == 'Alphabetic') |
101 | ? $this->showBrowseAlphabetic() : $this->showBrowseIndex(); |
102 | } |
103 | |
104 | /** |
105 | * Get the delimiter used to separate title from ID in the browse strings. |
106 | * |
107 | * @return string |
108 | */ |
109 | protected function getBrowseDelimiter() |
110 | { |
111 | return $this->config->Collections->browseDelimiter ?? '{{{_ID_}}}'; |
112 | } |
113 | |
114 | /** |
115 | * Show the Browse Menu |
116 | * |
117 | * @return mixed |
118 | */ |
119 | protected function showBrowseAlphabetic() |
120 | { |
121 | // Process incoming parameters: |
122 | $source = 'hierarchy'; |
123 | $from = $this->params()->fromQuery('from', ''); |
124 | $page = $this->params()->fromQuery('page', 0); |
125 | $limit = $this->getBrowseLimit(); |
126 | |
127 | // Load Solr data or die trying: |
128 | $result = $this->alphabeticBrowse($source, $from, $page, $limit); |
129 | |
130 | // No results? Try the previous page just in case we've gone past the |
131 | // end of the list.... |
132 | if ($result['Browse']['totalCount'] == 0) { |
133 | $page--; |
134 | $result = $this->alphabeticBrowse($source, $from, $page, $limit); |
135 | } |
136 | |
137 | // Begin building view model: |
138 | $view = $this->createViewModel(); |
139 | |
140 | // Only display next/previous page links when applicable: |
141 | if ($result['Browse']['totalCount'] > $limit) { |
142 | $view->nextpage = $page + 1; |
143 | } |
144 | if ($result['Browse']['offset'] + $result['Browse']['startRow'] > 1) { |
145 | $view->prevpage = $page - 1; |
146 | } |
147 | |
148 | // Send other relevant values to the template: |
149 | $view->from = $from; |
150 | $view->letters = $this->getAlphabetList(); |
151 | |
152 | // Format the results for proper display: |
153 | $finalresult = []; |
154 | $delimiter = $this->getBrowseDelimiter(); |
155 | foreach ($result['Browse']['items'] as $rkey => $collection) { |
156 | $collectionIdNamePair |
157 | = explode($delimiter, $collection['heading']); |
158 | $finalresult[$rkey]['displayText'] = $collectionIdNamePair[0]; |
159 | $finalresult[$rkey]['count'] = $collection['count']; |
160 | $finalresult[$rkey]['value'] = $collectionIdNamePair[1]; |
161 | } |
162 | $view->result = $finalresult; |
163 | |
164 | // Display the page: |
165 | return $view; |
166 | } |
167 | |
168 | /** |
169 | * Show the Browse Menu |
170 | * |
171 | * @return mixed |
172 | */ |
173 | protected function showBrowseIndex() |
174 | { |
175 | // Process incoming parameters: |
176 | $from = $this->params()->fromQuery('from', ''); |
177 | $page = $this->params()->fromQuery('page', 0); |
178 | $appliedFilters = $this->params()->fromQuery('filter', []); |
179 | $limit = $this->getBrowseLimit(); |
180 | |
181 | $browseField = 'hierarchy_browse'; |
182 | |
183 | $searchObject = $this->serviceLocator |
184 | ->get(\VuFind\Search\Results\PluginManager::class)->get('Solr'); |
185 | foreach ($appliedFilters as $filter) { |
186 | $searchObject->getParams()->addFilter($filter); |
187 | } |
188 | |
189 | // Only grab 150,000 facet values to avoid out-of-memory errors: |
190 | $result = $searchObject->getFullFieldFacets( |
191 | [$browseField], |
192 | false, |
193 | 150000, |
194 | 'index' |
195 | ); |
196 | $result = $result[$browseField]['data']['list'] ?? []; |
197 | |
198 | $delimiter = $this->getBrowseDelimiter(); |
199 | foreach ($result as $rkey => $collection) { |
200 | [$name, $id] = explode($delimiter, $collection['value'], 2); |
201 | $result[$rkey]['displayText'] = $name; |
202 | $result[$rkey]['value'] = $id; |
203 | } |
204 | |
205 | // Sort the $results and get the position of the from string once sorted |
206 | $key = $this->sortFindKeyLocation($result, $from); |
207 | |
208 | // Offset the key by how many pages in we are |
209 | $key += ($limit * $page); |
210 | |
211 | // Catch out of range keys |
212 | if ($key < 0) { |
213 | $key = 0; |
214 | } |
215 | if ($key >= count($result)) { |
216 | $key = count($result) - 1; |
217 | } |
218 | |
219 | // Begin building view model: |
220 | $view = $this->createViewModel(); |
221 | |
222 | // Only display next/previous page links when applicable: |
223 | if (count($result) > $key + $limit) { |
224 | $view->nextpage = $page + 1; |
225 | } |
226 | if ($key > 0) { |
227 | $view->prevpage = $page - 1; |
228 | } |
229 | |
230 | // Select just the records to display |
231 | $result = array_slice( |
232 | $result, |
233 | $key, |
234 | count($result) > $key + $limit ? $limit : null |
235 | ); |
236 | |
237 | // Send other relevant values to the template: |
238 | $view->from = $from; |
239 | $view->result = $result; |
240 | $view->letters = $this->getAlphabetList(); |
241 | $view->filters = $searchObject->getParams()->getFilterList(true); |
242 | |
243 | // Display the page: |
244 | return $view; |
245 | } |
246 | |
247 | /** |
248 | * Function to sort the results and find the position of the from |
249 | * value in the result set; if the value doesn't exist, it's inserted. |
250 | * |
251 | * @param array $result Array to sort |
252 | * @param string $from Position to find |
253 | * |
254 | * @return int |
255 | */ |
256 | protected function sortFindKeyLocation(&$result, $from) |
257 | { |
258 | // Normalize the from value so it matches the values we are looking up |
259 | $from = $this->normalizeForBrowse($from); |
260 | |
261 | // Push the from value into the array so we can find the matching position: |
262 | array_push($result, ['displayText' => $from, 'placeholder' => true]); |
263 | |
264 | // Declare array to hold the $result array in the right sort order |
265 | $sorted = []; |
266 | foreach (array_keys($this->normalizeAndSortFacets($result)) as $i) { |
267 | // If this is the placeholder we added earlier, we have found the |
268 | // array position we want to use as our start; otherwise, it is an |
269 | // element that needs to be moved into the sorted version of the |
270 | // array: |
271 | if (isset($result[$i]['placeholder'])) { |
272 | $key = count($sorted); |
273 | } else { |
274 | $sorted[] = $result[$i]; |
275 | unset($result[$i]); //clear this out of memory |
276 | } |
277 | } |
278 | $result = $sorted; |
279 | |
280 | return $key ?? 0; |
281 | } |
282 | |
283 | /** |
284 | * Function to normalize the names so they sort properly |
285 | * |
286 | * @param array $result Array to sort (passed by reference to use less |
287 | * memory) |
288 | * |
289 | * @return array $resultOut |
290 | */ |
291 | protected function normalizeAndSortFacets(&$result) |
292 | { |
293 | $valuesSorted = []; |
294 | foreach ($result as $resKey => $resVal) { |
295 | $valuesSorted[$resKey] |
296 | = $this->normalizeForBrowse($resVal['displayText']); |
297 | } |
298 | $this->getSorter()->asort($valuesSorted); |
299 | |
300 | // Now the $valuesSorted is in the right order |
301 | return $valuesSorted; |
302 | } |
303 | |
304 | /** |
305 | * Normalize the value for the browse sort |
306 | * |
307 | * @param string $val Value to normalize |
308 | * |
309 | * @return string $valNormalized |
310 | */ |
311 | protected function normalizeForBrowse($val) |
312 | { |
313 | $valNormalized = iconv('UTF-8', 'US-ASCII//TRANSLIT//IGNORE', $val); |
314 | $valNormalized = strtolower($valNormalized); |
315 | $valNormalized = preg_replace("/[^a-zA-Z0-9\s]/", '', $valNormalized); |
316 | $valNormalized = trim($valNormalized); |
317 | return $valNormalized; |
318 | } |
319 | |
320 | /** |
321 | * Get a list of initial letters to display. |
322 | * |
323 | * @return array |
324 | */ |
325 | protected function getAlphabetList() |
326 | { |
327 | return array_merge(range('0', '9'), range('A', 'Z')); |
328 | } |
329 | |
330 | /** |
331 | * Get the collection browse page size |
332 | * |
333 | * @return int |
334 | */ |
335 | protected function getBrowseLimit() |
336 | { |
337 | return $this->config->Collections->browseLimit ?? 20; |
338 | } |
339 | |
340 | /** |
341 | * Get collection information matching a given title: |
342 | * |
343 | * @param string $title Title to search for |
344 | * |
345 | * @return array |
346 | */ |
347 | protected function getCollectionsFromTitle($title) |
348 | { |
349 | $title = addcslashes($title, '"'); |
350 | $query = new Query("is_hierarchy_title:\"$title\"", 'AllFields'); |
351 | $searchService = $this->serviceLocator->get(\VuFindSearch\Service::class); |
352 | $command = new SearchCommand( |
353 | 'Solr', |
354 | $query, |
355 | 0, |
356 | $this->getBrowseLimit() |
357 | ); |
358 | $result = $searchService->invoke($command)->getResult(); |
359 | return $result->getRecords(); |
360 | } |
361 | } |