Index: main/plugin-odf-web/i18n/messages_en.xml =================================================================== --- main/plugin-odf-web/i18n/messages_en.xml (revision 33554) +++ main/plugin-odf-web/i18n/messages_en.xml (working copy) @@ -109,6 +109,7 @@ Choice lists only (as links) Classification of results by domain Default with a map for geolocation + Multiple-choice criteria Default Search fields Select the search fields you want to display. @@ -228,6 +229,9 @@ (unselect) Unselect the criterion See all... + » See all ... + « See less ... + No choice available Unavailable inside CMS This feature is not available inside the CMS.<br>You could test it in preview mode.<br><br>Submitting this form will display page Index: main/plugin-odf-web/i18n/messages_fr.xml =================================================================== --- main/plugin-odf-web/i18n/messages_fr.xml (revision 33554) +++ main/plugin-odf-web/i18n/messages_fr.xml (working copy) @@ -109,6 +109,7 @@ Listes à choix uniquement (sous forme de liens) Classification des résultats par domaine Par défaut avec carte de géolocalisation + Critères à choix multiple Par défaut Champs de la recherche Sélectionnez les champs de recherche que vous souhaitez afficher. @@ -228,6 +229,9 @@ Retirer Supprimer le critère » Tout voir ... + » Tout voir ... + « Voir moins ... + Aucun choix disponible Indisponible dans le CMS Cette fonctionnalité n'est pas disponible dans le CMS.<br>Vous pouvez la tester en mode prévisualisation.<br><br>La soumission de ce formulaire affichera la page Index: main/plugin-odf-web/pages/services/search/search-criteria/search-criteria-multiple.xsl =================================================================== --- main/plugin-odf-web/pages/services/search/search-criteria/search-criteria-multiple.xsl (revision 0) +++ main/plugin-odf-web/pages/services/search/search-criteria/search-criteria-multiple.xsl (revision 0) @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+
+ + + + +
+
+
+ + + + +
+
+
+
+
+ + + + + + + + + + +
+
+ +
+ +
+ + + + + + +
+
+ + + checked + + +
+
+ +
+
+
+
+
+
+
+
+
+ +
Index: main/plugin-odf-web/pages/services/search/search-multicriteria.xml =================================================================== --- main/plugin-odf-web/pages/services/search/search-multicriteria.xml (revision 0) +++ main/plugin-odf-web/pages/services/search/search-multicriteria.xml (revision 0) @@ -0,0 +1,19 @@ + + + + + Index: main/plugin-odf-web/pages/services/search/search-multicriteria.xsl =================================================================== --- main/plugin-odf-web/pages/services/search/search-multicriteria.xsl (revision 0) +++ main/plugin-odf-web/pages/services/search/search-multicriteria.xsl (revision 0) @@ -0,0 +1,24 @@ + + + + + + + + Index: main/plugin-odf-web/plugin.xml =================================================================== --- main/plugin-odf-web/plugin.xml (revision 33554) +++ main/plugin-odf-web/plugin.xml (working copy) @@ -431,6 +431,7 @@ pages/services/search/search-map.xsl pages/services/search/search-links_1.3.xsl pages/services/search/search-bydomain_1.3.xsl + pages/services/search/search-multicriteria.xsl Index: main/plugin-odf-web/resources/js/search/facet-checkboxes-multi.i18n.js =================================================================== --- main/plugin-odf-web/resources/js/search/facet-checkboxes-multi.i18n.js (revision 0) +++ main/plugin-odf-web/resources/js/search/facet-checkboxes-multi.i18n.js (revision 0) @@ -0,0 +1,398 @@ +/* + * Copyright 2015 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + if (window.setupFormForMultiFacets && $j.isFunction(window.setupFormForMultiFacets)) + { + // file already loaded + return; + } + + var _formsData = {}; + + // Add a 'onchange' listener on each checkbox when DOM is ready + // Used to update the facets when an item is selected or deselected; + // Same for the title and textfield input fields + $j(function() { + $j.each(_formsData, function(formId, formData) { + var $form = $j('#' + formId); + + initiliazeGroups($form); + handleFacetsVisibility($form); + + $form.find('.field .input .checkbox input').change(onFieldChange); + $form.find('#' + 'search-title-' + formData.uniqueId).change(onFieldChange); + $form.find('#' + 'search-textfield-' + formData.uniqueId).change(onFieldChange); + }); + }); + + function onFieldChange(evt) { + + var $this = $j(this), + $form = $this.closest('form'), + formId = $form.attr('id'); + + if ($this.is(':checkbox')) + { + var oldValue = $this.data('oldValue'), + newValue = $this.is(':checked'); + + // update old value + $this.data('oldValue', newValue); + + if (oldValue != null && oldValue == newValue) + { + // not a real change because the value has not changed, ignore it. + return; + } + } + + updateFacets(formId); + }; + + function updateFacets(formId) { + + var formData = _formsData[formId], + $form = $j('#' + formId), + params = getFormValues($form); + + // merge the form data params into the params object + $j.extend(params, formData.params); + + // server request + $j.ajax({ + type: "POST", + url: formData.url, + data: params, + traditional: true, // _do not remove_ otherwise parameter name with multiple values will not be handled as expected on the server + success: function(data) { + updateFacetsCb(data, $form); // binding the form as an argument to the callback (with a closure). + handleFacetsVisibility($form) + }, + dataType: 'xml' + }); + }; + + function getFormValues($form) + { + var valuesArray = $form.serializeArray(), + values = {}; + + $j.each(valuesArray, function() { + if (this.value !== undefined) + { + if (values[this.name] !== undefined) + { + values[this.name].push(this.value); + } + else + { + values[this.name] = [this.value]; + } + } + }); + + return values; + }; + + function updateFacetsCb(data, $form) + { + var uniqueId = _formsData[$form.attr('id')].uniqueId; + + // Iterates on list criteria + $j('> form > fields > criterion', data).each(function() { + var $this = $j(this), + criterionName = $this.attr('name'); + + $this.children('value').each(function() { + var $this = $j(this), + value = $this.attr('value'), + count = '(' + $this.attr('count') + ')', + $countEl = $form.find('div.field div.input div.checkbox label[for="search-' + criterionName + '-' + uniqueId + '-' + value + '"] span.facet-count'); + + if (count != $countEl.text()) + { + $countEl.text(count).hide(0).fadeIn('slow'); + } + }); + }); + }; + + function setupForm(uniqueId, url, siteName, lang, catalog, zoneItemId, maxEntryCount, hideZero) + { + var formId = 'search-program-' + uniqueId, + maxEntryCount = maxEntryCount || 0, + hideZero = hideZero === true; // false by default + + _formsData[formId] = { + url: url, + uniqueId: uniqueId, + maxEntryCount: maxEntryCount, + showHideFeatureEnabled: maxEntryCount > 0, + hideZero: hideZero, + groupsDisplayAllState: {}, + params: { + siteName: siteName, + lang: lang, + catalog: catalog, + zoneItemId: zoneItemId + } + }; + }; + + // exposing setupForm + window.setupFormForMultiFacets = setupForm; + + + // Hide facets if entry index is superior to maxEntryCount and if the entry is not selected + // Display a show all entry link + function handleFacetsVisibility($form) + { + var formId = $form.attr('id'), + formData = _formsData[formId], + showHideFeatureEnabled = formData.showHideFeatureEnabled, + maxEntryCount = formData.maxEntryCount, + hideZero = formData.hideZero, + $checkboxGroups = $form.find('.field .input:has(.checkbox)'); + + $j.each($checkboxGroups, function() { + + var $group = $j(this), + inDisplayAllState = isDisplayAllState($group, formData), + $checkboxes = $group.find('.checkbox'), + nonNullentriesAfterMaxCount = false, + atLeastOneVisible = false; + + if (showHideFeatureEnabled && $checkboxes.length > maxEntryCount) + { + $checkboxes = $checkboxes.sort(sortByCountDesc); + } + + $checkboxes.each(function(index) { + var $checkbox = $j(this), + hide = false, + show = false; + + if (!$checkbox.find('input:checked').prop('checked')) + { + if (hideZero && getCheckboxCount($checkbox) <= 0) + { + hide = true; + } + else if (showHideFeatureEnabled) + { + if (inDisplayAllState) + { + show = true; + atLeastOneVisible = true; + if (index >= maxEntryCount) + { + nonNullentriesAfterMaxCount = true; + } + } + else + { + if (index < maxEntryCount) + { + show = true; + atLeastOneVisible = true; + } + else + { + hide = true; + nonNullentriesAfterMaxCount = true; + } + } + } + else + { + show = true; + atLeastOneVisible = true; + } + + if (hide) + { + $checkbox.hide(); + } + else if (show && $checkbox.is(':hidden')) + { + $checkbox.fadeIn('slow'); + } + } + else + { + atLeastOneVisible = true; + } + }); + + // Handle the visibility of the div that must be shown when there is no entry available. + var $noEntryDiv = $group.find('div.no-entry-msg'); + $noEntryDiv[atLeastOneVisible ? 'hide' : 'show'](); + + // Handling link more/less visibility + if (showHideFeatureEnabled) + { + var $linkMore = $group.find('a.see-more'), + $linkLess = $group.find('a.see-less'); + + if (nonNullentriesAfterMaxCount) + { + // link more or link less must be visible depending on the display state + if (inDisplayAllState) + { + $linkMore.hide(); + $linkLess.show(); + } + else + { + $linkLess.hide(); + $linkMore.show(); + } + } + else + { + // nothing to hide/show, hide both links + $linkMore.hide(); + $linkLess.hide(); + } + } + }); + }; + + // Indicate the visibility state of the group (ie. a criterion) of checkboxes. + // If false, it means that some entries are hidden because there are more entries than maxEntryCount. + function isDisplayAllState($group, formData) + { + var groupId = $group.attr('id'), + isDisplayAllState; + + // Show/hide feature disabled -> always display all + if (!formData.showHideFeatureEnabled) + { + isDisplayAllState = true; + } + + // See more link visible implies that some entries are hidden. + var $linkMore = $group.find('a.see-more'); + if ($linkMore.is(':visible')) + { + isDisplayAllState = false; + } + + // See less link visible implies that all entries are displayed. + var $linkLess = $group.find('a.see-less'); + if ($linkLess.is(':visible')) + { + isDisplayAllState = true; + } + + if (isDisplayAllState == undefined) + // both links are hidden implies try to find the previous display all state + { + isDisplayAllState = formData.groupsDisplayAllState[groupId]; + } + + if (isDisplayAllState == undefined) + { + // default + isDisplayAllState = false; + } + + // memorize the state + formData.groupsDisplayAllState[groupId] = isDisplayAllState; + + return isDisplayAllState; + }; + + function initiliazeGroups($form) + { + var formId = $form.attr('id'), + formData = _formsData[formId], + showHideFeatureEnabled = formData.showHideFeatureEnabled, + maxEntryCount = formData.maxEntryCount, + hideZero = formData.hideZero, + $checkboxGroups = $form.find('.field .input:has(.checkbox)'); + + $j.each($checkboxGroups, function() { + + var $group = $j(this); + + // info div if group is empty + var $noEntryDiv = $j("<div class=\"no-entry-msg\"></div>"); // hidden by default + + $noEntryDiv.hide(); + $group.append($noEntryDiv); + + if (showHideFeatureEnabled) + { + var $checkboxes = $group.find('.checkbox'); + + var $linkWrapper = $j('<div class="see-more-less-wrapper"/>'), + $linkMore = $j("<a class=\"see-more\" href=\"#\" ></a>"), + $linkLess = $j("<a class=\"see-less\" href=\"#\" ></a>").hide(); // link less hidden by default. + + $linkMore.click(function() { + $checkboxes.each(function() { + var $checkbox = $j(this); + + if ($checkbox.is(':hidden') && (!hideZero || getCheckboxCount($checkbox) > 0)) + { + $checkbox.fadeIn('slow'); + } + }); + + $linkMore.hide(); + $linkLess.show(); + }); + + $linkLess.click(function() { + if ($checkboxes.length > maxEntryCount) + { + $checkboxes = $checkboxes.sort(sortByCountDesc); + } + + $checkboxes.each(function(index) { + var $checkbox = $j(this); + + if (index >= maxEntryCount && !$checkbox.find('input:checked').prop('checked') && $checkbox.is(':visible')) + { + $checkbox.fadeOut('slow'); + } + }); + + $linkLess.hide(); + $linkMore.show(); + }); + + $linkWrapper.append($linkMore); + $linkWrapper.append($linkLess); + $group.append($linkWrapper); + } + }); + }; + + function sortByCountDesc(a, b) + { + return getCheckboxCount($j(b)) - getCheckboxCount($j(a)); + }; + + function getCheckboxCount($checbkox) + { + var rawCount = $checbkox.find('label span.facet-count').text(); // include the parenthesis -> ( + number + ) + return rawCount.substring(1, rawCount.length - 1); + }; +})(); + Index: main/plugin-odf-web/sitemap.xmap =================================================================== --- main/plugin-odf-web/sitemap.xmap (revision 33554) +++ main/plugin-odf-web/sitemap.xmap (working copy) @@ -108,6 +108,7 @@ + @@ -126,6 +127,7 @@ + Index: main/plugin-odf-web/src/org/ametys/plugins/odfweb/generators/AbstractODFSearchGenerator.java =================================================================== --- main/plugin-odf-web/src/org/ametys/plugins/odfweb/generators/AbstractODFSearchGenerator.java (revision 33554) +++ main/plugin-odf-web/src/org/ametys/plugins/odfweb/generators/AbstractODFSearchGenerator.java (working copy) @@ -19,7 +19,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.BitSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -37,6 +36,7 @@ import org.apache.cocoon.environment.Request; import org.apache.cocoon.xml.AttributesImpl; import org.apache.cocoon.xml.XMLUtils; +import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceNotFoundException; @@ -129,10 +129,15 @@ } @Override + protected boolean useFacets() + { + return parameters.getParameterAsBoolean("facets", false); + } + + @Override public void generate() throws IOException, SAXException, ProcessingException { _searchFields = getSearchFields(); - super.generate(); } @@ -158,9 +163,9 @@ } @Override - protected Query getQuery (Request request) throws ParseException, IllegalArgumentException + protected Query getQuery (Request request, FacetData facetData) throws ParseException, IllegalArgumentException, IOException { - return getQuery(getParameterValues(request)); + return getQuery(getParameterValues(request), facetData); } /** @@ -168,13 +173,31 @@ * @param request the request. * @return the parameter values. */ - protected Map getParameterValues(Request request) + protected Map> getParameterValues(Request request) { - Map params = new HashMap(); + Map> params = new HashMap>(); for (String fieldName : _searchFields.keySet()) { - params.put(fieldName, request.getParameter(fieldName)); + String[] parameterValues = request.getParameterValues(fieldName); + if (ArrayUtils.isNotEmpty(parameterValues)) + { + // Discard empty values + List finalValues = new ArrayList(); + + for (String value : parameterValues) + { + if (StringUtils.isNotEmpty(value)) + { + finalValues.add(value); + } + } + + if (!finalValues.isEmpty()) + { + params.put(fieldName, finalValues); + } + } } return params; @@ -183,11 +206,13 @@ /** * Get the query. * @param params + * @param facetData * @return the lucene Query. * @throws ParseException * @throws IllegalArgumentException + * @throws IOException */ - protected Query getQuery(Map params) throws ParseException, IllegalArgumentException + protected Query getQuery(Map> params, FacetData facetData) throws ParseException, IllegalArgumentException, IOException { BooleanQuery query = new BooleanQuery(); @@ -205,10 +230,12 @@ } // Title - _addTitleQuery(query, params.get("title"), getBoost("title", serviceId)); + String title = params.containsKey("title") ? params.get("title").get(0) : null; + _addTitleQuery(query, title, getBoost("title", serviceId)); // Catalog - String catalog = StringUtils.defaultIfEmpty(params.get("catalog"), parameters.getParameter("catalog", "")); + String catalog = params.containsKey("catalog") ? params.get("catalog").get(0) : null; + catalog = StringUtils.defaultIfEmpty(catalog, parameters.getParameter("catalog", "")); if (StringUtils.isEmpty(catalog)) { if (zoneItem != null) @@ -225,18 +252,46 @@ } // Keywords - _addTextFieldQuery(query, params.get("textfield"), getBoost("title", serviceId)); + String textfield = params.containsKey("textfield") ? params.get("textfield").get(0) : null; + _addTextFieldQuery(query, textfield, getBoost("title", serviceId)); + + // TMP TEST FACET DATA + if (facetData != null) + { + Query baseQuery = (Query) query.clone(); + facetData.setBaseQuery(baseQuery); + } for (String fieldName : _searchFields.keySet()) { if (!fieldName.equals("title") && !fieldName.equals("textfield") && !fieldName.equals("catalog")) { - String value = params.get(fieldName); - if (StringUtils.isNotEmpty(value)) + // Real values are expected here (non-empty) + List values = params.get(fieldName); + + if (values != null) { - termQuery = new TermQuery(new Term(fieldName, value)); - termQuery.setBoost(getBoost(fieldName, serviceId)); - query.add(termQuery, BooleanClause.Occur.MUST); + if (values.size() == 1) + { + termQuery = new TermQuery(new Term(fieldName, values.get(0))); + termQuery.setBoost(getBoost(fieldName, serviceId)); + query.add(termQuery, BooleanClause.Occur.MUST); + } + else + { + // OR on each values + BooleanQuery orQuery = new BooleanQuery(); + orQuery.setMinimumNumberShouldMatch(1); + + for (String value : values) + { + termQuery = new TermQuery(new Term(fieldName, value)); + orQuery.add(termQuery, BooleanClause.Occur.SHOULD); + } + + orQuery.setBoost(getBoost(fieldName, serviceId)); + query.add(orQuery, BooleanClause.Occur.MUST); + } } } } @@ -407,9 +462,9 @@ } @Override - protected void saxCriteria(Map criteria, Request request, BitSet queryBitSet, String siteName, String lang, boolean withHitCount) throws SAXException + protected void saxCriteria(Map criteria, Request request, FacetData facetData, String siteName, String lang, boolean withHitCount) throws SAXException { - super.saxCriteria(criteria, request, queryBitSet, siteName, lang, withHitCount); + super.saxCriteria(criteria, request, facetData, siteName, lang, withHitCount); XMLUtils.startElement(contentHandler, "criteria"); for (String criterionName : criteria.keySet()) @@ -430,18 +485,22 @@ } @Override - protected void saxCriterion(Criterion criterion, String siteName, String lang, BitSet criteriaValuesBitSet) throws SAXException, IOException - { - super.saxCriterion(criterion, siteName, lang, criteriaValuesBitSet); - } - - @Override - protected void saxFormValues (Request request, int start, int offset) throws SAXException + protected void saxFormValues(Request request, int start, int offset) throws SAXException { for (String fieldName : _searchFields.keySet()) { - String value = request.getParameter(fieldName); - XMLUtils.createElement(contentHandler, fieldName, StringUtils.defaultString(value)); + String[] values = request.getParameterValues(fieldName); + + if (ArrayUtils.isNotEmpty(values)) + { + for (String value : values) + { + if (StringUtils.isNotEmpty(value)) + { + XMLUtils.createElement(contentHandler, fieldName, value); + } + } + } } } Index: main/plugin-odf-web/src/org/ametys/plugins/odfweb/generators/ODFSearchCriteria.java =================================================================== --- main/plugin-odf-web/src/org/ametys/plugins/odfweb/generators/ODFSearchCriteria.java (revision 33554) +++ main/plugin-odf-web/src/org/ametys/plugins/odfweb/generators/ODFSearchCriteria.java (working copy) @@ -16,7 +16,6 @@ package org.ametys.plugins.odfweb.generators; import java.io.IOException; -import java.util.BitSet; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -38,6 +37,16 @@ */ public class ODFSearchCriteria extends ODFSearch { + @Override + protected boolean useFacets() + { + Request request = ObjectModelHelper.getRequest(objectModel); + + String zoneItemId = parameters.getParameter("zoneItemId", request.getParameter("zoneItemId")); + ZoneItem zoneItem = _resolver.resolveById(zoneItemId); + + return zoneItem.getServiceParameters().getBoolean("facets", false); + } @Override public void generate() throws IOException, SAXException, ProcessingException @@ -69,16 +78,16 @@ Searcher searcher = null; try { - BitSet queryBitSet = null; - searcher = getSearchIndex(request, Collections.singletonList(siteName), lang); + FacetData facetData = useFacets() ? new FacetData(searcher, siteName, lang) : null; + if (searcher != null) { - queryBitSet = getDocuments(request, searcher); + getDocuments(request, searcher, facetData); } Map criteria = getCriteria(request, siteName, lang); - saxCriteria(criteria, request, queryBitSet, siteName, lang, true); + saxCriteria(criteria, request, facetData, siteName, lang, true); } catch (IllegalArgumentException e) { @@ -127,26 +136,29 @@ } @Override - protected void saxCriterion(Criterion criterion, String siteName, String lang, BitSet criteriaValuesBitSet) throws SAXException, IOException + protected void saxCriterion(Criterion criterion, FacetData facetData, String siteName, String lang) throws SAXException, IOException { AttributesImpl critAttrs = new AttributesImpl(); critAttrs.addCDATAAttribute("name", criterion.getFieldName()); - if (criteriaValuesBitSet != null) + Integer count = facetData != null ? facetData.getCount(criterion.getLuceneTermName()) : null; + if (count != null) { - int criterionCount = criteriaValuesBitSet.cardinality(); - critAttrs.addCDATAAttribute("count", Integer.toString(criterionCount)); + critAttrs.addCDATAAttribute("count", Integer.toString(count)); } XMLUtils.startElement(contentHandler, "criterion", critAttrs); for (String value : criterion.getValues().keySet()) { - int count = getCount(criteriaValuesBitSet, criterion, value, siteName, lang); - AttributesImpl valueAttrs = new AttributesImpl(); valueAttrs.addCDATAAttribute("value", value); - valueAttrs.addCDATAAttribute("count", Integer.toString(count)); + + count = facetData != null ? facetData.getCount(criterion.getLuceneTermName(), value) : null; + if (count != null) + { + valueAttrs.addCDATAAttribute("count", Integer.toString(count)); + } XMLUtils.startElement(contentHandler, "value", valueAttrs); criterion.getValues().get(value).toSAX(contentHandler);