From f4a3e78787619935a273518570f78c428a35744e Mon Sep 17 00:00:00 2001 From: Helmi Akermi Date: Thu, 16 Jan 2025 19:32:32 +0100 Subject: [PATCH] feat: Reindex targeted identities when used dropdown option value translation updated - EXO-8007 - Meeds-io/MIPs#171 Reindex targeted identities when used option value translation updated --- .../model/ProfilePropertyOption.java | 17 ++- ...pertySettingOptionTranslationListener.java | 79 +++++++++++++ .../ProfilePropertyServiceImpl.java | 85 +++++++++++--- .../core/jpa/test/InitContainerTestSuite.java | 2 + ...ySettingOptionTranslationListenerTest.java | 107 ++++++++++++++++++ .../social/rest/api/EntityBuilder.java | 3 +- .../entity/ProfilePropertyOptionEntity.java | 20 +++- .../component-plugins-configuration.xml | 6 + .../components/ProfileSettings.vue | 20 ---- 9 files changed, 294 insertions(+), 45 deletions(-) create mode 100644 component/core/src/main/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListener.java create mode 100644 component/core/src/test/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListenerTest.java diff --git a/component/api/src/main/java/org/exoplatform/social/core/profileproperty/model/ProfilePropertyOption.java b/component/api/src/main/java/org/exoplatform/social/core/profileproperty/model/ProfilePropertyOption.java index 6675b7f350c..5a74d2de602 100644 --- a/component/api/src/main/java/org/exoplatform/social/core/profileproperty/model/ProfilePropertyOption.java +++ b/component/api/src/main/java/org/exoplatform/social/core/profileproperty/model/ProfilePropertyOption.java @@ -24,14 +24,25 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.Locale; +import java.util.Map; + @Data @NoArgsConstructor @AllArgsConstructor public class ProfilePropertyOption { - private Long id; + private Long id; + + private String value; + + private Long propertySettingId; - private String value; + private Map translations; - private Long propertySettingId; + public ProfilePropertyOption(Long id, String value, Long propertySettingId) { + this.id = id; + this.value = value; + this.propertySettingId = propertySettingId; + } } diff --git a/component/core/src/main/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListener.java b/component/core/src/main/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListener.java new file mode 100644 index 00000000000..4ae45ccf025 --- /dev/null +++ b/component/core/src/main/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListener.java @@ -0,0 +1,79 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2025 Meeds Association contact@meeds.io + * + * This program 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 3 of the License, or (at your option) any later version. + * + * This program 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 program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +package org.exoplatform.social.core.listeners; + +import io.meeds.common.ContainerTransactional; +import org.exoplatform.commons.search.index.IndexingService; +import org.exoplatform.commons.utils.ListAccess; +import org.exoplatform.services.listener.Asynchronous; +import org.exoplatform.services.listener.Event; +import org.exoplatform.services.listener.Listener; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider; +import org.exoplatform.social.core.jpa.search.ProfileIndexingServiceConnector; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.profile.ProfileFilter; +import org.exoplatform.social.core.profileproperty.ProfilePropertyService; +import org.exoplatform.social.core.profileproperty.model.ProfilePropertyOption; +import org.exoplatform.social.core.profileproperty.model.ProfilePropertySetting; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Asynchronous +public class ProfilePropertySettingOptionTranslationListener extends Listener> { + + private final IdentityManager identityManager; + + private final ProfilePropertyService profilePropertyService; + + private final IndexingService indexingService; + + public ProfilePropertySettingOptionTranslationListener(IdentityManager identityManager, + ProfilePropertyService profilePropertyService, + IndexingService indexingService) { + this.identityManager = identityManager; + this.profilePropertyService = profilePropertyService; + this.indexingService = indexingService; + } + + @Override + @ContainerTransactional + public void onEvent(Event> event) throws Exception { + ProfilePropertyOption profilePropertyOption = event.getSource(); + Map oldTranslations = event.getData(); + ProfilePropertySetting propertySetting = + profilePropertyService.getProfileSettingById(profilePropertyOption.getPropertySettingId()); + String translations = String.join("-", oldTranslations.values()); + String indexedValue = String.join("-", profilePropertyOption.getValue(), translations); + + ProfileFilter profileFilter = new ProfileFilter(); + profileFilter.setProfileSettings(Map.of(propertySetting.getPropertyName(), indexedValue)); + ListAccess identities = identityManager.getIdentitiesByProfileFilter(OrganizationIdentityProvider.NAME, + profileFilter, + true); + List identityList = List.of(identities.load(0, identities.getSize())); + for (Identity identity : identityList) { + indexingService.reindex(ProfileIndexingServiceConnector.TYPE, String.valueOf(identity.getId())); + } + } +} diff --git a/component/core/src/main/java/org/exoplatform/social/core/profileproperty/ProfilePropertyServiceImpl.java b/component/core/src/main/java/org/exoplatform/social/core/profileproperty/ProfilePropertyServiceImpl.java index 366ec5e223e..757347d0b21 100644 --- a/component/core/src/main/java/org/exoplatform/social/core/profileproperty/ProfilePropertyServiceImpl.java +++ b/component/core/src/main/java/org/exoplatform/social/core/profileproperty/ProfilePropertyServiceImpl.java @@ -20,11 +20,15 @@ package org.exoplatform.social.core.profileproperty; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; +import io.meeds.social.translation.model.TranslationField; +import io.meeds.social.translation.service.TranslationService; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.social.core.profileproperty.model.ProfilePropertyOption; import org.picocontainer.Startable; import com.fasterxml.jackson.core.type.TypeReference; @@ -57,6 +61,8 @@ public class ProfilePropertyServiceImpl implements ProfilePropertyService, Start private final IndexingService indexingService; private final ListenerService listenerService; + + private final TranslationService translationService; private static final String SYNCHRONIZED_DISABLED_PROPERTIES = "synchronizationDisabledProperties"; @@ -77,16 +83,21 @@ public class ProfilePropertyServiceImpl implements ProfilePropertyService, Start private static final String HIDDEN_PROFILE_PROPERTY_SETTINGS_KEY = "HiddenProfilePropertySettings"; + private static final String PROFILE_PROPERTY_FIELD_NAME = "optionValue"; + + private static final String PROFILE_PROPERTY_OBJECT_TYPE = "propertySettingOption"; + public ProfilePropertyServiceImpl(InitParams params, ProfileSettingStorage profileSettingStorage, SettingService settingService, IndexingService indexingService, - ListenerService listenerService) { + ListenerService listenerService, TranslationService translationService) { this.profileSettingStorage = profileSettingStorage; this.settingService = settingService; this.indexingService = indexingService; this.listenerService = listenerService; - if (params != null) { + this.translationService = translationService; + if (params != null) { try { synchronizedGroupDisabledProperties = Arrays.asList(params.getValueParam(SYNCHRONIZED_DISABLED_PROPERTIES) .getValue() @@ -156,21 +167,22 @@ public ProfilePropertySetting createPropertySetting(ProfilePropertySetting profi } profilePropertySetting.setUpdated(System.currentTimeMillis()); - profilePropertySetting = profileSettingStorage.saveProfilePropertySetting(profilePropertySetting, true); - - if (profilePropertySetting.getOrder() == null) { - profilePropertySetting.setOrder(profilePropertySetting.getId()); - profilePropertySetting = profileSettingStorage.saveProfilePropertySetting(profilePropertySetting, false); + storedProfilePropertySetting = profileSettingStorage.saveProfilePropertySetting(profilePropertySetting, + true); + savePropertyOptionsTranslations(storedProfilePropertySetting, profilePropertySetting.getPropertyOptions(), false); + if (storedProfilePropertySetting.getOrder() == null) { + storedProfilePropertySetting.setOrder(storedProfilePropertySetting.getId()); + storedProfilePropertySetting = profileSettingStorage.saveProfilePropertySetting(storedProfilePropertySetting, false); } try { - listenerService.broadcast("profile-property-setting-created", this, profilePropertySetting); + listenerService.broadcast("profile-property-setting-created", this, storedProfilePropertySetting); } catch (Exception e) { LOG.error("An error occurred while broadcasting the creation event for the property setting '{}'.", - profilePropertySetting.getPropertyName(), + storedProfilePropertySetting.getPropertyName(), e); } - return profilePropertySetting; + return storedProfilePropertySetting; } @Override @@ -195,14 +207,15 @@ && getUnhiddenableProfileProperties().contains(profilePropertySetting.getPropert profilePropertySetting.setPropertyType(createdProfilePropertySetting.getPropertyType()); } profilePropertySetting.setUpdated(System.currentTimeMillis()); - profileSettingStorage.saveProfilePropertySetting(profilePropertySetting, false); + ProfilePropertySetting updatedPropertySetting = profileSettingStorage.saveProfilePropertySetting(profilePropertySetting, false); + savePropertyOptionsTranslations(updatedPropertySetting, profilePropertySetting.getPropertyOptions(), true); try { - listenerService.broadcast("profile-property-setting-updated", this, profilePropertySetting); + listenerService.broadcast("profile-property-setting-updated", this, updatedPropertySetting); } catch (Exception e) { - LOG.error("An error occurred when broadcasting the update event of the property setting {}", profilePropertySetting.getPropertyName(), e); + LOG.error("An error occurred when broadcasting the update event of the property setting {}", updatedPropertySetting.getPropertyName(), e); } } - + @Override public void deleteProfilePropertySetting(Long id) { if (id <= 0) { @@ -337,4 +350,42 @@ private void validatePropertySetting(ProfilePropertySetting profilePropertySetti throw new IllegalArgumentException("Only text properties can be dropdown lists."); } } + + private void savePropertyOptionsTranslations(ProfilePropertySetting savedProfilePropertySetting, + List newOptions, + boolean update) { + if (!savedProfilePropertySetting.isDropdownList()) { + return; + } + List savedOptions = + profileSettingStorage.getProfilePropertyOptions(savedProfilePropertySetting.getId(), + 0, + 0); + for (int i = 0; i < savedOptions.size(); i++) { + ProfilePropertyOption savedOption = savedOptions.get(i); + ProfilePropertyOption newOption = i < newOptions.size() ? newOptions.get(i) : null; + if (savedOption == null || newOption == null) { + continue; + } + try { + TranslationField translationField = translationService.getTranslationField(PROFILE_PROPERTY_OBJECT_TYPE, + savedOption.getId(), + PROFILE_PROPERTY_FIELD_NAME); + Map translations = newOption.getTranslations() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> Locale.forLanguageTag(entry.getKey()), + Map.Entry::getValue)); + translationService.saveTranslationLabels(PROFILE_PROPERTY_OBJECT_TYPE, + savedOption.getId(), + PROFILE_PROPERTY_FIELD_NAME, + translations); + if (update && translationField != null && !translationField.getLabels().equals(translations)) { + listenerService.broadcast("property_options_updated", newOption, translationField.getLabels()); + } + } catch (Exception e) { + LOG.error("Error while saving translation labels for profile property option {}", savedOption.getId(), e); + } + } + } } diff --git a/component/core/src/test/java/org/exoplatform/social/core/jpa/test/InitContainerTestSuite.java b/component/core/src/test/java/org/exoplatform/social/core/jpa/test/InitContainerTestSuite.java index 7213fb93db7..099c97f0e72 100644 --- a/component/core/src/test/java/org/exoplatform/social/core/jpa/test/InitContainerTestSuite.java +++ b/component/core/src/test/java/org/exoplatform/social/core/jpa/test/InitContainerTestSuite.java @@ -16,6 +16,7 @@ */ package org.exoplatform.social.core.jpa.test; +import org.exoplatform.social.core.listeners.ProfilePropertySettingOptionTranslationListenerTest; import org.exoplatform.social.core.plugin.ProfilePropertySettingOptionTranslationTest; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -131,6 +132,7 @@ OrganizationalChartHeaderTranslationTest.class, ManagerPropertySettingUpdatedListenerTest.class, ProfilePropertySettingOptionTranslationTest.class, + ProfilePropertySettingOptionTranslationListenerTest.class, }) @ConfigTestCase(AbstractCoreTest.class) public class InitContainerTestSuite extends BaseExoContainerTestSuite { diff --git a/component/core/src/test/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListenerTest.java b/component/core/src/test/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListenerTest.java new file mode 100644 index 00000000000..208b014673e --- /dev/null +++ b/component/core/src/test/java/org/exoplatform/social/core/listeners/ProfilePropertySettingOptionTranslationListenerTest.java @@ -0,0 +1,107 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2025 Meeds Association contact@meeds.io + * + * This program 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 3 of the License, or (at your option) any later version. + * + * This program 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 program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.exoplatform.social.core.listeners; + +import org.exoplatform.commons.search.index.IndexingService; +import org.exoplatform.commons.utils.ListAccess; +import org.exoplatform.services.listener.Event; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.jpa.search.ProfileIndexingServiceConnector; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.profileproperty.ProfilePropertyService; +import org.exoplatform.social.core.profileproperty.model.ProfilePropertyOption; +import org.exoplatform.social.core.profileproperty.model.ProfilePropertySetting; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.*; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class ProfilePropertySettingOptionTranslationListenerTest { + + @Mock + private ProfilePropertyService profilePropertyService; + + @Mock + private IdentityManager identityManager; + + @Mock + private IndexingService indexingService; + + private ProfilePropertySettingOptionTranslationListener profilePropertySettingOptionListener; + + @Before + public void setUp() throws Exception { + profilePropertySettingOptionListener = new ProfilePropertySettingOptionTranslationListener(identityManager, + profilePropertyService, + indexingService); + } + + @Test + public void testOnEvent() throws Exception { + Identity identity1 = mock(Identity.class); + Identity identity2 = mock(Identity.class); + + when(identity1.getId()).thenReturn("identity1"); + when(identity2.getId()).thenReturn("identity2"); + + Map labels = new HashMap<>(); + labels.put(Locale.US, "option en"); + labels.put(Locale.FRANCE, "option fr"); + + ProfilePropertySetting profilePropertySetting = new ProfilePropertySetting(); + profilePropertySetting.setId(1L); + profilePropertySetting.setPropertyName("test1"); + + ProfilePropertyOption profilePropertyOption1 = new ProfilePropertyOption(); + profilePropertyOption1.setId(1L); + profilePropertyOption1.setValue("value"); + profilePropertyOption1.setPropertySettingId(1L); + + Event> event = new Event<>("property_options_updated", + profilePropertyOption1, + labels); + + ListAccess identityListAccess = new ListAccess<>() { + public Identity[] load(int index, int length) { + List identities = new ArrayList<>(); + identities.add(identity1); + identities.add(identity2); + Identity[] result = new Identity[identities.size()]; + return identities.toArray(result); + } + + public int getSize() { + return 2; + } + }; + when(profilePropertyService.getProfileSettingById(eq(1L))).thenReturn(profilePropertySetting); + when(identityManager.getIdentitiesByProfileFilter(anyString(), any(), anyBoolean())).thenReturn(identityListAccess); + profilePropertySettingOptionListener.onEvent(event); + verify(indexingService, times(1)).reindex(ProfileIndexingServiceConnector.TYPE, "identity1"); + verify(indexingService, times(1)).reindex(ProfileIndexingServiceConnector.TYPE, "identity1"); + } +} diff --git a/component/service/src/main/java/org/exoplatform/social/rest/api/EntityBuilder.java b/component/service/src/main/java/org/exoplatform/social/rest/api/EntityBuilder.java index 15f2dcaeccd..bf04b1e9f37 100644 --- a/component/service/src/main/java/org/exoplatform/social/rest/api/EntityBuilder.java +++ b/component/service/src/main/java/org/exoplatform/social/rest/api/EntityBuilder.java @@ -2127,7 +2127,8 @@ public static List toProfilePropertyOptions(List new ProfilePropertyOption(option.getId(), option.getValue(), - option.getPropertySettingId())) + option.getPropertySettingId(), + option.getTranslations())) .toList(); } diff --git a/component/service/src/main/java/org/exoplatform/social/rest/entity/ProfilePropertyOptionEntity.java b/component/service/src/main/java/org/exoplatform/social/rest/entity/ProfilePropertyOptionEntity.java index b1f241f657b..2611bab6244 100644 --- a/component/service/src/main/java/org/exoplatform/social/rest/entity/ProfilePropertyOptionEntity.java +++ b/component/service/src/main/java/org/exoplatform/social/rest/entity/ProfilePropertyOptionEntity.java @@ -24,16 +24,28 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.Locale; +import java.util.Map; + @Data @NoArgsConstructor @AllArgsConstructor public class ProfilePropertyOptionEntity { - private Long id; + private Long id; + + private String value; + + private String translatedValue; - private String value; + private Long propertySettingId; - private String translatedValue; + private Map translations; - private Long propertySettingId; + public ProfilePropertyOptionEntity(Long id, String value, String translatedValue, Long propertySettingId) { + this.id = id; + this.value = value; + this.translatedValue = translatedValue; + this.propertySettingId = propertySettingId; + } } diff --git a/webapp/src/main/webapp/WEB-INF/conf/social-extension/social/component-plugins-configuration.xml b/webapp/src/main/webapp/WEB-INF/conf/social-extension/social/component-plugins-configuration.xml index 893c91f20df..7303389135e 100644 --- a/webapp/src/main/webapp/WEB-INF/conf/social-extension/social/component-plugins-configuration.xml +++ b/webapp/src/main/webapp/WEB-INF/conf/social-extension/social/component-plugins-configuration.xml @@ -145,6 +145,12 @@ org.exoplatform.social.core.listeners.ManagerPropertySettingUpdatedListener Updates visibility of pages containing Organization chart application + + property_options_updated + addListener + org.exoplatform.social.core.listeners.ProfilePropertySettingOptionTranslationListener + Updates user profile attributes uses dropdownList property type + diff --git a/webapp/src/main/webapp/vue-apps/profile-settings/components/ProfileSettings.vue b/webapp/src/main/webapp/vue-apps/profile-settings/components/ProfileSettings.vue index 4bdbd6549e3..985c9143ecc 100644 --- a/webapp/src/main/webapp/vue-apps/profile-settings/components/ProfileSettings.vue +++ b/webapp/src/main/webapp/vue-apps/profile-settings/components/ProfileSettings.vue @@ -186,8 +186,6 @@ export default { }, editSetting(setting, refresh) { this.$profileSettingsService.updateSetting(setting).then(() => { - return this.saveOptionsTranslations(null, setting.propertyOptions); - }).then(() => { if (refresh) { this.getSettings(); } @@ -206,8 +204,6 @@ export default { }); promises.push(this.$profileLabelService.addLabels(setting.labels)); } - - promises.push(this.saveOptionsTranslations(storedSetting, setting.propertyOptions)); return Promise.all(promises); }) .then(() => { @@ -228,22 +224,6 @@ export default { ); }); }, - saveOptionsTranslations(savedSetting, options) { - if (options?.length) { - const promises = options.filter(option => option?.translations) - .map((option, index) => { - option.id ??= savedSetting?.propertyOptions?.[index]?.id; - this.$translationService.saveTranslations( - this.settingOptionObjectType, - option.id, - this.settingOptionFieldName, - option.translations - ); - }); - return Promise.all(promises); - } - return Promise.resolve(); - }, updateLabels(labels) { this.$profileLabelService.updateLabels(labels); },