diff --git a/Classes/Common/Collection.php b/Classes/Common/Collection.php index de5c99b..0e2ff4d 100644 --- a/Classes/Common/Collection.php +++ b/Classes/Common/Collection.php @@ -27,4 +27,14 @@ public function implode($value, $glue = null) { return Str::of(parent::implode($value, $glue)); } + + public function recursive(): Collection + { + return $this->map( function($item) { + if (is_array($item)) { + return Collection::wrap($item)->recursive(); + } + return $item; + }); + } } diff --git a/Classes/Common/ElasticClientBuilder.php b/Classes/Common/ElasticClientBuilder.php index c2ab8a7..cd1c4df 100644 --- a/Classes/Common/ElasticClientBuilder.php +++ b/Classes/Common/ElasticClientBuilder.php @@ -51,7 +51,7 @@ protected function autoconfig (): ElasticClientBuilder { $this->setBasicAuthentication('elastic', $this->password); } if ($this->caFilePath) { - $this->setSSLVerification($this->caFilePath); + $this->setCABundle($this->caFilePath); } return $this; @@ -59,13 +59,14 @@ protected function autoconfig (): ElasticClientBuilder { private function setCaFilePath(): void { - if ($this->extConf->get('elasticCaFileName') == '') { + if ($this->extConf->get('elasticCaFileFilePath') == '') { $this->caFilePath = ''; return; } $this->caFilePath = $this->extConf-> - only('elastcCredentialsFilePath', 'elasticCaFileName')-> + sortKeysDesc()-> + only('elasticCredentialsFilePath', 'elasticCaFileFilePath')-> implode('/'); } @@ -77,11 +78,13 @@ private function setPassword(): void } $passwordFilePath = $this->extConf-> + sortKeys()-> only('elasticCredentialsFilePath', 'elasticPwdFileName')-> implode('/'); $passwordFile = fopen($passwordFilePath, 'r') or die($passwordFilePath . ' not found. Check your extension\'s configuration'); + $size = filesize($passwordFilePath); - $this->password = $passwordFile->getContents(); + $this->password = trim(fread($passwordFile, $size)); } } diff --git a/Classes/Common/Paginator.php b/Classes/Common/Paginator.php new file mode 100644 index 0000000..09a41fb --- /dev/null +++ b/Classes/Common/Paginator.php @@ -0,0 +1,191 @@ + + setPage($page)-> + setTotalItems($totalItems)-> + setExtensionConfiguration($extConf)-> + getPagination(); + } + + public function setTotalItems(int $totalItems): Paginator + { + $this->totalItems = $totalItems; + + return $this; + } + + public function setExtensionConfiguration(ExtensionConfiguration $extensionConfiguration): Paginator + { + $extConf = $extensionConfiguration->get('liszt_common'); + $this->itemsPerPage = (int) $extConf['itemsPerPage']; + $paginationRangeString = Str::of($extConf['paginationRange']); + + if(!$paginationRangeString->isMatch('/^\d* *(, *\d+)*$/')) { + throw new \Exception('Check the configuration of liszt common. The pagination range needs to be specified in the form "1,2,3..."'); + } + + $this->paginationRange = $paginationRangeString->explode(',')-> + map(function($rangeItem) { return self::getRangeItem($rangeItem); })-> + // we always want the neighboring pages of the current page + push(1)-> + unique()-> + sort(); + + return $this; + } + + public function setPage(int $page): Paginator + { + $this->currentPage = $page; + + return $this; + } + + public function getPagination(): array + { + if ( + $this->totalItems < 0 || + $this->currentPage < 0 || + $this->itemsPerPage < 0 + ) { + throw new \Exception('Please specify total items, items per page and current page before retrieving the pagination.'); + } + + $pagination = new Collection(); + $totalPages = (int) ceil($this->totalItems / $this->itemsPerPage); + $currentPage = $this->currentPage; + + $pagesBefore = $this->paginationRange-> + filter()-> + reverse()-> + map(function($page) use ($currentPage) { return self::getPageBefore($page, $currentPage); }); + $pagesAfter = $this->paginationRange-> + filter()-> + map(function($page) use ($currentPage, $totalPages) { return self::getPageAfter($page, $currentPage, $totalPages); }); + + return Collection::wrap([])-> + // we include the first page if it is not the current one + when($this->currentPage != 1, + function ($collection) { return $collection->push([ 'page' => 1, 'class' => self::SHOW_CLASS ]); } + )-> + // we include the range pages before the current page (which may be empty) + concat($pagesBefore)-> + // we include the current page + push(['page' => $this->currentPage, 'class' => self::CURRENT_CLASS])-> + // we include the range pages after the current page (which may be empty) + concat($pagesAfter)-> + // we include the last page if it is not the current one + when($this->currentPage != $totalPages, + function($collection) use ($totalPages) { return $collection->push([ 'page' => $totalPages, 'class' => self::SHOW_CLASS ]);} + )-> + // we filter out empty results from the pagesBefore or pagesAfter arrays + filter()-> + // we introduce dots wherever the distance between two pages is greater than one, so we prepare by adding a dummy + push(null)-> + // sliding through pairs of pages + sliding(2)-> + // returning page 1 if the distance is 1 and page 1 and dots elsewise (here we need the dummy) + mapSpread( function ($page1, $page2) { return self::insertDots($page1, $page2); })-> + // and flatten out everything + flatten(1)-> + values()-> + all(); + } + + private static function insertDots(?array $page1, ?array $page2): array + { + if ($page2 == null) return [ $page1 ]; + + if ($page2['page'] - $page1['page'] == 1) { + return [ $page1 ]; + } + + $dots = [ 'page' => self::DOTS, 'class' => self::DOTS_CLASS ]; + return [ $page1, $dots ]; + } + + private static function getPageBefore(?int $page, int $currentPage): ?array + { + $result = $currentPage - $page; + + if ($result < 2) return null; + + if ($page == 1) { + return [ + 'page' => $result, + 'class' => self::SHOW_CLASS + ]; + } + + return [ + 'page' => $result, + 'class' => self::HIDE_CLASS + ]; + } + + private static function getPageAfter(?int $page, int $currentPage, int $totalPages): ?array + { + $result = $currentPage + $page; + + if ($result >= $totalPages) return null; + + if ($page == 1) { + return [ + 'page' => $result, + 'class' => self::SHOW_CLASS + ]; + } + + return [ + 'page' => $result, + 'class' => self::HIDE_CLASS + ]; + } + + private static function getRangeItem(string $rangeItem): ?int + { + if ($rangeItem == '') return null; + return (int) trim($rangeItem); + } +} diff --git a/Classes/Common/QueryParamsBuilder.php b/Classes/Common/QueryParamsBuilder.php index c77cbe8..fe42c80 100644 --- a/Classes/Common/QueryParamsBuilder.php +++ b/Classes/Common/QueryParamsBuilder.php @@ -2,30 +2,125 @@ namespace Slub\LisztCommon\Common; +use Slub\LisztCommon\Common\Collection; +use Slub\LisztCommon\Processing\IndexProcessor; +use Illuminate\Support\Str; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Utility\GeneralUtility; + class QueryParamsBuilder { + const TYPE_FIELD = 'itemType'; + const HEADER_FIELD = 'tx_lisztcommon_header'; + const FOOTER_FIELD = 'tx_lisztcommon_footer'; + const BODY_FIELD = 'tx_lisztcommon_body'; + const SEARCHABLE_FIELD = 'tx_lisztcommon_searchable'; + + protected array $params = []; + protected array $settings = []; + protected array $query = []; + protected string $indexName = ''; + protected bool $searchAll = false; + + public static function createQueryParamsBuilder(array $searchParams, array $settings): QueryParamsBuilder + { + $queryParamsBuilder = new QueryParamsBuilder(); + + return $queryParamsBuilder-> + setSettings($settings)-> + setSearchParams($searchParams); + } + + public function setSettings(array $settings): QueryParamsBuilder + { + $this->settings = $settings; + + return $this; + } + + public function setSearchParams($searchParams): QueryParamsBuilder + { + if ($this->settings == []) { + throw new \Exception('Please pass settings to QueryParamsBuilder before setting search parameters.'); + } + + $this->params = $searchParams; + + if (isset($this->params['index'])) { + $this->searchAll = false; + $this->indexName = $this->params['index']; + } else { + $this->searchAll = true; + $indexNames = Collection::wrap($this->settings)-> + recursive()-> + get('entityTypes')-> + pluck('indexName'); + if ($indexNames->count() == 1) { + $this->searchAll = false; + } + $this->indexName = $indexNames-> + join(','); + } + + return $this; + } //Todo: get Config for bibIndex, aggs etc. from extension config? - public static function createElasticParams(string $bibIndex, array $searchParams): array + public function getQueryParams(): array { - $params = [ - 'index' => $bibIndex, + $commonConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('liszt_common'); + + $this->query = [ + 'size' => $commonConf['itemsPerPage'], 'body' => [ - 'size' => 10, - '_source' => ['itemType', 'title', 'creators', 'pages', 'date', 'language', 'localizedCitations', 'publicationTitle', 'archiveLocation'], - 'aggs' => [ - 'itemType' => [ - 'terms' => [ - 'field' => 'itemType.keyword', - ] - ], - 'place' => [ - 'terms' => [ - 'field' => 'place.keyword', + '_source' => [ + IndexProcessor::TYPE_FIELD, + IndexProcessor::HEADER_FIELD, + IndexProcessor::BODY_FIELD, + IndexProcessor::FOOTER_FIELD, + IndexProcessor::SEARCHABLE_FIELD + ], + ] + ]; + + if ($this->searchAll == false) { + $this->query['body']['aggs'] = self::getAggs($this->settings, $this->indexName); + } + + $this->setCommonParams(); + + // Todo: automate the creation of parameters + // filter creators name, Todo: its not a filter query because they need 100% match (with spaces from f_creators_name) + // better would be to build the field 'fullName' at build time with PHP? + if (isset($this->params['f_creators_name']) && $this->params['f_creators_name'] !== "") { + $this->query['body']['query']['bool']['must'][] = [ + 'nested' => [ + 'path' => 'creators', + 'query' => [ + 'match' => [ + 'creators.fullName' => $this->params['f_creators_name'] ] ] ] - ] + ]; + } + if (isset($this->params['page']) && $this->params['page'] !== "") { + $this->query['from'] = ($this->params['page'] - 1) * $commonConf['itemsPerPage']; + } + + return $this->query; + } + + public function getCountQueryParams(): array + { + $this->query = [ 'body' => [ ] ]; + + $this->setCommonParams(); + +/* + $params = [ + //'index' => $bibIndex, + 'body' => [ ] ]; if (!isset($searchParams['searchText']) || $searchParams['searchText'] == '') { $params['body']['query'] = [ @@ -36,18 +131,200 @@ public static function createElasticParams(string $bibIndex, array $searchParams ] ]; } else { + // search in field "fulltext" exakt phrase match boost over all words must contain $params['body']['query'] = [ 'bool' => [ - 'must' => [[ - 'query_string' => [ - 'query' => $searchParams['searchText'] + 'should' => [ + [ + 'match_phrase' => [ + 'tx_lisztcommon_searchable' => [ + 'query' => $searchParams['searchText'], + 'boost' => 2.0 // boosting for exakt phrases + ] + ] + ], + [ + 'query_string' => [ + 'query' => $searchParams['searchText'], + 'fields' => ['fulltext'], + 'default_operator' => 'AND' + ] ] - ]] + ] + ] + ]; + } + + // Todo: automate the creation of parameters + if (isset($searchParams['f_itemType']) && $searchParams['f_itemType'] !== "") { + $params['body']['query']['bool']['filter'][] = ['term' => ['itemType.keyword' => $searchParams['f_itemType']]]; + } + if (isset($searchParams['f_place']) && $searchParams['f_place'] !== "") { + $params['body']['query']['bool']['filter'][] = ['term' => ['place.keyword' => $searchParams['f_place']]]; + } + if (isset($searchParams['f_date']) && $searchParams['f_date'] !== "") { + $params['body']['query']['bool']['filter'][] = ['term' => ['date.keyword' => $searchParams['f_date']]]; + } + // filter creators name, Todo: its not a filter query because they need 100% match (with spaces from f_creators_name) + // better would be to build the field 'fullName' at build time with PHP? + if (isset($searchParams['f_creators_name']) && $searchParams['f_creators_name'] !== "") { + $params['body']['query']['bool']['must'][] = [ + 'nested' => [ + 'path' => 'creators', + 'query' => [ + 'match' => [ + 'creators.fullName' => $searchParams['f_creators_name'] + ] + ] ] ]; } - return $params; + +*/ + return $this->query; + } + private function getIndexName(): string + { + if (isset($this->params['index'])) { + return $this->params['index']; + } + return Collection::wrap($this->settings)-> + get('entityTypes')-> + pluck('indexName')-> + join(','); + } + + private static function getAggs(array $settings, string $index): array + { + return Collection::wrap($settings)-> + recursive()-> + get('entityTypes')-> + filter(function($entityType) use ($index) {return $entityType->get('indexName') === $index;})-> + values()-> + get(0)-> + get('filters')-> + mapWithKeys(function($entityType) { + if ($entityType['type'] == 'terms') { + return [$entityType['field'] => [ + 'terms' => [ + 'field' => $entityType['field'] . '.keyword' + ] + ]]; + } + return [ + $entityType['field'] => [ + 'nested' => [ + 'path' => $entityType['field'] + ], + 'aggs' => [ + 'names' => [ + 'terms' => [ + 'script' => [ + 'source' => $entityType['script'], + 'lang' => 'painless' + ], + 'size' => 15, + ] + ] + ] + ] + ]; + })-> + toArray(); + } + + private static function getFilter(array $field): array + { +/* + if ( + isset($field['type']) && + $field['type'] == 'terms' + ) { +*/ + return [ + 'term' => [ + $field['name']. '.keyword' => $field['value'] + ] + ]; + //} + + return [ + $field['name'] => [ + 'nested' => [ + 'path' => $field['name'] + ], + 'aggs' => [ + 'names' => [ + 'terms' => [ + 'script' => [ + 'source' => $field['script'], + 'lang' => 'painless' + ], + 'size' => 15, + ] + ] + ] + ] + ]; } + /** + * sets parameters needed for both search and count queries + */ + private function setCommonParams(): void + { + // set index name + $this->query['index'] = $this->indexName; + + // set body + if (!isset($this->params['searchText']) || $this->params['searchText'] == '') { + $this->query['body']['query'] = [ + 'bool' => [ + 'must' => [[ + 'match_all' => new \stdClass() + ]] + ] + ]; + } else { + // search in field "fulltext" exakt phrase match boost over all words must contain + $this->query['body']['query'] = [ + 'bool' => [ + 'should' => [ + [ + 'match_phrase' => [ + 'tx_lisztcommon_searchable' => [ + 'query' => $this->params['searchText'], + 'boost' => 2.0 // boosting for exakt phrases + ] + ] + ], + [ + 'query_string' => [ + 'query' => $this->params['searchText'], + 'fields' => ['fulltext'], + 'default_operator' => 'AND' + ] + ] + ] + ] + ]; + } + + // set filters + $query = $this->query; + Collection::wrap($this->params)-> + filter(function($_, $key) { return Str::of($key)->startsWith('f_'); })-> + each(function($value, $key) use (&$query) { + $field = Str::of($key)->replace('f_', '')->__toString(); + $query['body']['query']['bool']['filter'][] = self::getFilter([ + 'name' => $field, + //'type' => $field['type'], + 'type' => 'terms', + 'value' => $value + ]); + }); + $this->query = $query; + + } } diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php index 000a97d..19ab522 100644 --- a/Classes/Controller/SearchController.php +++ b/Classes/Controller/SearchController.php @@ -5,6 +5,8 @@ namespace Slub\LisztCommon\Controller; use Psr\Http\Message\ResponseInterface; use Slub\LisztCommon\Interfaces\ElasticSearchServiceInterface; +use Slub\LisztCommon\Common\Paginator; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; // ToDo: // Organize the transfer of the necessary parameters (index name, fields, etc.) from the other extensions (ExtensionConfiguration?) -> see in ElasticSearchServic @@ -13,36 +15,47 @@ final class SearchController extends ClientEnabledController { - // set resultLimit as intern variable from $this->settings['resultLimit']; protected int $resultLimit; - // Dependency Injection of Repository // https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/DependencyInjection/Index.html#Dependency-Injection - public function __construct(private readonly ElasticSearchServiceInterface $elasticSearchService) + public function __construct( + private readonly ElasticSearchServiceInterface $elasticSearchService, + protected ExtensionConfiguration $extConf + ) { $this->resultLimit = $this->settings['resultLimit'] ?? 25; } - - public function indexAction(array $searchParams = []): ResponseInterface { $language = $this->request->getAttribute('language'); $locale = $language->getLocale(); + if ( + isset($searchParams['searchParamsPage']) && + (int) $searchParams['searchParamsPage'] > 0 + ) { + $currentPage = (int) $searchParams['searchParamsPage']; + } else { + $currentPage = 1; + } - $elasticResponse = $this->elasticSearchService->search($searchParams); + $totalItems = $this->elasticSearchService->count($searchParams, $this->settings); + $pagination = Paginator::createPagination($currentPage, $totalItems, $this->extConf); - $this->view->assign('locale', $locale); + $elasticResponse = $this->elasticSearchService->search($searchParams, $this->settings); + $this->view->assign('locale', $locale); $this->view->assign('totalItems', $elasticResponse['hits']['total']['value']); - $this->view->assign('searchParams', $searchParams); - - $this->view->assign('searchResults', $elasticResponse); + $this->view->assign('pagination', $pagination); + $this->view->assign('totalItems', $totalItems); + $this->view->assign('currentString', Paginator::CURRENT_PAGE); + $this->view->assign('dots', Paginator::DOTS); + return $this->htmlResponse(); } @@ -52,5 +65,4 @@ public function searchBarAction(array $searchParams = []): ResponseInterface return $this->htmlResponse(); } - } diff --git a/Classes/Interfaces/ElasticSearchServiceInterface.php b/Classes/Interfaces/ElasticSearchServiceInterface.php index bdd2e41..280e92d 100644 --- a/Classes/Interfaces/ElasticSearchServiceInterface.php +++ b/Classes/Interfaces/ElasticSearchServiceInterface.php @@ -9,7 +9,8 @@ public function init(): bool; public function getElasticInfo(): array; - public function search(array $searchParams): Collection; + public function search(array $searchParams, array $settings): Collection; + public function count(array $searchParams, array $settings): int; } diff --git a/Classes/Processing/IndexProcessor.php b/Classes/Processing/IndexProcessor.php new file mode 100644 index 0000000..3afbc32 --- /dev/null +++ b/Classes/Processing/IndexProcessor.php @@ -0,0 +1,25 @@ +client->info()->asArray()); } - - - public function search($searchParams): Collection + public function search(array $searchParams, array $settings): Collection { $this->init(); - $this->params = QueryParamsBuilder::createElasticParams($this->bibIndex, $searchParams); + $this->params = QueryParamsBuilder::createQueryParamsBuilder($searchParams, $settings)->getQueryParams(); // ToDo: handle exceptions! $response = $this->client->search($this->params); return new Collection($response->asArray()); } + public function count(array $searchParams, array $settings): int + { + $this->init(); + $this->params = QueryParamsBuilder::createQueryParamsBuilder($searchParams, $settings)->getCountQueryParams(); + $response = $this->client->count($this->params); + return $response['count']; + } } diff --git a/Classes/ViewHelpers/ItemTypeIconNameViewHelper.php b/Classes/ViewHelpers/ItemTypeIconNameViewHelper.php new file mode 100644 index 0000000..81da14a --- /dev/null +++ b/Classes/ViewHelpers/ItemTypeIconNameViewHelper.php @@ -0,0 +1,42 @@ +registerArgument('iconPackKey', 'string', 'Name of the key for the IconPack (from Iconpack.yaml)', true); + $this->registerArgument('itemType', 'string', 'Name of the itemType (from Zotero)', true); + } + + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext) + : ?string + { + // get installed icon names from the t3x Iconpack Extension with Key 'lziconsr' as Array + $iconpackFactory = GeneralUtility::makeInstance(IconpackFactory::class); + $iconPackKey = $arguments['iconPackKey']; + $availableIconsArray = $iconpackFactory->queryConfig($iconPackKey, 'icons'); + + // Check if itemType exists as a key in the array + $itemType = $arguments['itemType']; + if (array_key_exists($itemType, $availableIconsArray)) { + return $iconPackKey.','.$itemType; + } + + // else Return default icon + return 'lziconsr,lisztDocument'; + + } +} diff --git a/Classes/ViewHelpers/SearchParamsViewHelper.php b/Classes/ViewHelpers/SearchParamsViewHelper.php new file mode 100644 index 0000000..10b7fc2 --- /dev/null +++ b/Classes/ViewHelpers/SearchParamsViewHelper.php @@ -0,0 +1,46 @@ +registerArgument('action', 'string', 'the operation to be performed with the search parameters', true); + $this->registerArgument('key', 'string', 'the key wich has to be operated (added oder removed) ', true); + $this->registerArgument('value', 'string', 'the value if is a add operation', false); + $this->registerArgument('searchParamsArray', 'array', 'the Array with SearchParams from Controller', true); + } + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext) + : ?array + { + $action = $arguments['action']; + $key = $arguments['key']; + $value = $arguments['value'] ?? null; + $searchParamsArray = $arguments['searchParamsArray']; + + if ($action === 'add') { + $searchParamsArray[$key] = $value; + } elseif ($action === 'remove') { + unset($searchParamsArray[$key]); + } + + // Convert the array to a string formatted as {key: 'value', key2: 'value2'} + /* + $formattedParams = []; + foreach ($searchParamsArray as $paramKey => $paramValue) { + $formattedParams[] = "{$paramKey}: '" . $paramValue . "'"; + } + */ + + // return '{' . implode(', ', $formattedParams) . '}'; + return ['searchParams' => $searchParamsArray]; + + } +} diff --git a/Configuration/Iconpack/LisztSearchResultsIconpack.yaml b/Configuration/Iconpack/LisztSearchResultsIconpack.yaml new file mode 100644 index 0000000..e7e06b3 --- /dev/null +++ b/Configuration/Iconpack/LisztSearchResultsIconpack.yaml @@ -0,0 +1,49 @@ +iconpack: + title: "Liszt Custom Icons for Search Results in LQWV" + # The key of the iconpack (!) + key: "lziconsr" + version: 1.0.0 + + renderTypes: + svg: + # Source folder of the SVG files, which are rendered as tag: + source: "EXT:liszt_common/Resources/Public/Icons/IconPackSearchResults/" + attributes: + class: "lzicon" + + svgInline: + # Source folder of the SVG files that are rendered as tag (inline SVG): + source: "EXT:liszt_common/Resources/Public/Icons/IconPackSearchResults/" + attributes: + class: "lzicon-inline" + fill: "currentColor" + + + svgSprite: + source: "EXT:liszt_common/Resources/Public/Icons/IconPackSearchResults/IconSpritesSearchResults.svg" + attributes: + class: "lzicon-sprite" + fill: "currentColor" + + # Define here which icons are provided by this iconpack + # In this case, the values here correspond to the file names (without file name extension) + icons: + - auctionCatalog + - lisztDocument + - book + - attachment + - encyclopediaArticle + - essay + - journalArticle + - lisztDocument + - musicSheet + - thesis + - performance + - profession + - birth + - corporation + - open-access + - place + - person + - death + - work diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index 9d42f96..01a1c28 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -31,16 +31,21 @@ lib.contentElement { settings { } - } - tt_content { list { templateName = SearchListing } } +// same namespace for both plugins on searchresults page +plugin.tx_lisztcommon_searchbar.view.pluginNamespace = tx_liszt_common_searchlisting +plugin.tx_lisztcommon_searchlisting.view.pluginNamespace = tx_liszt_common_searchlisting +// while two plugins are on searchresults page, the searchbar plugin brings "action index is not allowed by this plugin" by non searchbar actions +// set use default action to avoid this error +plugin.tx_lisztcommon_searchbar.mvc.callDefaultActionIfActionCantBeResolved = 1; + # get the selected frontend layout from page table for show/hide SearchBar, because bootstrap package not use this param @@ -77,6 +82,5 @@ tt_content { } }*/ - plugin.tx_lisztcommon_searchbar.view.pluginNamespace = tx_liszt_common_searchlisting -plugin.tx_lisztcommon_searchlisting.view.pluginNamespace = tx_liszt_common_searchlisting +plugin.tx_lisztcommon_searchlisting.view.pluginNamespace = tx_liszt_common_searchlisting \ No newline at end of file diff --git a/README.md b/README.md index 43c9f2c..76fcf37 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,32 @@ use Slub\LisztCommon\Controller\ClientEnabledController; class ActionController extends ClientEnabledController { - + public function ExampleAction() { $this->initializeClient(); $params = ... $entity = $this->elasticClient->search($params); ... - + } } ``` - + +## Elasticsearch Configuration + +In the extension configuration, you need to specify the elasticsearch host, +password file, credentials path and HTTP CA certificate path. + +## Search Hit List Configuration + +In the extension configuration, you may specify how many search results are +shown in one page. For the pagination, you may set which pages before and after +the current one are shown. The first and last page are always shown. Specify a +comma separated list of numbers, which will be added and subtracted respectively +from the current page. + ## Translation between XML and JSON You can read in an XML document and translate it to a PHP array or JSON. diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf index ea178a3..28c7178 100644 --- a/Resources/Private/Language/de.locallang.xlf +++ b/Resources/Private/Language/de.locallang.xlf @@ -1,13 +1,11 @@ - -
-
- - - Suchen - - + +
+ + + Suchen + Liszt Suchergebnisse @@ -26,6 +24,33 @@ Suchen - - + + Suchfilter + + + Filtermenü schließen + + + Treffer + + + Ihre Suchparameter + + + Suchparameter "%s" entfernen + + + ... + + + Seiten Navigation, Pagination + + + Gehe zur Seite %s + + + Sie befinden sich auf Seite %s + + + diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 42dc3c9..c7918be 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -1,14 +1,11 @@ - -
- - - - Search - - - + +
+ + + Suchen + Liszt Suchergebnisse @@ -27,7 +24,51 @@ Suchen - + + Suchfilter + + + Filtermenü schließen + + + Treffer + + + Ihre Suchparameter + + + Suchparameter "%s" entfernen + + + Hostname des Elasticsearch-Servers + + + Dateipfad der Elasticsearch-Credentials (Passwort, CA-Zertifikat) + + + Name der Datei, die das Elasticsearch-Passwort enthält + + + Name der Datei, die das HTTP-CA-Zertifikat enthält + + + Abstände der in der Paginierung anzuzeigenden Seiten zur aktuellen Seite. Kommaseparierte Liste. 1 ist immer implizit gesetzt. + + + Elemente pro Seite der Ergebnisliste + + + ... + + + Seiten Navigation, Pagination + + + Gehe zur Seite %s + + + Sie befinden sich auf Seite %s + - + diff --git a/Resources/Private/Partials/FilterBlock.html b/Resources/Private/Partials/FilterBlock.html new file mode 100644 index 0000000..29c4fb8 --- /dev/null +++ b/Resources/Private/Partials/FilterBlock.html @@ -0,0 +1,38 @@ +{namespace lc=Slub\LisztCommon\ViewHelpers} + + +
+

{key}

+ f_{key} + {lc:searchParams(action: 'remove', searchParamsArray: searchParams, key: 'searchParamsPage')} + {paramsRemovePage.searchParams} +
    + +
  • + + + + {filter.key} + {filter.doc_count} + + + + + {filter.key} + {filter.doc_count} + + + +
  • + + +
  • {filter.doc_count}
  • +
  • {filter.doc_count}
  • +
  • {filter.doc_count}
  • +
    + +
    +
+
+ + diff --git a/Resources/Private/Partials/Pagination.html b/Resources/Private/Partials/Pagination.html new file mode 100644 index 0000000..f1c36a2 --- /dev/null +++ b/Resources/Private/Partials/Pagination.html @@ -0,0 +1,24 @@ +{namespace lc=Slub\LisztCommon\ViewHelpers} + + + + diff --git a/Resources/Private/Templates/Search/Index.html b/Resources/Private/Templates/Search/Index.html index 022a15e..fcae99a 100644 --- a/Resources/Private/Templates/Search/Index.html +++ b/Resources/Private/Templates/Search/Index.html @@ -1,47 +1,78 @@ - +{namespace lc=Slub\LisztCommon\ViewHelpers} + +
+
+ {totalItems} {f:translate(key: 'searchResults_hits_label', extensionName: 'liszt_common')} + +
+ + Remove Page param from searchparams array for display filter tags + {lc:searchParams(action: 'remove', searchParamsArray: searchParams, key: 'searchParamsPage')} + + + +
-{totalItems} Treffer -{_all} +
-
- - {creator.firstName} {creator.lastName} - + {hit._source.tx_lisztcommon_header}
-

- {hit._source.title}, {hit._source.title} +
+
+

+ {hit._source.tx_lisztcommon_body}

+

- {hit._source.date}, {hit._source.pages}, {hit._source.archiveLocation} + {hit._source.tx_lisztcommon_footer}
+
diff --git a/Resources/Public/Icons/IconPackSearchResults/IconSpritesSearchResults.svg b/Resources/Public/Icons/IconPackSearchResults/IconSpritesSearchResults.svg new file mode 100644 index 0000000..efc70e4 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/IconSpritesSearchResults.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/attachment.svg b/Resources/Public/Icons/IconPackSearchResults/attachment.svg new file mode 100644 index 0000000..3acd51f --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/attachment.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/auctionCatalog.svg b/Resources/Public/Icons/IconPackSearchResults/auctionCatalog.svg new file mode 100644 index 0000000..0c0ab16 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/auctionCatalog.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/birth.svg b/Resources/Public/Icons/IconPackSearchResults/birth.svg new file mode 100644 index 0000000..e144c1a --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/birth.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/book.svg b/Resources/Public/Icons/IconPackSearchResults/book.svg new file mode 100644 index 0000000..ee6c2aa --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/book.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/corporation.svg b/Resources/Public/Icons/IconPackSearchResults/corporation.svg new file mode 100644 index 0000000..b27d88b --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/corporation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/death.svg b/Resources/Public/Icons/IconPackSearchResults/death.svg new file mode 100644 index 0000000..a53fe96 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/death.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/encyclopediaArticle.svg b/Resources/Public/Icons/IconPackSearchResults/encyclopediaArticle.svg new file mode 100644 index 0000000..1dcc447 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/encyclopediaArticle.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/essay.svg b/Resources/Public/Icons/IconPackSearchResults/essay.svg new file mode 100644 index 0000000..532f2a1 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/essay.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/journalArticle.svg b/Resources/Public/Icons/IconPackSearchResults/journalArticle.svg new file mode 100644 index 0000000..f7cd08e --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/journalArticle.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/lisztDocument.svg b/Resources/Public/Icons/IconPackSearchResults/lisztDocument.svg new file mode 100644 index 0000000..bcd5a02 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/lisztDocument.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/musicSheet.svg b/Resources/Public/Icons/IconPackSearchResults/musicSheet.svg new file mode 100644 index 0000000..c699d42 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/musicSheet.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/open-access.svg b/Resources/Public/Icons/IconPackSearchResults/open-access.svg new file mode 100644 index 0000000..817d7fe --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/open-access.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/performance.svg b/Resources/Public/Icons/IconPackSearchResults/performance.svg new file mode 100644 index 0000000..f98a5f4 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/performance.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/person.svg b/Resources/Public/Icons/IconPackSearchResults/person.svg new file mode 100644 index 0000000..52176cb --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/person.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/place.svg b/Resources/Public/Icons/IconPackSearchResults/place.svg new file mode 100644 index 0000000..4470532 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/place.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/profession.svg b/Resources/Public/Icons/IconPackSearchResults/profession.svg new file mode 100644 index 0000000..100d94a --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/profession.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/thesis.svg b/Resources/Public/Icons/IconPackSearchResults/thesis.svg new file mode 100644 index 0000000..ae2a011 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/thesis.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Resources/Public/Icons/IconPackSearchResults/work.svg b/Resources/Public/Icons/IconPackSearchResults/work.svg new file mode 100644 index 0000000..19cfc76 --- /dev/null +++ b/Resources/Public/Icons/IconPackSearchResults/work.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Tests/Unit/Common/PaginatorTest.php b/Tests/Unit/Common/PaginatorTest.php new file mode 100644 index 0000000..c25aa07 --- /dev/null +++ b/Tests/Unit/Common/PaginatorTest.php @@ -0,0 +1,320 @@ +extConf = $this->getAccessibleMock(ExtensionConfiguration::class, ['get'], [], '', false); + $this->confArray['itemsPerPage'] = self::ITEMS_PER_PAGE; + $totalItems = self::ITEMS_PER_PAGE * self::PAGE_COUNT - self::LAST_PAGE_UNDERFLOW; + + $this->subject = new Paginator(); + $this->subject->setTotalItems($totalItems); + } + + /** + * @test + */ + public function firstPageGetsCorrectPagination(): void + { + $this->confArray['paginationRange'] = '1,2,3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $this->subject->setPage(1); + $this->subject->setExtensionConfiguration($this->extConf); + + $expected = [ + [ 'page' => 1, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => 2, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => 4, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function lastPageGetsCorrectPagination(): void + { + $this->confArray['paginationRange'] = '1,2,3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $this->subject->setPage(self::PAGE_COUNT); + $this->subject->setExtensionConfiguration($this->extConf); + + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT - 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => self::PAGE_COUNT - 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => self::PAGE_COUNT - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::CURRENT_CLASS ] + ]; + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function midPageGetsCorrectPagination(): void + { + $this->confArray['paginationRange'] = '1,2,3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $midPage = ceil(self::PAGE_COUNT / 2); + $this->subject->setPage($midPage); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => $midPage + 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage + 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage + 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function secondPageGetsCorrectPagination() + { + $this->confArray['paginationRange'] = '1,2,3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $this->subject->setPage(2); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => 2, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => 3, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => 4, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => 5, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function incorrectExtConfLeadsToException() + { + $this->confArray['paginationRange'] = 'randomText'; + $this->extConf->method('get')-> + willReturn($this->confArray); + + $this->expectException(\Exception::class); + $this->subject->setExtensionConfiguration($this->extConf); + } + + /** + * @test + */ + public function mildlyIncorrectExtConfLeadsToException() + { + $this->confArray['paginationRange'] = '1,2,a'; + $this->extConf->method('get')-> + willReturn($this->confArray); + + $this->expectException(\Exception::class); + $this->subject->setExtensionConfiguration($this->extConf); + } + + /** + * @test + */ + public function incorrectSortingInExtConfGetsCorrectPagination() + { + $this->confArray['paginationRange'] = '2,1,3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $midPage = ceil(self::PAGE_COUNT / 2); + $this->subject->setPage($midPage); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => $midPage + 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage + 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage + 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function nonContiguousConfigInExtConfGetsCorrectPagination() + { + $this->confArray['paginationRange'] = '1,2,5'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $midPage = ceil(self::PAGE_COUNT / 2); + $this->subject->setPage($midPage); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 5, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => $midPage + 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage + 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage + 5, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + } + + /** + * @test + */ + public function paginationRangeMayBeFormattedWithSpaces(): void + { + $this->confArray['paginationRange'] = '1, 2, 3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $midPage = ceil(self::PAGE_COUNT / 2); + $this->subject->setPage($midPage); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => $midPage + 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage + 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage + 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function multiplePagesAreReturnedUniquely(): void + { + $this->confArray['paginationRange'] = '1,1,2,3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $midPage = ceil(self::PAGE_COUNT / 2); + $this->subject->setPage($midPage); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => $midPage + 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage + 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage + 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function neighboringPagesAreAlwaysIncluded(): void + { + $this->confArray['paginationRange'] = '2,3'; + $this->extConf->method('get')-> + willReturn($this->confArray); + $midPage = ceil(self::PAGE_COUNT / 2); + $this->subject->setPage($midPage); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => $midPage + 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage + 2, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => $midPage + 3, 'class' => Paginator::HIDE_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + + /** + * @test + */ + public function emptyPageRangeLeadsToSensibleResult(): void + { + $this->confArray['paginationRange'] = ''; + $this->extConf->method('get')-> + willReturn($this->confArray); + $midPage = ceil(self::PAGE_COUNT / 2); + $this->subject->setPage($midPage); + $this->subject->setExtensionConfiguration($this->extConf); + $expected = [ + [ 'page' => 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => $midPage - 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => $midPage, 'class' => Paginator::CURRENT_CLASS ], + [ 'page' => $midPage + 1, 'class' => Paginator::SHOW_CLASS ], + [ 'page' => Paginator::DOTS, 'class' => Paginator::DOTS_CLASS ], + [ 'page' => self::PAGE_COUNT, 'class' => Paginator::SHOW_CLASS ] + ]; + + self::assertEquals($expected, $this->subject->getPagination()); + } + +} diff --git a/Tests/Unit/Common/QueryParamsBuilderTest.php b/Tests/Unit/Common/QueryParamsBuilderTest.php new file mode 100644 index 0000000..8fce96b --- /dev/null +++ b/Tests/Unit/Common/QueryParamsBuilderTest.php @@ -0,0 +1,323 @@ +subject = new QueryParamsBuilder(); + $this->settings = []; + $this->params = [ + 'index' => self:: EX_INDEX, + 'page' => 3, + 'f_filter' => self::EX_VAL + ]; + + $confArray = []; + $confArray['itemsPerPage'] = PaginatorTest::ITEMS_PER_PAGE; + $this->extConf = $this->getAccessibleMock(ExtensionConfiguration::class, ['get'], [], '', false); + $this->extConf->method('get')-> + willReturn($confArray); + + $this->settings = [ + 'entityTypes' => [ + 0 => [ + 'labelKey' => self::EX_LABEL_KEY, + 'extensionName' => self::EX_EXTENSION, + 'indexName' => self::EX_INDEX, + 'filters' => [ + 0 => [ + 'field' => self::EX_FIELD1, + 'type' => 'terms' + ], + 1 => [ + 'field' => self::EX_FIELD2, + 'type' => 'nested', + 'script' => self::EX_SCRIPT + ] + ] + ], + 1 => [ + 'labelKey' => self::EX_LABEL_KEY2, + 'extensionName' => self::EX_EXTENSION2, + 'indexName' => self::EX_INDEX2, + 'filters' => [ + 0 => [ + 'field' => self::EX_FIELD1, + 'type' => 'terms' + ], + 1 => [ + 'field' => self::EX_FIELD2, + 'type' => 'nested', + 'script' => self::EX_SCRIPT + ] + ] + ] + ] + ]; + } + + /** + * @test + */ + public function emptySearchParamsAreProcessedCorrectly(): void + { + $this->subject-> + setSettings($this->settings)-> + setSearchParams([]); + GeneralUtility::addInstance(ExtensionConfiguration::class, $this->extConf); + + $expected = [ + 'index' => self::EX_INDEX . ',' . self::EX_INDEX2, + 'size' => PaginatorTest::ITEMS_PER_PAGE, + 'body' => [ + '_source' => [ + QueryParamsBuilder::TYPE_FIELD, + QueryParamsBuilder::HEADER_FIELD, + QueryParamsBuilder::BODY_FIELD, + QueryParamsBuilder::FOOTER_FIELD, + QueryParamsBuilder::SEARCHABLE_FIELD + + ], + 'query' => [ + 'bool' => [ + 'must' => [ + [ 'match_all' => new \StdClass() ] + ] + ] + ] + ] + ]; + + self::assertEquals($expected, $this->subject->getQueryParams()); + } + + /** + * @test + */ + public function IndexParamIsProcessedCorrectly(): void + { + $this->subject-> + setSettings($this->settings)-> + setSearchParams([ + 'index' => self:: EX_INDEX + ]); + GeneralUtility::addInstance(ExtensionConfiguration::class, $this->extConf); + + $expected = [ + 'index' => self::EX_INDEX, + 'size' => PaginatorTest::ITEMS_PER_PAGE, + 'body' => [ + '_source' => [ + QueryParamsBuilder::TYPE_FIELD, + QueryParamsBuilder::HEADER_FIELD, + QueryParamsBuilder::BODY_FIELD, + QueryParamsBuilder::FOOTER_FIELD, + QueryParamsBuilder::SEARCHABLE_FIELD + + ], + 'aggs' => [ + self::EX_FIELD1 => [ + 'terms' => [ + 'field' => self::EX_FIELD1 . '.keyword' + ] + ], + self::EX_FIELD2 => [ + 'nested' => [ + 'path' => self::EX_FIELD2 + ], + 'aggs' => [ + 'names' => [ + 'terms' => [ + 'script' => [ + 'source' => self::EX_SCRIPT, + 'lang' => 'painless' + ], + 'size' => 15 + ] + ] + ] + ] + ], + 'query' => [ + 'bool' => [ + 'must' => [ + [ 'match_all' => new \StdClass() ] + ] + ] + ] + ] + ]; + + self::assertEquals($expected, $this->subject->getQueryParams()); + } + + /** + * @test + */ + public function pageParamIsProcessedCorrectly(): void + { + + $this->subject-> + setSettings($this->settings)-> + setSearchParams([ + 'page' => self::EX_PAGE + ]); + GeneralUtility::addInstance(ExtensionConfiguration::class, $this->extConf); + + $expected = [ + 'index' => self::EX_INDEX . ',' . self::EX_INDEX2, + 'size' => PaginatorTest::ITEMS_PER_PAGE, + 'from' => PaginatorTest::ITEMS_PER_PAGE * (self::EX_PAGE - 1), + 'body' => [ + '_source' => [ + QueryParamsBuilder::TYPE_FIELD, + QueryParamsBuilder::HEADER_FIELD, + QueryParamsBuilder::BODY_FIELD, + QueryParamsBuilder::FOOTER_FIELD, + QueryParamsBuilder::SEARCHABLE_FIELD + + ], + 'query' => [ + 'bool' => [ + 'must' => [ + [ 'match_all' => new \StdClass() ] + ] + ] + ] + ] + ]; + + self::assertEquals($expected, $this->subject->getQueryParams()); + } + + /** + * @test + */ + public function filterParamIsProcessedCorrectly(): void + { + $this->subject-> + setSettings($this->settings)-> + setSearchParams([ + 'index' => self:: EX_INDEX, + 'f_filter' => self::EX_VAL + ]); + GeneralUtility::addInstance(ExtensionConfiguration::class, $this->extConf); + + $expected = [ + 'index' => self::EX_INDEX, + 'size' => PaginatorTest::ITEMS_PER_PAGE, + 'body' => [ + '_source' => [ + QueryParamsBuilder::TYPE_FIELD, + QueryParamsBuilder::HEADER_FIELD, + QueryParamsBuilder::BODY_FIELD, + QueryParamsBuilder::FOOTER_FIELD, + QueryParamsBuilder::SEARCHABLE_FIELD + + ], + 'aggs' => [ + self::EX_FIELD1 => [ + 'terms' => [ + 'field' => self::EX_FIELD1 . '.keyword' + ] + ], + self::EX_FIELD2 => [ + 'nested' => [ + 'path' => self::EX_FIELD2 + ], + 'aggs' => [ + 'names' => [ + 'terms' => [ + 'script' => [ + 'source' => self::EX_SCRIPT, + 'lang' => 'painless' + ], + 'size' => 15 + ] + ] + ] + ] + ], + 'query' => [ + 'bool' => [ + 'must' => [ + [ 'match_all' => new \StdClass() ] + ], + 'filter' => [ + [ 'term' => [ + 'filter.keyword' => self::EX_VAL + ] + ] + ] + ] + ] + ] + ]; + + self::assertEquals($expected, $this->subject->getQueryParams()); + } + + /** + * @test + */ + public function countQueryIsBuiltCorrectly(): void + { + $this->subject-> + setSettings($this->settings)-> + setSearchParams([ + 'index' => self:: EX_INDEX, + 'f_filter' => self::EX_VAL + ]); + + $expected = [ + 'index' => self::EX_INDEX, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [ + [ 'match_all' => new \StdClass() ] + ], + 'filter' => [ + [ 'term' => [ + 'filter.keyword' => self::EX_VAL + ] + ] + ] + ] + ] + ] + ]; + + self::assertEquals($expected, $this->subject->getCountQueryParams()); + } +} diff --git a/composer.json b/composer.json index 4ab04f6..c21e65c 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,9 @@ "typo3/cms-core": "^12", "elasticsearch/elasticsearch": "^8", "illuminate/collections": "^11", - "illuminate/support": "^11" - }, + "illuminate/support": "^11", + "quellenform/t3x-iconpack": "^1.0" + }, "require-dev": { "phpstan/phpstan": "^1", "phpunit/phpunit": "^9.4", diff --git a/ext_conf_template.txt b/ext_conf_template.txt index c0f5f5a..0020f59 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -1,8 +1,12 @@ -# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/Labels.xml:config.elasticHostName +# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.elasticHostName elasticHostName = https://sdvelasticmusikverlage:9200 -# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/Labels.xml:config.elasticCredentialsFilePath +# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.elasticCredentialsFilePath elasticCredentialsFilePath = http_ca.crt -# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/Labels.xml:config.elasticPwdFileName +# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.elasticPwdFileName elasticPwdFileName = elastic_pwd.txt -# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/Labels.xml:config.elasticCaFileName +# cat=Elasticsearch; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.elasticCaFileName elasticCaFileFilePath = http_ca.crt +# cat=Search; type=string; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.paginationRange +paginationRange = 2,3,4,5 +# cat=Search; type=int; label=LLL:EXT:liszt_common/Resources/Private/Language/locallang.xml:config.itemsPerPage +itemsPerPage = 10 diff --git a/ext_localconf.php b/ext_localconf.php index d8d8b0d..f3e537f 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -28,3 +28,11 @@ ExtensionManagementUtility::addPageTSConfig( '' ); + +if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('iconpack')) { + \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance( + \Quellenform\Iconpack\IconpackRegistry::class + )->registerIconpack( + 'EXT:liszt_common/Configuration/Iconpack/LisztSearchResultsIconpack.yaml', + ); +}