diff --git a/asset/css/reference.css b/asset/css/reference.css new file mode 100644 index 0000000..8f01825 --- /dev/null +++ b/asset/css/reference.css @@ -0,0 +1,126 @@ +/* Reference pages */ +.solr-list { + padding-left: 0; +} + +.solr-pagination.pagination { + display: inline-block; + float: none; + margin: 12px 0; + padding: 0 0 1em; + height: auto; + width: auto; +} +.solr-pagination ul.pagination-list { + height: auto; + margin: auto; + padding: 0; + list-style: none; + cursor: default; +} +.solr-pagination li.pagination-range { + height: auto; + line-height: initial; + display: initial; + margin-right: 2px; +} +.solr-pagination li.pagination-range a, +.solr-pagination li.pagination-range span { + display: inline-block; + margin-right: 0px; + padding: 0.375em 9px; + vertical-align: top; +} +.solr-pagination li.pagination-range a { + border: 1px solid #000000; + text-decoration: none; +} +.solr-pagination li.pagination-range span { + border: 1px solid transparent; +} +.solr-pagination li.pagination-range a:hover { + background-color: #CCCCCC; + color: #000; + text-decoration: none; +} +.solr-pagination li.pagination-range.active a { + background-color: #DDDDDD; +} + +ul.solr { + list-style: inside none disc; +} +ol.solr { + list-style: inside none decimal; +} +ul.solr ul, +ol.solr ul { + list-style: inside none circle; + margin-left: 15px; +} +ol.solr ol, +ul.solr ol { + list-style: inside none lower-latin; + margin-left: 15px; +} +ul.solr li { + margin-top: 12px; + margin-bottom: 12px; +} + +/* Use flexbox when there is a thumbnail. */ +.solr-record { + display: flex; + gap: 12px; + align-items: center; +} +.solr-record a { + display: inline-flex; + align-items: center; +} +.solr-record a img { + height: auto; + width: 36px; + margin-right: 6px; +} + +/* Recursive list */ +.list-unstyled { + padding-left: 0; + list-style: none; +} +.recursive-list .resource-link { + display: inline-flex; + align-items: center; + max-width: 100%; + min-width: 0; +} +.recursive-list .resource-link img { + height: 36px; + margin-right: 6px; +} +.recursive-list .resource-link .resource-name { + flex: 1; + min-width: 0; +} +.recursive-list .see-all { + font-style: italic; + list-style: none; +} + +/* Grid */ +.solr-grid { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 12px; +} +.solr-grid .reference-record a { + display: flex; + flex-direction: column; + align-items: initial; +} +.solr-grid .reference-record a img { + height: auto; + width: 200px; +} diff --git a/asset/js/glossaire-form.js b/asset/js/glossaire-form.js new file mode 100644 index 0000000..8523b84 --- /dev/null +++ b/asset/js/glossaire-form.js @@ -0,0 +1,106 @@ +/** + * Dependent selects for Omeka-S Glossaire block + * + * - #o:index_id : the select of search index id + * - #search_field : the select of search field + * - availableFields is injected from PHP as window.availableFields + */ + +(function ($) { + + // Registry to avoid initializing the same block twice + const initializedBlocks = new WeakSet(); + + function initializeGlossaireForm() { + + // Find ALL index selects (multiple blocks possible) + const indexSelects = document.querySelectorAll('[name$="[o\\:data][o\\:index_id]"]'); + + indexSelects.forEach(indexSelect => { + + const block = indexSelect.closest('.block'); // real Omeka block wrapper + + if (!block) return; + + // Skip already initialized blocks + if (initializedBlocks.has(block)) return; + initializedBlocks.add(block); + + // Find the field select in the same block + const fieldSelect = block.querySelector('[name$="[o\\:data][search_field]"]'); + const resourceClassFieldSelect = block.querySelector('[name$="[o\\:data][resource_class_field]"]'); + const languageFieldSelect = block.querySelector('[name$="[o\\:data][language_field]"]'); + + if (!fieldSelect) { + console.warn('Glossaire: field select not found, skipping block.'); + return; + } + + // ------------------------------- + // Update field options + // ------------------------------- + function updateFieldOptions(indexId) { + const facetFields = window.availableFacetFields[indexId] || {}; + const searchFields = window.availableSearchFields[indexId] || {}; + const sortFields = window.availableSortFields[indexId] || {}; + + fieldSelect.innerHTML = ''; + resourceClassFieldSelect.innerHTML = ''; + languageFieldSelect.innerHTML = ''; + + const opt2 = document.createElement('option'); + opt2.value = ''; + opt2.textContent = 'None'; + resourceClassFieldSelect.appendChild(opt2); + + const opt3 = document.createElement('option'); + opt3.value = ''; + opt3.textContent = 'None'; + languageFieldSelect.appendChild(opt3); + + + + Object.entries(facetFields).forEach(([value, label]) => { + const opt = document.createElement('option'); + opt.value = value; + opt.textContent = label; + fieldSelect.appendChild(opt); + + const opt2 = document.createElement('option'); + opt2.value = value; + opt2.textContent = label; + resourceClassFieldSelect.appendChild(opt2); + + const opt3 = document.createElement('option'); + opt3.value = value; + opt3.textContent = label; + languageFieldSelect.appendChild(opt3); + }); + + + } + + // Bind change event ONCE + indexSelect.addEventListener('change', function () { + updateFieldOptions(this.value); + }); + }); + } + + // ---------------------------------------- + // Called when a block is added + // ---------------------------------------- + $(document).on('o:block-added', function (event) { + + initializeGlossaireForm(); + }); + + // ---------------------------------------- + // Called on page load (existing blocks) + // ---------------------------------------- + $(document).ready(function () { + + initializeGlossaireForm(); + }); + +})(jQuery); \ No newline at end of file diff --git a/config/module.config.php b/config/module.config.php index 3181ab7..ab3746a 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -204,10 +204,17 @@ 'Solr\Form\Admin\SolrNodeForm' => Service\Form\SolrNodeFormFactory::class, 'Solr\Form\Admin\SolrMappingForm' => Service\Form\SolrMappingFormFactory::class, 'Solr\Form\Admin\SolrSearchFieldForm' => Service\Form\SolrSearchFieldFormFactory::class, + 'Solr\Form\Admin\GlossrForm' => Service\Form\GlossrFormFactory::class, ], 'invokables' => [ 'Solr\Form\Admin\SolrMappingImportForm' => Form\Admin\SolrMappingImportForm::class, 'Solr\Form\Element\Transformations' => Form\Element\Transformations::class, + 'Solr\Form\Element\OptionalMulticheckbox' => Form\Element\OptionalMulticheckbox::class, + ], + ], + 'block_layouts' => [ + 'factories' => [ + 'glossr' => Service\Site\BlockLayout\GlossrFactory::class, ], ], 'router' => [ @@ -235,6 +242,7 @@ ], 'invokables' => [ 'solrFormTransformations' => Form\View\Helper\FormTransformations::class, + 'glossrFacetLink' => View\Helper\GlossrFacetLink::class, ], ], 'view_manager' => [ diff --git a/src/Api/Adapter/SolrSearchFieldAdapter.php b/src/Api/Adapter/SolrSearchFieldAdapter.php index d32064e..24e94ef 100644 --- a/src/Api/Adapter/SolrSearchFieldAdapter.php +++ b/src/Api/Adapter/SolrSearchFieldAdapter.php @@ -128,30 +128,51 @@ public function buildQuery(QueryBuilder $qb, array $query) if (isset($query['facetable'])) { if ($query['facetable']) { - $qb->andWhere($qb->expr()->isNotNull('omeka_root.facetField')); + $qb->andWhere($qb->expr()->andX( + $qb->expr()->isNotNull('omeka_root.facetField'), + $qb->expr()->not($qb->expr()->eq('omeka_root.facetField', '\'\'')))); } else { - $qb->andWhere($qb->expr()->isNull('omeka_root.facetField')); + $qb->andWhere($qb->expr()->orX( + $qb->expr()->isNull('omeka_root.facetField'), + $qb->expr()->eq('omeka_root.facetField', '\'\''))); } } if (isset($query['sortable'])) { if ($query['sortable']) { - $qb->andWhere($qb->expr()->isNotNull('omeka_root.sortField')); + $qb->andWhere($qb->expr()->andX( + $qb->expr()->isNotNull('omeka_root.sortField'), + $qb->expr()->not($qb->expr()->eq('omeka_root.sortField', '\'\'')) + )); } else { - $qb->andWhere($qb->expr()->isNull('omeka_root.sortField')); + $qb->andWhere($qb->expr()->orX( + $qb->expr()->isNull('omeka_root.sortField'), + $qb->expr()->eq('omeka_root.sortFields', '\'\'')) + ); } } if (isset($query['searchable'])) { if ($query['searchable']) { $qb->andWhere($qb->expr()->orX( - $qb->expr()->isNotNull('omeka_root.textFields'), - $qb->expr()->isNotNull('omeka_root.stringFields') + $qb->expr()->andX( + $qb->expr()->isNotNull('omeka_root.textFields'), + $qb->expr()->not($qb->expr()->eq('omeka_root.textFields', '\'\'')) + ), + $qb->expr()->andX( + $qb->expr()->isNotNull('omeka_root.stringFields'), + $qb->expr()->not($qb->expr()->eq('omeka_root.stringFields', '\'\'')) + ) )); } else { $qb->andWhere($qb->expr()->andX( - $qb->expr()->isNull('omeka_root.textFields'), - $qb->expr()->isNull('omeka_root.stringFields') + $qb->expr()->orX( + $qb->expr()->isNull('omeka_root.textFields'), + $qb->expr()->eq('omeka_root.textFields', '\'\'')), + $qb->expr()->orX( + $qb->expr()->isNull('omeka_root.stringFields'), + $qb->expr()->eq('omeka_root.stringFields', '\'\'') + ) )); } } diff --git a/src/Form/Admin/GlossrForm.php b/src/Form/Admin/GlossrForm.php new file mode 100644 index 0000000..cf9534d --- /dev/null +++ b/src/Form/Admin/GlossrForm.php @@ -0,0 +1,307 @@ +add([ + 'name' => 'o:block[__blockIndex__][o:data][o:index_id]', + 'type' => 'Select', + 'options' => [ + 'label' => 'Index', // @translate + 'value_options' => $this->getIndexesOptions(), + ], + 'attributes' => [ + 'required' => true, + ], + ]); + + $pagesAux = $this->getApiManager()->search('search_pages')->getContent(); + $pagesValueOptions = []; + foreach ($pagesAux as $page) { + $pagesValueOptions[strval($page->id())] = sprintf('%s (%s)', $page->name(), $page->siteUrl($this->getOption('site-slug'))); + } + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][search_page]', + 'type' => \Laminas\Form\Element\Select::class, + 'options' => [ + 'label' => 'Search page to use', // @translate + 'value_options' => $pagesValueOptions, + ], + 'attributes' => [ + 'required' => false, + ], + ]); + + $indexesAux = $this->getApiManager()->search('search_indexes')->getContent(); + $allowedFacetable = []; + $allowedSearchable = []; + $allowedSortable = []; + $valueOptionsFacetable = []; + $valueOptionsSearchable = []; + $valueOptionsSortable = []; + + foreach ($indexesAux as $index) { + $facetFields = $index->adapter()->getAvailableFacetFields($index); + $searchFields = $index->adapter()->getAvailableSearchFields($index); + $sortFields = $index->adapter()->getAvailableSortFields($index); + $allowedFacetable[($index->id())] = array_column($facetFields, 'name'); + $allowedSearchable[($index->id())] = array_column($searchFields, 'name'); + $allowedSearchable[($index->id())] = array_column($sortFields, 'name'); + + if ( + !empty($this->getOption('o:index_id')) + && is_numeric($this->getOption('o:index_id')) + && ($index->id() == intval($this->getOption('o:index_id'))) + ) { + $valueOptionsFacetable = array_column($facetFields, 'label', 'name'); + $valueOptionsSearchable = array_column($searchFields, 'label', 'name'); + $valueOptionsSortable = array_column($sortFields, 'label', 'name'); + } + } + + if ($valueOptionsFacetable) { + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][search_field]', + 'required' => true, + 'type' => \Laminas\Form\Element\Select::class, + 'options' => [ + 'label' => 'Search fields', // @translate + 'value_options' => $valueOptionsFacetable, + ], + 'validators' => [ + [ + 'name' => \Laminas\Validator\Callback::class, + 'options' => [ + 'callback' => function ($value, $context) use ($allowedFacetable) { + $type = $context['o:index_id'] ?? null; + + return $type + && isset($allowedFacetable[$type]) + && in_array($value, $allowedFacetable[$type]); + }, + 'message' => 'Incompatible field with selected index.', // @translate + ], + ], + ], + ]); + } else { + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][search_field]', + 'required' => true, + 'type' => \Laminas\Form\Element\Select::class, + 'options' => [ + 'label' => 'Search fields', // @translate + 'empty_option' => 'Add a search field', // @translate + 'value_options' => $valueOptionsFacetable, + ], + 'validators' => [ + [ + 'name' => \Laminas\Validator\Callback::class, + 'options' => [ + 'callback' => function ($value, $context) use ($allowedFacetable) { + $type = $context['o:index_id'] ?? null; + + return $type + && isset($allowedFacetable[$type]) + && in_array($value, $allowedFacetable[$type]); + }, + 'message' => 'Incompatible field with selected index.', // @translate + ], + ], + ], + ]); + } + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][resource_class_field]', + 'required' => true, + 'type' => \Laminas\Form\Element\Select::class, + 'options' => [ + 'label' => 'Resource class field', // @translate + 'empty_option' => 'None', // @translate + 'value_options' => $valueOptionsFacetable, + ], + 'validators' => [ + [ + 'name' => \Laminas\Validator\Callback::class, + 'options' => [ + 'callback' => function ($value, $context) use ($allowedFacetable) { + $type = $context['o:index_id'] ?? null; + + return $type + && isset($allowedFacetable[$type]) + && in_array($value, $allowedFacetable[$type]); + }, + 'message' => 'Incompatible field with selected index.', // @translate + ], + ], + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][resource_class]', + 'type' => \Omeka\Form\Element\ResourceClassSelect::class, + 'options' => [ + 'label' => 'Resource classes', // @translate + 'term_as_value' => true, + ], + 'attributes' => [ + 'required' => false, + 'class' => 'chosen-select', + 'multiple' => 'multiple', + 'data-placeholder' => 'Select resource classes…', // @translate + 'data-fieldset' => 'args', + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][language_field]', + 'required' => true, + 'type' => \Laminas\Form\Element\Select::class, + 'options' => [ + 'label' => 'Language field', // @translate + 'empty_option' => 'None', // @translate + 'value_options' => $valueOptionsFacetable, + ], + 'validators' => [ + [ + 'name' => \Laminas\Validator\Callback::class, + 'options' => [ + 'callback' => function ($value, $context) use ($allowedFacetable) { + $type = $context['o:index_id'] ?? null; + + return $type + && isset($allowedFacetable[$type]) + && in_array($value, $allowedFacetable[$type]); + }, + 'message' => 'Incompatible field with selected index.', // @translate + ], + ], + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][language]', + 'type' => 'text', + 'options' => [ + 'label' => 'Language', // @translate + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][custom_query]', + 'type' => 'text', + 'options' => [ + 'label' => 'Custom query parameters', // @translate + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][letters_list_position]', + 'type' => OptionalMultiCheckbox::class, + 'options' => [ + 'label' => 'Position of index of letters', // @translate + 'value_options' => [ + 'before' => 'Before', // @translate + 'after' => 'After', // @translate + ], + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][display_letters]', + 'type' => OptionalMultiCheckbox::class, + 'options' => [ + 'label' => 'Display letters between results?', // @translate + 'value_options' => [ + 'yes' => 'Display letters', // @translate + ], + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][display_total]', + 'type' => OptionalMultiCheckbox::class, + 'options' => [ + 'label' => 'Display total between results?', // @translate + 'value_options' => [ + 'yes' => 'Display total', // @translate + ], + ], + ]); + + $this->add([ + 'name' => 'o:block[__blockIndex__][o:data][sort_by]', + 'type' => \Laminas\Form\Element\Select::class, + 'options' => [ + 'label' => 'Ascending or descending order for order of letters of the Glossr', // @translate + 'value_options' => [ + 'asc' => 'Ascending', // @translate + 'desc' => 'Descending', // @translate + ], + ], + ]); + } + + protected function getIndexesOptions() + { + $api = $this->getApiManager(); + + $indexes = $api->search('search_indexes')->getContent(); + $options = [ + '' => 'None', // @translate + ]; + foreach ($indexes as $index) { + $options[$index->id()] = + sprintf('%s (%s)', $index->name(), $index->adapterLabel()); + } + + return $options; + } + + public function setApiManager($apiManager) + { + $this->apiManager = $apiManager; + } + + public function getApiManager() + { + return $this->apiManager; + } +} diff --git a/src/Form/Element/OptionalMultiCheckbox.php b/src/Form/Element/OptionalMultiCheckbox.php new file mode 100644 index 0000000..51ac64b --- /dev/null +++ b/src/Form/Element/OptionalMultiCheckbox.php @@ -0,0 +1,21 @@ +attributes['required']); + return $inputSpecification; + } +} diff --git a/src/Querier.php b/src/Querier.php index 86223a6..d418649 100644 --- a/src/Querier.php +++ b/src/Querier.php @@ -355,6 +355,346 @@ public function query(Query $query) return $response; } + public function glossaire($indexId, $siteRep, $field, + $resourceClassField = null, $resourceClasses = null, $languageField = null, $languages = null, $customQuery = null, + $sortBy = null, $sortOrder = null, $dateField = null) + { + $serviceLocator = $this->getServiceLocator(); + $settings = $serviceLocator->get('Omeka\Settings'); + $api = $serviceLocator->get('Omeka\ApiManager'); + $logger = $serviceLocator->get('Omeka\Logger'); + $eventManager = $serviceLocator->get('EventManager'); + + $client = $this->getClient(); + + $solrNode = $this->getSolrNode(); + $solrNodeSettings = $solrNode->settings(); + $resource_name_field = $solrNodeSettings['resource_name_field']; + $sites_field = $solrNodeSettings['sites_field']; + $is_public_field = $solrNodeSettings['is_public_field']; + $has_media_field = $solrNodeSettings['has_media_field']; + $highlightSettings = $solrNodeSettings['highlight'] ?? []; + $highlighting = $highlightSettings['highlighting'] ?? false; + $highlightQueryParts = []; + + $solrQuery = new SolrQuery; + $solrQuery->setParam('defType', 'edismax'); + + if (!empty($solrNodeSettings['qf'])) { + $solrQuery->setParam('qf', $solrNodeSettings['qf']); + } + + if (!empty($solrNodeSettings['mm'])) { + $solrQuery->setParam('mm', $solrNodeSettings['mm']); + } + + $uf = []; + $searchFields = $this->getSearchFields(); + foreach ($searchFields as $name => $searchField) { + $textFields = $searchField->textFields(); + if (!empty($textFields)) { + $paramName = sprintf('f.%s.qf', $name); + $solrQuery->setParam($paramName, $textFields); + $uf[] = $name; + } + + $facetField = $searchField->facetField(); + if (!empty($facetField)) { + $searchFieldMapByFacetField[$facetField] = $searchField; + } + } + + $response = $api->read('search_indexes', $indexId); + $index = $response->getContent(); + + $query = new Query(); + if (empty($customQuery)) { + $q = []; + + if (!empty($languageField) && !empty($languages)) { + $languagesArray = explode('|', $languages); + + foreach ($languagesArray as $language) { + $query->addFacetFilter($languageField, $languages); + } + } + + if (!empty($resourceClassField) && !empty($resourceClasses)) { + foreach ($resourceClasses as $resourceClass) { + $query->addFacetFilter($resourceClassField, $resourceClass); + } + } + + $indexSettings = $index->settings(); + $query->setResources($indexSettings['resources']); + + $query->setSite($siteRep); + + $q = $this->getQueryStringFromSearchQuery($q); + + if (empty($q)) { + $q = '*:*'; + } + + $solrQuery->setQuery($q); + $solrQuery->addField('id'); + + $solrQuery->setGroup(true); + $solrQuery->addGroupField($resource_name_field); + + $resources = $query->getResources(); + $fq = sprintf('%s:(%s)', $resource_name_field, implode(' OR ', $resources)); + $solrQuery->addFilterQuery($fq); + + $site = $query->getSite(); + if (isset($site)) { + $fq = sprintf('%s:%d', $sites_field, $site->id()); + $solrQuery->addFilterQuery($fq); + } + + $query->setFacetLimit(-1); + $query->addFacetField($field); + $facetFields = $query->getFacetFields(); + if (!empty($facetFields)) { + foreach ($facetFields as $facetField) { + $searchField = $this->getSearchField($facetField); + if (!$searchField) { + throw new QuerierException(sprintf('Field %s does not exist', $facetField)); + } + $solrFacetField = $searchField->facetField(); + if (!$solrFacetField) { + throw new QuerierException(sprintf('Field %s is not facetable', $facetField)); + } + + $solrQuery->addFacetField($solrFacetField); + } + } + + $solrQuery->setGroupLimit(-1); + + $facetSorts = $query->getFacetSorts(); + foreach ($facetSorts as $field => $sort) { + $searchField = $this->getSearchField($field); + $facetField = $searchField->facetField(); + $solrQuery->setParam("facet.sort.$facetField", $sort); + } + + $facetFilters = $query->getFacetFilters(); + if (!empty($facetFilters)) { + foreach ($facetFilters as $name => $values) { + $values = array_filter($values); + foreach ($values as $value) { + if (is_array($value)) { + $value = array_filter($value); + if (empty($value)) { + continue; + } + + $value = '(' . implode(' OR ', array_map([$this, 'enclose'], $value)) . ')'; + } else { + $value = $this->enclose($value); + } + + $searchField = $this->getSearchField($name); + if (!$searchField) { + throw new QuerierException(sprintf('Field %s does not exist', $name)); + } + $solrFacetField = $searchField->facetField(); + if (!$solrFacetField) { + throw new QuerierException(sprintf('Field %s is not facetable', $name)); + } + + $solrQuery->addFilterQuery(sprintf('%s:%s', $solrFacetField, $value)); + } + } + } + } else { + $q = $customQuery->getQuery(); + + if (!empty($languageField) && !empty($languages)) { + $languagesArray = explode('|', $languages); + + foreach ($languagesArray as $language) { + $customQuery->addFacetFilter($languageField, $languages); + } + } + + if (!empty($resourceClassField) && !empty($resourceClasses)) { + foreach ($resourceClasses as $resourceClass) { + $customQuery->addFacetFilter($resourceClassField, $resourceClass); + } + } + + $q = $this->getQueryStringFromSearchQuery($q); + + if (empty($q)) { + $q = '*:*'; + } + + $indexSettings = $index->settings(); + $query->setResources($indexSettings['resources']); + + $query->setSite($siteRep); + + $solrQuery->setQuery($q); + $solrQuery->addField('id'); + + $solrQuery->setGroup(true); + $solrQuery->addGroupField($resource_name_field); + + $isPublic = $customQuery->getIsPublic(); + if (isset($isPublic)) { + $fq = sprintf('%s:%s', $is_public_field, $isPublic ? 'true' : 'false'); + $solrQuery->addFilterQuery($fq); + } + + $hasMedia = $customQuery->getHasMedia(); + if (isset($hasMedia)) { + $fq = sprintf('%s:%s', $has_media_field, $hasMedia ? 'true' : 'false'); + $solrQuery->addFilterQuery($fq); + } + + $query->addFacetField($field); + $facetFields = $query->getFacetFields(); + if (!empty($facetFields)) { + foreach ($facetFields as $facetField) { + $searchField = $this->getSearchField($facetField); + if (!$searchField) { + throw new QuerierException(sprintf('Field %s does not exist', $facetField)); + } + $solrFacetField = $searchField->facetField(); + if (!$solrFacetField) { + throw new QuerierException(sprintf('Field %s is not facetable', $facetField)); + } + + $solrQuery->addFacetField($solrFacetField); + } + } + + $solrQuery->setGroupLimit(-1); + + $facetFilters = $customQuery->getFacetFilters(); + if (!empty($facetFilters)) { + foreach ($facetFilters as $name => $values) { + $values = array_filter($values); + foreach ($values as $value) { + if (is_array($value)) { + $value = array_filter($value); + if (empty($value)) { + continue; + } + + $value = '(' . implode(' OR ', array_map([$this, 'enclose'], $value)) . ')'; + } else { + $value = $this->enclose($value); + } + + $searchField = $this->getSearchField($name); + if (!$searchField) { + throw new QuerierException(sprintf('Field %s does not exist', $name)); + } + $solrFacetField = $searchField->facetField(); + if (!$solrFacetField) { + throw new QuerierException(sprintf('Field %s is not facetable', $name)); + } + + $solrQuery->addFilterQuery(sprintf('%s:%s', $solrFacetField, $value)); + } + } + } + + $queryFilters = $customQuery->getQueryFilters(); + foreach ($queryFilters as $queryFilter) { + $fq = $this->getQueryStringFromSearchQuery($queryFilter); + if (!empty($fq)) { + $solrQuery->addFilterQuery($fq); + $highlightQueryParts[] = $fq; + } + } + + $dateRangeFilters = $customQuery->getDateRangeFilters(); + foreach ($dateRangeFilters as $name => $filterValues) { + foreach ($filterValues as $filterValue) { + $start = $filterValue['start'] ? $filterValue['start'] : '*'; + $end = $filterValue['end'] ? $filterValue['end'] : '*'; + $solrQuery->addFilterQuery("$name:[$start TO $end]"); + } + } + + $sort = $customQuery->getSort(); + if (isset($sort)) { + [$sortField, $sortOrder] = explode(' ', $sort); + + if ($sortField !== 'score') { + $searchField = $this->getSearchField($sortField); + if (!$searchField) { + throw new QuerierException(sprintf('Field %s does not exist', $sortField)); + } + $solrSortField = $searchField->sortField(); + if (!$solrSortField) { + throw new QuerierException(sprintf('Field %s is not sortable', $sortField)); + } + $sortField = $solrSortField; + } + + $solrQuery->addSortField($sortField, $sortOrder); + } + + if ($limit = $customQuery->getLimit()) { + $solrQuery->setGroupLimit($limit); + } + + if ($offset = $customQuery->getOffset()) { + $solrQuery->setGroupOffset($offset); + } + + $facetSorts = $customQuery->getFacetSorts(); + foreach ($facetSorts as $field => $sort) { + $searchField = $this->getSearchField($field); + $facetField = $searchField->facetField(); + $solrQuery->setParam("facet.sort.$facetField", $sort); + } + } + + $eventManager->setIdentifiers(['Solr\Querier']); + $eventManager->trigger('solr.query', $solrQuery, ['query' => $query, 'solrNode' => $solrNode]); + + try { + $logger->debug(sprintf('Solr query params: %s', json_encode($solrQuery))); + $solrQueryResponse = $client->query($solrQuery); + } catch (\Exception $e) { + throw new QuerierException($e->getMessage(), $e->getCode(), $e); + } + $solrResponse = $solrQueryResponse->getResponse(); + + $response = new Response; + $response->setTotalResults($solrResponse['grouped'][$resource_name_field]['matches']); + foreach ($solrResponse['grouped'][$resource_name_field]['groups'] as $group) { + $response->setResourceTotalResults($group['groupValue'], $group['doclist']['numFound']); + foreach ($group['doclist']['docs'] as $doc) { + [, $resourceId] = explode(':', $doc['id']); + $response->addResult($group['groupValue'], ['id' => $resourceId]); + } + } + + if (!empty($solrResponse['facets'])) { + foreach ($solrResponse['facets'] as $name => $facetData) { + if ($name === 'count') { + continue; + } + + foreach ($facetData['buckets'] as $bucket) { + if ($bucket['count'] > 0) { + $searchField = $searchFieldMapByFacetField[$name]; + $response->addFacetCount($searchField->name(), $bucket['val'], $bucket['count']); + } + } + } + } + + return $response; + } + protected function enclose($value) { return '"' . addcslashes($value, '"') . '"'; diff --git a/src/Service/Form/GlossrFormFactory.php b/src/Service/Form/GlossrFormFactory.php new file mode 100644 index 0000000..a6b51b8 --- /dev/null +++ b/src/Service/Form/GlossrFormFactory.php @@ -0,0 +1,16 @@ +setApiManager($services->get('Omeka\ApiManager')); + return $form; + } +} diff --git a/src/Service/Site/BlockLayout/GlossrFactory.php b/src/Service/Site/BlockLayout/GlossrFactory.php new file mode 100644 index 0000000..32f18d0 --- /dev/null +++ b/src/Service/Site/BlockLayout/GlossrFactory.php @@ -0,0 +1,14 @@ +get('FormElementManager'), $services->get('Omeka\ApiManager'), $services->get('Omeka\Logger')); + } +} diff --git a/src/Site/BlockLayout/Glossr.php b/src/Site/BlockLayout/Glossr.php new file mode 100644 index 0000000..8a31e61 --- /dev/null +++ b/src/Site/BlockLayout/Glossr.php @@ -0,0 +1,277 @@ +formElementManager = $formElementManager; + $this->setApiManager($apiManager); + $this->logger = $logger; + } + + public function getLabel() + { + return 'Glossr'; // @translate + } + + public function form(PhpRenderer $view, SiteRepresentation $site, SitePageRepresentation $page = null, SitePageBlockRepresentation $block = null) + { + $defaults = [ + 'o:index_id' => '', + 'search_page' => '', + 'search_field' => '', + 'resource_class_field' => '', + 'resource_class' => '', + 'language_field' => '', + 'language' => '', + 'custom_query' => '', + 'letters_list_position' => ['before', 'after'], + 'sort_by' => 'alphabetic', + 'sort_order' => 'asc', + 'display_letters' => [], + 'display_total' => [], + + ]; + + $data = $block ? $block->data() + $defaults : $defaults; + + $siteSlug = $block ? $block->page()->site()->slug() : $site->slug(); + $form = $this->formElementManager->get(GlossrForm::class, ['o:index_id' => $data['o:index_id'], 'site-slug' => $siteSlug]); + + $form->setData([ + 'o:block[__blockIndex__][o:data][o:index_id]' => $data['o:index_id'], + 'o:block[__blockIndex__][o:data][search_page]' => $data['search_page'], + 'o:block[__blockIndex__][o:data][search_field]' => $data['search_field'], + 'o:block[__blockIndex__][o:data][resource_class_field]' => $data['resource_class_field'], + 'o:block[__blockIndex__][o:data][resource_class]' => $data['resource_class'], + 'o:block[__blockIndex__][o:data][language_field]' => $data['language_field'], + 'o:block[__blockIndex__][o:data][language]' => $data['language'], + 'o:block[__blockIndex__][o:data][letters_list_position]' => $data['letters_list_position'], + 'o:block[__blockIndex__][o:data][sort_order]' => $data['sort_order'], + 'o:block[__blockIndex__][o:data][sort_by]' => $data['sort_by'], + 'o:block[__blockIndex__][o:data][display_letters]' => $data['display_letters'], + 'o:block[__blockIndex__][o:data][display_total]' => $data['display_total'], + 'o:block[__blockIndex__][o:data][custom_query]' => $data['custom_query'], + + ]); + + return $view->formCollection($form); + } + + public function setApiManager($apiManager) + { + $this->apiManager = $apiManager; + } + + public function getApiManager() + { + return $this->apiManager; + } + + public function prepareForm(PhpRenderer $view) + { + $indexesAux = $this->getApiManager()->search('search_indexes')->getContent(); + + $indexesSearch = []; + $indexesFacet = []; + $indexesSort = []; + foreach ($indexesAux as $index) { + $searchFields = $index->adapter()->getAvailableSearchFields($index); + $facetFields = $index->adapter()->getAvailableFacetFields($index); + $sortFields = $index->adapter()->getAvailableSortFields($index); + $indexesSearch[($index->id())] = array_column($searchFields, 'label', 'name'); + $indexesFacet[($index->id())] = array_column($facetFields, 'label', 'name'); + $indexesSort[($index->id())] = array_column($sortFields, 'label', 'name'); + } + + $view->headScript()->appendScript( + 'window.availableSearchFields = ' . json_encode($indexesSearch, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ';' + ); + + $view->headScript()->appendScript( + 'window.availableFacetFields = ' . json_encode($indexesFacet, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ';' + ); + + $view->headScript()->appendScript( + 'window.availableSortFields = ' . json_encode($indexesSort, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ';' + ); + + $view->headScript()->appendFile($view->assetUrl('js/glossaire-form.js', 'Solr')); + } + + public function render(PhpRenderer $view, SitePageBlockRepresentation $block) + { + $indexId = $block->dataValue('o:index_id'); + $pageId = $block->dataValue('search_page'); + $customQueryInput = $block->dataValue('custom_query'); + $fieldName = $block->dataValue('search_field'); + $resourceClassFieldName = $block->dataValue('resource_class_field'); + $resourceClasses = $block->dataValue('resource_class'); + $languageFieldName = $block->dataValue('language_field'); + $languages = $block->dataValue('language'); + + $sortBy = $block->dataValue('sort_order') ?: 'alphabetic'; // 'alphabetic', 'total' + $sortOrder = $block->dataValue('sort_by') ?: 'asc'; // 'asc', 'desc' + + try { + $indexResponse = $this->apiManager->read('search_indexes', $indexId)->getContent(); + } catch (\Exception $e) { + $view->messenger()->addError(sprintf('Index with id %s not found.', $indexId)); + return ''; + } + + try { + $page = $this->apiManager->read('search_pages', $pageId)->getContent(); + } catch (\Exception $e) { + $view->messenger()->addError(sprintf('Page with id %s not found.', $pageId)); + return ''; + } + + $site = $view->currentSite(); + + $customQuery = []; + if (!empty($customQueryInput)) { + parse_str($customQueryInput, $customQuery); + } + + $formAdapter = $page->formAdapter(); + if (!$formAdapter) { + $formAdapterName = $page->formAdapterName(); + $view->messenger()->addError(sprintf("Form adapter '%s' not found", $formAdapterName)); + return ''; + } + + $searchPageSettings = $page->settings(); + $searchFormSettings = []; + if (isset($searchPageSettings['form'])) { + $searchFormSettings = $searchPageSettings['form']; + } + + $query = $formAdapter->toQuery($customQuery, $searchFormSettings); + $querier = $indexResponse->querier(); + + $response = null; + $facets = []; + + $letters = range('a', 'z'); + try { + $response = $querier->glossaire( + $indexId, + $site, + $fieldName, + $resourceClassFieldName, + $resourceClasses, + $languageFieldName, + $languages, + $query + ); + if (array_key_exists($fieldName, $response->getFacetCounts())) { + $facets[$fieldName] = $response->getFacetCounts()[$fieldName]; + } + } catch (QuerierException $e) { + $view->messenger()->addError('An error occurred while executing the search query.'); + return ''; + } + + $facetLetter = []; + if (array_key_exists($fieldName, $response->getFacetCounts())) { + foreach ($letters as $letter) { + $facetLetter[$letter] = []; + foreach ($facets[$fieldName] as $facetValue) { + if (str_starts_with(strtolower($facetValue['value']), $letter)) { + $facetLetter[$letter][] = $facetValue; + } + } + + if (!empty($facetLetter[$letter])) { + $this->sortFacetsForLetter($facetLetter[$letter], $sortBy, $sortOrder); + } + } + } + + $lettersPosition = is_array($block->dataValue('letters_list_position')) ? + $block->dataValue('letters_list_position') + : [$block->dataValue('letters_list_position')]; + + $lettersBetweenResults = is_array($block->dataValue('display_letters')) ? + $block->dataValue('display_letters') + : [$block->dataValue('display_letters')]; + + $totalBetweenResults = is_array($block->dataValue('display_total')) ? + $block->dataValue('display_total') + : [$block->dataValue('display_total')]; + + return $view->partial('solr/block-layout/glossaire', [ + 'site' => $site, + 'response' => $response, + 'letters' => $letters, + 'lettersListPosition' => $lettersPosition, + 'lettersBetweenResults' => $lettersBetweenResults, + 'totalBetweenResults' => $totalBetweenResults, + 'facetLetter' => $facetLetter, + 'fieldName' => $fieldName, + 'searchPage' => $page, + 'siteSlug' => $site->slug(), + 'customQuery' => $customQuery, + 'languageField' => $languageFieldName, + 'languages' => $languages, + 'resourceClassField' => $resourceClassFieldName, + 'resourceClasses' => $resourceClasses, + ]); + } + + public function prepareRender(PhpRenderer $view): void + { + $view->headLink() + ->appendStylesheet($view->assetUrl('css/reference.css', 'Solr')); + } + + protected function sortFacetsForLetter(array &$facets, string $sortBy, string $sortOrder): void + { + if (empty($facets)) { + return; + } + + // Ensure we have valid sort parameters + $sortBy = $sortBy ?: 'alphabetic'; + $sortOrder = $sortOrder ?: 'asc'; + + switch ($sortBy) { + case 'alphabetic': + usort($facets, function ($a, $b) use ($sortOrder) { + $result = strcasecmp($a['value'], $b['value']); + return $sortOrder === 'desc' ? -$result : $result; + }); + break; + + case 'total': + usort($facets, function ($a, $b) use ($sortOrder) { + $countA = $a['count'] ?? 0; + $countB = $b['count'] ?? 0; + $result = $countB <=> $countA; + return $sortOrder === 'asc' ? -$result : $result; + }); + break; + + default: + break; + } + } +} diff --git a/src/SolrQuery.php b/src/SolrQuery.php index 3d9f31b..737f607 100644 --- a/src/SolrQuery.php +++ b/src/SolrQuery.php @@ -45,6 +45,12 @@ public function jsonSerialize(): mixed $data['facet'][$field]['limit'] = (int) $limit; unset($params["facet.limit.$field"]); } + + if (isset($params["facet.prefix.$field"])) { + $prefix = $params["facet.prefix.$field"]; + $data['facet'][$field]['prefix'] = $prefix; + unset($params["facet.prefix.$field"]); + } } unset($params['facet.field']); } diff --git a/src/ValueExtractor/MediaValueExtractor.php b/src/ValueExtractor/MediaValueExtractor.php index adbed77..cbeefd6 100644 --- a/src/ValueExtractor/MediaValueExtractor.php +++ b/src/ValueExtractor/MediaValueExtractor.php @@ -67,9 +67,6 @@ public function getAvailableFields() 'resource_template' => [ 'label' => 'Resource template', ], - 'content' => [ - 'label' => 'HTML Content', - ], ]; $properties = $this->api->search('properties')->getContent(); diff --git a/src/View/Helper/GlossrFacetLink.php b/src/View/Helper/GlossrFacetLink.php new file mode 100644 index 0000000..2309585 --- /dev/null +++ b/src/View/Helper/GlossrFacetLink.php @@ -0,0 +1,46 @@ +getView(); + + $active = false; + if (isset($query['limit'][$name]) && false !== array_search($facet['value'], $query['limit'][$name])) { + $values = $query['limit'][$name]; + $values = array_filter($values, function ($v) use ($facet) { + return $v != $facet['value']; + }); + $query['limit'][$name] = $values; + $active = true; + } else { + $query['limit'][$name][] = $facet['value']; + } + + if (!empty($resourceClassField) && !empty($resourceClasses)) { + $query['limit'][$resourceClassField] = $resourceClasses; + } + + if (!empty($langueField) && !empty($languages)) { + $query['limit'][$languageField] = explode('|', $languages); + } + + unset($query['page']); + + $url = $view->url('search-page-' . $searchPage->id(), ['site-slug' => $siteSlug], ['query' => $query]); + + return $view->partial('solr/glossr-facet-link', [ + 'url' => $url, + 'active' => $active, + 'name' => $name, + 'value' => $facet['value'], + 'count' => $facet['count'], + ]); + } +} diff --git a/view/solr/block-layout/glossaire.phtml b/view/solr/block-layout/glossaire.phtml new file mode 100644 index 0000000..57c4545 --- /dev/null +++ b/view/solr/block-layout/glossaire.phtml @@ -0,0 +1,106 @@ + + + +plugin('escapeHtml'); +$user = $this->identity(); +?> + +

translate('Glossaire'); ?>

+ +messages(); ?> + + + + partial('solr/glossr-pagination', ['letters' => $letters, 'facetLetter' => $facetLetter]); ?> + + $letter): ?> + + 0): ?> + +

+ + + + + + ( translatePlural('result', 'results', count($facets)); ?>) + + translatePlural('result', 'results', count($facets)); ?> + + +

+ +
+
+ partial( + 'solr/glossr-facets', + [ + 'facetValues' => $facets, + 'fieldName' => $fieldName, + 'searchPage' => $searchPage, + 'siteSlug' => $siteSlug, + 'customQuery' => $customQuery, + 'resourceClassField' => $resourceClassField, + 'resourceClasses' => $resourceClasses, + 'languageField' => $languageField, + 'languages' => $languages, + ] + ); ?> +
+
+ + + + partial('solr/glossr-pagination', ['letters' => $letters, 'facetLetter' => $facetLetter]); ?> + + +

translate('Glossr is empty!'); ?>

+ \ No newline at end of file diff --git a/view/solr/glossr-facet-link.phtml b/view/solr/glossr-facet-link.phtml new file mode 100644 index 0000000..f4e6eab --- /dev/null +++ b/view/solr/glossr-facet-link.phtml @@ -0,0 +1,13 @@ + + + + () + diff --git a/view/solr/glossr-facets.phtml b/view/solr/glossr-facets.phtml new file mode 100644 index 0000000..c6cdca1 --- /dev/null +++ b/view/solr/glossr-facets.phtml @@ -0,0 +1,53 @@ + + + +
+
+ +
  • + glossrFacetLink($fieldName, $facetValue, $searchPage, $siteSlug, $customQuery, + $resourceClassField, $resourceClasses, $languageField, $languages); ?> +
  • + +
    +
    \ No newline at end of file diff --git a/view/solr/glossr-pagination.phtml b/view/solr/glossr-pagination.phtml new file mode 100644 index 0000000..7e4a2d8 --- /dev/null +++ b/view/solr/glossr-pagination.phtml @@ -0,0 +1,52 @@ + + + + + diff --git a/view/solr/glossr-resource-list.phtml b/view/solr/glossr-resource-list.phtml new file mode 100644 index 0000000..aab469e --- /dev/null +++ b/view/solr/glossr-resource-list.phtml @@ -0,0 +1,56 @@ + + + +getResults($resourceName); ?> + + +

    escapeHtml($title); ?>

    + +
    + + api()->read($resourceName, $result['id'])->getContent(); ?> + partial('solr/glossr-resource', [ + 'resource' => $resource, + 'site' => $site, + 'tag' => 'div', + ]); ?> + +
    + diff --git a/view/solr/glossr-resource.phtml b/view/solr/glossr-resource.phtml new file mode 100644 index 0000000..844e640 --- /dev/null +++ b/view/solr/glossr-resource.phtml @@ -0,0 +1,45 @@ + + + +< class="resourceName(); ?> resource"> +
    link($resource->displayTitle()); ?>
    + + displayDescription()): ?> +
    + +
    + +>