diff --git a/config/configuration/bundles/src/bundle/org/sakaiproject/config/bundle/default.sakai.properties b/config/configuration/bundles/src/bundle/org/sakaiproject/config/bundle/default.sakai.properties index 0584f24c3701..cecfa8d47ff7 100644 --- a/config/configuration/bundles/src/bundle/org/sakaiproject/config/bundle/default.sakai.properties +++ b/config/configuration/bundles/src/bundle/org/sakaiproject/config/bundle/default.sakai.properties @@ -5341,6 +5341,18 @@ #Default: false #scorm.config.showNavBar.default=true +# ############################### +# MICROSOFT TEAMS +# ############################### + +#SAK-52355 Enable forced synchronization in Microsoft Teams +#DEFAULT: none, use "all" for all sites or specify a list of site types to allow. Comma-separated list of sitetype: course,project,... +#microsoft.forced.synchronization.sitetype=all + +#SAK-52355 Enable forced synchronization in Microsoft Teams +#DEFAULT: microsoft.synchronized=true, use your own name and value property. IMPORTANT: The administrator should add this name and optional value in the Microsoft Admin Tool settings, in the corresponding "Site property" input. +#microsoft.forced.synchronization.propertynameandoptionalvalue=microsoft.synchronized=true + # ############################### # Content-Review # ############################### diff --git a/library/src/skins/default/src/sass/modules/tool/sitemanage/_sitemanage.scss b/library/src/skins/default/src/sass/modules/tool/sitemanage/_sitemanage.scss index f3d9defe5f93..b42eb8cc5780 100644 --- a/library/src/skins/default/src/sass/modules/tool/sitemanage/_sitemanage.scss +++ b/library/src/skins/default/src/sass/modules/tool/sitemanage/_sitemanage.scss @@ -494,7 +494,7 @@ } #toolHolderWW { @include display-flex; - .mathJaxToggleArea, .subPageNavToggleArea { + .mathJaxToggleArea, .subPageNavToggleArea, .microsoftSynchronizationArea { @include display-flex; @include flex-direction(row-reverse); @include justify-content(flex-end); // align to the left diff --git a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/AutoConfigController.java b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/AutoConfigController.java index 5c9f2732d6a9..bbbf31800d98 100644 --- a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/AutoConfigController.java +++ b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/AutoConfigController.java @@ -118,8 +118,8 @@ public String autoConfig( if(!autoConfigSessionBean.isRunning()) { - //get all synchronizations - List ssList = microsoftSynchronizationService.getAllSiteSynchronizations(false); + //get all enabled synchronizations + List ssList = microsoftSynchronizationService.getAllEnabledSiteSynchronizations(false); //get (filtered) sites List sitesList = sakaiProxy.getSakaiSites(requestBody.getFilter()); diff --git a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/MainController.java b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/MainController.java index 6c0cf23db3fe..dc0993a43322 100644 --- a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/MainController.java +++ b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/MainController.java @@ -334,7 +334,24 @@ public AjaxResponse updateSiteSynchronizationForced(@PathVariable String id, @Re return ret; } - + + //called by AJAX - returns JSON + @PostMapping(path = {"/setDisabled-siteSynchronization/{id}"}, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public AjaxResponse updateSiteSynchronizationDisabled(@PathVariable String id, @RequestParam Boolean disabled, Model model) { + SiteSynchronization ss = microsoftSynchronizationService.getSiteSynchronization(SiteSynchronization.builder().id(id).build()); + AjaxResponse ret = new AjaxResponse(); + ret.setStatus(false); + ret.setError(rb.getString("error.changing_synchronization_status")); + if(ss != null) { + ss.setDisabled(disabled); + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); + ret.setStatus(true); + ret.setError(""); + } + return ret; + } + //called by AJAX - returns JSON @GetMapping(path = {"/setDate-siteSynchronization/{id}"}, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody diff --git a/microsoft-integration/admin-tool/src/main/resources/Messages.properties b/microsoft-integration/admin-tool/src/main/resources/Messages.properties index 4448d9ff6d17..75fac0efb935 100644 --- a/microsoft-integration/admin-tool/src/main/resources/Messages.properties +++ b/microsoft-integration/admin-tool/src/main/resources/Messages.properties @@ -77,6 +77,8 @@ index.run.disabled=Run Disabled index.refresh=Refresh index.groups=Groups index.edit_groups=Edit Groups +index.enable=Enable synchronization +index.disable=Disable synchronization index.selected_id=Selected id index.delete_selected=Delete Selected index.remove_team_users=Remove Team Users @@ -221,6 +223,7 @@ error.new_channel_empty=New channel name can not be empty error.new_channel_with_same_name= New channel name can not have the same name of one of the channels created error.delete_group_synchronization=Error removing selected Group Synchronization error.set_forced_synchronization=Error setting forced +error.changing_synchronization_status=Error changing the synchronization status error.deleting_site_synchronizations=Error removing one or more selected synchronizations error.cleaning_team=Error cleaning one or more selected synchronizations error.set_dates=Error setting dates diff --git a/microsoft-integration/admin-tool/src/main/resources/Messages_es.properties b/microsoft-integration/admin-tool/src/main/resources/Messages_es.properties index 631c7b55b9b3..6205618ed8a9 100644 --- a/microsoft-integration/admin-tool/src/main/resources/Messages_es.properties +++ b/microsoft-integration/admin-tool/src/main/resources/Messages_es.properties @@ -77,6 +77,8 @@ index.run.disabled=Ejecutar Deshabilitado index.refresh=Refrescar index.groups=Grupos index.edit_groups=Editar Grupos +index.enable=Habilitar sincronizaci\u00f3n +index.disable=Deshabilitar sincronizaci\u00f3n index.selected_id=Id seleccionado index.delete_selected=Borrar seleccionado index.remove_team_users=Eliminar Usuarios de Teams @@ -220,6 +222,7 @@ error.new_channel_empty=El nombre del nuevo canal no puede estar vac\u00edo error.new_channel_with_same_name=El nombre del nuevo canal no puede tener el mismo nombre que uno de los canales ya creados error.delete_group_synchronization=Error eliminando la sincronizaci\u00f3n de grupo seleccionada error.set_forced_synchronization=Error de configuraci\u00f3n forzado +error.changing_synchronization_status=Error al cambiar el estado de la sincronizaci\u00f3n error.deleting_site_synchronizations=Error al eliminar una o varias sincronizaciones seleccionadas error.cleaning_team=Error al limpiar una o m\u00e1s sincronizaciones seleccionadas error.set_dates=Error al establecer las fechas diff --git a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/synchronizationRow.html b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/synchronizationRow.html index 0f53df5fc8b1..a04fb3957564 100644 --- a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/synchronizationRow.html +++ b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/synchronizationRow.html @@ -92,6 +92,12 @@ + + + + +
diff --git a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/index.html b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/index.html index b02b49829b3e..bd0a2ff9587b 100644 --- a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/index.html +++ b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/index.html @@ -113,6 +113,35 @@

} } + async function toggleDisabled(elem) { + let baseURL = "[(@{/setDisabled-siteSynchronization/})]"; + let rowId = elem.closest('.table-row').id.replace('row_', ''); + let icon = elem.querySelector('i'); + let isDisabled = icon.classList.contains('fa-toggle-off'); + + let url = baseURL + rowId + "?disabled=" + !isDisabled; + + let response = await fetch(url, { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + let data = await response.json(); + + if (data.status == false) { + showAjaxError(data.error); + } else { + if (!isDisabled) { + icon.classList.replace('fa-toggle-on', 'fa-toggle-off'); + elem.title = /*[[#{index.enable}]]*/ ''; + } else { + icon.classList.replace('fa-toggle-off', 'fa-toggle-on'); + elem.title = /*[[#{index.disable}]]*/ ''; + } + } + } + var dateTimeout = null; async function changeDate(input) { if(dateTimeout != null){ diff --git a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java index 2650b6477798..b3634101afcc 100644 --- a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java +++ b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java @@ -109,6 +109,8 @@ public static enum PermissionRoles { READ, WRITE } void createTeamFromGroupAsync(String groupId) throws MicrosoftCredentialsException; boolean deleteTeam(String teamId) throws MicrosoftCredentialsException; + boolean archiveTeam(String teamId) throws MicrosoftCredentialsException; + boolean unarchiveTeam(String teamId) throws MicrosoftCredentialsException; MicrosoftMembersCollection getTeamMembers(String id, MicrosoftUserIdentifier key) throws MicrosoftCredentialsException; MicrosoftUser checkUserInTeam(String identifier, String teamId, MicrosoftUserIdentifier key) throws MicrosoftCredentialsException; diff --git a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftSynchronizationService.java b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftSynchronizationService.java index 65578e0dad8f..d775ea659649 100644 --- a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftSynchronizationService.java +++ b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftSynchronizationService.java @@ -28,7 +28,7 @@ public interface MicrosoftSynchronizationService { // ------------ Site Synchronization --------------------------- - List getAllSiteSynchronizations(boolean fillSite); + List getAllEnabledSiteSynchronizations(boolean fillSite); List getFilteredSiteSynchronizations(boolean fillSite, SakaiSiteFilter filter, ZonedDateTime fromDate, ZonedDateTime toDate); SiteSynchronization getSiteSynchronization(SiteSynchronization ss); diff --git a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/SiteSynchronization.java b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/SiteSynchronization.java index c6b06d1f7723..f785a4171f71 100644 --- a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/SiteSynchronization.java +++ b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/SiteSynchronization.java @@ -74,8 +74,9 @@ public class SiteSynchronization { @Convert(converter = JpaConverterSynchronizationStatus.class) private SynchronizationStatus status = SynchronizationStatus.NONE; - @Column(name = "forced") - private boolean forced; + @Column(name = "forced", nullable = false) + @Builder.Default + private boolean forced = false; @Column(name = "date_from") private ZonedDateTime syncDateFrom; @@ -86,6 +87,10 @@ public class SiteSynchronization { @Column(name = "status_updated_at") private ZonedDateTime statusUpdatedAt; + @Column(name = "disabled", nullable = false) + @Builder.Default + private boolean disabled = false; + @OneToMany(mappedBy = "siteSynchronization", fetch = FetchType.LAZY, orphanRemoval = true) @Cascade(CascadeType.ALL) @OnDelete(action = OnDeleteAction.CASCADE) diff --git a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftSiteSynchronizationRepository.java b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftSiteSynchronizationRepository.java index 4c60b0500dea..10e4ab2992be 100644 --- a/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftSiteSynchronizationRepository.java +++ b/microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftSiteSynchronizationRepository.java @@ -24,6 +24,7 @@ import org.sakaiproject.serialization.SerializableRepository; public interface MicrosoftSiteSynchronizationRepository extends SerializableRepository { + List findAllEnabled(); Optional findById(String id); Optional findBySiteTeam(String siteId, String teamId); List findBySite(String siteId); diff --git a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java index 72912beefe2d..299a93aba1ab 100644 --- a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java +++ b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java @@ -45,6 +45,7 @@ import com.google.gson.Gson; import com.google.gson.JsonPrimitive; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.microsoft.graph.requests.GraphServiceClient; import com.microsoft.graph.requests.UserCollectionPage; import com.microsoft.graph.requests.GroupCollectionPage; @@ -147,7 +148,9 @@ import com.microsoft.graph.models.OnlineMeetingRole; import com.microsoft.graph.models.Permission; import com.microsoft.graph.models.PermissionGrantParameterSet; +import com.microsoft.graph.models.Site; import com.microsoft.graph.models.Team; +import com.microsoft.graph.models.TeamArchiveParameterSet; import com.microsoft.graph.models.TeamVisibilityType; import com.microsoft.graph.models.ThumbnailSet; import com.microsoft.graph.models.UploadSession; @@ -1010,6 +1013,87 @@ public boolean deleteTeam(String teamId) throws MicrosoftCredentialsException { return false; } + public boolean archiveTeam(String teamId) throws MicrosoftCredentialsException { + try { + // 1. Archive team without shouldSetSpoSiteReadOnlyForMembers (no supported in app-only) + TeamArchiveParameterSet requestBody = TeamArchiveParameterSet + .newBuilder() + .withShouldSetSpoSiteReadOnlyForMembers(false) + .build(); + + getGraphClient().teams(teamId) + .archive(requestBody) + .buildRequest() + .post(); + + // 2. Obtain the associated SharePoint site to ensure the team is fully archived before setting it to read-only + Site site = getGraphClient().groups(teamId) + .sites("root") + .buildRequest() + .get(); + + if (site == null || site.id == null) { + log.error("Could not retrieve SharePoint site for team: {}, site will not be set to read-only", teamId); + return false; + } + + // 3. Set SharePoint site to read-only (as a backup in case the archive operation did not set it correctly) + JsonObject jsonBody = new JsonObject(); + jsonBody.addProperty("lockState", "readOnly"); + + getGraphClient().customRequest("/sites/" + site.id) + .buildRequest() + .patch(jsonBody); + + log.info("Team archived and SharePoint site set to read-only: teamId={}", teamId); + + return true; + } catch(MicrosoftCredentialsException e) { + throw e; + } catch(Exception ex){ + log.error("Error archiving Microsoft team: id={}", teamId, ex); + } + return false; + } + + public boolean unarchiveTeam(String teamId) throws MicrosoftCredentialsException { + try { + //1. Unarchive team + getGraphClient().teams(teamId) + .unarchive() + .buildRequest() + .post(); + + // 2. Obtain the associated SharePoint site to ensure the team is fully unarchived before returning + Site site = getGraphClient().groups(teamId) + .sites("root") + .buildRequest() + .get(); + + if (site == null || site.id == null) { + log.error("Could not retrieve SharePoint site for team: {}, site will remain read-only", teamId); + return false; + } + + //3. Set SharePoint site to unlocked (as a backup in case the unarchive operation did not set it correctly) + JsonObject jsonBody = new JsonObject(); + jsonBody.addProperty("lockState", "unlocked"); + + getGraphClient().customRequest("/sites/" + site.id) + .buildRequest() + .patch(jsonBody); + + log.info("Team unarchived and SharePoint site unlocked: teamId={}", teamId); + + return true; + } catch (MicrosoftCredentialsException e) { + throw e; + } catch (Exception ex) { + log.error("Error unarchiving Microsoft team: id={}", teamId, ex); + } + return false; + } + @Override public MicrosoftMembersCollection getTeamMembers(String id, MicrosoftUserIdentifier key) throws MicrosoftCredentialsException { MicrosoftMembersCollection ret = new MicrosoftMembersCollection(); diff --git a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftSynchronizationServiceImpl.java b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftSynchronizationServiceImpl.java index f48dd8cd2f3f..3fbe93506a71 100644 --- a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftSynchronizationServiceImpl.java +++ b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftSynchronizationServiceImpl.java @@ -197,8 +197,8 @@ private boolean checkTeam(String teamId) throws MicrosoftCredentialsException { } @Override - public List getAllSiteSynchronizations(boolean fillSite) { - List result = StreamSupport.stream(microsoftSiteSynchronizationRepository.findAll().spliterator(), false) + public List getAllEnabledSiteSynchronizations(boolean fillSite) { + List result = StreamSupport.stream(microsoftSiteSynchronizationRepository.findAllEnabled().spliterator(), false) .map(ss -> { if(fillSite) { ss.setSite(sakaiProxy.getSite(ss.getSiteId())); diff --git a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/jobs/RunSynchronizationsJob.java b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/jobs/RunSynchronizationsJob.java index 05773c6fcc93..2aa50636df6e 100644 --- a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/jobs/RunSynchronizationsJob.java +++ b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/jobs/RunSynchronizationsJob.java @@ -81,7 +81,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { session.setAttribute("origin", MicrosoftLogInvokers.JOB.getCode()); SakaiSiteFilter siteFilter = microsoftConfigurationService.getJobSiteFilter(); - List list = microsoftSynchronizationService.getAllSiteSynchronizations(true); + List list = microsoftSynchronizationService.getAllEnabledSiteSynchronizations(true); for(SiteSynchronization ss : list) { int retryCount = 0; while (retryCount < MAX_RETRIES) { @@ -91,7 +91,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } if(ss.getSite() == null || !siteFilter.match(ss.getSite())) { - log.debug("Site with id={} skipped due to filter restrinctions", ss.getSiteId()); + log.debug("Site with id={} skipped due to filter restrictions", ss.getSiteId()); break; } diff --git a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftSiteSynchronizationRepositoryImpl.java b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftSiteSynchronizationRepositoryImpl.java index d354e515bdaf..10cfd01385c3 100644 --- a/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftSiteSynchronizationRepositoryImpl.java +++ b/microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftSiteSynchronizationRepositoryImpl.java @@ -46,6 +46,14 @@ public List findAll(){ .list(); } + @Override + public List findAllEnabled() { + return (List) startCriteriaQuery() + .addOrder(Order.asc("status")) + .add(Restrictions.eq("disabled", false)) + .list(); + } + @Override public Optional findById(String id) { SiteSynchronization siteSynchronization = (SiteSynchronization) startCriteriaQuery().add(Restrictions.eq("id", id)).uniqueResult(); diff --git a/site-manage/site-manage-tool/tool/pom.xml b/site-manage/site-manage-tool/tool/pom.xml index 297dc1be2810..22d67e7f0578 100644 --- a/site-manage/site-manage-tool/tool/pom.xml +++ b/site-manage/site-manage-tool/tool/pom.xml @@ -176,6 +176,10 @@ org.sakaiproject.scheduler scheduler-api + + org.sakaiproject.microsoft + microsoft-api + diff --git a/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric.properties b/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric.properties index c3dff66325ac..ef007e3c85ac 100644 --- a/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric.properties +++ b/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric.properties @@ -1152,3 +1152,9 @@ sinfo.gradebookgroupvnav.site=A single site instance sinfo.gradebookgroupvnav.group=A different instance for the selected group(s) sinfo.gradebookgroupvnav.selectGroups=Select one or more groups sinfo.gradebookgroupvnav.noGroupsPresent=Note - There are currently no groups present in this site. Create groups in Site Info to enable additional options. + +# SAK-52355 +sinfo.synchronization.microsoft.name=Synchronization with Microsoft Teams +sinfo.synchronization.microsoft.allowForSite=Enable forced synchronization with Microsoft Teams for this site. +sinfo.synchronization.microsoft.confirmEnabled=You have enabled forced synchronization with Microsoft Teams for this site. +sinfo.synchronization.microsoft.enabled=Enabled for the site diff --git a/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric_es.properties b/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric_es.properties index 5b537bf80fe4..b1208ed619c4 100644 --- a/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric_es.properties +++ b/site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric_es.properties @@ -1124,3 +1124,9 @@ sinfo.gradebookgroupvnav.site=Una instancia de sitio sinfo.gradebookgroupvnav.group=Una instancia diferente para cada grupo seleccionado sinfo.gradebookgroupvnav.selectGroups=Nota - Debes seleccionar al menos un grupo sinfo.gradebookgroupvnav.noGroupsPresent=Nota - Actualmente no hay grupos en este sitio. Debe crear grupos antes de poder asignar la tarea a un grupo concreto. + +# SAK-52355 +sinfo.synchronization.microsoft.name=Sincronizaci\u00f3n con Microsoft Teams +sinfo.synchronization.microsoft.allowForSite=Habilitar la sincronizaci\u00f3n forzada con Microsoft Teams para este sitio. +sinfo.synchronization.microsoft.confirmEnabled=Has habilitado la sincronizaci\u00f3n forzada con Microsoft Teams para este sitio. +sinfo.synchronization.microsoft.enabled=Activado para este sitio diff --git a/site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/MicrosoftSynchronizationEnabler.java b/site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/MicrosoftSynchronizationEnabler.java new file mode 100644 index 000000000000..238a6cb38552 --- /dev/null +++ b/site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/MicrosoftSynchronizationEnabler.java @@ -0,0 +1,346 @@ +/** + * Copyright (c) 2003-2017 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.site.tool; + +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.sakaiproject.cheftool.Context; +import org.sakaiproject.component.cover.ComponentManager; +import org.sakaiproject.component.cover.ServerConfigurationService; +import org.sakaiproject.entity.api.ResourcePropertiesEdit; +import org.sakaiproject.event.api.SessionState; +import org.sakaiproject.microsoft.api.MicrosoftCommonService; +import org.sakaiproject.microsoft.api.MicrosoftConfigurationService; +import org.sakaiproject.microsoft.api.MicrosoftSynchronizationService; +import org.sakaiproject.microsoft.api.data.MicrosoftCredentials; +import org.sakaiproject.microsoft.api.exceptions.MicrosoftCredentialsException; +import org.sakaiproject.microsoft.api.model.SiteSynchronization; +import org.sakaiproject.site.api.Site; +import org.sakaiproject.util.ParameterParser; + +@Slf4j +public class MicrosoftSynchronizationEnabler { + + private static final String SITE_PROPERTY = "microsoftSynchronization"; + private static final String STATE_KEY = "isMicrosoftSynchronizationEnabled"; + private static final String FORM_INPUT_ID = "isMicrosoftSynchronizationEnabled"; + private static final String CONTEXT_ENABLED_KEY = "isMicrosoftSynchronizationEnabled"; + private static final String CONTEXT_ALLOWED_KEY = "isMicrosoftSynchronizationAllowed"; + + private static final String SAKAI_PROPERTY_SITETYPE = "microsoft.forced.synchronization.sitetype"; + private static final String SAKAI_PROPERTY_SITE_PROPERTY_NAME_AND_OPTIONAL_VALUE = "microsoft.forced.synchronization.propertynameandoptionalvalue"; + + /** + * Add MicrosoftSynchronization settings to the context for the edit tools page. + * The checkbox will only be available if the site type is allowed in sakai.properties. + * + * @param context the context + * @param site the site + * @return true if context was modified + */ + public static boolean addToContext(Context context, Site site) { + if (context == null || site == null) return false; + + final boolean isAllowed = isSiteTypeAllowed(site); + context.put(CONTEXT_ALLOWED_KEY, isAllowed); + + if (isAllowed) { + context.put(CONTEXT_ENABLED_KEY, isEnabledForSite(site)); + } + + return true; + } + + /** + * Applies the MicrosoftSynchronization settings to the state. + * Only applies if the site type is allowed in sakai.properties. + * + * @param state the state + * @param params the params + * @param site the site + * @return true if the state was modified + */ + public static boolean applySettingsToState(SessionState state, ParameterParser params, Site site) { + if (state == null || params == null || site == null) return false; + + if (!isSiteTypeAllowed(site)) { + return false; + } + + if ("on".equalsIgnoreCase(params.getString(FORM_INPUT_ID))) { + state.setAttribute(STATE_KEY, true); + } else { + state.setAttribute(STATE_KEY, false); + } + + return true; + } + + /** + * Add the current MicrosoftSynchronization state to the context for the edit tools confirmation page. + * + * @param context the context + * @param state the state + * @return true if the context was modified + */ + public static boolean addStateToEditToolsConfirmationContext(Context context, SessionState state) { + if (context == null || state == null) { + return false; + } + + final Boolean isEnabled = (Boolean) state.getAttribute(STATE_KEY); + if (Boolean.TRUE.equals(isEnabled)) { + context.put(CONTEXT_ENABLED_KEY, Boolean.TRUE); + return true; + } + + return false; + } + + /** + * When user selects to enable MicrosoftSynchronization, update the site property + * and create the Microsoft Team + SiteSynchronization if it was just enabled. + * Only persists if the site type is allowed in sakai.properties. + * + * @param site the site + * @param state the session state + * @return true if the site properties were modified + */ + public static boolean prepareSiteForSave(Site site, SessionState state) { + if (site == null || state == null) return false; + + if (!isSiteTypeAllowed(site)) { + return false; + } + + if (state.getAttribute(STATE_KEY) != null) { + final boolean isEnabled = (Boolean) state.getAttribute(STATE_KEY); + final ResourcePropertiesEdit props = site.getPropertiesEdit(); + final String[] nameAndValueForSynchronization = getSitePropertyNameAndOptionalValueForSynchronization(); + MicrosoftCommonService microsoftCommonService = (MicrosoftCommonService) ComponentManager.get(MicrosoftCommonService.class); + MicrosoftSynchronizationService microsoftSynchronizationService = (MicrosoftSynchronizationService) ComponentManager.get(MicrosoftSynchronizationService.class); + + if (isEnabled) { + props.addProperty(SITE_PROPERTY, Boolean.TRUE.toString()); + + MicrosoftConfigurationService microsoftConfigurationService = (MicrosoftConfigurationService) ComponentManager.get(MicrosoftConfigurationService.class); + + // Check if MicrosoftSynchronization was already enabled for the site before creating/restoring the Team + createOrRestoreMicrosoftTeamForSite(site, microsoftCommonService, microsoftConfigurationService, microsoftSynchronizationService); + + // Add site property defined in microsoft.forced.synchronization.propertynameandoptionalvalue + if (nameAndValueForSynchronization.length > 0) { + props.addProperty(nameAndValueForSynchronization[0], nameAndValueForSynchronization[1]); + log.debug("Added site property {}={} for site: {}", nameAndValueForSynchronization[0], nameAndValueForSynchronization[1], site.getId()); + } + } else { + props.removeProperty(SITE_PROPERTY); + + // If a SiteSynchronization exists with a Team, archive it and delete it + if (microsoftSynchronizationService != null && microsoftCommonService != null) { + List ssList = microsoftSynchronizationService.getSiteSynchronizationsBySite(site.getId()); + if (ssList != null) { + for (SiteSynchronization ss : ssList) { + if (ss.getTeamId() != null && !ss.getTeamId().isEmpty()) { + try { + if (microsoftCommonService.archiveTeam(ss.getTeamId())) { + log.info("Microsoft Team archived for site: {}, teamId: {}", site.getId(), ss.getTeamId()); + } + + // Remove site property defined in microsoft.forced.synchronization.propertynameandoptionalvalue + if (nameAndValueForSynchronization.length > 0) { + props.removeProperty(nameAndValueForSynchronization[0]); + log.debug("Removed site property {} for site: {}", nameAndValueForSynchronization[0], site.getId()); + } + } catch (Exception e) { + log.warn("Could not archive Microsoft Team for site: {}, teamId: {}, {}", site.getId(), ss.getTeamId(), e.toString()); + } + } + + ss.setDisabled(true); + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); + log.info("SiteSynchronization disabled for site: {}", site.getId()); + } + } + } + } + } + + return true; + } + + /** + * Remove MicrosoftSynchronization from the state. + * + * @param state the state + * @return true if the state was modified + */ + public static boolean removeFromState(SessionState state) { + if (state != null) { + state.removeAttribute(STATE_KEY); + return true; + } + + return false; + } + + /** + * Creates or restores a Microsoft Team for the given site and saves the SiteSynchronization. + * If a SiteSynchronization already exists for the site, the associated Team is unarchived and restored. + * Otherwise, a new Team is created and a new SiteSynchronization is saved. + * + * @param site the site + * @param microsoftCommonService the Microsoft common service + * @param microsoftConfigurationService the Microsoft configuration service + * @param microsoftSynchronizationService the Microsoft synchronization service + */ + private static void createOrRestoreMicrosoftTeamForSite( + Site site, + MicrosoftCommonService microsoftCommonService, + MicrosoftConfigurationService microsoftConfigurationService, + MicrosoftSynchronizationService microsoftSynchronizationService) { + + if (microsoftCommonService == null || microsoftConfigurationService == null || microsoftSynchronizationService == null) { + log.warn("One or more Microsoft services are null, cannot create team for site: {}", site.getId()); + return; + } + + try { + final MicrosoftCredentials credentials = microsoftConfigurationService.getCredentials(); + + if (credentials == null || credentials.getEmail() == null) { + log.warn("Could not resolve Microsoft credentials email for site: {}, team will not be created", site.getId()); + return; + } + + final long syncDuration = microsoftConfigurationService.getSyncDuration(); + final ZonedDateTime syncDateFrom = ZonedDateTime.now(); + final ZonedDateTime syncDateTo = ZonedDateTime.now().plusMonths(syncDuration); + + List existingSsList = microsoftSynchronizationService.getSiteSynchronizationsBySite(site.getId()); + if (existingSsList != null && !existingSsList.isEmpty()) { + // Team already existed before — unarchive it and restore the SiteSynchronization + for (SiteSynchronization existingSs : existingSsList) { + if (existingSs.getTeamId() != null && !existingSs.getTeamId().isEmpty()) { + try { + if (!microsoftCommonService.unarchiveTeam(existingSs.getTeamId())) { + log.error("Could not unarchive Microsoft Team for site: {}, teamId: {}, skipping state update", site.getId(), existingSs.getTeamId()); + continue; + } + } catch (Exception e) { + log.error("Could not unarchive Microsoft Team for site: {}, teamId: {}, {}", site.getId(), existingSs.getTeamId(), e.toString()); + continue; + } + } + // Only reached if unarchiveTeam succeeded + existingSs.setSyncDateFrom(syncDateFrom); + existingSs.setSyncDateTo(syncDateTo); + existingSs.setDisabled(false); + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(existingSs); + log.info("SiteSynchronization restored for site: {}, teamId: {}", site.getId(), existingSs.getTeamId()); + } + } else { + // No existing SiteSynchronization — create a new Team and SiteSynchronization + final String teamId; + try { + teamId = microsoftCommonService.createTeam(site.getTitle(), credentials.getEmail()); + } catch (Exception e) { + log.error("Microsoft Team creation failed for site: {}, {}", site.getId(), e.toString()); + return; + } + + if (teamId == null || teamId.isEmpty()) { + log.warn("Microsoft Team creation returned null or empty teamId for site: {}", site.getId()); + return; + } + + final SiteSynchronization ss = SiteSynchronization.builder() + .siteId(site.getId()) + .teamId(teamId) + .forced(true) + .syncDateFrom(syncDateFrom) + .syncDateTo(syncDateTo) + .build(); + + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); + log.info("Microsoft Team created and SiteSynchronization saved: teamId={}, siteId={}", teamId, site.getId()); + } + } catch (Exception e) { + log.error("Unexpected error while creating Microsoft Team for site: {}, {}", site.getId(), e.toString()); + } + } + + /** + * Check the site's properties for the MicrosoftSynchronization property. + * + * @param site the site to check + * @return true if MicrosoftSynchronization is enabled for the site + */ + private static boolean isEnabledForSite(Site site) { + return Boolean.parseBoolean(site.getProperties().getProperty(SITE_PROPERTY)); + } + + /** + * Check whether the site's type is allowed to use MicrosoftSynchronization, + * based on the "microsoft.forced.synchronization.sitetype" property in sakai.properties. + * Use "all" to allow all site types, or a comma-separated list (e.g. "course,project"). + * + * @param site the site to check + * @return true if the site type is allowed + */ + private static boolean isSiteTypeAllowed(Site site) { + final String configValue = ServerConfigurationService.getString(SAKAI_PROPERTY_SITETYPE, "").trim(); + + if (configValue.isEmpty()) { + return false; + } + + if ("all".equalsIgnoreCase(configValue)) { + return true; + } + + final String siteType = site.getType(); + if (siteType == null || siteType.isEmpty()) { + return false; + } + + final List allowedTypes = Arrays.asList(configValue.split("\\s*,\\s*")); + return allowedTypes.contains(siteType); + } + + /** + * Reads the "microsoft.forced.synchronization.propertynameandoptionalvalue" sakai.property. + * Supports two formats: + * - "propertyName=propertyValue" (e.g. "microsoft.synchronized=true") + * - "propertyName" (e.g. "microsoft.synchronized"), in which case value will be empty string + * + * @return a two-element array [propertyName, propertyValue], or null if not defined or blank + */ + private static String[] getSitePropertyNameAndOptionalValueForSynchronization() { + final String configValue = ServerConfigurationService.getString(SAKAI_PROPERTY_SITE_PROPERTY_NAME_AND_OPTIONAL_VALUE, null); + if (configValue == null || configValue.isBlank()) { + return new String[0]; + } + if (configValue.contains("=")) { + return configValue.split("=", 2); + } + return new String[] { configValue.trim(), "" }; + } +} \ No newline at end of file diff --git a/site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/SiteAction.java b/site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/SiteAction.java index cd2180d4ac4a..cbc430797afc 100644 --- a/site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/SiteAction.java +++ b/site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/SiteAction.java @@ -1312,6 +1312,8 @@ private void cleanState(SessionState state) { SubNavEnabler.removeFromState(state); + MicrosoftSynchronizationEnabler.removeFromState(state); + state.removeAttribute(STATE_CREATE_FROM_ARCHIVE); } // cleanState @@ -1860,6 +1862,7 @@ private String buildContextForTemplate(String preIndex, int index, VelocityPortl MathJaxEnabler.addMathJaxSettingsToEditToolsContext(context, site, state); // SAK-22384 SubNavEnabler.addToContext(context, site); + MicrosoftSynchronizationEnabler.addToContext(context, site); context.put("SiteTitle", site.getTitle()); context.put("existSite", Boolean.TRUE); context.put("backIndex", SITE_INFO_TEMPLATE_INDEX); // back to site info list page @@ -2426,6 +2429,7 @@ else if (state.getAttribute(STATE_CM_REQUESTED_SECTIONS) != null) { MathJaxEnabler.addMathJaxSettingsToSiteInfoContext(context, site, state); context.put("isGradebookGroupEnabledForSite", GradebookGroupEnabler.isEnabledForSite(site)); SubNavEnabler.addToContext(context, site); + MicrosoftSynchronizationEnabler.addToContext(context, site); return (String) getContext(data).get("template") + TEMPLATE[12]; @@ -2586,6 +2590,8 @@ else if (state.getAttribute(STATE_CM_REQUESTED_SECTIONS) != null) { SubNavEnabler.addToContext(context, site); + MicrosoftSynchronizationEnabler.addToContext(context, site); + return (String) getContext(data).get("template") + TEMPLATE[13]; case 14: /* @@ -2656,6 +2662,7 @@ else if (state.getAttribute(STATE_CM_REQUESTED_SECTIONS) != null) { // SAK-22384 mathjax support MathJaxEnabler.addMathJaxSettingsToSiteInfoContext(context, site, state); SubNavEnabler.addToContext(context, site); + MicrosoftSynchronizationEnabler.addToContext(context, site); return (String) getContext(data).get("template") + TEMPLATE[14]; case 15: @@ -2679,6 +2686,7 @@ else if (state.getAttribute(STATE_CM_REQUESTED_SECTIONS) != null) { MathJaxEnabler.addMathJaxSettingsToEditToolsConfirmationContext(context, site, state, STATE_TOOL_REGISTRATION_TITLE_LIST); // SAK-22384 GradebookGroupEnabler.addSettingsToEditToolsConfirmationContext(context, site, state); SubNavEnabler.addStateToEditToolsConfirmationContext(context, state); + MicrosoftSynchronizationEnabler.addStateToEditToolsConfirmationContext(context, state); return (String) getContext(data).get("template") + TEMPLATE[15]; case 18: @@ -8018,6 +8026,7 @@ public void doCancel(RunData data) { state.setAttribute(STATE_TEMPLATE_INDEX, SiteConstants.SITE_INFO_TEMPLATE_INDEX); GradebookGroupEnabler.removeFromState(state); SubNavEnabler.removeFromState(state); + MicrosoftSynchronizationEnabler.removeFromState(state); } else if ("15".equals(currentIndex)) { params = data.getParameters(); state.setAttribute(STATE_TEMPLATE_INDEX, params @@ -8881,6 +8890,8 @@ public void doSave_siteInfo(RunData data) { SubNavEnabler.prepareSiteForSave(Site, state); + MicrosoftSynchronizationEnabler.prepareSiteForSave(Site, state); + if (state.getAttribute(STATE_MESSAGE) == null) { try { siteService.save(Site); @@ -12289,6 +12300,7 @@ else if (multiAllowed && toolId.startsWith(toolRegId)) updateSite = MathJaxEnabler.prepareMathJaxToolSettingsForSave(site, state); updateSite = GradebookGroupEnabler.prepareSiteForSave(site, state) || updateSite; updateSite = SubNavEnabler.prepareSiteForSave(site, state) || updateSite; + updateSite = MicrosoftSynchronizationEnabler.prepareSiteForSave(site, state) || updateSite; if (updateSite) { commitSite(site); } @@ -13174,6 +13186,7 @@ private void removeEditToolState(SessionState state) { state.removeAttribute(STATE_TOOL_REGISTRATION_SELECTED_LIST); GradebookGroupEnabler.removeFromState(state); SubNavEnabler.removeFromState(state); + MicrosoftSynchronizationEnabler.removeFromState(state); } private List orderToolIds(SessionState state, String type, List toolIdList, boolean synoptic) { @@ -13382,6 +13395,7 @@ public void doAdd_features(RunData data) { // continue MathJaxEnabler.applySettingsToState(state, params); // SAK-22384 SubNavEnabler.applySettingsToState(state, params); + MicrosoftSynchronizationEnabler.applySettingsToState(state, params, site); doContinue(data); } else if (option.equalsIgnoreCase("back")) { @@ -15927,6 +15941,9 @@ else if ("continue".equals(option)) // continue with site information edit MathJaxEnabler.applySettingsToState(state, params); // SAK-22384 + SubNavEnabler.applySettingsToState(state, params); + Site site = getStateSite(state); + MicrosoftSynchronizationEnabler.applySettingsToState(state, params, site); doContinue(data); } diff --git a/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-addRemoveFeatureConfirm.vm b/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-addRemoveFeatureConfirm.vm index 992fecb16233..55aa8cd96dc6 100644 --- a/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-addRemoveFeatureConfirm.vm +++ b/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-addRemoveFeatureConfirm.vm @@ -246,6 +246,12 @@

#end + #if($!isMicrosoftSynchronizationEnabled) +

+ $tlang.getString("sinfo.synchronization.microsoft.confirmEnabled") +

+ #end +
diff --git a/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-siteInfo-list.vm b/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-siteInfo-list.vm index 325460021e39..fee231108dea 100644 --- a/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-siteInfo-list.vm +++ b/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/chef_site-siteInfo-list.vm @@ -283,6 +283,17 @@ $tlang.getString("sinfo.subnav.enabled") + #end + + #if ($isMicrosoftSynchronizationEnabled) + + + $tlang.getString("sinfo.synchronization.microsoft.name") + + + $tlang.getString("sinfo.synchronization.microsoft.enabled") + + #end #if ($siteJoinable && $allowUnjoin) diff --git a/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/toolGroupMultipleDisplay.vm b/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/toolGroupMultipleDisplay.vm index f67f4d60c6c8..497e2b041303 100644 --- a/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/toolGroupMultipleDisplay.vm +++ b/site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/toolGroupMultipleDisplay.vm @@ -199,6 +199,16 @@ + #if($isMicrosoftSynchronizationAllowed) +
+ +
+ +
+
+ #end