diff --git a/xwiki-platform-core/xwiki-platform-eventstream/xwiki-platform-eventstream-api/src/main/java/org/xwiki/eventstream/internal/DefaultRecordableEventDescriptorManager.java b/xwiki-platform-core/xwiki-platform-eventstream/xwiki-platform-eventstream-api/src/main/java/org/xwiki/eventstream/internal/DefaultRecordableEventDescriptorManager.java index 354f1e41ecf9..cb28d9ba6cbd 100644 --- a/xwiki-platform-core/xwiki-platform-eventstream/xwiki-platform-eventstream-api/src/main/java/org/xwiki/eventstream/internal/DefaultRecordableEventDescriptorManager.java +++ b/xwiki-platform-core/xwiki-platform-eventstream/xwiki-platform-eventstream-api/src/main/java/org/xwiki/eventstream/internal/DefaultRecordableEventDescriptorManager.java @@ -103,6 +103,7 @@ public List getRecordableEventDescriptors(boolean all public RecordableEventDescriptor getDescriptorForEventType(String eventType, boolean allWikis) throws EventStreamException { + // FIXME: We should cache the descriptors for improving perf when calling multiple times this method. return getRecordableEventDescriptors(allWikis).stream().filter(descriptor -> eventType.equals(descriptor.getEventType())).findAny().orElse(null); } diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/AbstractLiveDataAdvancedPanelElement.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/AbstractLiveDataAdvancedPanelElement.java new file mode 100644 index 000000000000..3b636256930f --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/AbstractLiveDataAdvancedPanelElement.java @@ -0,0 +1,78 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.livedata.test.po; + +import org.openqa.selenium.By; +import org.openqa.selenium.InvalidElementStateException; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebElement; +import org.xwiki.test.ui.po.BaseElement; + +/** + * Abstract class representing different type of advanced panels in livedata. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +public abstract class AbstractLiveDataAdvancedPanelElement extends BaseElement +{ + protected final WebElement container; + protected final LiveDataElement liveData; + + /** + * Default constructor. + * @param liveData the livedata the panel belongs to. + * @param container the container of the panel. + */ + public AbstractLiveDataAdvancedPanelElement(LiveDataElement liveData, WebElement container) + { + this.liveData = liveData; + this.container = container; + } + + /** + * Close the panel. + */ + public void closePanel() + { + getDriver().findElementWithoutWaiting(this.container, By.className("close-button")).click(); + } + + /** + * Allow to click on links that are only visible when the mouse is on them (e.g. delete icons of filters) + * @param linkIdentifiers the identifier to find the link element in the dom. + */ + protected void clickOnMouseOverLinks(By linkContainer, By linkIdentifiers) + { + getDriver().findElementsWithoutWaiting(this.container, linkContainer) + .forEach(webElement -> { + // First we move to the link container to make the link appearing + getDriver().createActions().moveToElement(webElement).build().perform(); + WebElement linkElement = getDriver().findElementWithoutWaiting(webElement, linkIdentifiers); + if (!linkElement.isDisplayed()) { + throw new InvalidElementStateException(String.format("The link [%s] should be displayed.", + linkElement)); + } + // Click on the actual link + getDriver().createActions().moveToElement(linkElement).click().build().perform(); + }); + } +} diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/FiltersPanelElement.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/FiltersPanelElement.java new file mode 100644 index 000000000000..adabb75304a7 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/FiltersPanelElement.java @@ -0,0 +1,52 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.livedata.test.po; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Represents the advanced panel for filtering columns. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +public class FiltersPanelElement extends AbstractLiveDataAdvancedPanelElement +{ + /** + * Default constructor. + * @param liveData the livedata of the panel. + * @param container the container of the panel. + */ + public FiltersPanelElement(LiveDataElement liveData, WebElement container) + { + super(liveData, container); + } + + /** + * Remove all filters. + */ + public void clearAllFilters() + { + this.clickOnMouseOverLinks(By.className("filter-group-title"), By.className("delete-filter-group")); + this.liveData.waitUntilReady(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/LiveDataElement.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/LiveDataElement.java index 67219c5bf6e1..ebb8af055b8f 100644 --- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/LiveDataElement.java +++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/LiveDataElement.java @@ -104,6 +104,13 @@ public boolean isReady() return isVueLoaded() && areComponentsLoaded(); } + private WebElement openDropDownMenu() + { + WebElement dropdownMenu = getRootElement().findElement(By.cssSelector(".livedata-dropdown-menu ")); + dropdownMenu.click(); + return dropdownMenu; + } + /** * Click on the refresh button from the actions menu. * @@ -111,9 +118,7 @@ public boolean isReady() */ public void refresh() { - WebElement dropdownMenu = getRootElement().findElement(By.cssSelector(".livedata-dropdown-menu ")); - dropdownMenu.click(); - dropdownMenu.findElement(By.cssSelector(".livedata-action-refresh")).click(); + openDropDownMenu().findElement(By.cssSelector(".livedata-action-refresh")).click(); } /** @@ -139,7 +144,7 @@ public LiveDataElement setPagination(int paginationNumber) return this; } - private void waitUntilReady() + public void waitUntilReady() { getDriver().waitUntilCondition(input -> isVueLoaded()); @@ -180,4 +185,48 @@ private WebElement getRootElement() { return getDriver().findElement(By.id(this.id)); } + + /** + * Open the panel for advanced filter and returns it. + * @return an instance of {@link FiltersPanelElement} once it's opened. + * @since 16.3.0RC1 + */ + public FiltersPanelElement openFiltersPanel() + { + openDropDownMenu().findElement(By.linkText("Filter...")).click(); + return new FiltersPanelElement(this, + getRootElement().findElement(By.className("livedata-advanced-panel-filter"))); + } + + /** + * Open the panel for advanced sorting and returns it. + * @return an instance of {@link SortPanelElement} once it's opened. + * @since 16.3.0RC1 + */ + public SortPanelElement openSortPanel() + { + openDropDownMenu().findElement(By.linkText("Sort...")).click(); + return new SortPanelElement(this, + getRootElement().findElement(By.className("livedata-advanced-panel-sort"))); + } + + /** + * Clear all custom sorting that might have been put. + */ + public void clearAllSort() + { + SortPanelElement sortPanelElement = openSortPanel(); + sortPanelElement.clearAllSort(); + sortPanelElement.closePanel(); + } + + /** + * Clear all custom filters that might have been put. + */ + public void clearAllFilters() + { + FiltersPanelElement filtersPanelElement = openFiltersPanel(); + filtersPanelElement.clearAllFilters(); + filtersPanelElement.closePanel(); + } } diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/SortPanelElement.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/SortPanelElement.java new file mode 100644 index 000000000000..e6bd12e3ba1e --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/SortPanelElement.java @@ -0,0 +1,52 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.livedata.test.po; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Represents the advanced panel for sorting columns. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +public class SortPanelElement extends AbstractLiveDataAdvancedPanelElement +{ + /** + * Default constructor. + * @param liveData the livedata of the panel. + * @param container the container of the panel. + */ + public SortPanelElement(LiveDataElement liveData, WebElement container) + { + super(liveData, container); + } + + /** + * Remove all sorting on all columns. + */ + public void clearAllSort() + { + this.clickOnMouseOverLinks(By.className("sort-entry"), By.className("delete-sort")); + this.liveData.waitUntilReady(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/TableLayoutElement.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/TableLayoutElement.java index 9f0162ec7ebd..ead6d9334b3c 100644 --- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/TableLayoutElement.java +++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-test/xwiki-platform-livedata-test-pageobjects/src/main/java/org/xwiki/livedata/test/po/TableLayoutElement.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.http.NameValuePair; @@ -76,6 +77,9 @@ public class TableLayoutElement extends BaseElement private final LiveDataElement liveData; + private static final Pattern PAGINATION_SENTENCE_PATTERN = + Pattern.compile("^Entries (?\\d+) - (?\\d+) out of (?\\d+)$"); + /** * @return the list of rows {@link WebElement}s * @since 16.1.0RC1 @@ -688,6 +692,24 @@ public Set getPaginationSizes() .map(it -> it.getAttribute("value")).collect(Collectors.toSet()); } + private String getPaginationEntriesString() + { + return getRoot().findElement(By.className("pagination-current-entries")).getText().trim(); + } + + private java.util.regex.Matcher getPaginationMatcher() + { + return PAGINATION_SENTENCE_PATTERN.matcher(getPaginationEntriesString()); + } + + public long getTotalEntries() + { + java.util.regex.Matcher paginationMatcher = getPaginationMatcher(); + if (!paginationMatcher.matches()) { + throw new IllegalStateException("Matcher does not match: " + getPaginationEntriesString()); + } + return Long.parseLong(paginationMatcher.group("totalEntries")); + } /** * Clicks on an action button identified by its name, on a given row. diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/CachedFilterPreferencesModelBridge.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/CachedFilterPreferencesModelBridge.java index 264e194572ec..2df6c28e8b13 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/CachedFilterPreferencesModelBridge.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/CachedFilterPreferencesModelBridge.java @@ -75,7 +75,7 @@ public class CachedFilterPreferencesModelBridge implements FilterPreferencesMode private Map> preferenceFilterCache; - private Map> toggleCache; + private Map> toggleCache; void invalidatePreferencefilter(EntityReference reference) { @@ -132,10 +132,10 @@ public Set getFilterPreferences(WikiReference wiki } @Override - public Map getToggleableFilterActivations(DocumentReference userReference) - throws NotificationException + public Map getToggleableFilterActivations( + DocumentReference userReference) throws NotificationException { - Map values = this.toggleCache.get(userReference); + Map values = this.toggleCache.get(userReference); if (values != null) { return values; } diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManager.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManager.java index 49479ed780bb..542f45e0e560 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManager.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManager.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Inject; @@ -211,7 +212,10 @@ public Stream getToggleableFilters(Collection getToggeableFilterActivations(DocumentReference user) throws NotificationException { - return filterPreferencesModelBridge.getToggleableFilterActivations(user); + return filterPreferencesModelBridge.getToggleableFilterActivations(user) + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().isEnabled())); } @Override diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/FilterPreferencesModelBridge.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/FilterPreferencesModelBridge.java index 4a02cb0c5965..c275f68a39e4 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/FilterPreferencesModelBridge.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/FilterPreferencesModelBridge.java @@ -75,7 +75,8 @@ default Set getFilterPreferences(WikiReference wik * @throws NotificationException if an error happens * @since 10.1RC1 */ - Map getToggleableFilterActivations(DocumentReference user) throws NotificationException; + Map getToggleableFilterActivations(DocumentReference user) + throws NotificationException; /** * Delete a filter preference. diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/ToggleableNotificationFilter.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/ToggleableNotificationFilter.java index 7d791a60e0af..beeb2bbd8060 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/ToggleableNotificationFilter.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/ToggleableNotificationFilter.java @@ -20,7 +20,6 @@ package org.xwiki.notifications.filters.internal; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.xwiki.notifications.NotificationFormat; @@ -49,12 +48,4 @@ default List getFormats() { return Arrays.asList(NotificationFormat.values()); } - - /** - * @return the events handled by this filter - */ - default List getEventTypes() - { - return Collections.emptyList(); - } } diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/ToggleableNotificationFilterActivation.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/ToggleableNotificationFilterActivation.java new file mode 100644 index 000000000000..a2997b58eb8c --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/main/java/org/xwiki/notifications/filters/internal/ToggleableNotificationFilterActivation.java @@ -0,0 +1,135 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.notifications.filters.internal; + +import java.io.Serializable; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.xwiki.model.reference.DocumentReference; + +/** + * Provide information about the activation of {@link ToggleableNotificationFilter}. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +public class ToggleableNotificationFilterActivation implements Serializable +{ + private final String name; + private final boolean isEnabled; + private final DocumentReference objectLocation; + private final int objectNumber; + + /** + * Default constructor. + * + * @param name the name of the filter + * @param isEnabled {@code true} if the filter is enabled + * @param objectLocation the location of the object holding activation information + * @param objectNumber the number of the actual object holding activation information or {@code -1} if there's no + * object + */ + public ToggleableNotificationFilterActivation(String name, boolean isEnabled, DocumentReference objectLocation, + int objectNumber) + { + this.name = name; + this.isEnabled = isEnabled; + this.objectLocation = objectLocation; + this.objectNumber = objectNumber; + } + + /** + * @return the name of the filter + */ + public String getName() + { + return name; + } + + /** + * @return {@code true} if the filter is enabled + */ + public boolean isEnabled() + { + return isEnabled; + } + + /** + * @return the location of the object holding activation information + */ + public DocumentReference getObjectLocation() + { + return objectLocation; + } + + /** + * @return the number of the actual object holding activation information or {@code -1} if there's no object + */ + public int getObjectNumber() + { + return objectNumber; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + ToggleableNotificationFilterActivation that = (ToggleableNotificationFilterActivation) o; + + return new EqualsBuilder() + .append(isEnabled, that.isEnabled) + .append(objectNumber, that.objectNumber) + .append(name, that.name) + .append(objectLocation, that.objectLocation) + .isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(15, 33) + .append(name) + .append(isEnabled) + .append(objectLocation) + .append(objectNumber) + .toHashCode(); + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("name", name) + .append("isEnabled", isEnabled) + .append("objectLocation", objectLocation) + .append("objectNumber", objectNumber) + .toString(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/test/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManagerTest.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/test/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManagerTest.java index 2da8f7258975..74f9bffa9eac 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/test/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManagerTest.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-api/src/test/java/org/xwiki/notifications/filters/internal/DefaultNotificationFilterManagerTest.java @@ -211,13 +211,13 @@ void getFiltersWithSpecificFilteringPhase() throws Exception when(filter6.getName()).thenReturn("filter6"); when(filter7.getName()).thenReturn("filter7"); - Map filterActivations = new HashMap<>(); - filterActivations.put("filter1", true); - filterActivations.put("filter2", false); - filterActivations.put("filter3", true); - filterActivations.put("filter4", false); - filterActivations.put("filter5", true); - filterActivations.put("filter6", false); + Map filterActivations = new HashMap<>(); + filterActivations.put("filter1", new ToggleableNotificationFilterActivation("filter1", true, null, -1)); + filterActivations.put("filter2", new ToggleableNotificationFilterActivation("filter2", false, null, -1)); + filterActivations.put("filter3", new ToggleableNotificationFilterActivation("filter3", true, null, -1)); + filterActivations.put("filter4", new ToggleableNotificationFilterActivation("filter4", false, null, -1)); + filterActivations.put("filter5", new ToggleableNotificationFilterActivation("filter5", true, null, -1)); + filterActivations.put("filter6", new ToggleableNotificationFilterActivation("filter6", false, null, -1)); // We don't put filter7 so it should default as being considered activated when(this.filterPreferencesModelBridge.getToggleableFilterActivations(testUser)).thenReturn(filterActivations); diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/pom.xml b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/pom.xml index 592bec55b400..140d6c1de9b7 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/pom.xml +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/pom.xml @@ -45,6 +45,11 @@ xwiki-platform-oldcore ${project.version} + + org.xwiki.platform + xwiki-platform-livedata-api + ${project.version} + diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/checkstyle/checkstyle-suppressions.xml b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/checkstyle/checkstyle-suppressions.xml index 62cbad91217c..c5886cb2a9ab 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/checkstyle/checkstyle-suppressions.xml +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/checkstyle/checkstyle-suppressions.xml @@ -28,4 +28,5 @@ + diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridge.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridge.java index 4d4ff989d456..b466e61c52c5 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridge.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridge.java @@ -19,7 +19,6 @@ */ package org.xwiki.notifications.filters.internal; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -39,7 +38,6 @@ import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.EntityReferenceSerializer; -import org.xwiki.model.reference.LocalDocumentReference; import org.xwiki.model.reference.WikiReference; import org.xwiki.notifications.NotificationException; import org.xwiki.notifications.NotificationFormat; @@ -63,13 +61,6 @@ @Singleton public class DefaultFilterPreferencesModelBridge implements FilterPreferencesModelBridge { - private static final LocalDocumentReference TOGGLEABLE_FILTER_PREFERENCE_CLASS = - new LocalDocumentReference(Arrays.asList("XWiki", "Notifications", "Code"), "ToggleableFilterPreferenceClass"); - - private static final String FIELD_FILTER_NAME = "filterName"; - - private static final String FIELD_IS_ENABLED = "isEnabled"; - @Inject private Provider contextProvider; @@ -104,28 +95,34 @@ public Set getFilterPreferences(WikiReference wiki } @Override - public Map getToggleableFilterActivations(DocumentReference user) throws NotificationException + public Map getToggleableFilterActivations(DocumentReference user) + throws NotificationException { XWikiContext context = contextProvider.get(); WikiReference currentWiki = context.getWikiReference(); context.setWikiReference(user.getWikiReference()); XWiki xwiki = context.getWiki(); - Map filterStatus = new HashMap<>(); + Map filterStatus = new HashMap<>(); try { XWikiDocument doc = xwiki.getDocument(user, context); for (NotificationFilter filter : componentManager.getInstanceList( - NotificationFilter.class)) { - if (filter instanceof ToggleableNotificationFilter) { - ToggleableNotificationFilter toggleableFilter = (ToggleableNotificationFilter) filter; + NotificationFilter.class)) { + if (filter instanceof ToggleableNotificationFilter toggleableFilter) { boolean status = toggleableFilter.isEnabledByDefault(); - BaseObject obj = doc.getXObject(TOGGLEABLE_FILTER_PREFERENCE_CLASS, FIELD_FILTER_NAME, + BaseObject obj = doc.getXObject(ToggleableFilterPreferenceDocumentInitializer.XCLASS, + ToggleableFilterPreferenceDocumentInitializer.FIELD_FILTER_NAME, filter.getName(), false); + int objNumber = -1; if (obj != null) { - status = obj.getIntValue(FIELD_IS_ENABLED, status ? 1 : 0) != 0; + status = obj.getIntValue(ToggleableFilterPreferenceDocumentInitializer.FIELD_IS_ENABLED, + status ? 1 : 0) != 0; + objNumber = obj.getNumber(); } - filterStatus.put(filter.getName(), status); + ToggleableNotificationFilterActivation filterActivation = + new ToggleableNotificationFilterActivation(filter.getName(), status, user, objNumber); + filterStatus.put(filter.getName(), filterActivation); } } } catch (Exception e) { diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/NotificationFilterPreferenceStore.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/NotificationFilterPreferenceStore.java index dfd4f380a70a..65877d9e7e91 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/NotificationFilterPreferenceStore.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/NotificationFilterPreferenceStore.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import javax.inject.Inject; @@ -64,6 +65,7 @@ public class NotificationFilterPreferenceStore { private static final String FILTER_PREFIX = "NFP_"; + private static final String ID = "id"; @Inject private EntityReferenceSerializer entityReferenceSerializer; @@ -77,6 +79,45 @@ public class NotificationFilterPreferenceStore @Inject private ObservationManager observation; + /** + * Retrieve the notification preference that corresponds to the given id and wiki. + * + * @param wikiReference the wiki for which to retrieve a notification preference + * @param filterPreferenceId a filter preference id + * @return the corresponding preference or {@link Optional#empty()} if none can be found + * @throws NotificationException if an error occurs + * @since 16.3.0RC1 + */ + public Optional getFilterPreference(String filterPreferenceId, + WikiReference wikiReference) throws NotificationException + { + Optional result = Optional.empty(); + DefaultNotificationFilterPreference filterPreference = configureContextWrapper(wikiReference, () -> { + Query query; + try { + query = this.queryManager.createQuery( + "select nfp from DefaultNotificationFilterPreference nfp where nfp.id = :id", + Query.HQL); + query.setLimit(1); + query.bindValue(ID, filterPreferenceId); + + List results = query.execute(); + if (!results.isEmpty()) { + return results.get(0); + } + } catch (QueryException e) { + throw new NotificationException( + String.format("Error while retrieving notification with id [%s]", filterPreferenceId), e); + } + return null; + }); + if (filterPreference != null) { + result = Optional.of(filterPreference); + } + + return result; + } + /** * Get the notification preference that corresponds to the given id and user. * @@ -354,7 +395,7 @@ private void deleteFilterPreferences(WikiReference wikiReference, Set inte try { hibernateStore.executeWrite(context, session -> session.createQuery("delete from DefaultNotificationFilterPreference where internalId in (:id)") - .setParameter("id", internalFilterPreferenceIds) + .setParameter(ID, internalFilterPreferenceIds) .executeUpdate()); } catch (XWikiException e) { throw new NotificationException( diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/ToggleableFilterPreferenceDocumentInitializer.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/ToggleableFilterPreferenceDocumentInitializer.java index 1efda3b7d72a..3fa418433484 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/ToggleableFilterPreferenceDocumentInitializer.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/ToggleableFilterPreferenceDocumentInitializer.java @@ -19,7 +19,6 @@ */ package org.xwiki.notifications.filters.internal; -import java.util.Arrays; import java.util.List; import javax.inject.Named; @@ -44,22 +43,37 @@ public class ToggleableFilterPreferenceDocumentInitializer extends AbstractMandatoryClassInitializer { /** - * The path to the class parent document. + * Reference of the xclass. + * @since 16.3.0RC1 */ - private static final List PARENT_PATH = Arrays.asList("XWiki", "Notifications", "Code"); + public static final LocalDocumentReference XCLASS = + new LocalDocumentReference(List.of("XWiki", "Notifications", "Code"), "ToggleableFilterPreferenceClass"); + + /** + * Name of field holding the filter name. + * @since 16.3.0RC1 + */ + public static final String FIELD_FILTER_NAME = "filterName"; + + /** + * Name of the field holding the activation value. + * @since 16.3.0RC1 + */ + public static final String FIELD_IS_ENABLED = "isEnabled"; + /** * Default constructor. */ public ToggleableFilterPreferenceDocumentInitializer() { - super(new LocalDocumentReference(PARENT_PATH, "ToggleableFilterPreferenceClass")); + super(XCLASS); } @Override protected void createClass(BaseClass xclass) { - xclass.addTextField("filterName", "Filter name", 64); - xclass.addBooleanField("isEnabled", "Is enabled ?", "checkbox", "", true); + xclass.addTextField(FIELD_FILTER_NAME, "Filter name", 64); + xclass.addBooleanField(FIELD_IS_ENABLED, "Is enabled ?", "checkbox", "", true); } } diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/AbstractNotificationFilterLiveDataEntryStore.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/AbstractNotificationFilterLiveDataEntryStore.java new file mode 100644 index 000000000000..1703313b4aea --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/AbstractNotificationFilterLiveDataEntryStore.java @@ -0,0 +1,149 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.notifications.filters.internal.livedata; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.inject.Inject; +import javax.inject.Provider; + +import org.xwiki.livedata.LiveDataEntryStore; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataQuery; +import org.xwiki.model.EntityType; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.EntityReference; +import org.xwiki.model.reference.EntityReferenceResolver; +import org.xwiki.model.reference.EntityReferenceSerializer; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.notifications.NotificationFormat; +import org.xwiki.notifications.filters.internal.DefaultNotificationFilterPreference; +import org.xwiki.security.authorization.ContextualAuthorizationManager; +import org.xwiki.security.authorization.Right; + +import com.xpn.xwiki.XWikiContext; + +/** + * Abstract class providing helpers for both + * {@link org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersLiveDataEntryStore} and + * {@link org.xwiki.notifications.filters.internal.livedata.system.NotificationSystemFiltersLiveDataEntryStore}. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +public abstract class AbstractNotificationFilterLiveDataEntryStore implements LiveDataEntryStore +{ + private static final String TARGET_SOURCE_PARAMETER = "target"; + private static final String WIKI_SOURCE_PARAMETER = "wiki"; + private static final String UNAUTHORIZED_EXCEPTION_MSG = "You don't have rights to access those information."; + + @Inject + protected NotificationFilterLiveDataTranslationHelper translationHelper; + + @Inject + protected EntityReferenceResolver entityReferenceResolver; + + @Inject + protected EntityReferenceSerializer entityReferenceSerializer; + + @Inject + private Provider contextProvider; + + @Inject + private ContextualAuthorizationManager contextualAuthorizationManager; + + protected static final class TargetInformation + { + public boolean isWikiTarget; + public EntityReference ownerReference; + + } + + protected Map getStaticListInfo(List items) + { + return Map.of( + "extraClass", "list-unstyled", + "items", items + ); + } + + protected Map displayNotificationFormats(Collection notificationFormats) + { + List items = notificationFormats + .stream() + .sorted(Comparator.comparing(NotificationFormat::name)) + .map(notificationFormat -> this.translationHelper.getFormatTranslation(notificationFormat)) + .toList(); + + return getStaticListInfo(items); + } + + protected TargetInformation getTargetInformation(LiveDataQuery query) throws LiveDataException + { + Map sourceParameters = query.getSource().getParameters(); + if (!sourceParameters.containsKey(TARGET_SOURCE_PARAMETER)) { + throw new LiveDataException("The target source parameter is mandatory."); + } + String target = String.valueOf(sourceParameters.get(TARGET_SOURCE_PARAMETER)); + TargetInformation result = new TargetInformation(); + if (WIKI_SOURCE_PARAMETER.equals(target)) { + result.isWikiTarget = true; + result.ownerReference = + this.entityReferenceResolver.resolve(String.valueOf(sourceParameters.get(WIKI_SOURCE_PARAMETER)), + EntityType.WIKI); + } else { + result.isWikiTarget = false; + result.ownerReference = this.entityReferenceResolver.resolve(String.valueOf(sourceParameters.get(target)), + EntityType.DOCUMENT); + } + if (!this.contextualAuthorizationManager.hasAccess(Right.ADMIN) + && !result.ownerReference.equals(getCurrentUserReference())) { + throw new LiveDataException(UNAUTHORIZED_EXCEPTION_MSG); + } + return result; + } + + protected WikiReference getCurrentWikiReference() + { + XWikiContext context = this.contextProvider.get(); + return context.getWikiReference(); + } + + private DocumentReference getCurrentUserReference() + { + XWikiContext context = this.contextProvider.get(); + return context.getUserReference(); + } + + protected void checkAccessFilterPreference(DefaultNotificationFilterPreference filterPreference) + throws LiveDataException + { + if (!this.contextualAuthorizationManager.hasAccess(Right.ADMIN) + && !Objects.equals(filterPreference.getOwner(), + this.entityReferenceSerializer.serialize(getCurrentUserReference()))) { + throw new LiveDataException(UNAUTHORIZED_EXCEPTION_MSG); + } + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/NotificationFilterLiveDataTranslationHelper.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/NotificationFilterLiveDataTranslationHelper.java new file mode 100644 index 000000000000..6ab7ca600251 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/NotificationFilterLiveDataTranslationHelper.java @@ -0,0 +1,160 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; +import org.xwiki.eventstream.EventStreamException; +import org.xwiki.eventstream.RecordableEventDescriptor; +import org.xwiki.eventstream.RecordableEventDescriptorManager; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.localization.ContextualLocalizationManager; +import org.xwiki.notifications.NotificationFormat; +import org.xwiki.notifications.filters.NotificationFilterType; +import org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersLiveDataConfigurationProvider; +import org.xwiki.wiki.descriptor.WikiDescriptorManager; + +/** + * Helper for getting various translations for live data custom sources. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component(roles = NotificationFilterLiveDataTranslationHelper.class) +@Singleton +public class NotificationFilterLiveDataTranslationHelper +{ + @Inject + private ContextualLocalizationManager contextualLocalizationManager; + + @Inject + private RecordableEventDescriptorManager recordableEventDescriptorManager; + + @Inject + private WikiDescriptorManager wikiDescriptorManager; + + @Inject + private Logger logger; + + private String getTranslationWithFallback(String translationKey) + { + String translationPlain = this.contextualLocalizationManager.getTranslationPlain(translationKey); + if (translationPlain == null) { + translationPlain = translationKey; + } + return translationPlain; + } + + /** + * Get a plain text translation with a fallback to the full key if none can be found. + * @param prefix the translation prefix to use + * @param key the key to add to the prefix + * @return the plain text translation or the prefix and key if none can be found + */ + public String getTranslationWithPrefix(String prefix, String key) + { + return getTranslationWithFallback(prefix + key); + } + + /** + * @param filterType the filter type for which to get a translation + * @return the plain text translation of the filter type + */ + public String getFilterTypeTranslation(NotificationFilterType filterType) + { + return getTranslationWithPrefix("notifications.filters.type.custom.", filterType.name().toLowerCase()); + } + + /** + * @param scope the scope for which to get a translation + * @return the plain text translation of the scope + */ + public String getScopeTranslation(NotificationCustomFiltersLiveDataConfigurationProvider.Scope scope) + { + return getTranslationWithPrefix("notifications.filters.preferences.scopeNotificationFilter.", + scope.name().toLowerCase()); + } + + /** + * @return the translation for all events. + */ + public String getAllEventTypesTranslation() + { + return getTranslationWithFallback("notifications.filters.preferences.allEvents"); + } + + /** + * @param eventType the event type for which to get a description translation + * @return the plain text event type description translation + * @throws LiveDataException if the event type descriptor cannot be found + */ + public String getEventTypeTranslation(String eventType) + { + String result = eventType; + try { + RecordableEventDescriptor descriptor = + this.recordableEventDescriptorManager.getDescriptorForEventType(eventType, true); + if (descriptor != null) { + result = getTranslationWithFallback(descriptor.getDescription()); + } + } catch (EventStreamException e) { + this.logger.error("Error while getting description for event type [{}] falling back on event name", + eventType, e); + } + return result; + } + + /** + * @param format the notification format for which to get a translation + * @return the plain text translation of the format + */ + public String getFormatTranslation(NotificationFormat format) + { + return getTranslationWithPrefix("notifications.format.", format.name().toLowerCase()); + } + + /** + * Get event type information from all descriptor to populate a select. + * @return a list of maps containing two information: {@code value} holding the event type, and {@code label} + * holding a translation of the description + * @throws LiveDataException in case of problem to load the descriptors + */ + public List> getAllEventTypesOptions() throws LiveDataException + { + try { + boolean isMainWiki = this.wikiDescriptorManager.isMainWiki(this.wikiDescriptorManager.getCurrentWikiId()); + List recordableEventDescriptors = + this.recordableEventDescriptorManager.getRecordableEventDescriptors(isMainWiki); + return recordableEventDescriptors.stream().map(descriptor -> Map.of( + "value", descriptor.getEventType(), + "label", getTranslationWithFallback(descriptor.getDescription()) + )).collect(Collectors.toList()); + } catch (EventStreamException e) { + throw new LiveDataException("Error while retrieving event descriptors", e); + } + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataConfigurationProvider.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataConfigurationProvider.java new file mode 100644 index 000000000000..77225e0b1d24 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataConfigurationProvider.java @@ -0,0 +1,349 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.custom; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; +import org.xwiki.icon.IconException; +import org.xwiki.icon.IconManager; +import org.xwiki.livedata.LiveDataActionDescriptor; +import org.xwiki.livedata.LiveDataConfiguration; +import org.xwiki.livedata.LiveDataEntryDescriptor; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataMeta; +import org.xwiki.livedata.LiveDataPaginationConfiguration; +import org.xwiki.livedata.LiveDataPropertyDescriptor; +import org.xwiki.localization.ContextualLocalizationManager; +import org.xwiki.notifications.NotificationFormat; +import org.xwiki.notifications.filters.NotificationFilterType; +import org.xwiki.notifications.filters.internal.livedata.NotificationFilterLiveDataTranslationHelper; + +/** + * Configuration of the {@link NotificationCustomFiltersLiveDataSource}. + * + * @since 16.3.0RC1 + * @version $Id$ + */ +@Component +@Singleton +@Named(NotificationCustomFiltersLiveDataSource.NAME) +public class NotificationCustomFiltersLiveDataConfigurationProvider implements Provider +{ + static final String ALL_EVENTS_OPTION_VALUE = "__ALL_EVENTS__"; + static final String ID_FIELD = "filterPreferenceId"; + static final String SCOPE_FIELD = "scope"; + static final String LOCATION_FIELD = "location"; + static final String DISPLAY_FIELD = "display"; + static final String FILTER_TYPE_FIELD = "filterType"; + static final String EVENT_TYPES_FIELD = "eventTypes"; + static final String NOTIFICATION_FORMATS_FIELD = "notificationFormats"; + static final String IS_ENABLED_FIELD = "isEnabled"; + static final String ACTIONS_FIELD = "actions"; + static final String DOC_HAS_DELETE_FIELD = "doc_hasdelete"; + private static final String TRANSLATION_PREFIX = "notifications.settings.filters.preferences.custom.table."; + private static final String DELETE = "delete"; + // FIXME: We should define those constants in LiveData module + private static final String STRING_TYPE = "String"; + private static final String HTML_DISPLAYER = "html"; + private static final String BOOLEAN = "boolean"; + private static final String STATIC_LIST_DISPLAYER = "staticList"; + private static final String VALUE_KEY = "value"; + private static final String LABEL_KEY = "label"; + + /** + * Defines the different scopes a filter might have. + * + * @version $Id$ + */ + public enum Scope + { + /** + * Filter preference targeting a wiki. + */ + WIKI("wiki"), + + /** + * Filter preference targeting a space. + */ + SPACE("page"), + + /** + * Filter preference targeting a page. + */ + PAGE("pageOnly"), + + /** + * Filter preference targeting a user. + */ + USER("user"); + + private final String fieldName; + Scope(String fieldName) + { + this.fieldName = fieldName; + } + + /** + * @return the database field name used to hold value of the concerned scope. + */ + String getFieldName() + { + return this.fieldName; + } + }; + + @Inject + private ContextualLocalizationManager l10n; + + @Inject + private NotificationFilterLiveDataTranslationHelper translationHelper; + + @Inject + private IconManager iconManager; + + @Inject + private Logger logger; + + @Override + public LiveDataConfiguration get() + { + LiveDataConfiguration input = new LiveDataConfiguration(); + LiveDataMeta meta = new LiveDataMeta(); + input.setMeta(meta); + + LiveDataPaginationConfiguration pagination = new LiveDataPaginationConfiguration(); + pagination.setShowPageSizeDropdown(true); + meta.setPagination(pagination); + + LiveDataEntryDescriptor entryDescriptor = new LiveDataEntryDescriptor(); + entryDescriptor.setIdProperty(ID_FIELD); + meta.setEntryDescriptor(entryDescriptor); + + LiveDataActionDescriptor deleteAction = new LiveDataActionDescriptor(); + deleteAction.setName(this.l10n.getTranslationPlain("liveData.action.delete")); + deleteAction.setId(DELETE); + deleteAction.setAllowProperty(DOC_HAS_DELETE_FIELD); + try { + // FIXME: we should map delete action icon to the cross.. + deleteAction.setIcon(this.iconManager.getMetaData("cross")); + } catch (IconException e) { + this.logger.error("Error while getting icon for the remove action", e); + } + meta.setActions(List.of(deleteAction)); + + meta.setPropertyDescriptors(List.of( + getIDDescriptor(), + getDisplayDescriptor(), + getScopeDescriptor(), + getLocationDescriptor(), + getFilterTypeDescriptor(), + getNotificationFormatsDescriptor(), + getEventTypesDescriptor(), + getIsEnabledDescriptor(), + getActionDescriptor() + )); + + return input; + } + + private LiveDataPropertyDescriptor getIDDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + ID_FIELD)); + descriptor.setId(ID_FIELD); + descriptor.setType(STRING_TYPE); + // This is not visible we want to use that field only for sorting + descriptor.setVisible(false); + descriptor.setEditable(false); + descriptor.setSortable(true); + descriptor.setFilterable(false); + + return descriptor; + } + + private LiveDataPropertyDescriptor getDisplayDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + "name")); + descriptor.setId(DISPLAY_FIELD); + descriptor.setType(STRING_TYPE); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor(HTML_DISPLAYER)); + // This is not visible because we don't want to keep that property and it's aiming at being removed. + descriptor.setVisible(false); + descriptor.setEditable(false); + descriptor.setSortable(false); + descriptor.setFilterable(false); + + return descriptor; + } + + private LiveDataPropertyDescriptor.FilterDescriptor createFilterList(List> options) + { + LiveDataPropertyDescriptor.FilterDescriptor filterList = + new LiveDataPropertyDescriptor.FilterDescriptor("list"); + filterList.setParameter("options", options); + String equalsOperator = "equals"; + // We do not want any other operator than equals + filterList.addOperator(equalsOperator, null); + filterList.setDefaultOperator(equalsOperator); + return filterList; + } + + private LiveDataPropertyDescriptor getScopeDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + SCOPE_FIELD)); + descriptor.setId(SCOPE_FIELD); + descriptor.setType(STRING_TYPE); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor(SCOPE_FIELD)); + descriptor.setFilter(createFilterList(Stream.of(Scope.values()) + .map(item -> Map.of( + VALUE_KEY, item.name(), + LABEL_KEY, this.translationHelper.getScopeTranslation(item) + )).toList())); + descriptor.setVisible(true); + descriptor.setEditable(false); + descriptor.setSortable(true); + descriptor.setFilterable(true); + + return descriptor; + } + + private LiveDataPropertyDescriptor getLocationDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + LOCATION_FIELD)); + descriptor.setId(LOCATION_FIELD); + descriptor.setType(STRING_TYPE); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor(HTML_DISPLAYER)); + descriptor.setVisible(true); + descriptor.setEditable(false); + descriptor.setSortable(true); + descriptor.setFilterable(true); + + return descriptor; + } + + private LiveDataPropertyDescriptor getFilterTypeDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + FILTER_TYPE_FIELD)); + descriptor.setId(FILTER_TYPE_FIELD); + descriptor.setType(STRING_TYPE); + descriptor.setFilter(createFilterList(Stream.of(NotificationFilterType.values()) + .map(item -> Map.of( + VALUE_KEY, item.name(), + LABEL_KEY, this.translationHelper.getFilterTypeTranslation(item) + )).toList())); + descriptor.setVisible(true); + descriptor.setEditable(false); + descriptor.setSortable(true); + descriptor.setFilterable(true); + + return descriptor; + } + + private LiveDataPropertyDescriptor getNotificationFormatsDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + NOTIFICATION_FORMATS_FIELD)); + descriptor.setId(NOTIFICATION_FORMATS_FIELD); + descriptor.setType(STRING_TYPE); + descriptor.setFilter(createFilterList(Stream.of(NotificationFormat.values()).map(item -> + Map.of( + VALUE_KEY, item.name(), + LABEL_KEY, this.translationHelper.getFormatTranslation(item) + )).toList())); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor(STATIC_LIST_DISPLAYER)); + descriptor.setVisible(true); + descriptor.setEditable(false); + descriptor.setSortable(true); + descriptor.setFilterable(true); + + return descriptor; + } + + private LiveDataPropertyDescriptor getEventTypesDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + EVENT_TYPES_FIELD)); + descriptor.setId(EVENT_TYPES_FIELD); + descriptor.setType(STRING_TYPE); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor(STATIC_LIST_DISPLAYER)); + List> options = new ArrayList<>(); + options.add(Map.of( + VALUE_KEY, ALL_EVENTS_OPTION_VALUE, + LABEL_KEY, this.translationHelper.getAllEventTypesTranslation())); + try { + options.addAll(this.translationHelper.getAllEventTypesOptions()); + } catch (LiveDataException e) { + this.logger.error("Cannot provide event filter options", e); + } + descriptor.setFilter(createFilterList(options)); + descriptor.setVisible(true); + descriptor.setEditable(false); + descriptor.setSortable(true); + descriptor.setFilterable(true); + + return descriptor; + } + + private LiveDataPropertyDescriptor getIsEnabledDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + IS_ENABLED_FIELD)); + descriptor.setId(IS_ENABLED_FIELD); + descriptor.setType(BOOLEAN); + descriptor.setFilter(new LiveDataPropertyDescriptor.FilterDescriptor(BOOLEAN)); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor("toggle")); + descriptor.setVisible(true); + descriptor.setEditable(false); + descriptor.setSortable(true); + descriptor.setFilterable(true); + + return descriptor; + } + + private LiveDataPropertyDescriptor getActionDescriptor() + { + LiveDataPropertyDescriptor actionDescriptor = new LiveDataPropertyDescriptor(); + actionDescriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + "_actions")); + actionDescriptor.setId(ACTIONS_FIELD); + LiveDataPropertyDescriptor.DisplayerDescriptor displayer = + new LiveDataPropertyDescriptor.DisplayerDescriptor(ACTIONS_FIELD); + displayer.setParameter(ACTIONS_FIELD, List.of(DELETE)); + actionDescriptor.setDisplayer(displayer); + actionDescriptor.setVisible(true); + actionDescriptor.setEditable(false); + actionDescriptor.setSortable(false); + actionDescriptor.setFilterable(false); + return actionDescriptor; + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataConfigurationResolver.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataConfigurationResolver.java new file mode 100644 index 000000000000..c074e4c3f34a --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataConfigurationResolver.java @@ -0,0 +1,54 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.custom; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataConfiguration; +import org.xwiki.livedata.LiveDataConfigurationResolver; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.internal.JSONMerge; + +/** + * Needed component for the live data configuration. + * + * @since 16.3.0RC1 + * @version $Id$ + */ +@Component +@Singleton +@Named(NotificationCustomFiltersLiveDataSource.NAME) +public class NotificationCustomFiltersLiveDataConfigurationResolver implements + LiveDataConfigurationResolver +{ + @Inject + @Named(NotificationCustomFiltersLiveDataSource.NAME) + private Provider notificationFiltersLiveDataConfigurationProvider; + + @Override + public LiveDataConfiguration resolve(LiveDataConfiguration input) throws LiveDataException + { + return new JSONMerge().merge(input, this.notificationFiltersLiveDataConfigurationProvider.get()); + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataEntryStore.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataEntryStore.java new file mode 100644 index 000000000000..69a2e56b207d --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataEntryStore.java @@ -0,0 +1,254 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.custom; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.script.ScriptContext; + +import org.apache.commons.lang3.StringUtils; +import org.xwiki.component.annotation.Component; +import org.xwiki.component.manager.ComponentManager; +import org.xwiki.livedata.LiveData; +import org.xwiki.livedata.LiveDataEntryStore; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataQuery; +import org.xwiki.model.EntityType; +import org.xwiki.model.reference.EntityReference; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.notifications.NotificationException; +import org.xwiki.notifications.filters.NotificationFilter; +import org.xwiki.notifications.filters.NotificationFilterManager; +import org.xwiki.notifications.filters.NotificationFilterPreference; +import org.xwiki.notifications.filters.internal.DefaultNotificationFilterPreference; +import org.xwiki.notifications.filters.internal.NotificationFilterPreferenceStore; +import org.xwiki.notifications.filters.internal.livedata.AbstractNotificationFilterLiveDataEntryStore; +import org.xwiki.query.QueryException; +import org.xwiki.rendering.block.Block; +import org.xwiki.rendering.renderer.BlockRenderer; +import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter; +import org.xwiki.rendering.renderer.printer.WikiPrinter; +import org.xwiki.script.ScriptContextManager; +import org.xwiki.template.TemplateManager; + +/** + * Dedicated {@link LiveDataEntryStore} for the {@link NotificationCustomFiltersLiveDataSource}. + * This component is in charge of performing the actual HQL queries to display the live data. + * + * @since 16.3.0RC1 + * @version $Id$ + */ +@Component +@Singleton +@Named(NotificationCustomFiltersLiveDataSource.NAME) +public class NotificationCustomFiltersLiveDataEntryStore extends AbstractNotificationFilterLiveDataEntryStore +{ + private static final String WIKI = "wiki"; + private static final String LOCATION_TEMPLATE = "notification/filters/livedatalocation.vm"; + + @Inject + private NotificationFilterPreferenceStore notificationFilterPreferenceStore; + + @Inject + private NotificationFilterManager notificationFilterManager; + + @Inject + @Named("context") + private ComponentManager componentManager; + + @Inject + @Named("html/5.0") + private BlockRenderer blockRenderer; + + @Inject + private TemplateManager templateManager; + + @Inject + private ScriptContextManager scriptContextManager; + + @Inject + private NotificationCustomFiltersQueryHelper queryHelper; + + @Override + public Optional> get(Object entryId) throws LiveDataException + { + Optional> result = Optional.empty(); + Optional filterPreferenceOpt; + try { + filterPreferenceOpt = this.notificationFilterPreferenceStore + .getFilterPreference(String.valueOf(entryId), getCurrentWikiReference()); + if (filterPreferenceOpt.isPresent()) { + DefaultNotificationFilterPreference filterPreference = + (DefaultNotificationFilterPreference) filterPreferenceOpt.get(); + checkAccessFilterPreference(filterPreference); + result = Optional.of(getPreferenceInformation(filterPreference)); + } + } catch (NotificationException e) { + throw new LiveDataException( + String.format("Error while retrieving LiveData entry for notification filter with id [%s]", + entryId), e); + } + + return result; + } + + private Map getPreferenceInformation(NotificationFilterPreference filterPreference) + throws LiveDataException + { + NotificationCustomFiltersLiveDataConfigurationProvider.Scope scope = getScope(filterPreference); + + Map result = new LinkedHashMap<>(); + // Map.of only accept 10 args + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.ID_FIELD, filterPreference.getId()); + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.EVENT_TYPES_FIELD, + this.displayEventTypes(filterPreference)); + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.NOTIFICATION_FORMATS_FIELD, + displayNotificationFormats(filterPreference.getNotificationFormats())); + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.SCOPE_FIELD, getScopeInfo(scope)); + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.LOCATION_FIELD, + this.displayLocation(filterPreference, scope)); + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.DISPLAY_FIELD, + this.renderDisplay(filterPreference)); + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.FILTER_TYPE_FIELD, + this.translationHelper.getFilterTypeTranslation(filterPreference.getFilterType())); + result.put("isEnabled_checked", filterPreference.isEnabled()); + result.put("isEnabled_disabled", filterPreference.getId().startsWith("watchlist_")); + result.put("isEnabled_data", Map.of( + "preferenceId", filterPreference.getId() + )); + // We don't care: if we access the LD we do have delete. + result.put(NotificationCustomFiltersLiveDataConfigurationProvider.DOC_HAS_DELETE_FIELD, true); + return result; + } + + private String displayLocation(NotificationFilterPreference filterPreference, + NotificationCustomFiltersLiveDataConfigurationProvider.Scope scope) + { + EntityReference location = switch (scope) { + case USER -> this.entityReferenceResolver.resolve(filterPreference.getUser(), EntityType.DOCUMENT); + case WIKI -> this.entityReferenceResolver.resolve(filterPreference.getWiki(), EntityType.WIKI); + case SPACE -> this.entityReferenceResolver.resolve(filterPreference.getPage(), EntityType.SPACE); + case PAGE -> + this.entityReferenceResolver.resolve(filterPreference.getPageOnly(), EntityType.DOCUMENT); + }; + // TODO: Create an improvment ticket for having a displayer + ScriptContext currentScriptContext = this.scriptContextManager.getCurrentScriptContext(); + currentScriptContext.setAttribute("location", location, ScriptContext.ENGINE_SCOPE); + return this.templateManager.renderNoException(LOCATION_TEMPLATE); + } + + private Map displayEventTypes(NotificationFilterPreference filterPreference) + { + List items; + if (filterPreference.getEventTypes().isEmpty()) { + items = List.of(this.translationHelper.getAllEventTypesTranslation()); + } else { + items = filterPreference.getEventTypes() + .stream() + .sorted(Comparator.naturalOrder()) + .map(eventType -> this.translationHelper.getEventTypeTranslation(eventType)) + .toList(); + } + + return getStaticListInfo(items); + } + + private Map getScopeInfo(NotificationCustomFiltersLiveDataConfigurationProvider.Scope scope) + { + String icon = switch (scope) { + case USER -> "user"; + case WIKI -> WIKI; + case SPACE -> "chart-organisation"; + case PAGE -> "page"; + }; + return Map.of("icon", icon, "name", this.translationHelper.getScopeTranslation(scope)); + } + + private String renderDisplay(NotificationFilterPreference filterPreference) throws LiveDataException + { + String result = ""; + WikiPrinter printer = new DefaultWikiPrinter(); + String missingComponentExceptionMessage = String.format("Cannot find NotificationFilter component for " + + "preference named [%s]", filterPreference.getFilterName()); + if (this.componentManager.hasComponent(NotificationFilter.class, filterPreference.getFilterName())) { + try { + NotificationFilter filter = + this.componentManager.getInstance(NotificationFilter.class, filterPreference.getFilterName()); + Block block = this.notificationFilterManager.displayFilter(filter, filterPreference); + blockRenderer.render(block, printer); + result = printer.toString(); + } catch (Exception e) { + throw new LiveDataException("Error while rendering a block for notification filter", e); + } + } else { + throw new LiveDataException(missingComponentExceptionMessage); + } + + return result; + } + + private NotificationCustomFiltersLiveDataConfigurationProvider.Scope getScope(NotificationFilterPreference + filterPreference) + { + if (!StringUtils.isBlank(filterPreference.getUser())) { + return NotificationCustomFiltersLiveDataConfigurationProvider.Scope.USER; + } else if (!StringUtils.isBlank(filterPreference.getPageOnly())) { + return NotificationCustomFiltersLiveDataConfigurationProvider.Scope.PAGE; + } else if (!StringUtils.isBlank(filterPreference.getPage())) { + return NotificationCustomFiltersLiveDataConfigurationProvider.Scope.SPACE; + } else { + return NotificationCustomFiltersLiveDataConfigurationProvider.Scope.WIKI; + } + } + + @Override + public LiveData get(LiveDataQuery query) throws LiveDataException + { + if (query.getOffset() > Integer.MAX_VALUE) { + throw new LiveDataException("Currently only integer offsets are supported."); + } + TargetInformation targetInformation = getTargetInformation(query); + String serializedOwner = this.entityReferenceSerializer.serialize(targetInformation.ownerReference); + WikiReference wikiReference = + new WikiReference(targetInformation.ownerReference.extractReference(EntityType.WIKI)); + + LiveData liveData = new LiveData(); + try { + long filterCount = this.queryHelper.countTotalFilters(query, serializedOwner, wikiReference); + List filterPreferences = + this.queryHelper.getFilterPreferences(query, serializedOwner, wikiReference); + liveData.setCount(filterCount); + List> entries = liveData.getEntries(); + for (NotificationFilterPreference notificationFilterPreference : filterPreferences) { + entries.add(getPreferenceInformation(notificationFilterPreference)); + } + } catch (QueryException e) { + throw new LiveDataException("Error when querying notification filter preferences", e); + } + return liveData; + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataPropertyDescriptorStore.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataPropertyDescriptorStore.java new file mode 100644 index 000000000000..24db20625abc --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataPropertyDescriptorStore.java @@ -0,0 +1,55 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.custom; + +import java.util.Collection; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataConfiguration; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataPropertyDescriptor; +import org.xwiki.livedata.LiveDataPropertyDescriptorStore; + +/** + * Descriptor for the {@link NotificationCustomFiltersLiveDataSource}. + * + * @since 16.3.0RC1 + * @version $Id$ + */ +@Component +@Singleton +@Named(NotificationCustomFiltersLiveDataSource.NAME) +public class NotificationCustomFiltersLiveDataPropertyDescriptorStore implements LiveDataPropertyDescriptorStore +{ + @Inject + @Named(NotificationCustomFiltersLiveDataSource.NAME) + private Provider liveDataConfigurationProvider; + + @Override + public Collection get() throws LiveDataException + { + return liveDataConfigurationProvider.get().getMeta().getPropertyDescriptors(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataSource.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataSource.java new file mode 100644 index 000000000000..688845b88be0 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataSource.java @@ -0,0 +1,63 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.custom; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataEntryStore; +import org.xwiki.livedata.LiveDataPropertyDescriptorStore; +import org.xwiki.livedata.LiveDataSource; + +/** + * Live data source for custom notification filters. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component +@Singleton +@Named(NotificationCustomFiltersLiveDataSource.NAME) +public class NotificationCustomFiltersLiveDataSource implements LiveDataSource +{ + static final String NAME = "notificationCustomFilters"; + + @Inject + @Named(NAME) + private LiveDataEntryStore notificationFiltersLiveDataEntryStore; + + @Inject + @Named(NAME) + private LiveDataPropertyDescriptorStore notificationFiltersLiveDataPropertyDescriptorStore; + + @Override + public LiveDataEntryStore getEntries() + { + return this.notificationFiltersLiveDataEntryStore; + } + + @Override + public LiveDataPropertyDescriptorStore getProperties() + { + return this.notificationFiltersLiveDataPropertyDescriptorStore; + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersQueryHelper.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersQueryHelper.java new file mode 100644 index 000000000000..0612fb155497 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersQueryHelper.java @@ -0,0 +1,344 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.notifications.filters.internal.livedata.custom; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.commons.lang3.StringUtils; +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataQuery; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.notifications.NotificationFormat; +import org.xwiki.notifications.filters.NotificationFilterPreference; +import org.xwiki.notifications.filters.NotificationFilterType; +import org.xwiki.query.Query; +import org.xwiki.query.QueryException; +import org.xwiki.query.QueryManager; +import org.xwiki.query.internal.DefaultQueryParameter; + +/** + * Helper to perform DB queries for retrieving notification custom filters with live data filters and sort. + * TODO: maybe this could be improved with some APIs to put in NotificationFilterPreferenceStore. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component(roles = NotificationCustomFiltersQueryHelper.class) +@Singleton +public class NotificationCustomFiltersQueryHelper +{ + private static final String BASE_QUERY = "select nfp from DefaultNotificationFilterPreference nfp " + + "where owner = :owner"; + private static final String OWNER_BINDING = "owner"; + private static final String STARTS_WITH_OPERATOR = "startsWith"; + private static final String CONTAINS_OPERATOR = "contains"; + private static final String EQUALS_OPERATOR = "equals"; + private static final String AND = " and "; + + @Inject + private QueryManager queryManager; + + private static final class FiltersHQLQuery + { + private String whereClause; + private final Map bindings = new LinkedHashMap<>(); + } + + private Optional handleFilter(List queryFilters) + { + FiltersHQLQuery result = new FiltersHQLQuery(); + List queryWhereClauses = new ArrayList<>(); + for (LiveDataQuery.Filter queryFilter : queryFilters) { + switch (queryFilter.getProperty()) { + case NotificationCustomFiltersLiveDataConfigurationProvider.IS_ENABLED_FIELD -> + this.handleIsEnabledFilter(queryFilter, queryWhereClauses); + + case NotificationCustomFiltersLiveDataConfigurationProvider.NOTIFICATION_FORMATS_FIELD -> + this.handleNotificationFormatsFilter(queryFilter, queryWhereClauses); + + case NotificationCustomFiltersLiveDataConfigurationProvider.SCOPE_FIELD -> + this.handleScopeFilter(queryFilter, queryWhereClauses); + + case NotificationCustomFiltersLiveDataConfigurationProvider.FILTER_TYPE_FIELD -> + this.handleFilterTypeFilter(queryFilter, queryWhereClauses, result); + + case NotificationCustomFiltersLiveDataConfigurationProvider.LOCATION_FIELD -> + this.handleLocationFilter(queryFilter, queryWhereClauses, result); + + case NotificationCustomFiltersLiveDataConfigurationProvider.EVENT_TYPES_FIELD -> + this.handleEventTypeFilter(queryFilter, queryWhereClauses, result); + + default -> { + } + } + } + if (!queryWhereClauses.isEmpty()) { + result.whereClause = queryWhereClauses.stream().collect(Collectors.joining(AND, AND, "")); + return Optional.of(result); + } else { + return Optional.empty(); + } + } + + private void handleFilterTypeFilter(LiveDataQuery.Filter queryFilter, List queryWhereClauses, + FiltersHQLQuery result) + { + // We authorize only a single constraint here + LiveDataQuery.Constraint constraint = queryFilter.getConstraints().get(0); + if (EQUALS_OPERATOR.equals(constraint.getOperator())) { + queryWhereClauses.add("nfp.filterType = :filterType"); + result.bindings.put("filterType", + NotificationFilterType.valueOf(String.valueOf(constraint.getValue()))); + } + } + + private void handleScopeFilter(LiveDataQuery.Filter queryFilter, List queryWhereClauses) + { + // We authorize only a single constraint here + LiveDataQuery.Constraint constraint = queryFilter.getConstraints().get(0); + if (EQUALS_OPERATOR.equals(constraint.getOperator()) + && !StringUtils.isBlank(String.valueOf(constraint.getValue()))) { + NotificationCustomFiltersLiveDataConfigurationProvider.Scope scope = + NotificationCustomFiltersLiveDataConfigurationProvider.Scope.valueOf( + String.valueOf(constraint.getValue())); + queryWhereClauses.add(String.format("length(nfp.%s) > 0", scope.getFieldName())); + } + } + + private void handleNotificationFormatsFilter(LiveDataQuery.Filter queryFilter, List queryWhereClauses) + { + // We authorize only a single constraint here + LiveDataQuery.Constraint constraint = queryFilter.getConstraints().get(0); + String constraintValue = String.valueOf(constraint.getValue()); + if (EQUALS_OPERATOR.equals(constraint.getOperator()) + && !StringUtils.isEmpty(constraintValue)) { + if (NotificationFormat.ALERT.name().equals(constraintValue)) { + queryWhereClauses.add("nfp.alertEnabled = 1"); + } + if (NotificationFormat.EMAIL.name().equals(constraintValue)) { + queryWhereClauses.add("nfp.emailEnabled = 1"); + } + } + } + + private void handleIsEnabledFilter(LiveDataQuery.Filter queryFilter, List queryWhereClauses) + { + // We only check first constraint: if there's more they are either redundant or contradictory. + LiveDataQuery.Constraint constraint = queryFilter.getConstraints().get(0); + if (Boolean.parseBoolean(String.valueOf(constraint.getValue()))) { + queryWhereClauses.add("nfp.enabled = 1"); + } else { + queryWhereClauses.add("nfp.enabled = 0"); + } + } + + private void handleEventTypeFilter(LiveDataQuery.Filter queryFilter, List queryWhereClauses, + FiltersHQLQuery result) + { + // We authorize only a single constraint here + LiveDataQuery.Constraint constraint = queryFilter.getConstraints().get(0); + if (EQUALS_OPERATOR.equals(constraint.getOperator())) { + if (NotificationCustomFiltersLiveDataConfigurationProvider.ALL_EVENTS_OPTION_VALUE.equals( + constraint.getValue())) + { + queryWhereClauses.add("length(nfp.allEventTypes) = 0"); + } else { + queryWhereClauses.add("nfp.allEventTypes like :eventTypes"); + DefaultQueryParameter queryParameter = new DefaultQueryParameter(null); + queryParameter.anyChars().literal(String.valueOf(constraint.getValue())).anyChars(); + result.bindings.put("eventTypes", queryParameter); + } + } + } + + private void handleLocationFilter(LiveDataQuery.Filter locationFilter, List queryWhereClauses, + FiltersHQLQuery result) + { + List clauses = new ArrayList<>(); + int clauseCounter = 0; + + // for searching in location we actually need to look in 4 columns in DB, so we reuse same constraint binding + // in those 4 columns, and since we might have multiple constraints we use a counter to name those bindings + String clauseValue = "(nfp.pageOnly like :constraint_%1$s or nfp.page like :constraint_%1$s or " + + "nfp.wiki like :constraint_%1$s or nfp.user like :constraint_%1$s)"; + String constraintName = "constraint_%s"; + + for (LiveDataQuery.Constraint constraint : locationFilter.getConstraints()) { + if (EQUALS_OPERATOR.equals(constraint.getOperator())) { + clauses.add(String.format("(nfp.pageOnly = :constraint_%1$s or nfp.page = :constraint_%1$s or " + + "nfp.wiki = :constraint_%1$s or nfp.user = :constraint_%1$s)", clauseCounter)); + DefaultQueryParameter queryParameter = new DefaultQueryParameter(null); + queryParameter.literal(String.valueOf(constraint.getValue())); + result.bindings.put(String.format(constraintName, clauseCounter), queryParameter); + } else if (STARTS_WITH_OPERATOR.equals(constraint.getOperator())) { + clauses.add(String.format(clauseValue, clauseCounter)); + DefaultQueryParameter queryParameter = new DefaultQueryParameter(null); + queryParameter.literal(String.valueOf(constraint.getValue())).anyChars(); + result.bindings.put(String.format(constraintName, clauseCounter), queryParameter); + } else if (CONTAINS_OPERATOR.equals(constraint.getOperator())) { + clauses.add(String.format(clauseValue, clauseCounter)); + DefaultQueryParameter queryParameter = new DefaultQueryParameter(null); + queryParameter.anyChars().literal(String.valueOf(constraint.getValue())).anyChars(); + result.bindings.put(String.format(constraintName, clauseCounter), queryParameter); + } + clauseCounter++; + } + if (!clauses.isEmpty()) { + queryWhereClauses.add(buildQueryClause(locationFilter, clauses)); + } + } + + private String buildQueryClause(LiveDataQuery.Filter filter, List clauses) + { + String operatorAppender; + if (filter.isMatchAll()) { + operatorAppender = AND; + } else { + operatorAppender = " or "; + } + return clauses.stream().collect(Collectors.joining(operatorAppender, "(", ")")); + } + + private Query getHQLQuery(LiveDataQuery query, boolean isCount, String owner, WikiReference wikiReference) + throws QueryException, LiveDataException + { + String baseQuery = (isCount) ? "select count(nfp.id) " + + "from DefaultNotificationFilterPreference nfp where owner = :owner" : BASE_QUERY; + Optional optionalFiltersHQLQuery = handleFilter(query.getFilters()); + if (optionalFiltersHQLQuery.isPresent()) { + baseQuery += optionalFiltersHQLQuery.get().whereClause; + } + if (!isCount) { + baseQuery += handleSortEntries(query.getSort()); + } + Query hqlQuery = this.queryManager.createQuery(baseQuery, Query.HQL) + .bindValue(OWNER_BINDING, owner) + .setWiki(wikiReference.getName()); + if (optionalFiltersHQLQuery.isPresent()) { + for (Map.Entry binding : optionalFiltersHQLQuery.get().bindings.entrySet()) { + hqlQuery = hqlQuery.bindValue(binding.getKey(), binding.getValue()); + } + } + return hqlQuery; + } + + /** + * Count the total number of filter preferences for given owner on given wiki. + * + * @param query the query to use for applying the filters + * @param owner the owner for which to count all filter preferences + * @param wikiReference the wiki where to count all filter preferences + * @return the total number of filter preferences + * @throws QueryException in case of problem to perform the query + * @throws LiveDataException in case of problem with the livedata query + */ + public long countTotalFilters(LiveDataQuery query, String owner, WikiReference wikiReference) + throws QueryException, LiveDataException + { + return getHQLQuery(query, true, owner, wikiReference) + .execute() + .get(0); + } + + private String handleSortEntries(List sortEntries) throws LiveDataException + { + List clauses = new ArrayList<>(); + for (LiveDataQuery.SortEntry sortEntry : sortEntries) { + String sortOperator = (sortEntry.isDescending()) ? "desc" : "asc"; + String clause = switch (sortEntry.getProperty()) { + case NotificationCustomFiltersLiveDataConfigurationProvider.IS_ENABLED_FIELD -> + String.format("nfp.enabled %s", sortOperator); + + case NotificationCustomFiltersLiveDataConfigurationProvider.NOTIFICATION_FORMATS_FIELD -> + handleNotificationFormatSort(sortEntry); + + case NotificationCustomFiltersLiveDataConfigurationProvider.LOCATION_FIELD, + NotificationCustomFiltersLiveDataConfigurationProvider.SCOPE_FIELD -> + handleLocationSort(sortEntry); + + case NotificationCustomFiltersLiveDataConfigurationProvider.FILTER_TYPE_FIELD -> + String.format("nfp.filterType %s", sortOperator); + + case NotificationCustomFiltersLiveDataConfigurationProvider.EVENT_TYPES_FIELD -> + String.format("nfp.allEventTypes %s", sortOperator); + + case NotificationCustomFiltersLiveDataConfigurationProvider.ID_FIELD -> + String.format("nfp.internalId %s", sortOperator); + + default -> throw new LiveDataException("Unexpected sort value: " + sortEntry.getProperty()); + }; + clauses.add(clause); + } + if (!clauses.isEmpty()) { + return clauses.stream().collect(Collectors.joining(", ", " order by ", "")); + } else { + return ""; + } + } + + private String handleLocationSort(LiveDataQuery.SortEntry sortEntry) + { + if (sortEntry.isDescending()) { + return "nfp.user desc, nfp.wiki desc, nfp.page desc, nfp.pageOnly desc"; + } else { + return "nfp.pageOnly asc, nfp.page asc, nfp.wiki asc, nfp.user asc"; + } + } + + private String handleNotificationFormatSort(LiveDataQuery.SortEntry sortEntry) + { + // We use on purpose asc/desc in combination to ensure having both formats in the middle of the data + // since it's the most common: by doing that we'll have [only alerts, both formats, only emails] + if (sortEntry.isDescending()) { + return "nfp.emailEnabled asc, nfp.alertEnabled desc"; + } else { + return "nfp.alertEnabled asc, nfp.emailEnabled desc"; + } + } + + /** + * Retrieve all filter preferences matching the live data query for the given owner on the given wiki. + * + * @param query the livedata query containing filters and sort criteria + * @param owner the owner of the filter preferences to retrieve + * @param wikiReference the wiki where to retrieve the filter preferences + * @return the list of filter preferences + * @throws QueryException in case of problem to perform the query + * @throws LiveDataException in case of problem with the livedata query + */ + public List getFilterPreferences(LiveDataQuery query, String owner, + WikiReference wikiReference) throws QueryException, LiveDataException + { + return getHQLQuery(query, false, owner, wikiReference) + .setLimit(query.getLimit()) + .setOffset(query.getOffset().intValue()) + .execute(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataConfigurationProvider.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataConfigurationProvider.java new file mode 100644 index 000000000000..2cdba1c3d2ef --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataConfigurationProvider.java @@ -0,0 +1,132 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.system; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataConfiguration; +import org.xwiki.livedata.LiveDataEntryDescriptor; +import org.xwiki.livedata.LiveDataMeta; +import org.xwiki.livedata.LiveDataPaginationConfiguration; +import org.xwiki.livedata.LiveDataPropertyDescriptor; +import org.xwiki.localization.ContextualLocalizationManager; + +/** + * Configuration of the {@link NotificationSystemFiltersLiveDataSource}. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component +@Singleton +@Named(NotificationSystemFiltersLiveDataSource.NAME) +public class NotificationSystemFiltersLiveDataConfigurationProvider implements Provider +{ + static final String NAME_FIELD = "name"; + static final String DESCRIPTION_FIELD = "filterDescription"; + static final String NOTIFICATION_FORMATS_FIELD = "notificationFormats"; + static final String IS_ENABLED_FIELD = "isEnabled"; + private static final String TRANSLATION_PREFIX = "notifications.settings.filters.preferences.system.table."; + private static final String STRING_TYPE = "String"; + + @Inject + private ContextualLocalizationManager l10n; + + @Override + public LiveDataConfiguration get() + { + LiveDataConfiguration input = new LiveDataConfiguration(); + LiveDataMeta meta = new LiveDataMeta(); + input.setMeta(meta); + + LiveDataPaginationConfiguration pagination = new LiveDataPaginationConfiguration(); + pagination.setShowPageSizeDropdown(false); + meta.setPagination(pagination); + + LiveDataEntryDescriptor entryDescriptor = new LiveDataEntryDescriptor(); + entryDescriptor.setIdProperty(NAME_FIELD); + meta.setEntryDescriptor(entryDescriptor); + + meta.setPropertyDescriptors(List.of( + getNameDescriptor(), + getDescriptionDescriptor(), + getNotificationFormatsDescriptor(), + getIsEnabledDescriptor() + )); + + return input; + } + + private void setDescriptorValues(LiveDataPropertyDescriptor descriptor) + { + descriptor.setVisible(true); + descriptor.setEditable(false); + descriptor.setSortable(false); + descriptor.setFilterable(false); + } + + private LiveDataPropertyDescriptor getNameDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + NAME_FIELD)); + descriptor.setId(NAME_FIELD); + descriptor.setType(STRING_TYPE); + this.setDescriptorValues(descriptor); + return descriptor; + } + + private LiveDataPropertyDescriptor getDescriptionDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + DESCRIPTION_FIELD)); + descriptor.setId(DESCRIPTION_FIELD); + descriptor.setType(STRING_TYPE); + this.setDescriptorValues(descriptor); + return descriptor; + } + + private LiveDataPropertyDescriptor getNotificationFormatsDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + NOTIFICATION_FORMATS_FIELD)); + descriptor.setId(NOTIFICATION_FORMATS_FIELD); + descriptor.setType(STRING_TYPE); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor("staticList")); + this.setDescriptorValues(descriptor); + return descriptor; + } + + private LiveDataPropertyDescriptor getIsEnabledDescriptor() + { + LiveDataPropertyDescriptor descriptor = new LiveDataPropertyDescriptor(); + descriptor.setName(this.l10n.getTranslationPlain(TRANSLATION_PREFIX + IS_ENABLED_FIELD)); + descriptor.setId(IS_ENABLED_FIELD); + descriptor.setType("boolean"); + descriptor.setDisplayer(new LiveDataPropertyDescriptor.DisplayerDescriptor("toggle")); + this.setDescriptorValues(descriptor); + return descriptor; + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataConfigurationResolver.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataConfigurationResolver.java new file mode 100644 index 000000000000..45a566b83199 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataConfigurationResolver.java @@ -0,0 +1,54 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.system; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataConfiguration; +import org.xwiki.livedata.LiveDataConfigurationResolver; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.internal.JSONMerge; + +/** + * Needed component for the live data configuration. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component +@Singleton +@Named(NotificationSystemFiltersLiveDataSource.NAME) +public class NotificationSystemFiltersLiveDataConfigurationResolver implements + LiveDataConfigurationResolver +{ + @Inject + @Named(NotificationSystemFiltersLiveDataSource.NAME) + private Provider notificationFiltersLiveDataConfigurationProvider; + + @Override + public LiveDataConfiguration resolve(LiveDataConfiguration input) throws LiveDataException + { + return new JSONMerge().merge(input, this.notificationFiltersLiveDataConfigurationProvider.get()); + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataEntryStore.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataEntryStore.java new file mode 100644 index 000000000000..9e23a4fb10d4 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataEntryStore.java @@ -0,0 +1,161 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.system; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveData; +import org.xwiki.livedata.LiveDataEntryStore; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataQuery; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.LocalDocumentReference; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.notifications.NotificationException; +import org.xwiki.notifications.filters.NotificationFilter; +import org.xwiki.notifications.filters.NotificationFilterManager; +import org.xwiki.notifications.filters.internal.FilterPreferencesModelBridge; +import org.xwiki.notifications.filters.internal.ToggleableNotificationFilter; +import org.xwiki.notifications.filters.internal.ToggleableNotificationFilterActivation; +import org.xwiki.notifications.filters.internal.livedata.AbstractNotificationFilterLiveDataEntryStore; + +/** + * Dedicated {@link LiveDataEntryStore} for the {@link NotificationSystemFiltersLiveDataSource}. + * This component is in charge of performing the actual HQL queries to display the live data. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component +@Singleton +@Named(NotificationSystemFiltersLiveDataSource.NAME) +public class NotificationSystemFiltersLiveDataEntryStore extends AbstractNotificationFilterLiveDataEntryStore +{ + private static final LocalDocumentReference NOTIFICATION_ADMINISTRATION_REF = + new LocalDocumentReference(List.of("XWiki", "Notifications", "Code"), "NotificationAdministration"); + + @Inject + private NotificationFilterManager notificationFilterManager; + + @Inject + private FilterPreferencesModelBridge filterPreferencesModelBridge; + + @Override + public Optional> get(Object entryId) throws LiveDataException + { + // We don't need to retrieve a single element for now. + return Optional.empty(); + } + + private Map getPreferencesInformation(ToggleableNotificationFilter notificationFilter, + ToggleableNotificationFilterActivation filterActivation) + { + return Map.of( + NotificationSystemFiltersLiveDataConfigurationProvider.NAME_FIELD, + this.translationHelper.getTranslationWithPrefix("notifications.filters.name.", + notificationFilter.getName()), + NotificationSystemFiltersLiveDataConfigurationProvider.DESCRIPTION_FIELD, + this.translationHelper.getTranslationWithPrefix("notifications.filters.description.", + notificationFilter.getName()), + NotificationSystemFiltersLiveDataConfigurationProvider.NOTIFICATION_FORMATS_FIELD, + displayNotificationFormats(notificationFilter.getFormats()), + // We don't need to return isEnabled, but only the metadata defined below. + "isEnabled_data", + displayIsEnabledData(notificationFilter, filterActivation), + "isEnabled_checked", + isEnabled(notificationFilter, filterActivation) + ); + } + + private String getObjectNumber(ToggleableNotificationFilterActivation filterActivation) + { + return (filterActivation != null && filterActivation.getObjectNumber() != -1) + ? String.valueOf(filterActivation.getObjectNumber()) : ""; + } + + private Map displayIsEnabledData(NotificationFilter notificationFilter, + ToggleableNotificationFilterActivation filterActivation) + { + return Map.of( + "objectNumber", getObjectNumber(filterActivation), + "filterName", notificationFilter.getName() + ); + } + + private boolean isEnabled(ToggleableNotificationFilter notificationFilter, + ToggleableNotificationFilterActivation filterActivation) + { + return (filterActivation != null && filterActivation.isEnabled()) + || (filterActivation == null && notificationFilter.isEnabledByDefault()); + } + + @Override + public LiveData get(LiveDataQuery query) throws LiveDataException + { + TargetInformation targetInformation = getTargetInformation(query); + Map filtersActivations; + + Collection notificationFilters; + try { + if (targetInformation.isWikiTarget) { + WikiReference wikiReference = new WikiReference(targetInformation.ownerReference); + notificationFilters = this.notificationFilterManager.getAllFilters(wikiReference); + filtersActivations = + this.filterPreferencesModelBridge.getToggleableFilterActivations( + new DocumentReference(NOTIFICATION_ADMINISTRATION_REF, wikiReference)); + } else { + DocumentReference userDoc = new DocumentReference(targetInformation.ownerReference); + notificationFilters = this.notificationFilterManager.getAllFilters(userDoc, false); + filtersActivations = this.filterPreferencesModelBridge.getToggleableFilterActivations(userDoc); + } + } catch (NotificationException e) { + throw new LiveDataException("Error when getting list of filters", e); + } + + List allFilters = notificationFilters.stream() + .filter(filter -> filter instanceof ToggleableNotificationFilter) + .map(item -> (ToggleableNotificationFilter) item) + .toList(); + + int totalFilters = allFilters.size(); + LiveData liveData = new LiveData(); + liveData.setCount(totalFilters); + List> entries = liveData.getEntries(); + + allFilters + .stream() + .sorted(Comparator.comparing(NotificationFilter::getName)) + .skip(query.getOffset()) + .limit(query.getLimit()) + .forEach(filter -> + entries.add(getPreferencesInformation(filter, filtersActivations.get(filter.getName())))); + + return liveData; + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataPropertyDescriptorStore.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataPropertyDescriptorStore.java new file mode 100644 index 000000000000..af86635a3c61 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataPropertyDescriptorStore.java @@ -0,0 +1,55 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.system; + +import java.util.Collection; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataConfiguration; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataPropertyDescriptor; +import org.xwiki.livedata.LiveDataPropertyDescriptorStore; + +/** + * Descriptor for the {@link NotificationSystemFiltersLiveDataSource}. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component +@Singleton +@Named(NotificationSystemFiltersLiveDataSource.NAME) +public class NotificationSystemFiltersLiveDataPropertyDescriptorStore implements LiveDataPropertyDescriptorStore +{ + @Inject + @Named(NotificationSystemFiltersLiveDataSource.NAME) + private Provider liveDataConfigurationProvider; + + @Override + public Collection get() throws LiveDataException + { + return liveDataConfigurationProvider.get().getMeta().getPropertyDescriptors(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataSource.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataSource.java new file mode 100644 index 000000000000..9ea8d93cec7b --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataSource.java @@ -0,0 +1,63 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.notifications.filters.internal.livedata.system; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.livedata.LiveDataEntryStore; +import org.xwiki.livedata.LiveDataPropertyDescriptorStore; +import org.xwiki.livedata.LiveDataSource; + +/** + * Live data source for notification system filter preferences. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@Component +@Singleton +@Named(NotificationSystemFiltersLiveDataSource.NAME) +public class NotificationSystemFiltersLiveDataSource implements LiveDataSource +{ + static final String NAME = "notificationSystemFilters"; + + @Inject + @Named(NAME) + private LiveDataEntryStore notificationFiltersLiveDataEntryStore; + + @Inject + @Named(NAME) + private LiveDataPropertyDescriptorStore notificationFiltersLiveDataPropertyDescriptorStore; + + @Override + public LiveDataEntryStore getEntries() + { + return this.notificationFiltersLiveDataEntryStore; + } + + @Override + public LiveDataPropertyDescriptorStore getProperties() + { + return this.notificationFiltersLiveDataPropertyDescriptorStore; + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/resources/META-INF/components.txt index 15b7154af1d4..0f2cb49877b7 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/resources/META-INF/components.txt +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/main/resources/META-INF/components.txt @@ -1,15 +1,27 @@ +org.xwiki.notifications.filters.internal.event.NotificationFilterPreferenceEventConverter +org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersLiveDataConfigurationProvider +org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersLiveDataConfigurationResolver +org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersLiveDataEntryStore +org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersLiveDataPropertyDescriptorStore +org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersLiveDataSource +org.xwiki.notifications.filters.internal.livedata.custom.NotificationCustomFiltersQueryHelper +org.xwiki.notifications.filters.internal.livedata.system.NotificationSystemFiltersLiveDataConfigurationProvider +org.xwiki.notifications.filters.internal.livedata.system.NotificationSystemFiltersLiveDataConfigurationResolver +org.xwiki.notifications.filters.internal.livedata.system.NotificationSystemFiltersLiveDataEntryStore +org.xwiki.notifications.filters.internal.livedata.system.NotificationSystemFiltersLiveDataPropertyDescriptorStore +org.xwiki.notifications.filters.internal.livedata.system.NotificationSystemFiltersLiveDataSource +org.xwiki.notifications.filters.internal.livedata.NotificationFilterLiveDataTranslationHelper +org.xwiki.notifications.filters.internal.migrators.NotificationFilterPreferencesMigrator +org.xwiki.notifications.filters.internal.migrators.ScopeNotificationFilterClassMigrator org.xwiki.notifications.filters.internal.CachedModelBridgeInvalidatorListener org.xwiki.notifications.filters.internal.DefaultFilterPreferencesModelBridge org.xwiki.notifications.filters.internal.DocumentMovedListener org.xwiki.notifications.filters.internal.NotificationFilterPreferenceStore org.xwiki.notifications.filters.internal.ToggleableFilterPreferenceDocumentInitializer +org.xwiki.notifications.filters.internal.UserAddedEventListener org.xwiki.notifications.filters.internal.WikiNotificationFilterDisplayer org.xwiki.notifications.filters.internal.WikiNotificationFilterDisplayerComponentBuilder org.xwiki.notifications.filters.internal.WikiNotificationFilterDisplayerDocumentInitializer -org.xwiki.notifications.filters.internal.event.NotificationFilterPreferenceEventConverter -org.xwiki.notifications.filters.internal.migrators.NotificationFilterPreferencesMigrator -org.xwiki.notifications.filters.internal.migrators.ScopeNotificationFilterClassMigrator -org.xwiki.notifications.filters.internal.UserAddedEventListener org.xwiki.notifications.filters.migration.R140401000XWIKI15460DataMigration org.xwiki.notifications.filters.migration.R151002000XWIKI21448DataMigration org.xwiki.notifications.filters.migration.R160000000XWIKI17243DataMigration diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridgeTest.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridgeTest.java index cc96ec506a0d..b1b724afcca8 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridgeTest.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/DefaultFilterPreferencesModelBridgeTest.java @@ -235,8 +235,10 @@ void getToggleableFilterActivations() throws Exception BaseObject filter4Obj = mock(BaseObject.class, "filter4Obj"); when(userDoc.getXObject(TOGGLEABLE_FILTER_PREFERENCE_CLASS, FIELD_FILTER_NAME, "filter2", false)).thenReturn(filter2Obj); + when(filter2Obj.getNumber()).thenReturn(2); when(userDoc.getXObject(TOGGLEABLE_FILTER_PREFERENCE_CLASS, FIELD_FILTER_NAME, "filter4", false)).thenReturn(filter4Obj); + when(filter4Obj.getNumber()).thenReturn(14); when(filter2Obj.getIntValue(FIELD_IS_ENABLED, 1)).thenReturn(1); when(filter4Obj.getIntValue(FIELD_IS_ENABLED, 1)).thenReturn(0); @@ -245,19 +247,21 @@ void getToggleableFilterActivations() throws Exception BaseObject filter7Obj = mock(BaseObject.class, "filter7Obj"); when(userDoc.getXObject(TOGGLEABLE_FILTER_PREFERENCE_CLASS, FIELD_FILTER_NAME, "filter6", false)).thenReturn(filter6Obj); + when(filter6Obj.getNumber()).thenReturn(16); when(userDoc.getXObject(TOGGLEABLE_FILTER_PREFERENCE_CLASS, FIELD_FILTER_NAME, "filter7", false)).thenReturn(filter7Obj); + when(filter7Obj.getNumber()).thenReturn(17); when(filter6Obj.getIntValue(FIELD_IS_ENABLED, 0)).thenReturn(1); when(filter7Obj.getIntValue(FIELD_IS_ENABLED, 0)).thenReturn(0); - Map expectedResult = Map.of( - "filter2", true, - "filter3", true, - "filter4", false, - "filter5", false, - "filter6", true, - "filter7", false + Map expectedResult = Map.of( + "filter2", new ToggleableNotificationFilterActivation("filter2", true, user, 2), + "filter3", new ToggleableNotificationFilterActivation("filter3", true, user, -1), + "filter4", new ToggleableNotificationFilterActivation("filter4", false, user, 14), + "filter5", new ToggleableNotificationFilterActivation("filter5", false, user, -1), + "filter6", new ToggleableNotificationFilterActivation("filter6", true, user, 16), + "filter7", new ToggleableNotificationFilterActivation("filter7", false, user, 17) ); assertEquals(expectedResult, this.defaultModelBridge.getToggleableFilterActivations(user)); verify(context).setWikiReference(user.getWikiReference()); diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataEntryStoreTest.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataEntryStoreTest.java new file mode 100644 index 000000000000..54a9c8efacb9 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersLiveDataEntryStoreTest.java @@ -0,0 +1,484 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.notifications.filters.internal.livedata.custom; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.inject.Named; +import javax.inject.Provider; +import javax.script.ScriptContext; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xwiki.component.manager.ComponentManager; +import org.xwiki.livedata.LiveData; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataQuery; +import org.xwiki.model.EntityType; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.EntityReference; +import org.xwiki.model.reference.SpaceReference; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.notifications.NotificationException; +import org.xwiki.notifications.NotificationFormat; +import org.xwiki.notifications.filters.NotificationFilter; +import org.xwiki.notifications.filters.NotificationFilterManager; +import org.xwiki.notifications.filters.NotificationFilterPreference; +import org.xwiki.notifications.filters.NotificationFilterType; +import org.xwiki.notifications.filters.internal.DefaultNotificationFilterPreference; +import org.xwiki.notifications.filters.internal.NotificationFilterPreferenceStore; +import org.xwiki.notifications.filters.internal.livedata.NotificationFilterLiveDataTranslationHelper; +import org.xwiki.query.QueryException; +import org.xwiki.rendering.block.Block; +import org.xwiki.rendering.renderer.BlockRenderer; +import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter; +import org.xwiki.script.ScriptContextManager; +import org.xwiki.security.authorization.ContextualAuthorizationManager; +import org.xwiki.security.authorization.Right; +import org.xwiki.template.TemplateManager; +import org.xwiki.test.annotation.BeforeComponent; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; +import org.xwiki.test.mockito.MockitoComponentManager; + +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.test.reference.ReferenceComponentList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link NotificationCustomFiltersLiveDataEntryStore}. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@ComponentTest +@ReferenceComponentList +class NotificationCustomFiltersLiveDataEntryStoreTest +{ + @InjectMockComponents + private NotificationCustomFiltersLiveDataEntryStore entryStore; + + @MockComponent + private ContextualAuthorizationManager contextualAuthorizationManager; + + @MockComponent + private NotificationFilterLiveDataTranslationHelper translationHelper; + + @MockComponent + private Provider contextProvider; + + @MockComponent + private NotificationFilterPreferenceStore notificationFilterPreferenceStore; + + @MockComponent + private NotificationFilterManager notificationFilterManager; + + @MockComponent + @Named("html/5.0") + private BlockRenderer blockRenderer; + + @MockComponent + private TemplateManager templateManager; + + @MockComponent + private ScriptContextManager scriptContextManager; + + @MockComponent + private NotificationCustomFiltersQueryHelper queryHelper; + + private XWikiContext context; + + @BeforeComponent + void beforeComponent(MockitoComponentManager componentManager) throws Exception + { + componentManager.registerComponent(ComponentManager.class, "context", componentManager); + } + + @BeforeEach + void beforeEach() + { + this.context = mock(XWikiContext.class); + when(this.contextProvider.get()).thenReturn(this.context); + + when(this.translationHelper.getFormatTranslation(any())) + .thenAnswer(invocationOnMock -> "Format " + invocationOnMock.getArgument(0)); + when(this.translationHelper.getScopeTranslation(any())) + .thenAnswer(invocationOnMock -> "Scope " + invocationOnMock.getArgument(0)); + when(this.translationHelper.getFilterTypeTranslation(any())) + .thenAnswer(invocationOnMock -> "FilterType " + invocationOnMock.getArgument(0)); + when(this.translationHelper.getAllEventTypesTranslation()).thenReturn("All event types"); + when(this.translationHelper.getEventTypeTranslation(any())) + .thenAnswer(invocationOnMock -> "EventType " + invocationOnMock.getArgument(0)); + } + + @Test + void getMissingTarget() + { + LiveDataQuery query = mock(LiveDataQuery.class); + LiveDataQuery.Source source = mock(LiveDataQuery.Source.class); + when(query.getSource()).thenReturn(source); + when(source.getParameters()).thenReturn(Map.of()); + LiveDataException liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(query)); + assertEquals("The target source parameter is mandatory.", liveDataException.getMessage()); + } + + @Test + void getBadAuthorization() throws LiveDataException + { + LiveDataQuery query = mock(LiveDataQuery.class); + LiveDataQuery.Source source = mock(LiveDataQuery.Source.class); + when(query.getSource()).thenReturn(source); + when(source.getParameters()).thenReturn(Map.of( + "target", "wiki", + "wiki", "foo" + )); + DocumentReference userDoc = new DocumentReference("xwiki", "XWiki", "Foo"); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + when(context.getUserReference()).thenReturn(userDoc); + LiveDataException liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(query)); + assertEquals("You don't have rights to access those information.", liveDataException.getMessage()); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(true); + LiveData emptyLiveData = new LiveData(); + assertEquals(emptyLiveData, this.entryStore.get(query)); + + when(source.getParameters()).thenReturn(Map.of( + "target", "user", + "user", "xwiki:XWiki.Bar" + )); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(query)); + assertEquals("You don't have rights to access those information.", liveDataException.getMessage()); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(true); + assertEquals(emptyLiveData, this.entryStore.get(query)); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + when(source.getParameters()).thenReturn(Map.of( + "target", "user", + "user", "xwiki:XWiki.Foo" + )); + assertEquals(emptyLiveData, this.entryStore.get(query)); + } + + @Test + void getEntry(MockitoComponentManager componentManager) throws Exception + { + String entryId = "entryId"; + WikiReference wikiReference = new WikiReference("foo"); + when(this.context.getWikiReference()).thenReturn(wikiReference); + assertEquals(Optional.empty(), this.entryStore.get(entryId)); + DefaultNotificationFilterPreference notificationFilterPreference = + mock(DefaultNotificationFilterPreference.class); + when(this.notificationFilterPreferenceStore.getFilterPreference(entryId, wikiReference)) + .thenReturn(Optional.of(notificationFilterPreference)); + DocumentReference userDoc = new DocumentReference("xwiki", "XWiki", "Foo"); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + when(context.getUserReference()).thenReturn(userDoc); + + LiveDataException liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(entryId)); + assertEquals("You don't have rights to access those information.", liveDataException.getMessage()); + + String filterId = "watchlist_id432"; + when(notificationFilterPreference.getId()).thenReturn(filterId); + when(notificationFilterPreference.getEventTypes()).thenReturn(Set.of()); + when(notificationFilterPreference.getNotificationFormats()) + .thenReturn(Set.of(NotificationFormat.ALERT, NotificationFormat.EMAIL)); + String page = "foo:Space.Page"; + SpaceReference spaceReference = new SpaceReference("foo", List.of("Space", "Page")); + when(notificationFilterPreference.getPage()).thenReturn(page); + when(notificationFilterPreference.getFilterType()).thenReturn(NotificationFilterType.EXCLUSIVE); + when(notificationFilterPreference.isEnabled()).thenReturn(true); + + ScriptContext scriptContext = mock(ScriptContext.class); + when(this.scriptContextManager.getCurrentScriptContext()).thenReturn(scriptContext); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(true); + String filterName = "scopeFilter"; + when(notificationFilterPreference.getFilterName()).thenReturn(filterName); + + liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(entryId)); + assertEquals("Cannot find NotificationFilter component for preference named [scopeFilter]", + liveDataException.getMessage()); + + NotificationFilter notificationFilter = + componentManager.registerMockComponent(NotificationFilter.class, filterName); + + String displayLocation = "Location hierarchy"; + when(this.templateManager.renderNoException("notification/filters/livedatalocation.vm")) + .thenReturn(displayLocation); + Block block = mock(Block.class); + when(this.notificationFilterManager.displayFilter(notificationFilter, notificationFilterPreference)) + .thenReturn(block); + String displayData = "Scope and location here"; + doAnswer(invocationOnMock -> { + DefaultWikiPrinter wikiPrinter = invocationOnMock.getArgument(1); + wikiPrinter.print(displayData); + return null; + }).when(this.blockRenderer).render(eq(block), any()); + + Map expectedResult = new LinkedHashMap<>(); + expectedResult.put("filterPreferenceId", filterId); + expectedResult.put("eventTypes", Map.of( + "extraClass", "list-unstyled", + "items", List.of("All event types") + )); + expectedResult.put("notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT", "Format EMAIL") + )); + expectedResult.put("scope", Map.of( + "icon", "chart-organisation", + "name", "Scope SPACE" + )); + + expectedResult.put("location", displayLocation); + expectedResult.put("display", displayData); + expectedResult.put("filterType", "FilterType EXCLUSIVE"); + expectedResult.put("isEnabled_checked", true); + expectedResult.put("isEnabled_disabled", true); + expectedResult.put("isEnabled_data", Map.of( + "preferenceId", filterId + )); + expectedResult.put("doc_hasdelete", true); + + assertEquals(Optional.of(expectedResult), this.entryStore.get(entryId)); + + verify(scriptContext, times(2)).setAttribute("location", new EntityReference(spaceReference), + ScriptContext.ENGINE_SCOPE); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + when(notificationFilterPreference.getOwner()).thenReturn("xwiki:XWiki.Foo"); + assertEquals(Optional.of(expectedResult), this.entryStore.get(entryId)); + } + + @Test + void getEntries(MockitoComponentManager componentManager) throws Exception + { + LiveDataQuery query = mock(LiveDataQuery.class); + LiveDataQuery.Source source = mock(LiveDataQuery.Source.class); + when(query.getSource()).thenReturn(source); + String owner = "foo"; + when(source.getParameters()).thenReturn(Map.of( + "target", "wiki", + "wiki", owner + )); + DocumentReference userDoc = new DocumentReference("xwiki", "XWiki", "Foo"); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(true); + when(context.getUserReference()).thenReturn(userDoc); + WikiReference wikiReference = new WikiReference(owner); + Long count = 12L; + when(this.queryHelper.countTotalFilters(query, owner, wikiReference)).thenReturn(count); + + NotificationFilterPreference filterPref1 = mock(NotificationFilterPreference.class); + NotificationFilterPreference filterPref2 = mock(NotificationFilterPreference.class); + NotificationFilterPreference filterPref3 = mock(NotificationFilterPreference.class); + when(this.queryHelper.getFilterPreferences(query, owner, wikiReference)).thenReturn(List.of( + filterPref1, + filterPref2, + filterPref3 + )); + + String filterId1 = "id432"; + when(filterPref1.getId()).thenReturn(filterId1); + when(filterPref1.getEventTypes()).thenReturn(Set.of("event foo")); + when(filterPref1.getNotificationFormats()).thenReturn(Set.of(NotificationFormat.EMAIL)); + String user = "xwiki:XWiki.User1"; + //DocumentReference user1DocRef = new DocumentReference("xwiki", "XWiki", "User1"); + when(filterPref1.getUser()).thenReturn(user); + when(filterPref1.getFilterType()).thenReturn(NotificationFilterType.INCLUSIVE); + when(filterPref1.isEnabled()).thenReturn(false); + + String filterId2 = "bka_id432"; + when(filterPref2.getId()).thenReturn(filterId2); + when(filterPref2.getEventTypes()).thenReturn(Set.of()); + when(filterPref2.getNotificationFormats()).thenReturn(Set.of(NotificationFormat.ALERT)); + String wikiFilter2 = "bla"; + //WikiReference wikiReferenceFilter = new WikiReference(wikiFilter2); + when(filterPref2.getWiki()).thenReturn(wikiFilter2); + when(filterPref2.getFilterType()).thenReturn(NotificationFilterType.INCLUSIVE); + when(filterPref2.isEnabled()).thenReturn(true); + + String filterId3 = "444564"; + when(filterPref3.getId()).thenReturn(filterId3); + when(filterPref3.getEventTypes()).thenReturn(Set.of("event foo", "bar", "buz")); + when(filterPref3.getNotificationFormats()).thenReturn(Set.of(NotificationFormat.EMAIL, + NotificationFormat.ALERT)); + String pageOnly = "xwiki:XWiki.User1"; + DocumentReference pageOnlyRef = new DocumentReference("xwiki", "XWiki", "User1"); + when(filterPref3.getPageOnly()).thenReturn(pageOnly); + when(filterPref3.getFilterType()).thenReturn(NotificationFilterType.EXCLUSIVE); + when(filterPref3.isEnabled()).thenReturn(true); + + ScriptContext scriptContext1 = mock(ScriptContext.class, "scriptContext1"); + ScriptContext scriptContext2 = mock(ScriptContext.class, "scriptContext1"); + ScriptContext scriptContext3 = mock(ScriptContext.class, "scriptContext1"); + when(this.scriptContextManager.getCurrentScriptContext()).thenReturn( + scriptContext1, + scriptContext2, + scriptContext3 + ); + + String displayLocation1 = "Location hierarchy 1"; + String displayLocation2 = "Location hierarchy 2"; + String displayLocation3 = "Location hierarchy 3"; + when(this.templateManager.renderNoException("notification/filters/livedatalocation.vm")).thenReturn( + displayLocation1, + displayLocation2, + displayLocation3 + ); + + String filterName1 = "filter1"; + String filterName2 = "filter2"; + NotificationFilter notificationFilter1 = + componentManager.registerMockComponent(NotificationFilter.class, filterName1); + NotificationFilter notificationFilter2 = + componentManager.registerMockComponent(NotificationFilter.class, filterName2); + + when(filterPref1.getFilterName()).thenReturn(filterName1); + when(filterPref2.getFilterName()).thenReturn(filterName2); + when(filterPref3.getFilterName()).thenReturn(filterName2); + + Block block1 = mock(Block.class, "block1"); + Block block2 = mock(Block.class, "block2"); + Block block3 = mock(Block.class, "block3"); + + when(this.notificationFilterManager.displayFilter(notificationFilter1, filterPref1)) + .thenReturn(block1); + when(this.notificationFilterManager.displayFilter(notificationFilter2, filterPref2)) + .thenReturn(block2); + when(this.notificationFilterManager.displayFilter(notificationFilter2, filterPref3)) + .thenReturn(block3); + + String displayData1 = "Scope and location here 1"; + String displayData2 = "Scope and location here 2"; + String displayData3 = "Scope and location here 3"; + doAnswer(invocationOnMock -> { + DefaultWikiPrinter wikiPrinter = invocationOnMock.getArgument(1); + wikiPrinter.print(displayData1); + return null; + }).when(this.blockRenderer).render(eq(block1), any()); + + doAnswer(invocationOnMock -> { + DefaultWikiPrinter wikiPrinter = invocationOnMock.getArgument(1); + wikiPrinter.print(displayData2); + return null; + }).when(this.blockRenderer).render(eq(block2), any()); + + doAnswer(invocationOnMock -> { + DefaultWikiPrinter wikiPrinter = invocationOnMock.getArgument(1); + wikiPrinter.print(displayData3); + return null; + }).when(this.blockRenderer).render(eq(block3), any()); + + Map expectedResult1 = new LinkedHashMap<>(); + expectedResult1.put("filterPreferenceId", filterId1); + expectedResult1.put("eventTypes", Map.of( + "extraClass", "list-unstyled", + "items", List.of("EventType event foo") + )); + expectedResult1.put("notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format EMAIL") + )); + expectedResult1.put("scope", Map.of( + "icon", "user", + "name", "Scope USER" + )); + expectedResult1.put("location", displayLocation1); + expectedResult1.put("display", displayData1); + expectedResult1.put("filterType", "FilterType INCLUSIVE"); + expectedResult1.put("isEnabled_checked", false); + expectedResult1.put("isEnabled_disabled", false); + expectedResult1.put("isEnabled_data", Map.of( + "preferenceId", filterId1 + )); + expectedResult1.put("doc_hasdelete", true); + + Map expectedResult2 = new LinkedHashMap<>(); + expectedResult2.put("filterPreferenceId", filterId2); + expectedResult2.put("eventTypes", Map.of( + "extraClass", "list-unstyled", + "items", List.of("All event types") + )); + expectedResult2.put("notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT") + )); + expectedResult2.put("scope", Map.of( + "icon", "wiki", + "name", "Scope WIKI" + )); + expectedResult2.put("location", displayLocation2); + expectedResult2.put("display", displayData2); + expectedResult2.put("filterType", "FilterType INCLUSIVE"); + expectedResult2.put("isEnabled_checked", true); + expectedResult2.put("isEnabled_disabled", false); + expectedResult2.put("isEnabled_data", Map.of( + "preferenceId", filterId2 + )); + expectedResult2.put("doc_hasdelete", true); + + Map expectedResult3 = new LinkedHashMap<>(); + expectedResult3.put("filterPreferenceId", filterId3); + expectedResult3.put("eventTypes", Map.of( + "extraClass", "list-unstyled", + "items", List.of("EventType bar", "EventType buz", "EventType event foo") + )); + expectedResult3.put("notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT", "Format EMAIL") + )); + expectedResult3.put("scope", Map.of( + "icon", "page", + "name", "Scope PAGE" + )); + expectedResult3.put("location", displayLocation3); + expectedResult3.put("display", displayData3); + expectedResult3.put("filterType", "FilterType EXCLUSIVE"); + expectedResult3.put("isEnabled_checked", true); + expectedResult3.put("isEnabled_disabled", false); + expectedResult3.put("isEnabled_data", Map.of( + "preferenceId", filterId3 + )); + expectedResult3.put("doc_hasdelete", true); + + LiveData liveData = new LiveData(); + liveData.setCount(count); + liveData.getEntries().addAll(List.of(expectedResult1, expectedResult2, expectedResult3)); + + assertEquals(liveData, this.entryStore.get(query)); + } +} \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersQueryHelperTest.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersQueryHelperTest.java new file mode 100644 index 000000000000..3979d50e6812 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/custom/NotificationCustomFiltersQueryHelperTest.java @@ -0,0 +1,667 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.notifications.filters.internal.livedata.custom; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataQuery; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.notifications.filters.NotificationFilterPreference; +import org.xwiki.notifications.filters.NotificationFilterType; +import org.xwiki.query.Query; +import org.xwiki.query.QueryException; +import org.xwiki.query.QueryManager; +import org.xwiki.query.internal.DefaultQueryParameter; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link NotificationCustomFiltersQueryHelper}. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@ComponentTest +class NotificationCustomFiltersQueryHelperTest +{ + @InjectMockComponents + private NotificationCustomFiltersQueryHelper queryHelper; + + @MockComponent + private QueryManager queryManager; + + @Test + void getFilterPreferencesNoFilterNoSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + WikiReference wikiReference = new WikiReference(wikiName); + String queryString = "select nfp from DefaultNotificationFilterPreference nfp where owner = :owner"; + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + when(query.setWiki(wikiName)).thenReturn(query); + Long offset = 3L; + int limit = 12; + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + when(query.setOffset(offset.intValue())).thenReturn(query); + when(query.setLimit(limit)).thenReturn(query); + + List expectedList = List.of( + mock(NotificationFilterPreference.class, "filterPref1"), + mock(NotificationFilterPreference.class, "filterPref2"), + mock(NotificationFilterPreference.class, "filterPref3") + ); + when(query.execute()).thenReturn(expectedList); + assertEquals(expectedList, this.queryHelper.getFilterPreferences(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).setWiki(wikiName); + verify(query).setOffset(offset.intValue()); + verify(query).setLimit(limit); + } + + @Test + void countFilterPreferencesNoFilterNoSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + WikiReference wikiReference = new WikiReference(wikiName); + String queryString = "select count(nfp.id) from DefaultNotificationFilterPreference nfp where owner = :owner"; + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + when(query.setWiki(wikiName)).thenReturn(query); + Long offset = 3L; + int limit = 12; + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + + when(query.execute()).thenReturn(List.of(3L)); + assertEquals(3L, this.queryHelper.countTotalFilters(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).setWiki(wikiName); + verify(query, never()).setOffset(anyInt()); + verify(query, never()).setLimit(anyInt()); + } + + @Test + void getFilterPreferencesNoFilterSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + WikiReference wikiReference = new WikiReference(wikiName); + Long offset = 3L; + int limit = 12; + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + + LiveDataQuery.SortEntry sortEntry1 = mock(LiveDataQuery.SortEntry.class, "sortEntry1"); + when(sortEntry1.isDescending()).thenReturn(false); + when(sortEntry1.getProperty()).thenReturn("scope"); + + LiveDataQuery.SortEntry sortEntry2 = mock(LiveDataQuery.SortEntry.class, "sortEntry2"); + when(sortEntry2.isDescending()).thenReturn(true); + when(sortEntry2.getProperty()).thenReturn("isEnabled"); + + LiveDataQuery.SortEntry sortEntry3 = mock(LiveDataQuery.SortEntry.class, "sortEntry3"); + when(sortEntry3.isDescending()).thenReturn(true); + when(sortEntry3.getProperty()).thenReturn("notificationFormats"); + + when(ldQuery.getSort()).thenReturn(List.of(sortEntry1, sortEntry2, sortEntry3)); + + String queryString = "select nfp from DefaultNotificationFilterPreference nfp where owner = :owner " + + "order by nfp.pageOnly asc, nfp.page asc, nfp.wiki asc, nfp.user asc, nfp.enabled desc, " + + "nfp.emailEnabled asc, nfp.alertEnabled desc"; + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + when(query.setWiki(wikiName)).thenReturn(query); + when(query.setOffset(offset.intValue())).thenReturn(query); + when(query.setLimit(limit)).thenReturn(query); + + List expectedList = List.of( + mock(NotificationFilterPreference.class, "filterPref1"), + mock(NotificationFilterPreference.class, "filterPref2"), + mock(NotificationFilterPreference.class, "filterPref3") + ); + when(query.execute()).thenReturn(expectedList); + assertEquals(expectedList, this.queryHelper.getFilterPreferences(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).setWiki(wikiName); + verify(query).setOffset(offset.intValue()); + verify(query).setLimit(limit); + } + + @Test + void countFilterPreferencesNoFilterSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + WikiReference wikiReference = new WikiReference(wikiName); + + Long offset = 3L; + int limit = 12; + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + + LiveDataQuery.SortEntry sortEntry1 = mock(LiveDataQuery.SortEntry.class, "sortEntry1"); + when(sortEntry1.isDescending()).thenReturn(false); + when(sortEntry1.getProperty()).thenReturn("scope"); + + LiveDataQuery.SortEntry sortEntry2 = mock(LiveDataQuery.SortEntry.class, "sortEntry2"); + when(sortEntry2.isDescending()).thenReturn(true); + when(sortEntry2.getProperty()).thenReturn("isEnabled"); + + LiveDataQuery.SortEntry sortEntry3 = mock(LiveDataQuery.SortEntry.class, "sortEntry3"); + when(sortEntry3.isDescending()).thenReturn(true); + when(sortEntry3.getProperty()).thenReturn("notificationFormats"); + + when(ldQuery.getSort()).thenReturn(List.of(sortEntry1, sortEntry2, sortEntry3)); + + String queryString = "select count(nfp.id) from DefaultNotificationFilterPreference nfp where owner = :owner"; + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + when(query.setWiki(wikiName)).thenReturn(query); + + when(query.execute()).thenReturn(List.of(3L)); + assertEquals(3L, this.queryHelper.countTotalFilters(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).setWiki(wikiName); + verify(query, never()).setOffset(anyInt()); + verify(query, never()).setLimit(anyInt()); + } + + @Test + void getFilterPreferencesFilterNoSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + Long offset = 3L; + int limit = 12; + WikiReference wikiReference = new WikiReference(wikiName); + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + + LiveDataQuery.Filter filter1 = mock(LiveDataQuery.Filter.class, "filter1"); + when(filter1.getProperty()).thenReturn("location"); + when(filter1.isMatchAll()).thenReturn(false); + LiveDataQuery.Constraint filter1Constraint1 = mock(LiveDataQuery.Constraint.class, "filter1Constraint1"); + when(filter1Constraint1.getOperator()).thenReturn("startsWith"); + when(filter1Constraint1.getValue()).thenReturn("foo"); + LiveDataQuery.Constraint filter1Constraint2 = mock(LiveDataQuery.Constraint.class, "filter1Constraint2"); + when(filter1Constraint2.getOperator()).thenReturn("contains"); + when(filter1Constraint2.getValue()).thenReturn("bar"); + LiveDataQuery.Constraint filter1Constraint3 = mock(LiveDataQuery.Constraint.class, "filter1Constraint3"); + when(filter1Constraint3.getOperator()).thenReturn("equals"); + when(filter1Constraint3.getValue()).thenReturn("buz"); + when(filter1.getConstraints()).thenReturn(List.of(filter1Constraint1, filter1Constraint2, filter1Constraint3)); + + LiveDataQuery.Filter filter2 = mock(LiveDataQuery.Filter.class, "filter2"); + when(filter2.getProperty()).thenReturn("eventTypes"); + LiveDataQuery.Constraint filter2Constraint = mock(LiveDataQuery.Constraint.class, "filter2Constraint"); + when(filter2Constraint.getOperator()).thenReturn("equals"); + when(filter2Constraint.getValue()).thenReturn("__ALL_EVENTS__"); + when(filter2.getConstraints()).thenReturn(List.of(filter2Constraint)); + + LiveDataQuery.Filter filter3 = mock(LiveDataQuery.Filter.class, "filter3"); + when(filter3.getProperty()).thenReturn("isEnabled"); + LiveDataQuery.Constraint filter3Constraint = mock(LiveDataQuery.Constraint.class, "filter3Constraint"); + when(filter3Constraint.getOperator()).thenReturn("equals"); + when(filter3Constraint.getValue()).thenReturn("true"); + when(filter3.getConstraints()).thenReturn(List.of(filter3Constraint)); + + LiveDataQuery.Filter filter4 = mock(LiveDataQuery.Filter.class, "filter4"); + when(filter4.getProperty()).thenReturn("scope"); + LiveDataQuery.Constraint filter4Constraint = mock(LiveDataQuery.Constraint.class, "filter4Constraint"); + when(filter4Constraint.getOperator()).thenReturn("equals"); + when(filter4Constraint.getValue()).thenReturn("PAGE"); + when(filter4.getConstraints()).thenReturn(List.of(filter4Constraint)); + + LiveDataQuery.Filter filter5 = mock(LiveDataQuery.Filter.class, "filter5"); + when(filter5.getProperty()).thenReturn("filterType"); + LiveDataQuery.Constraint filter5Constraint = mock(LiveDataQuery.Constraint.class, "filter5Constraint"); + when(filter5Constraint.getOperator()).thenReturn("equals"); + when(filter5Constraint.getValue()).thenReturn("INCLUSIVE"); + when(filter5.getConstraints()).thenReturn(List.of(filter5Constraint)); + + when(ldQuery.getFilters()).thenReturn(List.of( + filter1, + filter2, + filter3, + filter4, + filter5 + )); + + String queryString = "select nfp from DefaultNotificationFilterPreference nfp where owner = :owner " + + "and ((nfp.pageOnly like :constraint_0 or nfp.page like :constraint_0 or nfp.wiki like :constraint_0 or " + + "nfp.user like :constraint_0) or (nfp.pageOnly like :constraint_1 or nfp.page like :constraint_1 or " + + "nfp.wiki like :constraint_1 or nfp.user like :constraint_1) or (nfp.pageOnly = :constraint_2 or " + + "nfp.page = :constraint_2 or nfp.wiki = :constraint_2 or nfp.user = :constraint_2)) and " + + "length(nfp.allEventTypes) = 0 and nfp.enabled = 1 and length(nfp.pageOnly) > 0 and " + + "nfp.filterType = :filterType"; + + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + + DefaultQueryParameter queryParameter1 = new DefaultQueryParameter(null); + queryParameter1.literal("foo").anyChars(); + when(query.bindValue("constraint_0", queryParameter1)).thenReturn(query); + + DefaultQueryParameter queryParameter2 = new DefaultQueryParameter(null); + queryParameter2.anyChars().literal("bar").anyChars(); + when(query.bindValue("constraint_1", queryParameter2)).thenReturn(query); + + DefaultQueryParameter queryParameter3 = new DefaultQueryParameter(null); + queryParameter3.literal("buz"); + when(query.bindValue("constraint_2", queryParameter3)).thenReturn(query); + + when(query.bindValue("filterType", NotificationFilterType.INCLUSIVE)).thenReturn(query); + + when(query.setWiki(wikiName)).thenReturn(query); + when(query.setOffset(offset.intValue())).thenReturn(query); + when(query.setLimit(limit)).thenReturn(query); + + List expectedList = List.of( + mock(NotificationFilterPreference.class, "filterPref1"), + mock(NotificationFilterPreference.class, "filterPref2"), + mock(NotificationFilterPreference.class, "filterPref3") + ); + when(query.execute()).thenReturn(expectedList); + assertEquals(expectedList, this.queryHelper.getFilterPreferences(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).bindValue("constraint_0", queryParameter1); + verify(query).bindValue("constraint_1", queryParameter2); + verify(query).bindValue("constraint_2", queryParameter3); + verify(query).bindValue("filterType", NotificationFilterType.INCLUSIVE); + verify(query).setWiki(wikiName); + verify(query).setOffset(offset.intValue()); + verify(query).setLimit(limit); + } + + @Test + void countFilterPreferencesFilterNoSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + Long offset = 3L; + int limit = 12; + WikiReference wikiReference = new WikiReference(wikiName); + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + + LiveDataQuery.Filter filter1 = mock(LiveDataQuery.Filter.class, "filter1"); + when(filter1.getProperty()).thenReturn("location"); + when(filter1.isMatchAll()).thenReturn(false); + LiveDataQuery.Constraint filter1Constraint1 = mock(LiveDataQuery.Constraint.class, "filter1Constraint1"); + when(filter1Constraint1.getOperator()).thenReturn("startsWith"); + when(filter1Constraint1.getValue()).thenReturn("foo"); + LiveDataQuery.Constraint filter1Constraint2 = mock(LiveDataQuery.Constraint.class, "filter1Constraint2"); + when(filter1Constraint2.getOperator()).thenReturn("contains"); + when(filter1Constraint2.getValue()).thenReturn("bar"); + LiveDataQuery.Constraint filter1Constraint3 = mock(LiveDataQuery.Constraint.class, "filter1Constraint3"); + when(filter1Constraint3.getOperator()).thenReturn("equals"); + when(filter1Constraint3.getValue()).thenReturn("buz"); + when(filter1.getConstraints()).thenReturn(List.of(filter1Constraint1, filter1Constraint2, filter1Constraint3)); + + LiveDataQuery.Filter filter2 = mock(LiveDataQuery.Filter.class, "filter2"); + when(filter2.getProperty()).thenReturn("eventTypes"); + LiveDataQuery.Constraint filter2Constraint = mock(LiveDataQuery.Constraint.class, "filter2Constraint"); + when(filter2Constraint.getOperator()).thenReturn("equals"); + when(filter2Constraint.getValue()).thenReturn("__ALL_EVENTS__"); + when(filter2.getConstraints()).thenReturn(List.of(filter2Constraint)); + + LiveDataQuery.Filter filter3 = mock(LiveDataQuery.Filter.class, "filter3"); + when(filter3.getProperty()).thenReturn("isEnabled"); + LiveDataQuery.Constraint filter3Constraint = mock(LiveDataQuery.Constraint.class, "filter3Constraint"); + when(filter3Constraint.getOperator()).thenReturn("equals"); + when(filter3Constraint.getValue()).thenReturn("true"); + when(filter3.getConstraints()).thenReturn(List.of(filter3Constraint)); + + LiveDataQuery.Filter filter4 = mock(LiveDataQuery.Filter.class, "filter4"); + when(filter4.getProperty()).thenReturn("scope"); + LiveDataQuery.Constraint filter4Constraint = mock(LiveDataQuery.Constraint.class, "filter4Constraint"); + when(filter4Constraint.getOperator()).thenReturn("equals"); + when(filter4Constraint.getValue()).thenReturn("PAGE"); + when(filter4.getConstraints()).thenReturn(List.of(filter4Constraint)); + + LiveDataQuery.Filter filter5 = mock(LiveDataQuery.Filter.class, "filter5"); + when(filter5.getProperty()).thenReturn("filterType"); + LiveDataQuery.Constraint filter5Constraint = mock(LiveDataQuery.Constraint.class, "filter5Constraint"); + when(filter5Constraint.getOperator()).thenReturn("equals"); + when(filter5Constraint.getValue()).thenReturn("INCLUSIVE"); + when(filter5.getConstraints()).thenReturn(List.of(filter5Constraint)); + + when(ldQuery.getFilters()).thenReturn(List.of( + filter1, + filter2, + filter3, + filter4, + filter5 + )); + + String queryString = "select count(nfp.id) from DefaultNotificationFilterPreference nfp where owner = :owner " + + "and ((nfp.pageOnly like :constraint_0 or nfp.page like :constraint_0 or nfp.wiki like :constraint_0 or " + + "nfp.user like :constraint_0) or (nfp.pageOnly like :constraint_1 or nfp.page like :constraint_1 or " + + "nfp.wiki like :constraint_1 or nfp.user like :constraint_1) or (nfp.pageOnly = :constraint_2 or " + + "nfp.page = :constraint_2 or nfp.wiki = :constraint_2 or nfp.user = :constraint_2)) and " + + "length(nfp.allEventTypes) = 0 and nfp.enabled = 1 and length(nfp.pageOnly) > 0 and " + + "nfp.filterType = :filterType"; + + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + + DefaultQueryParameter queryParameter1 = new DefaultQueryParameter(null); + queryParameter1.literal("foo").anyChars(); + when(query.bindValue("constraint_0", queryParameter1)).thenReturn(query); + + DefaultQueryParameter queryParameter2 = new DefaultQueryParameter(null); + queryParameter2.anyChars().literal("bar").anyChars(); + when(query.bindValue("constraint_1", queryParameter2)).thenReturn(query); + + DefaultQueryParameter queryParameter3 = new DefaultQueryParameter(null); + queryParameter3.literal("buz"); + when(query.bindValue("constraint_2", queryParameter3)).thenReturn(query); + + when(query.bindValue("filterType", NotificationFilterType.INCLUSIVE)).thenReturn(query); + + when(query.setWiki(wikiName)).thenReturn(query); + + when(query.execute()).thenReturn(List.of(3L)); + assertEquals(3L, this.queryHelper.countTotalFilters(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).bindValue("constraint_0", queryParameter1); + verify(query).bindValue("constraint_1", queryParameter2); + verify(query).bindValue("constraint_2", queryParameter3); + verify(query).bindValue("filterType", NotificationFilterType.INCLUSIVE); + verify(query).setWiki(wikiName); + verify(query, never()).setOffset(anyInt()); + verify(query, never()).setLimit(anyInt()); + } + + @Test + void getFilterPreferencesFilterSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + Long offset = 3L; + int limit = 12; + WikiReference wikiReference = new WikiReference(wikiName); + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + + LiveDataQuery.Filter filter1 = mock(LiveDataQuery.Filter.class, "filter1"); + when(filter1.getProperty()).thenReturn("location"); + when(filter1.isMatchAll()).thenReturn(false); + LiveDataQuery.Constraint filter1Constraint1 = mock(LiveDataQuery.Constraint.class, "filter1Constraint1"); + when(filter1Constraint1.getOperator()).thenReturn("startsWith"); + when(filter1Constraint1.getValue()).thenReturn("foo"); + LiveDataQuery.Constraint filter1Constraint2 = mock(LiveDataQuery.Constraint.class, "filter1Constraint2"); + when(filter1Constraint2.getOperator()).thenReturn("contains"); + when(filter1Constraint2.getValue()).thenReturn("bar"); + LiveDataQuery.Constraint filter1Constraint3 = mock(LiveDataQuery.Constraint.class, "filter1Constraint3"); + when(filter1Constraint3.getOperator()).thenReturn("equals"); + when(filter1Constraint3.getValue()).thenReturn("buz"); + when(filter1.getConstraints()).thenReturn(List.of(filter1Constraint1, filter1Constraint2, filter1Constraint3)); + + LiveDataQuery.Filter filter2 = mock(LiveDataQuery.Filter.class, "filter2"); + when(filter2.getProperty()).thenReturn("eventTypes"); + LiveDataQuery.Constraint filter2Constraint = mock(LiveDataQuery.Constraint.class, "filter2Constraint"); + when(filter2Constraint.getOperator()).thenReturn("equals"); + when(filter2Constraint.getValue()).thenReturn("__ALL_EVENTS__"); + when(filter2.getConstraints()).thenReturn(List.of(filter2Constraint)); + + LiveDataQuery.Filter filter3 = mock(LiveDataQuery.Filter.class, "filter3"); + when(filter3.getProperty()).thenReturn("isEnabled"); + LiveDataQuery.Constraint filter3Constraint = mock(LiveDataQuery.Constraint.class, "filter3Constraint"); + when(filter3Constraint.getOperator()).thenReturn("equals"); + when(filter3Constraint.getValue()).thenReturn("true"); + when(filter3.getConstraints()).thenReturn(List.of(filter3Constraint)); + + LiveDataQuery.Filter filter4 = mock(LiveDataQuery.Filter.class, "filter4"); + when(filter4.getProperty()).thenReturn("scope"); + LiveDataQuery.Constraint filter4Constraint = mock(LiveDataQuery.Constraint.class, "filter4Constraint"); + when(filter4Constraint.getOperator()).thenReturn("equals"); + when(filter4Constraint.getValue()).thenReturn("PAGE"); + when(filter4.getConstraints()).thenReturn(List.of(filter4Constraint)); + + LiveDataQuery.Filter filter5 = mock(LiveDataQuery.Filter.class, "filter5"); + when(filter5.getProperty()).thenReturn("filterType"); + LiveDataQuery.Constraint filter5Constraint = mock(LiveDataQuery.Constraint.class, "filter5Constraint"); + when(filter5Constraint.getOperator()).thenReturn("equals"); + when(filter5Constraint.getValue()).thenReturn("INCLUSIVE"); + when(filter5.getConstraints()).thenReturn(List.of(filter5Constraint)); + + when(ldQuery.getFilters()).thenReturn(List.of( + filter1, + filter2, + filter3, + filter4, + filter5 + )); + + LiveDataQuery.SortEntry sortEntry1 = mock(LiveDataQuery.SortEntry.class, "sortEntry1"); + when(sortEntry1.isDescending()).thenReturn(false); + when(sortEntry1.getProperty()).thenReturn("scope"); + + LiveDataQuery.SortEntry sortEntry2 = mock(LiveDataQuery.SortEntry.class, "sortEntry2"); + when(sortEntry2.isDescending()).thenReturn(true); + when(sortEntry2.getProperty()).thenReturn("isEnabled"); + + LiveDataQuery.SortEntry sortEntry3 = mock(LiveDataQuery.SortEntry.class, "sortEntry3"); + when(sortEntry3.isDescending()).thenReturn(true); + when(sortEntry3.getProperty()).thenReturn("notificationFormats"); + + when(ldQuery.getSort()).thenReturn(List.of(sortEntry1, sortEntry2, sortEntry3)); + + String queryString = "select nfp from DefaultNotificationFilterPreference nfp where owner = :owner " + + "and ((nfp.pageOnly like :constraint_0 or nfp.page like :constraint_0 or nfp.wiki like :constraint_0 or " + + "nfp.user like :constraint_0) or (nfp.pageOnly like :constraint_1 or nfp.page like :constraint_1 or " + + "nfp.wiki like :constraint_1 or nfp.user like :constraint_1) or (nfp.pageOnly = :constraint_2 or " + + "nfp.page = :constraint_2 or nfp.wiki = :constraint_2 or nfp.user = :constraint_2)) and " + + "length(nfp.allEventTypes) = 0 and nfp.enabled = 1 and length(nfp.pageOnly) > 0 and " + + "nfp.filterType = :filterType " + + "order by nfp.pageOnly asc, nfp.page asc, nfp.wiki asc, nfp.user asc, nfp.enabled desc, " + + "nfp.emailEnabled asc, nfp.alertEnabled desc"; + + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + + DefaultQueryParameter queryParameter1 = new DefaultQueryParameter(null); + queryParameter1.literal("foo").anyChars(); + when(query.bindValue("constraint_0", queryParameter1)).thenReturn(query); + + DefaultQueryParameter queryParameter2 = new DefaultQueryParameter(null); + queryParameter2.anyChars().literal("bar").anyChars(); + when(query.bindValue("constraint_1", queryParameter2)).thenReturn(query); + + DefaultQueryParameter queryParameter3 = new DefaultQueryParameter(null); + queryParameter3.literal("buz"); + when(query.bindValue("constraint_2", queryParameter3)).thenReturn(query); + + when(query.bindValue("filterType", NotificationFilterType.INCLUSIVE)).thenReturn(query); + + when(query.setWiki(wikiName)).thenReturn(query); + when(query.setOffset(offset.intValue())).thenReturn(query); + when(query.setLimit(limit)).thenReturn(query); + + List expectedList = List.of( + mock(NotificationFilterPreference.class, "filterPref1"), + mock(NotificationFilterPreference.class, "filterPref2"), + mock(NotificationFilterPreference.class, "filterPref3") + ); + when(query.execute()).thenReturn(expectedList); + assertEquals(expectedList, this.queryHelper.getFilterPreferences(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).bindValue("constraint_0", queryParameter1); + verify(query).bindValue("constraint_1", queryParameter2); + verify(query).bindValue("constraint_2", queryParameter3); + verify(query).bindValue("filterType", NotificationFilterType.INCLUSIVE); + verify(query).setWiki(wikiName); + verify(query).setOffset(offset.intValue()); + verify(query).setLimit(limit); + } + + @Test + void countFilterPreferencesFilterSort() throws QueryException, LiveDataException + { + String owner = "xwiki:XWiki.Foo"; + String wikiName = "foo"; + Long offset = 3L; + int limit = 12; + WikiReference wikiReference = new WikiReference(wikiName); + + LiveDataQuery ldQuery = mock(LiveDataQuery.class); + when(ldQuery.getOffset()).thenReturn(offset); + when(ldQuery.getLimit()).thenReturn(limit); + + LiveDataQuery.Filter filter1 = mock(LiveDataQuery.Filter.class, "filter1"); + when(filter1.getProperty()).thenReturn("location"); + when(filter1.isMatchAll()).thenReturn(false); + LiveDataQuery.Constraint filter1Constraint1 = mock(LiveDataQuery.Constraint.class, "filter1Constraint1"); + when(filter1Constraint1.getOperator()).thenReturn("startsWith"); + when(filter1Constraint1.getValue()).thenReturn("foo"); + LiveDataQuery.Constraint filter1Constraint2 = mock(LiveDataQuery.Constraint.class, "filter1Constraint2"); + when(filter1Constraint2.getOperator()).thenReturn("contains"); + when(filter1Constraint2.getValue()).thenReturn("bar"); + LiveDataQuery.Constraint filter1Constraint3 = mock(LiveDataQuery.Constraint.class, "filter1Constraint3"); + when(filter1Constraint3.getOperator()).thenReturn("equals"); + when(filter1Constraint3.getValue()).thenReturn("buz"); + when(filter1.getConstraints()).thenReturn(List.of(filter1Constraint1, filter1Constraint2, filter1Constraint3)); + + LiveDataQuery.Filter filter2 = mock(LiveDataQuery.Filter.class, "filter2"); + when(filter2.getProperty()).thenReturn("eventTypes"); + LiveDataQuery.Constraint filter2Constraint = mock(LiveDataQuery.Constraint.class, "filter2Constraint"); + when(filter2Constraint.getOperator()).thenReturn("equals"); + when(filter2Constraint.getValue()).thenReturn("__ALL_EVENTS__"); + when(filter2.getConstraints()).thenReturn(List.of(filter2Constraint)); + + LiveDataQuery.Filter filter3 = mock(LiveDataQuery.Filter.class, "filter3"); + when(filter3.getProperty()).thenReturn("isEnabled"); + LiveDataQuery.Constraint filter3Constraint = mock(LiveDataQuery.Constraint.class, "filter3Constraint"); + when(filter3Constraint.getOperator()).thenReturn("equals"); + when(filter3Constraint.getValue()).thenReturn("true"); + when(filter3.getConstraints()).thenReturn(List.of(filter3Constraint)); + + LiveDataQuery.Filter filter4 = mock(LiveDataQuery.Filter.class, "filter4"); + when(filter4.getProperty()).thenReturn("scope"); + LiveDataQuery.Constraint filter4Constraint = mock(LiveDataQuery.Constraint.class, "filter4Constraint"); + when(filter4Constraint.getOperator()).thenReturn("equals"); + when(filter4Constraint.getValue()).thenReturn("PAGE"); + when(filter4.getConstraints()).thenReturn(List.of(filter4Constraint)); + + LiveDataQuery.Filter filter5 = mock(LiveDataQuery.Filter.class, "filter5"); + when(filter5.getProperty()).thenReturn("filterType"); + LiveDataQuery.Constraint filter5Constraint = mock(LiveDataQuery.Constraint.class, "filter5Constraint"); + when(filter5Constraint.getOperator()).thenReturn("equals"); + when(filter5Constraint.getValue()).thenReturn("INCLUSIVE"); + when(filter5.getConstraints()).thenReturn(List.of(filter5Constraint)); + + when(ldQuery.getFilters()).thenReturn(List.of( + filter1, + filter2, + filter3, + filter4, + filter5 + )); + + LiveDataQuery.SortEntry sortEntry1 = mock(LiveDataQuery.SortEntry.class, "sortEntry1"); + when(sortEntry1.isDescending()).thenReturn(false); + when(sortEntry1.getProperty()).thenReturn("scope"); + + LiveDataQuery.SortEntry sortEntry2 = mock(LiveDataQuery.SortEntry.class, "sortEntry2"); + when(sortEntry2.isDescending()).thenReturn(true); + when(sortEntry2.getProperty()).thenReturn("isEnabled"); + + LiveDataQuery.SortEntry sortEntry3 = mock(LiveDataQuery.SortEntry.class, "sortEntry3"); + when(sortEntry3.isDescending()).thenReturn(true); + when(sortEntry3.getProperty()).thenReturn("notificationFormats"); + + when(ldQuery.getSort()).thenReturn(List.of(sortEntry1, sortEntry2, sortEntry3)); + + String queryString = "select count(nfp.id) from DefaultNotificationFilterPreference nfp where owner = :owner " + + "and ((nfp.pageOnly like :constraint_0 or nfp.page like :constraint_0 or nfp.wiki like :constraint_0 or " + + "nfp.user like :constraint_0) or (nfp.pageOnly like :constraint_1 or nfp.page like :constraint_1 or " + + "nfp.wiki like :constraint_1 or nfp.user like :constraint_1) or (nfp.pageOnly = :constraint_2 or " + + "nfp.page = :constraint_2 or nfp.wiki = :constraint_2 or nfp.user = :constraint_2)) and " + + "length(nfp.allEventTypes) = 0 and nfp.enabled = 1 and length(nfp.pageOnly) > 0 and " + + "nfp.filterType = :filterType"; + + Query query = mock(Query.class); + when(this.queryManager.createQuery(queryString, Query.HQL)).thenReturn(query); + when(query.bindValue("owner", owner)).thenReturn(query); + + DefaultQueryParameter queryParameter1 = new DefaultQueryParameter(null); + queryParameter1.literal("foo").anyChars(); + when(query.bindValue("constraint_0", queryParameter1)).thenReturn(query); + + DefaultQueryParameter queryParameter2 = new DefaultQueryParameter(null); + queryParameter2.anyChars().literal("bar").anyChars(); + when(query.bindValue("constraint_1", queryParameter2)).thenReturn(query); + + DefaultQueryParameter queryParameter3 = new DefaultQueryParameter(null); + queryParameter3.literal("buz"); + when(query.bindValue("constraint_2", queryParameter3)).thenReturn(query); + + when(query.bindValue("filterType", NotificationFilterType.INCLUSIVE)).thenReturn(query); + + when(query.setWiki(wikiName)).thenReturn(query); + + when(query.execute()).thenReturn(List.of(3L)); + assertEquals(3L, this.queryHelper.countTotalFilters(ldQuery, owner, wikiReference)); + verify(query).bindValue("owner", owner); + verify(query).bindValue("constraint_0", queryParameter1); + verify(query).bindValue("constraint_1", queryParameter2); + verify(query).bindValue("constraint_2", queryParameter3); + verify(query).bindValue("filterType", NotificationFilterType.INCLUSIVE); + verify(query).setWiki(wikiName); + verify(query, never()).setOffset(anyInt()); + verify(query, never()).setLimit(anyInt()); + } +} \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataEntryStoreTest.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataEntryStoreTest.java new file mode 100644 index 000000000000..16c9a18e036c --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-filters/xwiki-platform-notifications-filters-default/src/test/java/org/xwiki/notifications/filters/internal/livedata/system/NotificationSystemFiltersLiveDataEntryStoreTest.java @@ -0,0 +1,505 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.notifications.filters.internal.livedata.system; + +import java.util.List; +import java.util.Map; + +import javax.inject.Provider; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xwiki.livedata.LiveData; +import org.xwiki.livedata.LiveDataException; +import org.xwiki.livedata.LiveDataQuery; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.notifications.NotificationException; +import org.xwiki.notifications.NotificationFormat; +import org.xwiki.notifications.filters.NotificationFilter; +import org.xwiki.notifications.filters.NotificationFilterManager; +import org.xwiki.notifications.filters.internal.FilterPreferencesModelBridge; +import org.xwiki.notifications.filters.internal.ToggleableNotificationFilter; +import org.xwiki.notifications.filters.internal.ToggleableNotificationFilterActivation; +import org.xwiki.notifications.filters.internal.livedata.NotificationFilterLiveDataTranslationHelper; +import org.xwiki.security.authorization.ContextualAuthorizationManager; +import org.xwiki.security.authorization.Right; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.test.reference.ReferenceComponentList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link NotificationSystemFiltersLiveDataEntryStore}. + * + * @version $Id$ + * @since 16.2.0RC1 + */ +@ComponentTest +@ReferenceComponentList +class NotificationSystemFiltersLiveDataEntryStoreTest +{ + @InjectMockComponents + private NotificationSystemFiltersLiveDataEntryStore entryStore; + + @MockComponent + private NotificationFilterManager notificationFilterManager; + + @MockComponent + private FilterPreferencesModelBridge filterPreferencesModelBridge; + + @MockComponent + private ContextualAuthorizationManager contextualAuthorizationManager; + + @MockComponent + private NotificationFilterLiveDataTranslationHelper translationHelper; + + @MockComponent + private Provider contextProvider; + + private XWikiContext context; + + @BeforeEach + void beforeEach() + { + this.context = mock(XWikiContext.class); + when(this.contextProvider.get()).thenReturn(this.context); + } + + @Test + void getMissingTarget() + { + LiveDataQuery query = mock(LiveDataQuery.class); + LiveDataQuery.Source source = mock(LiveDataQuery.Source.class); + when(query.getSource()).thenReturn(source); + when(source.getParameters()).thenReturn(Map.of()); + LiveDataException liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(query)); + assertEquals("The target source parameter is mandatory.", liveDataException.getMessage()); + } + + @Test + void getBadAuthorization() throws LiveDataException + { + LiveDataQuery query = mock(LiveDataQuery.class); + LiveDataQuery.Source source = mock(LiveDataQuery.Source.class); + when(query.getSource()).thenReturn(source); + when(source.getParameters()).thenReturn(Map.of( + "target", "wiki", + "wiki", "foo" + )); + DocumentReference userDoc = new DocumentReference("xwiki", "XWiki", "Foo"); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + when(context.getUserReference()).thenReturn(userDoc); + LiveDataException liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(query)); + assertEquals("You don't have rights to access those information.", liveDataException.getMessage()); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(true); + LiveData emptyLiveData = new LiveData(); + assertEquals(emptyLiveData, this.entryStore.get(query)); + + when(source.getParameters()).thenReturn(Map.of( + "target", "user", + "user", "xwiki:XWiki.Bar" + )); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + liveDataException = assertThrows(LiveDataException.class, () -> this.entryStore.get(query)); + assertEquals("You don't have rights to access those information.", liveDataException.getMessage()); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(true); + assertEquals(emptyLiveData, this.entryStore.get(query)); + + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + when(source.getParameters()).thenReturn(Map.of( + "target", "user", + "user", "xwiki:XWiki.Foo" + )); + assertEquals(emptyLiveData, this.entryStore.get(query)); + } + + @Test + void getFromWiki() throws NotificationException, LiveDataException + { + LiveDataQuery query = mock(LiveDataQuery.class); + LiveDataQuery.Source source = mock(LiveDataQuery.Source.class); + when(query.getSource()).thenReturn(source); + when(source.getParameters()).thenReturn(Map.of( + "target", "wiki", + "wiki", "foo" + )); + DocumentReference userDoc = new DocumentReference("xwiki", "XWiki", "Foo"); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(true); + when(context.getUserReference()).thenReturn(userDoc); + + String filter1Name = "filter1"; + String filter2Name = "filter2"; + String filter3Name = "filter3"; + String filter4Name = "filter4"; + String filter5Name = "filter5"; + String filter6Name = "filter6"; + String filter7Name = "filter7"; + + NotificationFilter filter1 = mock(NotificationFilter.class, filter1Name); + ToggleableNotificationFilter filter2 = mock(ToggleableNotificationFilter.class, filter2Name); + NotificationFilter filter3 = mock(NotificationFilter.class, filter3Name); + ToggleableNotificationFilter filter4 = mock(ToggleableNotificationFilter.class, filter4Name); + ToggleableNotificationFilter filter5 = mock(ToggleableNotificationFilter.class, filter5Name); + ToggleableNotificationFilter filter6 = mock(ToggleableNotificationFilter.class, filter6Name); + ToggleableNotificationFilter filter7 = mock(ToggleableNotificationFilter.class, filter7Name); + + when(filter1.getName()).thenReturn(filter1Name); + when(filter2.getName()).thenReturn(filter2Name); + when(filter3.getName()).thenReturn(filter3Name); + when(filter4.getName()).thenReturn(filter4Name); + when(filter5.getName()).thenReturn(filter5Name); + when(filter6.getName()).thenReturn(filter6Name); + when(filter7.getName()).thenReturn(filter7Name); + + WikiReference wikiReference = new WikiReference("foo"); + DocumentReference prefReference = new DocumentReference("foo", List.of("XWiki", "Notifications", "Code"), + "NotificationAdministration"); + when(this.notificationFilterManager.getAllFilters(wikiReference)).thenReturn(List.of( + // They are shuffled to test ordering + filter3, + filter4, + filter6, + filter2, + filter5, + filter1, + filter7 + )); + + when(filter2.getFormats()).thenReturn(List.of(NotificationFormat.ALERT, NotificationFormat.EMAIL)); + when(filter4.getFormats()).thenReturn(List.of()); + when(filter5.getFormats()).thenReturn(List.of(NotificationFormat.ALERT)); + when(filter6.getFormats()).thenReturn(List.of(NotificationFormat.EMAIL)); + when(filter7.getFormats()).thenReturn(List.of(NotificationFormat.EMAIL, NotificationFormat.ALERT)); + + // There's explicitely no activation data for filter2 and filter6 + ToggleableNotificationFilterActivation activationFilter4 = mock(ToggleableNotificationFilterActivation.class, + filter4Name); + ToggleableNotificationFilterActivation activationFilter5 = mock(ToggleableNotificationFilterActivation.class, + filter5Name); + ToggleableNotificationFilterActivation activationFilter7 = mock(ToggleableNotificationFilterActivation.class, + filter7Name); + when(this.filterPreferencesModelBridge.getToggleableFilterActivations(prefReference)).thenReturn(Map.of( + filter4Name, activationFilter4, + filter5Name, activationFilter5, + filter7Name, activationFilter7 + )); + + when(filter2.isEnabledByDefault()).thenReturn(true); + when(filter6.isEnabledByDefault()).thenReturn(false); + + when(filter4.isEnabledByDefault()).thenReturn(true); + when(filter5.isEnabledByDefault()).thenReturn(false); + when(filter7.isEnabledByDefault()).thenReturn(false); + + when(activationFilter4.isEnabled()).thenReturn(false); + when(activationFilter5.isEnabled()).thenReturn(true); + when(activationFilter7.isEnabled()).thenReturn(false); + + when(activationFilter4.getObjectNumber()).thenReturn(-1); + when(activationFilter5.getObjectNumber()).thenReturn(2); + when(activationFilter7.getObjectNumber()).thenReturn(14); + + // Get all info to start + when(query.getOffset()).thenReturn(0L); + when(query.getLimit()).thenReturn(10); + + when(this.translationHelper.getTranslationWithPrefix(eq("notifications.filters.name."), anyString())) + .thenAnswer(invocationOnMock -> "Name:" + invocationOnMock.getArgument(1)); + when(this.translationHelper.getTranslationWithPrefix(eq("notifications.filters.description."), anyString())) + .thenAnswer(invocationOnMock -> "Description " + invocationOnMock.getArgument(1)); + when(this.translationHelper.getFormatTranslation(any())) + .thenAnswer(invocationOnMock -> "Format " + invocationOnMock.getArgument(0)); + + Map dataFilter2 = Map.of( + "name", "Name:" + filter2Name, + "filterDescription", "Description " + filter2Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT", "Format EMAIL") + ), + "isEnabled_data", Map.of( + "objectNumber", "", + "filterName", filter2Name + ), + "isEnabled_checked", true + ); + + Map dataFilter4 = Map.of( + "name", "Name:" + filter4Name, + "filterDescription", "Description " + filter4Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of() + ), + "isEnabled_data", Map.of( + "objectNumber", "", + "filterName", filter4Name + ), + "isEnabled_checked", false + ); + + Map dataFilter5 = Map.of( + "name", "Name:" + filter5Name, + "filterDescription", "Description " + filter5Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT") + ), + "isEnabled_data", Map.of( + "objectNumber", "2", + "filterName", filter5Name + ), + "isEnabled_checked", true + ); + + Map dataFilter6 = Map.of( + "name", "Name:" + filter6Name, + "filterDescription", "Description " + filter6Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format EMAIL") + ), + "isEnabled_data", Map.of( + "objectNumber", "", + "filterName", filter6Name + ), + "isEnabled_checked", false + ); + + Map dataFilter7 = Map.of( + "name", "Name:" + filter7Name, + "filterDescription", "Description " + filter7Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT", "Format EMAIL") + ), + "isEnabled_data", Map.of( + "objectNumber", "14", + "filterName", filter7Name + ), + "isEnabled_checked", false + ); + + LiveData liveData = new LiveData(); + liveData.setCount(5L); + liveData.getEntries().addAll(List.of(dataFilter2, dataFilter4, dataFilter5, dataFilter6, dataFilter7)); + assertEquals(liveData, this.entryStore.get(query)); + + when(query.getOffset()).thenReturn(2L); + when(query.getLimit()).thenReturn(2); + + liveData = new LiveData(); + liveData.setCount(5L); + liveData.getEntries().addAll(List.of(dataFilter5, dataFilter6)); + assertEquals(liveData, this.entryStore.get(query)); + } + + @Test + void getFromUser() throws NotificationException, LiveDataException + { + LiveDataQuery query = mock(LiveDataQuery.class); + LiveDataQuery.Source source = mock(LiveDataQuery.Source.class); + when(query.getSource()).thenReturn(source); + when(source.getParameters()).thenReturn(Map.of( + "target", "user", + "user", "xwiki:XWiki.Foo" + )); + DocumentReference userDoc = new DocumentReference("xwiki", "XWiki", "Foo"); + when(this.contextualAuthorizationManager.hasAccess(Right.ADMIN)).thenReturn(false); + when(context.getUserReference()).thenReturn(userDoc); + + String filter1Name = "filter1"; + String filter2Name = "filter2"; + String filter3Name = "filter3"; + String filter4Name = "filter4"; + String filter5Name = "filter5"; + String filter6Name = "filter6"; + String filter7Name = "filter7"; + + NotificationFilter filter1 = mock(NotificationFilter.class, filter1Name); + ToggleableNotificationFilter filter2 = mock(ToggleableNotificationFilter.class, filter2Name); + NotificationFilter filter3 = mock(NotificationFilter.class, filter3Name); + ToggleableNotificationFilter filter4 = mock(ToggleableNotificationFilter.class, filter4Name); + ToggleableNotificationFilter filter5 = mock(ToggleableNotificationFilter.class, filter5Name); + ToggleableNotificationFilter filter6 = mock(ToggleableNotificationFilter.class, filter6Name); + ToggleableNotificationFilter filter7 = mock(ToggleableNotificationFilter.class, filter7Name); + + when(filter1.getName()).thenReturn(filter1Name); + when(filter2.getName()).thenReturn(filter2Name); + when(filter3.getName()).thenReturn(filter3Name); + when(filter4.getName()).thenReturn(filter4Name); + when(filter5.getName()).thenReturn(filter5Name); + when(filter6.getName()).thenReturn(filter6Name); + when(filter7.getName()).thenReturn(filter7Name); + + when(this.notificationFilterManager.getAllFilters(userDoc, false)).thenReturn(List.of( + // They are shuffled to test ordering + filter3, + filter4, + filter6, + filter2, + filter5, + filter1, + filter7 + )); + + when(filter2.getFormats()).thenReturn(List.of(NotificationFormat.ALERT, NotificationFormat.EMAIL)); + when(filter4.getFormats()).thenReturn(List.of()); + when(filter5.getFormats()).thenReturn(List.of(NotificationFormat.ALERT)); + when(filter6.getFormats()).thenReturn(List.of(NotificationFormat.EMAIL)); + when(filter7.getFormats()).thenReturn(List.of(NotificationFormat.EMAIL, NotificationFormat.ALERT)); + + // There's explicitely no activation data for filter2 and filter6 + ToggleableNotificationFilterActivation activationFilter4 = mock(ToggleableNotificationFilterActivation.class, + filter4Name); + ToggleableNotificationFilterActivation activationFilter5 = mock(ToggleableNotificationFilterActivation.class, + filter5Name); + ToggleableNotificationFilterActivation activationFilter7 = mock(ToggleableNotificationFilterActivation.class, + filter7Name); + when(this.filterPreferencesModelBridge.getToggleableFilterActivations(userDoc)).thenReturn(Map.of( + filter4Name, activationFilter4, + filter5Name, activationFilter5, + filter7Name, activationFilter7 + )); + + when(filter2.isEnabledByDefault()).thenReturn(true); + when(filter6.isEnabledByDefault()).thenReturn(false); + + when(filter4.isEnabledByDefault()).thenReturn(true); + when(filter5.isEnabledByDefault()).thenReturn(false); + when(filter7.isEnabledByDefault()).thenReturn(false); + + when(activationFilter4.isEnabled()).thenReturn(false); + when(activationFilter5.isEnabled()).thenReturn(true); + when(activationFilter7.isEnabled()).thenReturn(false); + + when(activationFilter4.getObjectNumber()).thenReturn(-1); + when(activationFilter5.getObjectNumber()).thenReturn(2); + when(activationFilter7.getObjectNumber()).thenReturn(14); + + // Get all info to start + when(query.getOffset()).thenReturn(0L); + when(query.getLimit()).thenReturn(10); + + when(this.translationHelper.getTranslationWithPrefix(eq("notifications.filters.name."), anyString())) + .thenAnswer(invocationOnMock -> "Name:" + invocationOnMock.getArgument(1)); + when(this.translationHelper.getTranslationWithPrefix(eq("notifications.filters.description."), anyString())) + .thenAnswer(invocationOnMock -> "Description " + invocationOnMock.getArgument(1)); + when(this.translationHelper.getFormatTranslation(any())) + .thenAnswer(invocationOnMock -> "Format " + invocationOnMock.getArgument(0)); + + Map dataFilter2 = Map.of( + "name", "Name:" + filter2Name, + "filterDescription", "Description " + filter2Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT", "Format EMAIL") + ), + "isEnabled_data", Map.of( + "objectNumber", "", + "filterName", filter2Name + ), + "isEnabled_checked", true + ); + + Map dataFilter4 = Map.of( + "name", "Name:" + filter4Name, + "filterDescription", "Description " + filter4Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of() + ), + "isEnabled_data", Map.of( + "objectNumber", "", + "filterName", filter4Name + ), + "isEnabled_checked", false + ); + + Map dataFilter5 = Map.of( + "name", "Name:" + filter5Name, + "filterDescription", "Description " + filter5Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT") + ), + "isEnabled_data", Map.of( + "objectNumber", "2", + "filterName", filter5Name + ), + "isEnabled_checked", true + ); + + Map dataFilter6 = Map.of( + "name", "Name:" + filter6Name, + "filterDescription", "Description " + filter6Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format EMAIL") + ), + "isEnabled_data", Map.of( + "objectNumber", "", + "filterName", filter6Name + ), + "isEnabled_checked", false + ); + + Map dataFilter7 = Map.of( + "name", "Name:" + filter7Name, + "filterDescription", "Description " + filter7Name, + "notificationFormats", Map.of( + "extraClass", "list-unstyled", + "items", List.of("Format ALERT", "Format EMAIL") + ), + "isEnabled_data", Map.of( + "objectNumber", "14", + "filterName", filter7Name + ), + "isEnabled_checked", false + ); + + LiveData liveData = new LiveData(); + liveData.setCount(5L); + liveData.getEntries().addAll(List.of(dataFilter2, dataFilter4, dataFilter5, dataFilter6, dataFilter7)); + assertEquals(liveData, this.entryStore.get(query)); + + when(query.getOffset()).thenReturn(2L); + when(query.getLimit()).thenReturn(2); + + liveData = new LiveData(); + liveData.setCount(5L); + liveData.getEntries().addAll(List.of(dataFilter5, dataFilter6)); + assertEquals(liveData, this.entryStore.get(query)); + } +} \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsIT.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsIT.java index c193cbdd00cb..5fcf4cbca484 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsIT.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsIT.java @@ -243,8 +243,8 @@ void compositeNotifications(TestUtils setup, TestReference testReference, List minorEvent = p.getSystemNotificationFilterPreferences() .stream() - .filter(fp -> fp.getFilterName().equals("Minor Event (Alert)")) - .collect(Collectors.toList()); + .filter(fp -> fp.getName().equals("Minor Event (Alert)")) + .toList(); assertEquals(1, minorEvent.size()); minorEvent.get(0).setEnabled(false); @@ -391,16 +391,18 @@ void ownEventNotifications(TestUtils setup, TestReference testReference) throws DocumentReference page2 = new DocumentReference("page2", testReference.getLastSpaceReference()); try { + int filterPreferenceNumber = 4; NotificationsUserProfilePage p = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); List preferences = p.getSystemNotificationFilterPreferences(); // Now let's do some changes (own even filter) + SystemNotificationFilterPreference filterPreference = preferences.get(filterPreferenceNumber); p.setApplicationState(SYSTEM, "alert", BootstrapSwitch.State.ON); - assertEquals("Own Events Filter", preferences.get(2).getFilterName()); - preferences.get(2).setEnabled(false); + assertEquals("Own Events Filter", filterPreference.getName()); + filterPreference.setEnabled(false); setup.gotoPage(page2); p = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); - assertFalse(p.getSystemNotificationFilterPreferences().get(2).isEnabled()); + assertFalse(p.getSystemNotificationFilterPreferences().get(filterPreferenceNumber).isEnabled()); // Watch the entire wiki so that we receive notifications NotificationsTrayPage tray = new NotificationsTrayPage(); @@ -419,9 +421,10 @@ void ownEventNotifications(TestUtils setup, TestReference testReference) throws // Go back to enable the own even filter p = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); preferences = p.getSystemNotificationFilterPreferences(); - assertEquals("Own Events Filter", preferences.get(2).getFilterName()); - assertFalse(preferences.get(2).isEnabled()); - preferences.get(2).setEnabled(true); + filterPreference = preferences.get(filterPreferenceNumber); + assertEquals("Own Events Filter", filterPreference.getName()); + assertFalse(filterPreference.isEnabled()); + filterPreference.setEnabled(true); setup.createPage(page2, "", "Page 2"); setup.gotoPage(testReference.getLastSpaceReference().getName(), testReference.getName()); notificationsTrayPage = new NotificationsTrayPage(); diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsSettingsIT.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsSettingsIT.java index 70c6a67bf77f..0fffbb5e27c1 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsSettingsIT.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-docker/src/test/it/org/xwiki/platform/notifications/test/ui/NotificationsSettingsIT.java @@ -34,6 +34,7 @@ import org.xwiki.platform.notifications.test.po.NotificationsTrayPage; import org.xwiki.platform.notifications.test.po.NotificationsUserProfilePage; import org.xwiki.platform.notifications.test.po.preferences.ApplicationPreferences; +import org.xwiki.platform.notifications.test.po.preferences.CustomNotificationFilterPreferencesLiveDataElement; import org.xwiki.platform.notifications.test.po.preferences.filters.CustomNotificationFilterModal; import org.xwiki.platform.notifications.test.po.preferences.filters.CustomNotificationFilterPreference; import org.xwiki.platform.notifications.test.po.preferences.filters.SystemNotificationFilterPreference; @@ -45,6 +46,7 @@ import org.xwiki.test.ui.po.ViewPage; import org.xwiki.tree.test.po.TreeNodeElement; import org.xwiki.user.test.po.AbstractUserProfilePage; +import org.xwiki.user.test.po.ProfileUserProfilePage; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -101,7 +103,7 @@ void setup(TestUtils testUtils) @AfterEach void tearDown(TestUtils testUtils) { - testUtils.deletePage("XWiki", FIRST_USER_NAME); + //testUtils.deletePage("XWiki", FIRST_USER_NAME); } @Test @@ -198,42 +200,48 @@ void notificationFiltersDefaultValues(TestUtils testUtils) assertEquals(6, preferences.size()); // Filter 0 - assertEquals("Minor Event (Alert)", preferences.get(0).getFilterName()); - assertEquals("Hide notifications concerning minor changes on pages", preferences.get(0).getDescription()); - assertEquals(List.of("Alert"), preferences.get(0).getFormats()); - assertTrue(preferences.get(0).isEnabled()); + SystemNotificationFilterPreference filter0 = preferences.get(0); + assertEquals("Read Event Filter (Alert)", filter0.getName()); + assertEquals("Hide notifications that you have marked as read", filter0.getDescription()); + assertEquals(List.of("Alert"), filter0.getFormats()); + assertFalse(filter0.isEnabled()); // Filter 1 - assertEquals("Minor Event (Email)", preferences.get(1).getFilterName()); - assertEquals("Hide notifications concerning minor changes on pages", preferences.get(1).getDescription()); - assertEquals(List.of("Email"), preferences.get(1).getFormats()); - assertTrue(preferences.get(1).isEnabled()); + SystemNotificationFilterPreference filter1 = preferences.get(1); + assertEquals("Read Event Filter (Email)", filter1.getName()); + assertEquals("Hide notifications that you have marked as read", filter1.getDescription()); + assertEquals(List.of("Email"), filter1.getFormats()); + assertFalse(filter1.isEnabled()); // Filter 2 - assertEquals("Own Events Filter", preferences.get(2).getFilterName()); - assertEquals("Hide notifications about your own activity unless the event specifically targets you", - preferences.get(2).getDescription()); - assertEquals(List.of("Alert", "Email"), preferences.get(2).getFormats()); - assertTrue(preferences.get(2).isEnabled()); + SystemNotificationFilterPreference filter2 = preferences.get(2); + assertEquals("Minor Event (Alert)", filter2.getName()); + assertEquals("Hide notifications concerning minor changes on pages", filter2.getDescription()); + assertEquals(List.of("Alert"), filter2.getFormats()); + assertTrue(filter2.isEnabled()); // Filter 3 - assertEquals("Read Event Filter (Alert)", preferences.get(3).getFilterName()); - assertEquals("Hide notifications that you have marked as read", preferences.get(3).getDescription()); - assertEquals(List.of("Alert"), preferences.get(3).getFormats()); - assertFalse(preferences.get(3).isEnabled()); + SystemNotificationFilterPreference filter3 = preferences.get(3); + assertEquals("Minor Event (Email)", filter3.getName()); + assertEquals("Hide notifications concerning minor changes on pages", filter3.getDescription()); + assertEquals(List.of("Email"), filter3.getFormats()); + assertTrue(filter3.isEnabled()); // Filter 4 - assertEquals("Read Event Filter (Email)", preferences.get(4).getFilterName()); - assertEquals("Hide notifications that you have marked as read", preferences.get(4).getDescription()); - assertEquals(List.of("Email"), preferences.get(4).getFormats()); - assertFalse(preferences.get(4).isEnabled()); + SystemNotificationFilterPreference filter4 = preferences.get(4); + assertEquals("Own Events Filter", filter4.getName()); + assertEquals("Hide notifications about your own activity unless the event specifically targets you", + filter4.getDescription()); + assertEquals(List.of("Alert", "Email"), filter4.getFormats()); + assertTrue(filter4.isEnabled()); // Filter 5 - assertEquals("System Filter", preferences.get(5).getFilterName()); + SystemNotificationFilterPreference filter5 = preferences.get(5); + assertEquals("System Filter", filter5.getName()); assertEquals("Hide notifications from the System user unless the event specifically targets you", - preferences.get(5).getDescription()); - assertEquals(List.of("Alert", "Email"), preferences.get(5).getFormats()); - assertTrue(preferences.get(5).isEnabled()); + filter5.getDescription()); + assertEquals(List.of("Alert", "Email"), filter5.getFormats()); + assertTrue(filter5.isEnabled()); } @Test @@ -258,12 +266,14 @@ void filterAndWatchedPage(TestUtils testUtils, TestReference testReference) thro // Go back to the preferences to ensure the filter has been created NotificationsUserProfilePage notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); + CustomNotificationFilterPreferencesLiveDataElement customPrefLiveData = + notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); List preferences = - notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(1, preferences.size()); // Filter 0 - assertTrue(preferences.get(0).getFilterName().contains("Page only")); + assertEquals("Page only", preferences.get(0).getScope()); assertEquals(testReference.getLastSpaceReference().getName() + ".WebHome", preferences.get(0).getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, @@ -284,7 +294,9 @@ void filterAndWatchedPage(TestUtils testUtils, TestReference testReference) thro // Go back to the preferences to ensure the filter has been deleted notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); - preferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData = + notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); + preferences = customPrefLiveData.getCustomNotificationFilterPreferences(); assertTrue(preferences.isEmpty()); // back to the page @@ -299,11 +311,13 @@ void filterAndWatchedPage(TestUtils testUtils, TestReference testReference) thro // Go back to the preferences to ensure the filter has been created notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); - preferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData = + notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); + preferences = customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(1, preferences.size()); // Filter 1 - assertTrue(preferences.get(0).getFilterName().contains("Page and children")); + assertEquals("Page and children", preferences.get(0).getScope()); assertEquals(testReference.getLastSpaceReference().getName() + ".WebHome", preferences.get(0).getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, @@ -326,11 +340,13 @@ void filterAndWatchedPage(TestUtils testUtils, TestReference testReference) thro // Go back to the preferences notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); - preferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData = + notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); + preferences = customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(2, preferences.size()); // Filter 2 - assertTrue(preferences.get(1).getFilterName().contains("Page only")); + assertEquals("Page only", preferences.get(1).getScope()); assertEquals(testReference.getLastSpaceReference().getName() + "." + testReference.getName(), preferences.get(1).getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.IGNORE_EVENT, @@ -343,7 +359,9 @@ void filterAndWatchedPage(TestUtils testUtils, TestReference testReference) thro preferences.get(1).setEnabled(false); // Refresh the page notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); - preferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData = + notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); + preferences = customPrefLiveData.getCustomNotificationFilterPreferences(); // Verify the change have been saved assertFalse(preferences.get(1).isEnabled()); @@ -358,9 +376,12 @@ void filterAndWatchedPage(TestUtils testUtils, TestReference testReference) thro // Delete the filters notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); + customPrefLiveData = + notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); + preferences = customPrefLiveData.getCustomNotificationFilterPreferences(); // The livetable page is refreshed so we need to load back the filter preferences - notificationsUserProfilePage.getCustomNotificationFilterPreferences().get(1).delete(); - notificationsUserProfilePage.getCustomNotificationFilterPreferences().get(0).delete(); + preferences.get(1).delete(); + preferences.get(0).delete(); // Verify it's all like the beginning testUtils.gotoPage(testReference.getLastSpaceReference().getName(), testReference.getName()); @@ -372,18 +393,22 @@ void filterAndWatchedPage(TestUtils testUtils, TestReference testReference) thro // Go back to the preferences notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); - preferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData = + notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); + preferences = customPrefLiveData.getCustomNotificationFilterPreferences(); assertTrue(preferences.isEmpty()); } finally { // Clean up testUtils.rest().deletePage(testReference.getLastSpaceReference().getName(), testReference.getName()); NotificationsUserProfilePage p = NotificationsUserProfilePage.gotoPage(FIRST_USER_NAME); - List preferences = p.getCustomNotificationFilterPreferences(); + CustomNotificationFilterPreferencesLiveDataElement customPrefLiveData = + p.getCustomNotificationFilterPreferencesLiveData(); + List preferences = + customPrefLiveData.getCustomNotificationFilterPreferences(); while (!preferences.isEmpty()) { preferences.get(preferences.size() - 1).delete(); - // Reload the livetable - preferences = p.getCustomNotificationFilterPreferences(); + preferences = customPrefLiveData.getCustomNotificationFilterPreferences(); } } } @@ -556,10 +581,11 @@ void globalAndOtherUserSettings(TestUtils testUtils, TestReference testReference * - create a new user and check that the filter exist for it * - add a new filter to the user with admin user * - login with the new user and add another new filter + * - Add several new preferences and check the modal sort/filter operations */ @Test @Order(5) - void addCustomFilters(TestUtils testUtils) + void customFiltersAndLiveData(TestUtils testUtils, TestReference testReference) throws Exception { // Create pages for the filter locations SpaceReference lastSpaceReference = new SpaceReference("xwiki", NotificationsSettingsIT.class.getSimpleName()); @@ -573,8 +599,10 @@ void addCustomFilters(TestUtils testUtils) testUtils.loginAsSuperAdmin(); NotificationsAdministrationPage administrationPage = NotificationsAdministrationPage.gotoPage(); + CustomNotificationFilterPreferencesLiveDataElement customPrefLiveData = + administrationPage.getCustomNotificationFilterPreferencesLiveData(); List customNotificationFilterPreferences = - administrationPage.getCustomNotificationFilterPreferences(); + customPrefLiveData.getCustomNotificationFilterPreferences(); assertTrue(customNotificationFilterPreferences.isEmpty()); CustomNotificationFilterModal customNotificationFilterModal = administrationPage.clickAddCustomFilter(); @@ -599,12 +627,12 @@ void addCustomFilters(TestUtils testUtils) // check newly created filter customNotificationFilterPreferences = - administrationPage.getCustomNotificationFilterPreferences(); + customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(1, customNotificationFilterPreferences.size()); CustomNotificationFilterPreference filterPreference = customNotificationFilterPreferences.get(0); - assertTrue(filterPreference.getFilterName().contains("Page and children")); + assertEquals("Page and children", filterPreference.getScope()); assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".WebHome", filterPreference.getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); assertEquals(List.of("Email"), filterPreference.getFormats()); @@ -620,12 +648,13 @@ void addCustomFilters(TestUtils testUtils) // go to notification settings of new user and check that the custom filter exists there too NotificationsUserProfilePage notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(secondUserUsername); - customNotificationFilterPreferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData = notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); + customNotificationFilterPreferences = customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(1, customNotificationFilterPreferences.size()); filterPreference = customNotificationFilterPreferences.get(0); - assertTrue(filterPreference.getFilterName().contains("Page and children")); + assertEquals("Page and children", filterPreference.getScope()); assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".WebHome", filterPreference.getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); assertEquals(List.of("Email"), filterPreference.getFormats()); @@ -643,13 +672,13 @@ void addCustomFilters(TestUtils testUtils) customNotificationFilterModal.clickSubmit(); // Check the newly created filter - customNotificationFilterPreferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customNotificationFilterPreferences = customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(2, customNotificationFilterPreferences.size()); - filterPreference = customNotificationFilterPreferences.get(1); + // Filters are ordered in descending order of creation + filterPreference = customNotificationFilterPreferences.get(0); - assertTrue(filterPreference.getFilterName().contains("Page")); - assertFalse(filterPreference.getFilterName().contains("Page and children")); + assertEquals("Page only", filterPreference.getScope()); assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".SubSpace.SubPage", filterPreference.getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.IGNORE_EVENT, filterPreference.getFilterAction()); @@ -662,29 +691,29 @@ void addCustomFilters(TestUtils testUtils) // check the filters notificationsUserProfilePage = NotificationsUserProfilePage.gotoPage(secondUserUsername); + customPrefLiveData = notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); customNotificationFilterPreferences = - notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(2, customNotificationFilterPreferences.size()); filterPreference = customNotificationFilterPreferences.get(0); - assertTrue(filterPreference.getFilterName().contains("Page and children")); - assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".WebHome", filterPreference.getLocation()); - assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); - assertEquals(List.of("Email"), filterPreference.getFormats()); - assertEquals(List.of("A page is modified"), filterPreference.getEventTypes()); - - filterPreference = customNotificationFilterPreferences.get(1); - - assertTrue(filterPreference.getFilterName().contains("Page")); - assertFalse(filterPreference.getFilterName().contains("Page and children")); + assertEquals("Page only", filterPreference.getScope()); assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".SubSpace.SubPage", filterPreference.getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.IGNORE_EVENT, filterPreference.getFilterAction()); assertEquals(List.of("Alert", "Email"), filterPreference.getFormats()); assertTrue(filterPreference.getEventTypes().isEmpty()); + filterPreference = customNotificationFilterPreferences.get(1); + + assertEquals("Page and children", filterPreference.getScope()); + assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".WebHome", filterPreference.getLocation()); + assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); + assertEquals(List.of("Email"), filterPreference.getFormats()); + assertEquals(List.of("A page is modified"), filterPreference.getEventTypes()); + // add a final filter: it will actually create 2 filters since we select several locations customNotificationFilterModal = notificationsUserProfilePage.clickAddCustomFilter(); customNotificationFilterModal.selectAction(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT); @@ -700,26 +729,104 @@ void addCustomFilters(TestUtils testUtils) customNotificationFilterModal.clickSubmit(2); // check created filters - customNotificationFilterPreferences = notificationsUserProfilePage.getCustomNotificationFilterPreferences(); + customNotificationFilterPreferences = customPrefLiveData.getCustomNotificationFilterPreferences(); assertEquals(4, customNotificationFilterPreferences.size()); - filterPreference = customNotificationFilterPreferences.get(2); + filterPreference = customNotificationFilterPreferences.get(0); + + assertEquals("Page only", filterPreference.getScope()); + assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".Page2", filterPreference.getLocation()); + assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); + assertEquals(List.of("Alert"), filterPreference.getFormats()); + // core.events.delete before core.events.update + assertEquals(List.of("A page is deleted","A page is modified"), filterPreference.getEventTypes()); + + filterPreference = customNotificationFilterPreferences.get(1); - assertTrue(filterPreference.getFilterName().contains("Page")); - assertFalse(filterPreference.getFilterName().contains("Page and children")); + assertEquals("Page only", filterPreference.getScope()); assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".Page1", filterPreference.getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); assertEquals(List.of("Alert"), filterPreference.getFormats()); - assertEquals(List.of("A page is modified", "A page is deleted"), filterPreference.getEventTypes()); + // core.events.delete before core.events.update + assertEquals(List.of("A page is deleted", "A page is modified"), filterPreference.getEventTypes()); + + // follow a user + ProfileUserProfilePage userProfilePage = ProfileUserProfilePage.gotoPage(FIRST_USER_NAME); + assertFalse(userProfilePage.isFollowed()); + userProfilePage.toggleFollowButton(); + + notificationsUserProfilePage = + NotificationsUserProfilePage.gotoPage(secondUserUsername); + notificationsUserProfilePage.setAutoWatchMode(AbstractNotificationsSettingsPage.AutowatchMode.NEW); + + // Create multiple pages + for (int i = 0; i < 15; i++) { + DocumentReference pageRef = new DocumentReference("Page_" + i, testReference.getLastSpaceReference()); + testUtils.rest().savePage(pageRef, "Content of page " + i, "Title of page " + i ); + } + String pageSpaceName = + NotificationsSettingsIT.class.getSimpleName() + "." + testReference.getLastSpaceReference().getName(); + + notificationsUserProfilePage = + NotificationsUserProfilePage.gotoPage(secondUserUsername); + customPrefLiveData = notificationsUserProfilePage.getCustomNotificationFilterPreferencesLiveData(); - filterPreference = customNotificationFilterPreferences.get(3); + // 4 filters created before following a user + // +1 when following the user + // +15 when creating the pages after setting autowatch + assertEquals(20, customPrefLiveData.getTableLayout().getTotalEntries()); - assertTrue(filterPreference.getFilterName().contains("Page")); - assertFalse(filterPreference.getFilterName().contains("Page and children")); + customPrefLiveData.filterScope("USER"); + assertEquals(1, customPrefLiveData.getTableLayout().getTotalEntries()); + filterPreference = customPrefLiveData.getCustomNotificationFilterPreferences().get(0); + assertEquals("User", filterPreference.getScope()); + assertEquals(List.of("Alert", "Email"), filterPreference.getFormats()); + assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); + assertTrue(filterPreference.getEventTypes().isEmpty()); + assertEquals("XWiki." + FIRST_USER_NAME, filterPreference.getLocation()); + + customPrefLiveData.clearAllFilters(); + customPrefLiveData.filterLocation(testReference.getLastSpaceReference().getName()); + // Click twice to order descending + customPrefLiveData.sortLocation(); + customPrefLiveData.sortLocation(); + assertEquals(15, customPrefLiveData.getTableLayout().getTotalEntries()); + filterPreference = customPrefLiveData.getCustomNotificationFilterPreferences().get(0); + assertEquals("Page only", filterPreference.getScope()); + assertEquals(List.of("Alert", "Email"), filterPreference.getFormats()); + assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); + assertTrue(filterPreference.getEventTypes().isEmpty()); + // Order is alphabetical so Page_9 is > Page_15 + assertEquals(pageSpaceName + ".Page_9", filterPreference.getLocation()); + + customPrefLiveData.clearAllFilters(); + customPrefLiveData.clearAllSort(); + assertEquals(20, customPrefLiveData.getTableLayout().getTotalEntries()); + customPrefLiveData.filterFilterAction("INCLUSIVE"); + assertEquals(19, customPrefLiveData.getTableLayout().getTotalEntries()); + customPrefLiveData.filterLocation("2"); + // 3 possibles matches: Page2,Page_2,Page_12 + assertEquals(3, customPrefLiveData.getTableLayout().getTotalEntries()); + customPrefLiveData.sortFormats(); + customPrefLiveData.sortFormats(); + filterPreference = customPrefLiveData.getCustomNotificationFilterPreferences().get(0); + assertEquals("Page only", filterPreference.getScope()); assertEquals(NotificationsSettingsIT.class.getSimpleName() + ".Page2", filterPreference.getLocation()); assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); assertEquals(List.of("Alert"), filterPreference.getFormats()); - assertEquals(List.of("A page is modified", "A page is deleted"), filterPreference.getEventTypes()); + // core.events.delete before core.events.update + assertEquals(List.of("A page is deleted","A page is modified"), filterPreference.getEventTypes()); + + customPrefLiveData.filterFormat("EMAIL"); + assertEquals(2, customPrefLiveData.getTableLayout().getTotalEntries()); + customPrefLiveData.sortLocation(); + + filterPreference = customPrefLiveData.getCustomNotificationFilterPreferences().get(0); + assertEquals("Page only", filterPreference.getScope()); + assertEquals(List.of("Alert", "Email"), filterPreference.getFormats()); + assertEquals(CustomNotificationFilterPreference.FilterAction.NOTIFY_EVENT, filterPreference.getFilterAction()); + assertTrue(filterPreference.getEventTypes().isEmpty()); + assertEquals(pageSpaceName + ".Page_12", filterPreference.getLocation()); } @Test diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/AbstractNotificationsSettingsPage.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/AbstractNotificationsSettingsPage.java index eb99e74a8521..860bcb4841d2 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/AbstractNotificationsSettingsPage.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/AbstractNotificationsSettingsPage.java @@ -31,9 +31,9 @@ import org.xwiki.livedata.test.po.TableLayoutElement; import org.xwiki.platform.notifications.test.po.preferences.AbstractNotificationPreferences; import org.xwiki.platform.notifications.test.po.preferences.ApplicationPreferences; +import org.xwiki.platform.notifications.test.po.preferences.CustomNotificationFilterPreferencesLiveDataElement; import org.xwiki.platform.notifications.test.po.preferences.EventTypePreferences; import org.xwiki.platform.notifications.test.po.preferences.filters.CustomNotificationFilterModal; -import org.xwiki.platform.notifications.test.po.preferences.filters.CustomNotificationFilterPreference; import org.xwiki.platform.notifications.test.po.preferences.filters.SystemNotificationFilterPreference; import org.xwiki.test.ui.po.BootstrapSwitch; import org.xwiki.test.ui.po.Select; @@ -287,27 +287,12 @@ public List getSystemNotificationFilterPrefe } /** - * @return the custom notification filter preferences - * @since 13.2RC1 + * @return the livedata containing the custom filter preferences. + * @since 16.3.0RC1 */ - public List getCustomNotificationFilterPreferences() + public CustomNotificationFilterPreferencesLiveDataElement getCustomNotificationFilterPreferencesLiveData() { - List preferences = new ArrayList<>(); - LiveDataElement liveDataElement = - new LiveDataElement("notificationCustomFilterPreferencesLiveData"); - for (WebElement row : liveDataElement.getTableLayout().getRows()) { - preferences.add(new CustomNotificationFilterPreference(this, row, this.getDriver())); - } - preferences.sort((o1, o2) -> { - String name = "data-livedata-entry-id"; - int substringIndex = "NFP_".length(); - // Sort by numbering value of the custom filter indexes. If sorting alphanumerically, 10 is lower than 2 - // which can lead to unexpected sorting. - Long attribute = Long.parseLong(o1.getRow().getAttribute(name).substring(substringIndex)); - Long attribute1 = Long.parseLong(o2.getRow().getAttribute(name).substring(substringIndex)); - return attribute.compareTo(attribute1); - }); - return preferences; + return new CustomNotificationFilterPreferencesLiveDataElement(this); } /** diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/CustomNotificationFilterPreferencesLiveDataElement.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/CustomNotificationFilterPreferencesLiveDataElement.java new file mode 100644 index 000000000000..9ab468667b09 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/CustomNotificationFilterPreferencesLiveDataElement.java @@ -0,0 +1,150 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.platform.notifications.test.po.preferences; + +import java.util.List; + +import org.xwiki.livedata.test.po.LiveDataElement; +import org.xwiki.platform.notifications.test.po.AbstractNotificationsSettingsPage; +import org.xwiki.platform.notifications.test.po.preferences.filters.CustomNotificationFilterPreference; + +/** + * Page object representing the live data which contains custom filter preferences. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +public class CustomNotificationFilterPreferencesLiveDataElement extends LiveDataElement +{ + private static final String SCOPE_COLUMN = "Scope"; + private static final String LOCATION_COLUMN = "Location"; + private static final String FORMAT_COLUMN = "Formats"; + private static final String EVENT_TYPES_COLUMN = "Events"; + private static final String FILTER_ACTION_COLUMN = "Filter Action"; + private final AbstractNotificationsSettingsPage parentPage; + + /** + * Default constructor. + * + * @param parentPage the page holding the live data. + */ + public CustomNotificationFilterPreferencesLiveDataElement(AbstractNotificationsSettingsPage parentPage) + { + super("notificationCustomFilterPreferencesLiveData"); + this.parentPage = parentPage; + } + + /** + * @return the custom filter preferences entries. + */ + public List getCustomNotificationFilterPreferences() + { + return getTableLayout().getRows() + .stream() + .map(row -> new CustomNotificationFilterPreference(this.parentPage, row, this.getDriver())) + .toList(); + } + + /** + * Filter the scope column. + * @param scope the value to put in the filter. + */ + public void filterScope(String scope) + { + getTableLayout().filterColumn(SCOPE_COLUMN, scope); + } + + /** + * Sort the scope column. + */ + public void sortScope() + { + getTableLayout().sortBy(SCOPE_COLUMN); + } + + /** + * Filter the location column. + * @param location the value to put in the filter. + */ + public void filterLocation(String location) + { + getTableLayout().filterColumn(LOCATION_COLUMN, location); + } + + /** + * Sort the location column. + */ + public void sortLocation() + { + getTableLayout().sortBy(LOCATION_COLUMN); + } + + /** + * Filter the event type column. + * @param eventType the value to put in the filter. + */ + public void filterEventType(String eventType) + { + getTableLayout().filterColumn(EVENT_TYPES_COLUMN, eventType); + } + + /** + * Sort the event type column. + */ + public void sortEventType() + { + getTableLayout().sortBy(EVENT_TYPES_COLUMN); + } + + /** + * Filter the filter action column. + * @param filterAction the value to put in the filter. + */ + public void filterFilterAction(String filterAction) + { + getTableLayout().filterColumn(FILTER_ACTION_COLUMN, filterAction); + } + + /** + * Sort the filter action column. + */ + public void sortFilterAction() + { + getTableLayout().sortBy(FILTER_ACTION_COLUMN); + } + + /** + * Filter the format column. + * @param format the value to put in the filter. + */ + public void filterFormat(String format) + { + getTableLayout().filterColumn(FORMAT_COLUMN, format); + } + + /** + * Sort the format column. + */ + public void sortFormats() + { + getTableLayout().sortBy(FORMAT_COLUMN); + } +} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/AbstractNotificationFilterPreference.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/AbstractNotificationFilterPreference.java index b0f6552ad412..653a4b5d1de6 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/AbstractNotificationFilterPreference.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/AbstractNotificationFilterPreference.java @@ -46,8 +46,6 @@ public abstract class AbstractNotificationFilterPreference private WebElement row; - private String filterName; - private List formats = new ArrayList<>(); private BootstrapSwitch enabledSwitch; @@ -64,7 +62,6 @@ public AbstractNotificationFilterPreference(AbstractNotificationsSettingsPage pa { this.parentPage = parentPage; this.row = row; - this.filterName = getNameElement(row).getText(); List formatElements = getFormatsElement(row) .findElements(By.tagName("li")); for (WebElement format : formatElements) { @@ -74,13 +71,6 @@ public AbstractNotificationFilterPreference(AbstractNotificationsSettingsPage pa this.enabledSwitch = new BootstrapSwitch(getBootstrapSwitchElement(row), webDriver); } - /** - * @param row the row to get the name from - * @return the {@link WebElement} containing the name of the row - * @since 16.1.0RC1 - */ - protected abstract WebElement getNameElement(WebElement row); - /** * @param row the row to get the formats from * @return the {@link WebElement} containing the formats of the row @@ -100,14 +90,6 @@ private WebElement getBootstrapSwitchElement(WebElement row) return row.findElement(By.className("displayer-toggle")).findElement(By.className("bootstrap-switch")); } - /** - * @return the filter name. - */ - public String getFilterName() - { - return filterName; - } - /** * @return the parent page where the settings are displayed. */ diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/CustomNotificationFilterPreference.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/CustomNotificationFilterPreference.java index 910cce433547..53a14dfad0f0 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/CustomNotificationFilterPreference.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/CustomNotificationFilterPreference.java @@ -91,10 +91,12 @@ public CustomNotificationFilterPreference(AbstractNotificationsSettingsPage pare } } - @Override - protected WebElement getNameElement(WebElement row) + /** + * @return the scope of the filter. + */ + public String getScope() { - return row.findElement(By.cssSelector("td[data-title='Location']")); + return getRow().findElement(By.cssSelector("td[data-title='Scope']")).getText(); } /** @@ -110,7 +112,7 @@ public List getEventTypes() */ public String getLocation() { - return this.getRow().findElement(By.cssSelector("td[data-title='Location'] .html-wrapper ol")) + return getRow().findElement(By.cssSelector("td[data-title='Location'] .html-wrapper ol")) .getAttribute("data-entity"); } diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/NotificationFilterPreference.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/NotificationFilterPreference.java deleted file mode 100644 index df49f65ee77d..000000000000 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/NotificationFilterPreference.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.xwiki.platform.notifications.test.po.preferences.filters; - -import java.util.ArrayList; -import java.util.List; - -import org.openqa.selenium.By; -import org.openqa.selenium.WebElement; -import org.xwiki.platform.notifications.test.po.AbstractNotificationsSettingsPage; -import org.xwiki.test.ui.XWikiWebDriver; -import org.xwiki.test.ui.po.BootstrapSwitch; -import org.xwiki.test.ui.po.ConfirmationBox; - -/** - * Represent a livetable row describing a notification filter preference. - * - * @version $Id$ - * @since 10.8RC1 - * @since 9.11.8 - * @deprecated Since 13.2RC1: this page object is related to the usage of the deprecated XWiki macro - * {@code NotificationsFiltersPreferences} which was mixing system filters and custom filters. You should - * now use either {@link CustomNotificationFilterPreference} or {@link SystemNotificationFilterPreference}. - */ -@Deprecated -public class NotificationFilterPreference -{ - private static final String LIST_HTML_TAG = "li"; - private static final String EVENT_TYPES = "eventTypes"; - - private AbstractNotificationsSettingsPage parentPage; - - private WebElement livetableRow; - - private String filterName; - - private String filterType; - - private List eventTypes = new ArrayList<>(); - - private List formats = new ArrayList<>(); - - private BootstrapSwitch enabledSwitch; - - /** - * Construct a NotificationFilterPreference. - * @param parentPage the user profile's page holding the livetable. - * @param webElement the livetable row - * @param driver the current webdriver in used - */ - public NotificationFilterPreference(AbstractNotificationsSettingsPage parentPage, WebElement webElement, - XWikiWebDriver driver) - { - this.parentPage = parentPage; - this.livetableRow = webElement; - - this.filterName = webElement.findElement(By.className("name")).getText(); - this.filterType = webElement.findElement(By.className("filterType")).getText(); - - // Since the changes performed in 13.2RC1, not all filters LT have the event types. - if (driver.hasElement(webElement, By.className(EVENT_TYPES))) { - List eventTypeElements = webElement.findElement(By.className(EVENT_TYPES)).findElements( - By.tagName(LIST_HTML_TAG)); - for (WebElement eventType : eventTypeElements) { - String text = eventType.getText(); - if (!"-".equals(text)) { - this.eventTypes.add(text); - } - } - } - - List formatElements = webElement.findElement(By.className("notificationFormats")) - .findElements(By.tagName(LIST_HTML_TAG)); - for (WebElement format : formatElements) { - this.formats.add(format.getText()); - } - - enabledSwitch = new BootstrapSwitch( - webElement.findElement(By.className("isEnabled")).findElement(By.className("bootstrap-switch")), - driver - ); - } - - /** - * @return the name of the filter - */ - public String getFilterName() - { - return filterName; - } - - /** - * @return the type of the filter - */ - public String getFilterType() - { - return filterType; - } - - /** - * @return the event types concerned by the filters (empty means "all"). - */ - public List getEventTypes() - { - return eventTypes; - } - - /** - * @return the formats concerned by the filter - */ - public List getFormats() - { - return formats; - } - - /** - * @return either or not the preference is enabled - */ - public boolean isEnabled() - { - return enabledSwitch.getState() == BootstrapSwitch.State.ON; - } - - /** - * Enable or disable the current filter. - * @param enabled either or not the filter must be enabled - * @throws Exception if the expected state cannot be set - */ - public void setEnabled(boolean enabled) throws Exception - { - if (isEnabled() == enabled) { - return; - } - - this.enabledSwitch.setState(enabled ? BootstrapSwitch.State.ON : BootstrapSwitch.State.OFF); - this.parentPage.waitForNotificationSuccessMessage("Filter preference saved!"); - } - - /** - * @return the watched location if the current filter is a ScopeNotificationFilter. - */ - public String getLocation() - { - return this.livetableRow.findElement(By.cssSelector("td.name ol")).getAttribute("data-entity"); - } - - /** - * Delete the filter preference. - */ - public void delete() - { - this.livetableRow.findElement(By.cssSelector("td.actions a.actiondelete")).click(); - ConfirmationBox confirmationBox = new ConfirmationBox(); - confirmationBox.clickYes(); - this.parentPage.waitForNotificationSuccessMessage("Filter preference deleted!"); - } - - @Override - public String toString() - { - return String.format("NotificationFilterPreference{filterName='%s', filterType='%', evenTypes=%s, formats=%s," - + " enabled=%s", filterName, filterName, eventTypes, formats, isEnabled()); - } - - /** - * @return the ID of the filter. - * @since 12.4 - */ - public String getID() - { - return this.livetableRow.findElement(By.className("notificationFilterPreferenceCheckbox")) - .getAttribute("data-preferenceid"); - } -} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/SystemNotificationFilterPreference.java b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/SystemNotificationFilterPreference.java index 9c93039b3390..ddf529d39fff 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/SystemNotificationFilterPreference.java +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-test/xwiki-platform-notifications-test-pageobjects/src/main/java/org/xwiki/platform/notifications/test/po/preferences/filters/SystemNotificationFilterPreference.java @@ -32,8 +32,6 @@ */ public class SystemNotificationFilterPreference extends AbstractNotificationFilterPreference { - private final String description; - /** * Default constructor. * @param parentPage the page where the settings are displayed. @@ -44,13 +42,14 @@ public SystemNotificationFilterPreference(AbstractNotificationsSettingsPage pare XWikiWebDriver webDriver) { super(parentPage, row, webDriver); - this.description = row.findElement(By.cssSelector("td[data-title='Description'] .view")).getText(); } - @Override - protected WebElement getNameElement(WebElement row) + /** + * @return the name of the filter. + */ + public String getName() { - return row.findElement(By.cssSelector("td[data-title='Name'] .view")); + return getRow().findElement(By.cssSelector("td[data-title='Name'] .view")).getText(); } /** @@ -58,6 +57,6 @@ protected WebElement getNameElement(WebElement row) */ public String getDescription() { - return this.description; + return getRow().findElement(By.cssSelector("td[data-title='Description'] .view")).getText(); } } diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationFilterPreferenceLivetableResults.xml b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationFilterPreferenceLivetableResults.xml deleted file mode 100644 index 6d8040ff6808..000000000000 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationFilterPreferenceLivetableResults.xml +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - XWiki.Notifications.Code - NotificationFilterPreferenceLivetableResults - - - 0 - xwiki:XWiki.Admin - XWiki.Notifications.Code.WebHome - xwiki:XWiki.Admin - xwiki:XWiki.Admin - 1.1 - NotificationFilterPreferenceLivetableResults - - false - xwiki/2.1 - true - {{velocity wiki="false"}} -#if ($xcontext.action == 'get') -## -## Offset = item # at which to start displaying data -## -#set ($offset = $numbertool.toNumber($request.get('offset'))) -## offset starts from 0 in velocity and 1 in javascript -#set ($offset = $offset - 1) -#if ($offset < 0) - #set ($offset = 0) -#end -## -## Limit = # of items to display -## -#set ($limit = $numbertool.toNumber($request.get('limit'))) -#set ($limitOffset = $offset + $limit) -## -## Build the list of NotificationFilterPreference related to the given user. -## -#set ($isAccessGranted = false) -#if ("$!request.user" != "") - #set ($requestedUserDocRef = $services.model.resolveDocument($request.user)) - #set ($isAccessGranted = ($services.security.authorization.hasAccess('admin', $requestedUserDocRef) || $xcontext.userReference.equals($requestedUserDocRef))) - #set ($document = $xwiki.getDocument($requestedUserDocRef)) - #set ($target = $request.user) - #set ($userTarget = true) -## when request user is not given we are sending the list of NotificationFilterPreference of the current wiki. -#else - #set ($target = $services.wiki.getCurrentWikiReference()) - #set ($document = $xwiki.getDocument('XWiki.Notifications.Code.NotificationAdministration')) - #set ($userTarget = false) - #set ($isAccessGranted = true) -#end -#set ($elements = []) -#set ($index = 0) - -## For backward compatibility reason, we display both system filters and custom filters. -#set ($displayCustom = true) -#set ($displaySystem = true) -## This is the translation prefix used for filter types in the deprecated macro. -#set ($filterTypeTranslationPrefix = "notifications.filters.type.") - -#if ("$!request.type" == "custom") - #set ($displaySystem = false) - ## Those translations should now be used. - #set ($filterTypeTranslationPrefix = "notifications.filters.type.custom.") -#elseif ("$!request.type" == "system") - #set ($displayCustom = false) -#end - -#if ($displaySystem && $isAccessGranted) - ## First: get the list of available toggeable filters for the user - ## We display them first because we don't want them to be hidden by thousands of page filters the autowatch option - ## might have created. It would be not good to have to go to the last page of the livetable to find out these commonly - ## used filters. - #if ($userTarget) - #set ($filters = $services.notification.filters.getToggleableNotificationFilters($target)) - #else - #set ($filters = $services.notification.filters.getWikiToggleableNotificationFilters($target)) - #end - #foreach ($filter in $filters) - #set ($index = $index + 1) - ## Optimization to render only displayed rows (between $offset and $limitOffset) - #if ($index > $offset and $index <= $limitOffset) - #set ($checked = $filter.isEnabledByDefault()) - #set ($objectNumber = '') - #set ($obj = $document.getObject('XWiki.Notifications.Code.ToggleableFilterPreferenceClass', 'filterName', $filter.name)) - #if ($obj) - #set ($checked = $obj.getValue('isEnabled') != 0) - #set ($objectNumber = $!obj.reference.objectNumber) - #end - #if ($checked) - #set ($checkedAttr = 'checked = "checked"') - #else - #set ($checkedAttr = '') - #end - #set ($checkbox = "<input type='checkbox' class='toggleableFilterPreferenceCheckbox' data-objectNumber='${objectNumber}' data-filtername='${filter.name}' $checkedAttr />") - #set ($name = $services.localization.render("notifications.filters.name.$filter.name")) - #set ($element = { - 'filterPreferenceId' : $name, - 'name' : $name, - 'filterType' : $services.localization.render("notifications.filters.description.$filter.name"), - 'eventTypes' : "#displayNotificationsEventTypeList($filter.eventTypes)", - 'notificationFormats' : "#displayNotificationFormatsList($filter.formats)", - 'isEnabled' : $checkbox, - 'doc_viewable' : true, - 'doc_hasdelete' : false, - 'isEnabled_checked': $checked, - 'isEnabled_data': { - 'objectNumber': $objectNumber, - 'filtername': $filter.name - } - }) - #set ($discard = $elements.add($element)) - #end - #end - #set ($elements = $collectiontool.sort($elements, ['name'])) -#end - -#if ($displayCustom && $isAccessGranted) - ## Also get the list of available filters for the user - #set ($filters = $collectiontool.arrayList) - #if ($userTarget) - #set ($discard = $filters.addAll($services.notification.filters.getFilters($target))) - #else - #set ($discard = $filters.addAll($services.notification.filters.getWikiFilters($target))) - #end - #set ($filters = $collectiontool.sort($filters, ['name'])) - #set ($displayHiddenDocument = "$xwiki.getUserPreference('displayHiddenDocuments')" == '1') - #foreach ($filter in $filters) - #set ($filtersPreferences = $collectiontool.arrayList) - #if ($userTarget) - #set ($discard = $filtersPreferences.addAll($services.notification.filters.getFilterPreferences($filter, $target))) - #else - #set ($discard = $filtersPreferences.addAll($services.notification.filters.getWikiFilterPreferences($filter, $target))) - #end - #set ($filtersPreferences = $collectiontool.sort($filtersPreferences, ['id'])) - #foreach ($preference in $filtersPreferences) - #set ($page = $preference.pageOnly) - #set ($space = $preference.page) - #if (!$displayHiddenDocument && ($stringtool.isNotBlank($page) && $xwiki.getDocument($page).isHidden() - || $stringtool.isNotBlank($space) && $xwiki.getDocument($services.model.resolveSpace($space, 'default')).isHidden())) - ## Don't display a preference for an hidden page or space. - ## Why? If a user has the autowatch option enabled and create a new hidden document (for example, when she creates an application with AWM, hidden code pages are created as well), - ## we don't want to pollute the livetable with these hidden pages that the user don't know about. Moreover, the notifications will filter events concerning hidden pages, - ## so even if the filter exists, it has no effect to the user. - ## Here we handle only the case where pages.size() == 1 or spaces.size() == 1. - ## In the case there are several elements in the lists, they might have different "hidden" properties, and we don't handle it - ## Note: in the current implementation of watched entities, it's never the case. - ## TODO: handle these cases too in case custom filters need this. - #else - #set ($index = $index + 1) - ## Optimization to render only displayed rows (between $offset and $limitOffset) - #if ($index > $offset and $index <= $limitOffset) - #set ($display = $services.rendering.render($services.notification.filters.displayFilterPreference($filter, $preference), 'html/5.0')) - #set ($isEnabled = '') - #set ($isDisabled = '') - ## The watchlist bridge does not handle disabling a specific filter - #if ($preference.id.startsWith('watchlist_')) - #set ($isDisabled = 'disabled = "disabled"') - #end - #if ($preference.isEnabled()) - #set ($isEnabled = 'checked = "checked"') - #end - #set ($isEnabled = "<input type='checkbox' class='notificationFilterPreferenceCheckbox' data-preferenceId='${preference.id}' $isEnabled $isDisabled />") - - ## In case of backport of 15.10.x, isEnabled_disabled must be introduced to disabled legacy filters. - #set ($element = { - 'filterPreferenceId' : $preference.id, - 'name' : $display, - 'filterType' : $services.localization.render("$filterTypeTranslationPrefix$!preference.getFilterType().name().toLowerCase()"), - 'eventTypes' : "#displayNotificationsEventTypeList($preference.eventTypes)", - 'notificationFormats' : "#displayNotificationFormatsList($preference.notificationFormats)", - 'isEnabled' : $isEnabled, - 'doc_viewable' : true, - 'isEnabled_checked': $preference.isEnabled(), - 'isEnabled_data': { - 'preferenceId': $preference.id - } - }) - #set ($discard = $elements.add($element)) - #end - #end - #end - #end -#end -#if ($isAccessGranted) - ## - ## JSON. - ## - #set ($discard = $response.setContentType('application/json')) - #set ($offset = $numbertool.toNumber($request.offset)) - #if (!$offset) - #set ($offset = 1) - #end - $jsontool.serialize({ - 'totalrows' : $index, - 'reqNo' : $request.reqNo, - 'returnedrows': $elements.size(), - 'offset' : $offset, - 'rows' : $elements - }) -#else - #set ($discard = $response.sendError(401)) -#end -#end ## (context action) -## -## MACROS -## -#macro (displayNotificationsEventTypeList $list) - <ul class="list-unstyled"> - #if ($list.isEmpty()) - <li>$escapetool.xml($services.localization.render('notifications.filters.preferences.allEvents'))</li> - #else - #set ($types = {}) - #foreach ($descriptor in $services.eventstream.getRecordableEventDescriptors($xcontext.isMainWiki())) - #set ($discard = $types.put($descriptor.eventType, "$!services.localization.render($descriptor.description)")) - #end - #foreach ($eventType in $list) - <li>$escapetool.xml($types.get($eventType))</li> - #end - #end - </ul> -#end -#macro (displayNotificationFormatsList $formatList) - #set ($sortedFormatList = $collectiontool.sort($formatList)) - <ul class="list-unstyled"> - #foreach ($notificationFormat in $sortedFormatList) - <li>$services.localization.render("notifications.format.$!notificationFormat.name().toLowerCase()")</li> - #end - </ul> -#end -{{/velocity}} - diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsCustomFiltersPreferencesMacro.xml b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsCustomFiltersPreferencesMacro.xml index d5645352c095..99f1029890d8 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsCustomFiltersPreferencesMacro.xml +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsCustomFiltersPreferencesMacro.xml @@ -469,6 +469,8 @@ require(['jquery', 'NewCustomNotificationFilterModal', 'xwiki-bootstrap-switch', 'xwiki-events-bridge' ], function ($, xm, Vue, notifications, l10n) { Vue.component('DisplayerToggle', notifications.DisplayerToggle); + Vue.component('DisplayerScope', notifications.DisplayerScope); + Vue.component('DisplayerStaticList', notifications.DisplayerStaticList); // Globals var serviceReference = XWiki.Model.resolve('XWiki.Notifications.Code.NotificationPreferenceService', @@ -783,6 +785,7 @@ require(['jquery', 'NewCustomNotificationFilterModal', 'xwiki-bootstrap-switch', #set ($discard = $xwiki.jsx.use('XWiki.Notifications.Code.NotificationsCustomFiltersPreferencesMacro')) ## TODO: replace by $xwiki.sswx.use() or something like this when XWIKI-12788 is closed. #set ($discard = $xwiki.linkx.use($services.webjars.url('bootstrap-switch', 'css/bootstrap3/bootstrap-switch.min.css'), {'type': 'text/css', 'rel': 'stylesheet'})) +#set ($discard = $xwiki.linkx.use($services.webjars.url('org.xwiki.platform:xwiki-platform-notifications-webjar', 'style.css'), {'type': 'text/css', 'rel': 'stylesheet'})) ###################################################### ### MACRO CONTENT ###################################################### @@ -799,39 +802,21 @@ require(['jquery', 'NewCustomNotificationFilterModal', 'xwiki-bootstrap-switch', </div> </div> #set ($sourceParameters = { - 'translationPrefix': 'notifications.settings.filters.preferences.custom.table.', - 'resultPage': 'XWiki.Notifications.Code.NotificationFilterPreferenceLivetableResults', - 'eventType': '', - 'format': '', - 'type': 'custom' + 'target': $wikimacro.parameters.target }) #if ($wikimacro.parameters.target == 'user') #set ($discard = $sourceParameters.put('user', $services.model.serialize($targetUser, 'default'))) + #else + #set ($discard = $sourceParameters.put('wiki', $services.model.serialize($services.wiki.getCurrentWikiReference(), 'default'))) #end - #set ($liveDataConfig = { - 'meta': { - 'propertyDescriptors': [ - { 'id': 'notificationFormats', 'displayer': 'html', 'filterable': false, 'sortable': false }, - { 'id': 'isEnabled', 'displayer': 'toggle', 'filterable': false, 'sortable': false }, - { 'id': 'name', 'displayer': 'html', 'filterable': false, 'sortable': false}, - { 'id': 'filterType', 'filterable': false, 'sortable': false}, - { 'id': 'eventTypes', 'displayer': 'html', 'filterable': false, 'sortable': false} - ], - 'actions': [ - {'id': 'delete', 'allowProperty': 'doc.viewable'} - ], - 'entryDescriptor': { - 'idProperty': 'filterPreferenceId' - } - } - }) $services.liveData.render({ 'id': 'notificationCustomFilterPreferencesLiveData', - 'properties': 'name,filterType,eventTypes,notificationFormats,isEnabled,_actions', - 'source': 'liveTable', + 'properties': 'filterPreferenceId,display,scope,location,filterType,eventTypes,notificationFormats,isEnabled,actions', + 'source': 'notificationCustomFilters', 'sourceParameters': $escapetool.url($sourceParameters), - 'limit': 10 - }, $liveDataConfig) + 'limit': 10, + 'sort': 'filterPreferenceId:desc' + }) </div> ###################################################### ### ADD FILTER MODAL diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsSystemFiltersPreferencesMacro.xml b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsSystemFiltersPreferencesMacro.xml index 6d02977aad50..098d83e97a9e 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsSystemFiltersPreferencesMacro.xml +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/NotificationsSystemFiltersPreferencesMacro.xml @@ -136,6 +136,7 @@ require(['jquery', 'xwiki-l10n!notifications-system-filters-preferences-macro', 'vue', 'xwiki-platform-notifications-webjar'], function ($, l10n, Vue, notifications) { Vue.component('DisplayerToggle', notifications.DisplayerToggle) + Vue.component('DisplayerStaticList', notifications.DisplayerStaticList); function save(checked, filterName, objectNumber, docURL) { return new Promise((resolve) => { @@ -185,7 +186,7 @@ require(['jquery', 'xwiki-l10n!notifications-system-filters-preferences-macro', $(document).on('xwiki:livedata:toggle', '#notificationSystemFilterPreferencesLiveData', (event) => { const detail = event.detail; const data = detail.data - save(detail.checked, data.filtername, data.objectNumber, docURL).then(({checked, objectNumber}) => { + save(detail.checked, data.filterName, data.objectNumber, docURL).then(({checked, objectNumber}) => { detail.callback({ ...detail, data: { @@ -596,40 +597,23 @@ path=$services.webjars.url('org.xwiki.platform:xwiki-platform-notifications-webj $escapetool.xml($services.localization.render('notifications.settings.filters.preferences.system.hint')) </p> </div> -</div> -{{/html}} #set ($sourceParameters = { - 'translationPrefix': 'notifications.settings.filters.preferences.system.table.', - 'resultPage': 'XWiki.Notifications.Code.NotificationFilterPreferenceLivetableResults', - 'eventType': '', - 'format': '', - 'type': 'system' + 'target': $wikimacro.parameters.target }) #if ($wikimacro.parameters.target == 'user') -#set($discard = $sourceParameters.put('user', $services.model.serialize($targetUser, 'default'))) + #set ($discard = $sourceParameters.put('user', $services.model.serialize($targetUser, 'default'))) +#else + #set ($discard = $sourceParameters.put('wiki', $services.model.serialize($services.wiki.getCurrentWikiReference(), 'default'))) #end - -#set ($liveDataConfig = { - 'meta': { - 'propertyDescriptors': [ - { 'id': 'notificationFormats', 'displayer': 'html', 'filterable': false, 'sortable': false }, - { 'id': 'isEnabled', 'displayer': 'toggle', 'filterable': false, 'sortable': false }, - { 'id': 'name', 'filterable': false, 'sortable': false}, - { 'id': 'filterType', 'filterable': false, 'sortable': false} - ], - 'entryDescriptor': { - 'idProperty': 'filterPreferenceId' - } - } +$services.liveData.render({ + 'id': 'notificationSystemFilterPreferencesLiveData', + 'properties': 'name,filterDescription,notificationFormats,isEnabled', + 'source': 'notificationSystemFilters', + 'sourceParameters': $escapetool.url($sourceParameters), + 'limit': 10 }) -#set ($sourceParameters = $services.rendering.escape($escapetool.url($sourceParameters), 'xwiki/2.1')) -{{liveData - id='notificationSystemFilterPreferencesLiveData' - properties="name,filterType,notificationFormats,isEnabled" - source='liveTable' - sourceParameters="$sourceParameters" - limit='10' -}}$jsontool.serialize($liveDataConfig){{/liveData}} +</div> +{{/html}} #end {{/velocity}} diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/Translations.xml b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/Translations.xml index 72dc9ae589e8..11d9220e0391 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/Translations.xml +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-ui/src/main/resources/XWiki/Notifications/Code/Translations.xml @@ -20,7 +20,7 @@ * 02110-1301 USA, or see the FSF site: http://www.fsf.org. --> - + XWiki.Notifications.Code Translations @@ -71,6 +71,7 @@ notifications.filters.preferences.scopeNotificationFilter.name=Scope Notificatio notifications.filters.preferences.scopeNotificationFilter.page=Page only notifications.filters.preferences.scopeNotificationFilter.space=Page and children notifications.filters.preferences.scopeNotificationFilter.wiki=Wiki +notifications.filters.preferences.scopeNotificationFilter.user=User ## Notifications toggles notifications.toggle.disabled.hint=You need to enable notifications in your settings if you wish to watch these locations @@ -94,6 +95,7 @@ notifications.settings.filters.preferences.system.title=System Filters notifications.settings.filters.preferences.system.hint=System filters that you can switches to have a more fine-grained control of the notifications you want to receive. ## Header definition for notification filter preferences notifications.settings.filters.preferences.system.table.name=Name +notifications.settings.filters.preferences.system.table.filterDescription=Description notifications.settings.filters.preferences.system.table.filterType=Description notifications.settings.filters.preferences.system.table.notificationFormats=Formats notifications.settings.filters.preferences.system.table.isEnabled=Is enabled? @@ -102,7 +104,9 @@ notifications.settings.filters.preferences.system.table.isEnabled=Is enabled? notifications.settings.filters.preferences.custom.title=Custom Filters notifications.settings.filters.preferences.custom.hint=Create filters to control the notifications you want to receive. ## Header definition for notification filter preferences -notifications.settings.filters.preferences.custom.table.name=Location +notifications.settings.filters.preferences.custom.table.filterPreferenceId=Identifier +notifications.settings.filters.preferences.custom.table.scope=Scope +notifications.settings.filters.preferences.custom.table.location=Location notifications.settings.filters.preferences.custom.table.filterType=Filter Action notifications.settings.filters.preferences.custom.table.eventTypes=Events notifications.settings.filters.preferences.custom.table.notificationFormats=Formats @@ -268,6 +272,7 @@ notifications.filters.type.exclusive=Exclusive ## until 16.2.0RC1 notifications.settings.filters.preferences.custom.table.description=This table lists every custom filter registered for the given user or current wiki. notifications.settings.filters.preferences.system.table.description=This table lists every system filter registered in the wiki. +notifications.settings.filters.preferences.custom.table.name=Location ## Used to indicate where deprecated keys end #@deprecatedend diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/components/DisplayerScope.vue b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/components/DisplayerScope.vue new file mode 100644 index 000000000000..85d5eaefcda6 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/components/DisplayerScope.vue @@ -0,0 +1,65 @@ + + + + + + + + \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/components/DisplayerStaticList.vue b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/components/DisplayerStaticList.vue new file mode 100644 index 000000000000..3d31adb65306 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/components/DisplayerStaticList.vue @@ -0,0 +1,64 @@ + + + + + + \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/main.js b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/main.js index fc8ddcb896a3..b6b669eb2aa8 100644 --- a/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/main.js +++ b/xwiki-platform-core/xwiki-platform-notifications/xwiki-platform-notifications-webjar/src/main/vue/src/main.js @@ -18,6 +18,8 @@ * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ import DisplayerToggle from "./components/DisplayerToggle.vue"; +import DisplayerScope from "./components/DisplayerScope.vue"; +import DisplayerStaticList from "./components/DisplayerStaticList.vue"; // Export the elements that are expected to be imported from other modules. -export {DisplayerToggle} \ No newline at end of file +export {DisplayerToggle,DisplayerScope,DisplayerStaticList} \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/DefaultQueryParameter.java b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/DefaultQueryParameter.java index 59eeda298385..e0ef0a067418 100644 --- a/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/DefaultQueryParameter.java +++ b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/DefaultQueryParameter.java @@ -22,6 +22,8 @@ import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; import org.xwiki.query.Query; import org.xwiki.query.QueryParameter; @@ -96,4 +98,27 @@ public Query query() { return this.query; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + DefaultQueryParameter that = (DefaultQueryParameter) o; + + return new EqualsBuilder().append(parts, that.parts).append(query, that.query) + .isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 95).append(parts).append(query).toHashCode(); + } } diff --git a/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ParameterPart.java b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ParameterPart.java index 8b7ff696e607..6f3681372396 100644 --- a/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ParameterPart.java +++ b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ParameterPart.java @@ -19,6 +19,9 @@ */ package org.xwiki.query.internal; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + /** * Represents a part of a HQL parameter. There can be several parts since we separate literals (ie characters that * will be escaped) vs special characters having a SQL meaning and that won't be escaped (ie {@code _} and {@code %}). @@ -46,4 +49,26 @@ public String getValue() { return this.value; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + ParameterPart that = (ParameterPart) o; + + return new EqualsBuilder().append(value, that.value).isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(63, 37).append(value).toHashCode(); + } } diff --git a/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ScriptQuery.java b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ScriptQuery.java index 1193b6d8fc56..fda1952f179a 100644 --- a/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ScriptQuery.java +++ b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/main/java/org/xwiki/query/internal/ScriptQuery.java @@ -24,6 +24,8 @@ import java.util.concurrent.Callable; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -339,4 +341,29 @@ public SecureQuery checkCurrentUser(boolean checkCurrentUser) return this; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + ScriptQuery that = (ScriptQuery) o; + + return new EqualsBuilder().append(switchAuthor, that.switchAuthor) + .append(componentManager, that.componentManager).append(query, that.query) + .append(authorReference, that.authorReference).append(sourceReference, that.sourceReference).isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 37).append(componentManager).append(query).append(switchAuthor) + .append(authorReference).append(sourceReference).toHashCode(); + } } diff --git a/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/test/java/org/xwiki/query/script/QueryManagerScriptServiceTest.java b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/test/java/org/xwiki/query/script/QueryManagerScriptServiceTest.java new file mode 100644 index 000000000000..9bff6a94661d --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-query/xwiki-platform-query-manager/src/test/java/org/xwiki/query/script/QueryManagerScriptServiceTest.java @@ -0,0 +1,103 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.xwiki.query.script; + +import javax.inject.Named; + +import org.junit.jupiter.api.Test; +import org.xwiki.component.manager.ComponentManager; +import org.xwiki.query.Query; +import org.xwiki.query.QueryException; +import org.xwiki.query.QueryManager; +import org.xwiki.query.SecureQuery; +import org.xwiki.query.internal.DefaultQueryParameter; +import org.xwiki.query.internal.ScriptQuery; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link QueryManagerScriptService}. + * + * @version $Id$ + * @since 16.3.0RC1 + */ +@ComponentTest +class QueryManagerScriptServiceTest +{ + @InjectMockComponents + private QueryManagerScriptService queryManagerScriptService; + + @MockComponent + @Named("secure") + private QueryManager secureQueryManager; + + @MockComponent + private ComponentManager componentManager; + + @Test + void xwql() throws QueryException + { + String statement = "some statement"; + SecureQuery secureQuery = mock(SecureQuery.class); + when(this.secureQueryManager.createQuery(statement, Query.XWQL)).thenReturn(secureQuery); + ScriptQuery scriptQuery = new ScriptQuery(secureQuery, this.componentManager); + assertEquals(scriptQuery, this.queryManagerScriptService.xwql(statement)); + verify(secureQuery).checkCurrentUser(false); + verify(secureQuery).checkCurrentAuthor(true); + } + + @Test + void hql() throws QueryException + { + String statement = "some statement"; + SecureQuery secureQuery = mock(SecureQuery.class); + when(this.secureQueryManager.createQuery(statement, Query.HQL)).thenReturn(secureQuery); + ScriptQuery scriptQuery = new ScriptQuery(secureQuery, this.componentManager); + assertEquals(scriptQuery, this.queryManagerScriptService.hql(statement)); + verify(secureQuery).checkCurrentUser(false); + verify(secureQuery).checkCurrentAuthor(true); + } + + @Test + void createQuery() throws QueryException + { + String statement = "some statement"; + String language = "some language"; + SecureQuery secureQuery = mock(SecureQuery.class); + when(this.secureQueryManager.createQuery(statement, language)).thenReturn(secureQuery); + ScriptQuery scriptQuery = new ScriptQuery(secureQuery, this.componentManager); + assertEquals(scriptQuery, this.queryManagerScriptService.createQuery(statement, language)); + verify(secureQuery).checkCurrentUser(true); + verify(secureQuery).checkCurrentAuthor(true); + } + + @Test + void parameter() + { + assertEquals(new DefaultQueryParameter(null), this.queryManagerScriptService.parameter()); + } +} \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-test/xwiki-platform-user-profile-test-pageobjects/src/main/java/org/xwiki/user/test/po/ProfileUserProfilePage.java b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-test/xwiki-platform-user-profile-test-pageobjects/src/main/java/org/xwiki/user/test/po/ProfileUserProfilePage.java index a35db2e44bb3..0182f79c45e6 100644 --- a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-test/xwiki-platform-user-profile-test-pageobjects/src/main/java/org/xwiki/user/test/po/ProfileUserProfilePage.java +++ b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-test/xwiki-platform-user-profile-test-pageobjects/src/main/java/org/xwiki/user/test/po/ProfileUserProfilePage.java @@ -68,8 +68,11 @@ public class ProfileUserProfilePage extends AbstractUserProfilePage @FindBy(xpath = "//div[@id='avatar']//img") private WebElement userAvatarImage; - @FindBy(css = ".activity-follow a") - private WebElement followUnfollowButton; + @FindBy(css = ".activity-follow .notificationWatchUserFollowing") + private WebElement followingContainer; + + @FindBy(css = ".activity-follow .notificationWatchUserNotFollowing") + private WebElement notFollowingContainer; @FindBy(id = "disable") private WebElement disableButton; @@ -161,19 +164,15 @@ public String getAvatarImageName() public boolean isFollowed() { - String[] classNames = followUnfollowButton.getAttribute("class").split(" "); - for (String className : classNames) { - if ("unfollow".equals(className)) { - return true; - } - } - - return false; + return followingContainer.isDisplayed(); } public ProfileUserProfilePage toggleFollowButton() { - this.followUnfollowButton.click(); + WebElement container = (isFollowed()) ? followingContainer : notFollowingContainer; + getDriver().findElementWithoutWaiting(container, By.tagName("button")).click(); + container.findElement(By.className("dropdown-menu")).findElement(By.tagName("a")).click(); + waitForNotificationSuccessMessage("Done"); return new ProfileUserProfilePage(this.getUsername()); } diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/notification/filters/livedatalocation.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/notification/filters/livedatalocation.vm new file mode 100644 index 000000000000..4db7c2793bf9 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/notification/filters/livedatalocation.vm @@ -0,0 +1,21 @@ +## --------------------------------------------------------------------------- +## See the NOTICE file distributed with this work for additional +## information regarding copyright ownership. +## +## This is free software; you can redistribute it and/or modify it +## under the terms of the GNU Lesser General Public License as +## published by the Free Software Foundation; either version 2.1 of +## the License, or (at your option) any later version. +## +## This software is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public +## License along with this software; if not, write to the Free +## Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +## 02110-1301 USA, or see the FSF site: http://www.fsf.org. +## --------------------------------------------------------------------------- +#template('hierarchy_macros.vm') +#hierarchy($location, {'limit': 5, 'plain': false, 'local': false, 'displayTitle': false}) \ No newline at end of file