diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 8c1e10b48952..f8dc8a90a886 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -15,16 +15,16 @@ jobs: with: ref: ${{ github.ref }} fetch-depth: 0 - - name: JDK 11 + - name: JDK 17 uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin cache: maven - name: Set up Maven uses: stCarolas/setup-maven@v5 with: - maven-version: 3.9.6 + maven-version: 3.9.9 - name: Search for bad unicode translations run: | find . -name \*.properties -exec grep '\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][^0-9a-fA-F]' {} \; &> grep.txt diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index ed6f328a2e62..1878b9287645 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -10,15 +10,14 @@ jobs: sakai-deploy: runs-on: ubuntu-22.04 env: - JAVA_OPTS: "-Dhttp.agent=Sakai -Xms2512m -Xmx2512m -Dsakai.cookieName=SAKAIID -Dorg.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING=false -Dsakai.demo=true -Djava.awt.headless=true --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED --add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED --add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --illegal-access=permit -Dsakai.demo=true" - + JAVA_OPTS: "-Dhttp.agent=Sakai -Xms2512m -Xmx2512m -Dsakai.cookieName=SAKAIID -Dsakai.demo=true" steps: - name: Git Checkout uses: actions/checkout@v4 - - name: JDK 11 + - name: JDK 17 uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin cache: maven - name: Build with Maven @@ -32,15 +31,17 @@ jobs: export TOMCAT_DIR=$PWD/tomcat mkdir $TOMCAT_DIR cd $TOMCAT_DIR - curl -s -o tomcat.tar.gz https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.96/bin/apache-tomcat-9.0.96.tar.gz + curl -s -o tomcat.tar.gz https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.98/bin/apache-tomcat-9.0.98.tar.gz tar --strip-components=1 -xzf tomcat.tar.gz git clone https://github.com/sakaiproject/nightly-config.git sakai + cp sakai/setenv.sh bin/setenv.sh cp sakai/cypress.properties sakai/sakai.properties - sed -i 's:::g' conf/context.xml + cp -f sakai/context.xml conf/context.xml + cp -f sakai/catalina.properties conf/catalina.properties sed -i 's:::g' conf/server.xml mysql -u root -proot -e "create database sakai"; cd .. - mvn --batch-mode -DskipTests install sakai:deploy-exploded -Dmaven.tomcat.home=$TOMCAT_DIR + mvn --batch-mode -DskipTests -Denforcer.skip -Dmaven.source.skip install sakai:deploy-exploded -Dmaven.tomcat.home=$TOMCAT_DIR cd $TOMCAT_DIR bin/catalina.sh start sleep 500s @@ -71,7 +72,7 @@ jobs: - name: Check number of MySQL statements if: always() run: | - export QUERIES=$(grep ProtocolLoggingProxy.info tomcat/logs/catalina.out|grep -v ROLLBACK|grep -v COMMIT | wc -l) + export QUERIES=$(grep StandardClient.debug tomcat/logs/catalina.out|grep -v ROLLBACK|grep -v COMMIT | wc -l) echo "::notice title={MySQL Queries}::$QUERIES" - name: Upload Tomcat log for review if: always() diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 39f0e4bfe8eb..69c2fd4de6b9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -14,16 +14,16 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: JDK 11 + - name: JDK 17 uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin cache: maven - name: Set up Maven uses: stCarolas/setup-maven@v5 with: - maven-version: 3.9.6 + maven-version: 3.9.9 - name: Build with Maven env: MAVEN_OPTS: -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true -Dmaven.wagon.http.retryHandler.count=2 -Dmaven.wagon.http.pool=true diff --git a/README.md b/README.md index 244c08d86d54..bccdb613c380 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ If you can't find your "at institution.edu" on the Apereo signup page then send ## Community supported versions These versions are actively supported by the community. -Sakai 23.2 ([release](http://source.sakaiproject.org/release/23.2/) | [fixes](https://confluence.sakaiproject.org/display/DOC/23.2+Fixes+by+tool) | [notes](https://confluence.sakaiproject.org/display/DOC/Sakai+23+Release+Notes)) +Sakai 23.3 ([release](http://source.sakaiproject.org/release/23.3/) | [fixes](https://confluence.sakaiproject.org/display/DOC/23.3+Fixes+by+tool) | [notes](https://confluence.sakaiproject.org/display/DOC/Sakai+23+Release+Notes)) Sakai 22.5 ([release](http://source.sakaiproject.org/release/22.5/) | [fixes](https://confluence.sakaiproject.org/display/DOC/22.5+Fixes+by+tool) | [notes](https://confluence.sakaiproject.org/display/DOC/Sakai+22+Release+Notes)) @@ -92,7 +92,7 @@ For full history of supported releases please see our [release information on co ## Under Development -[Sakai 23.3](https://confluence.sakaiproject.org/display/REL/Sakai+23+Straw+person) is the current development release of Sakai 23. It is expected to release Q3 2024. +[Sakai 23.4](https://confluence.sakaiproject.org/display/REL/Sakai+23+Straw+person) is the current development release of Sakai 23. It is expected to release Q1 2025. [Sakai 22.6](https://confluence.sakaiproject.org/display/REL/Sakai+22+Straw+person) is the current development release of Sakai 22. It is expected to release Q4 2024. diff --git a/admin-tools/src/webapp/vm/memory/chef_memory.vm b/admin-tools/src/webapp/vm/memory/chef_memory.vm index fb3790b57e1d..e52b62359019 100644 --- a/admin-tools/src/webapp/vm/memory/chef_memory.vm +++ b/admin-tools/src/webapp/vm/memory/chef_memory.vm @@ -9,8 +9,7 @@ #end #if ($status) - -
+
$formattedText.escapeHtml($status)
#end diff --git a/announcement/announcement-tool/tool/src/webapp/vm/announcement/chef_announcements.vm b/announcement/announcement-tool/tool/src/webapp/vm/announcement/chef_announcements.vm index e1a348d08267..10b12788dcb2 100644 --- a/announcement/announcement-tool/tool/src/webapp/vm/announcement/chef_announcements.vm +++ b/announcement/announcement-tool/tool/src/webapp/vm/announcement/chef_announcements.vm @@ -282,7 +282,7 @@ $formattedText.escapeHtml($ann_item.Header.subject) $formattedText.escapeHtml($ann_item.Header.subject) #end - + $formattedText.escapeHtml($ann_item.AuthorDisplayName) #if ($toolId.equals("sakai.announcements")) diff --git a/assignment/api/src/java/org/sakaiproject/assignment/api/AssignmentService.java b/assignment/api/src/java/org/sakaiproject/assignment/api/AssignmentService.java index 4624a3b14ff8..f825908b2c94 100644 --- a/assignment/api/src/java/org/sakaiproject/assignment/api/AssignmentService.java +++ b/assignment/api/src/java/org/sakaiproject/assignment/api/AssignmentService.java @@ -881,7 +881,7 @@ public String getDeepLinkWithPermissions(String context, String assignmentId, bo public boolean isTimeSheetEnabled(String siteId); /** - * The the name of the content review service being used e.g. Turnitin + * The name of the content review service being used e.g. Turnitin * @return A String containing the name of the content review service */ public String getContentReviewServiceName(); @@ -891,4 +891,9 @@ public String getDeepLinkWithPermissions(String context, String assignmentId, bo public boolean allowAddTags(String context); public FormattedText getFormattedText(); + + /** + * Returns true if the submission contains instructor feedback, whether as comment text (inline) or attachments. + */ + public boolean doesSubmissionHaveInstructorFeedback(AssignmentSubmission submission); } diff --git a/assignment/api/src/resources/assignment_ca.properties b/assignment/api/src/resources/assignment_ca.properties index 4ac6d5f42ec8..9783a7e45671 100644 --- a/assignment/api/src/resources/assignment_ca.properties +++ b/assignment/api/src/resources/assignment_ca.properties @@ -1267,7 +1267,7 @@ youmustrubric=Aquesta tasca \u00E9s autoavaluable. Pots autoavaluar la teva tasc youhavetorubric=Aquesta tasca \u00fas autoavaluable. Has d'autoavaluar la teva tasca amb la r\u00FAbrica abans d'enviar-la. Has de completar sencera la r\u00FAbrica per fer l'enviament. youhavetorubricone=Aquesta tasca \u00fas autoavaluable. Has d'autoavaluar la teva tasca amb la r\u00FAbrica abans d'enviar-la. Has de completar almenys un criteri de la r\u00FAbrica per fer l'enviament. studentrubric=Aquesta tasca \u00E9s autoavaluable. Pots revisar l'autoavaluaci\u00F3 de l'estudiant abans de puntuar la tasca. -autoevaluation=Autoevaluaci\u00f3: +autoevaluation=Autoavaluaci\u00f3: instructor_grading=Correcci\u00f3 del profesor: reviewrubric=Aquesta tasca \u00E9s autoavaluable. Pot revisar la autoavaluaci\u00F3 abans d'enviar-la. reviewrubricreport=Aquesta \u00E9s la teva autoavaluaci\u00F3. diff --git a/assignment/impl/pom.xml b/assignment/impl/pom.xml index fb82035a8675..c38a094c8364 100644 --- a/assignment/impl/pom.xml +++ b/assignment/impl/pom.xml @@ -217,16 +217,16 @@ test - org.sakaiproject.basiclti - basiclti-api + org.sakaiproject.lti + lti-api - org.sakaiproject.basiclti - basiclti-util + org.sakaiproject.lti + lti-util - org.sakaiproject.basiclti - basiclti-common + org.sakaiproject.lti + lti-common ${sakai.version} diff --git a/assignment/impl/src/java/org/sakaiproject/assignment/impl/AssignmentServiceImpl.java b/assignment/impl/src/java/org/sakaiproject/assignment/impl/AssignmentServiceImpl.java index 192b02cb78b6..71b5465ea958 100644 --- a/assignment/impl/src/java/org/sakaiproject/assignment/impl/AssignmentServiceImpl.java +++ b/assignment/impl/src/java/org/sakaiproject/assignment/impl/AssignmentServiceImpl.java @@ -104,7 +104,7 @@ import org.sakaiproject.authz.api.Member; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.api.SecurityService; -import org.sakaiproject.basiclti.util.SakaiBLTIUtil; +import org.sakaiproject.lti.util.SakaiLTIUtil; import org.sakaiproject.calendar.api.Calendar; import org.sakaiproject.calendar.api.CalendarEvent; import org.sakaiproject.calendar.api.CalendarService; @@ -2031,10 +2031,10 @@ private AssignmentConstants.SubmissionStatus getGradersCanonicalSubmissionStatus } else if (submission.getGraded()) { if (StringUtils.isNotBlank(submission.getGrade())) { return SubmissionStatus.GRADED; - } else if (StringUtils.isNotBlank(submission.getFeedbackComment())) { + } else if (doesSubmissionHaveInstructorFeedback(submission)) { return SubmissionStatus.COMMENTED; } - } else if (StringUtils.isNotBlank(submission.getFeedbackComment())) { + } else if (doesSubmissionHaveInstructorFeedback(submission)) { return SubmissionStatus.COMMENTED; } } else { @@ -2043,10 +2043,10 @@ private AssignmentConstants.SubmissionStatus getGradersCanonicalSubmissionStatus } else if (submission.getGraded()) { if (StringUtils.isNotBlank(submission.getGrade())) { return SubmissionStatus.GRADED; - } else if (StringUtils.isNotBlank(submission.getFeedbackComment())) { + } else if (doesSubmissionHaveInstructorFeedback(submission)) { return SubmissionStatus.COMMENTED; } - } else if (StringUtils.isNotBlank(submission.getFeedbackComment())) { + } else if (doesSubmissionHaveInstructorFeedback(submission)) { return SubmissionStatus.COMMENTED; } else { return SubmissionStatus.NO_SUBMISSION; @@ -2061,17 +2061,21 @@ private AssignmentConstants.SubmissionStatus getGradersCanonicalSubmissionStatus // grade saved but not release yet, show this to graders if (StringUtils.isNotBlank(submission.getGrade())) { return SubmissionStatus.GRADED; - } else if (StringUtils.isNotBlank(submission.getFeedbackComment())) { + } else if (doesSubmissionHaveInstructorFeedback(submission)) { return SubmissionStatus.COMMENTED; } } - } else if (StringUtils.isNotBlank(submission.getFeedbackComment())) { + } else if (doesSubmissionHaveInstructorFeedback(submission)) { return SubmissionStatus.COMMENTED; } } return SubmissionStatus.UNGRADED; } + public boolean doesSubmissionHaveInstructorFeedback(AssignmentSubmission submission) { + return StringUtils.isNotBlank(submission.getFeedbackComment()) || CollectionUtils.isNotEmpty(submission.getFeedbackAttachments()); + } + private AssignmentConstants.SubmissionStatus getSubmittersCanonicalSubmissionStatus(AssignmentSubmission submission) { if (submission == null) return SubmissionStatus.NOT_STARTED; @@ -4253,7 +4257,7 @@ public Map transferCopyEntities(String fromContext, String toCon // If there is a LTI launch associated with this copy it over if ( oAssignment.getContentId() != null ) { Long contentKey = oAssignment.getContentId().longValue(); - Object retval = SakaiBLTIUtil.copyLTIContent(contentKey, toContext, fromContext); + Object retval = SakaiLTIUtil.copyLTIContent(contentKey, toContext, fromContext); if ( retval instanceof Long ) { nAssignment.setContentId(((Long) retval).intValue()); // If something went wrong, we can't be an LTI submission in the new site diff --git a/assignment/tool/pom.xml b/assignment/tool/pom.xml index 91de5744cf4f..6f7fffc233d5 100644 --- a/assignment/tool/pom.xml +++ b/assignment/tool/pom.xml @@ -92,17 +92,17 @@ sakai-velocity-tool - org.sakaiproject.basiclti - basiclti-api + org.sakaiproject.lti + lti-api - org.sakaiproject.basiclti - basiclti-common + org.sakaiproject.lti + lti-common ${project.version} - org.sakaiproject.basiclti - basiclti-util + org.sakaiproject.lti + lti-util org.apache.commons diff --git a/assignment/tool/src/java/org/sakaiproject/assignment/entityproviders/AssignmentEntityProvider.java b/assignment/tool/src/java/org/sakaiproject/assignment/entityproviders/AssignmentEntityProvider.java index 9415ce0b13b6..534f30bf8ec9 100644 --- a/assignment/tool/src/java/org/sakaiproject/assignment/entityproviders/AssignmentEntityProvider.java +++ b/assignment/tool/src/java/org/sakaiproject/assignment/entityproviders/AssignmentEntityProvider.java @@ -689,7 +689,7 @@ private Map submissionToMap(Set activeSubmitters, Assign if ( content != null ) { String contentItem = StringUtils.trimToEmpty((String) content.get(LTIService.LTI_CONTENTITEM)); // Instead of parsing, the JSON we just look for a simple existance of the submission review entry - // Delegate the complex understanding of the launch to SakaiBLTIUtil + // Delegate the complex understanding of the launch to SakaiLTIUtil // TODO: Eventually, Sakai's LTIService will implement a submissionReview checkbox and we should check for that here boolean submissionReviewAvailable = contentItem.indexOf("\"submissionReview\"") > 0; @@ -757,7 +757,9 @@ private Map submissionToMap(Set activeSubmitters, Assign log.info("There was an attachment on submission {} that was invalid", as.getId()); return null; } - }).collect(Collectors.toList()); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); if (!submittedAttachments.isEmpty()) { submission.put("submittedAttachments", submittedAttachments); @@ -803,7 +805,7 @@ private Map submissionToMap(Set activeSubmitters, Assign && assignmentService.isPeerAssessmentClosed(assignment)) { List reviews = assignmentPeerAssessmentService.getPeerAssessmentItems(as.getId(), assignment.getScaleFactor()); if (reviews != null) { - List completedReviews = new ArrayList<>(); + List completedReviews = new ArrayList<>(); for (PeerAssessmentItem review : reviews) { if (!review.getRemoved() && (review.getScore() != null || (StringUtils.isNotBlank(review.getComment())))) { //only show peer reviews that have either a score or a comment saved @@ -839,13 +841,12 @@ private Map submissionToMap(Set activeSubmitters, Assign log.warn("Exception while creating reference: {}", e.toString()); } } - if (!attachmentRefList.isEmpty()) - review.setAttachmentRefList(attachmentRefList); + if (!attachmentRefList.isEmpty()) review.setAttachmentRefList(attachmentRefList); } - completedReviews.add(review); + completedReviews.add(new SimplePeerAssessmentItem(review)); } } - if (completedReviews.size() > 0) { + if (!completedReviews.isEmpty()) { submission.put("peerReviews", completedReviews); } } @@ -1098,7 +1099,7 @@ public ActionReturn getGradableForSite(EntityView view , Map par ltiSubmissionLaunch = "/access/lti/site/" + siteId + "/content:" + contentKey + "?for_user=" + submitter.get("id"); // Instead of parsing, the JSON we just look for a simple existance of the submission review entry - // Delegate the complex understanding of the launch to SakaiBLTIUtil + // Delegate the complex understanding of the launch to SakaiLTIUtil if ( contentItem.indexOf("\"submissionReview\"") > 0 ) { ltiSubmissionLaunch = ltiSubmissionLaunch + "&message_type=content_review"; } @@ -2216,4 +2217,39 @@ public SimpleGroup(Group g) { this.users = g.getUsers(); } } + + @Getter + public class SimplePeerAssessmentItem { + + private String assessorUserId; + private String submissionId; + private String assignmentId; + private Integer score; + private String scoreDisplay; + private String comment; + private Boolean removed; + private Boolean submitted; + private List attachmentUrlList; + private String assessorDisplayName; + private Integer scaledFactor; + private boolean draft; + + public SimplePeerAssessmentItem(PeerAssessmentItem item) { + this.assessorUserId = item.getId().getAssessorUserId(); + this.submissionId = item.getId().getSubmissionId(); + this.assignmentId = item.getAssignmentId(); + this.score = item.getScore(); + this.scoreDisplay = item.getScoreDisplay(); + this.comment = item.getComment(); + this.removed = item.getRemoved(); + this.submitted = item.getSubmitted(); + this.assessorDisplayName = item.getAssessorDisplayName(); + this.scaledFactor = item.getScaledFactor(); + this.draft = item.isDraft(); + + this.attachmentUrlList = item.getAttachmentRefList().stream() + .map(Reference::getUrl) + .collect(Collectors.toList()); + } + } } diff --git a/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java b/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java index 793c5eda4ae8..a1e7bf83193d 100644 --- a/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java +++ b/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java @@ -165,7 +165,7 @@ import org.sakaiproject.authz.api.Role; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.api.SecurityService; -import org.tsugi.basiclti.BasicLTIUtil; +import org.tsugi.lti.LTIUtil; import org.tsugi.lti13.LTICustomVars; import org.tsugi.lti13.DeepLinkResponse; import org.tsugi.lti13.LTI13Util; @@ -256,7 +256,7 @@ import org.sakaiproject.util.comparator.AlphaNumericComparator; import org.sakaiproject.util.comparator.UserSortNameComparator; import org.sakaiproject.lti.api.LTIService; -import org.sakaiproject.basiclti.util.SakaiBLTIUtil; +import org.sakaiproject.lti.util.SakaiLTIUtil; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; @@ -1998,12 +1998,12 @@ private String build_view_external_tool_launch_context(VelocityPortlet portlet, } // Ignore the Content Item - use the value in the assignment if tool allows - context.put("newpage", Boolean.valueOf(SakaiBLTIUtil.getNewpage(tool, null, newpage))); - context.put("height",SakaiBLTIUtil.getFrameHeight(tool, content, "1200px")); + context.put("newpage", Boolean.valueOf(SakaiLTIUtil.getNewpage(tool, null, newpage))); + context.put("height",SakaiLTIUtil.getFrameHeight(tool, content, "1200px")); context.put("browser-feature-allow", String.join(";", serverConfigurationService.getStrings("browser.feature.allow"))); // Copy title, description, and dates from Assignment to content if mis-match - int protect = SakaiBLTIUtil.getInt(content.get(LTIService.LTI_PROTECT)); + int protect = SakaiLTIUtil.getInt(content.get(LTIService.LTI_PROTECT)); String assignmentTitle = StringUtils.trimToEmpty(assignment.getTitle()); String assignmentDesc = StringUtils.trimToEmpty(assignment.getInstructions()); Instant visibleDate = assignment.getVisibleDate(); @@ -2021,7 +2021,7 @@ private String build_view_external_tool_launch_context(VelocityPortlet portlet, String placement_secret = StringUtils.trimToNull((String) content.get(LTIService.LTI_PLACEMENTSECRET)); String content_settings = (String) content.get(LTIService.LTI_SETTINGS); - JSONObject content_json = BasicLTIUtil.parseJSONObject(content_settings); + JSONObject content_json = LTIUtil.parseJSONObject(content_settings); String contentVisibleDate = StringUtils.trimToEmpty((String) content_json.get(DeepLinkResponse.RESOURCELINK_AVAILABLE_STARTDATETIME)); String contentOpenDate = StringUtils.trimToEmpty((String) content_json.get(DeepLinkResponse.RESOURCELINK_SUBMISSION_STARTDATETIME)); String contentDueDate = StringUtils.trimToEmpty((String) content_json.get(DeepLinkResponse.RESOURCELINK_SUBMISSION_ENDDATETIME)); @@ -2045,7 +2045,7 @@ private String build_view_external_tool_launch_context(VelocityPortlet portlet, content_json.put(LTIService.LTI_DESCRIPTION, assignmentDesc); content_json.put(LTIService.LTI_PROTECT, new Integer(1)); - // Copy assignment specific custom parameter substitutions to pass into SakaiBLTIUtil + // Copy assignment specific custom parameter substitutions to pass into SakaiLTIUtil content_json.put(DeepLinkResponse.RESOURCELINK_AVAILABLE_STARTDATETIME, assignmentVisibleDate); content_json.put(DeepLinkResponse.RESOURCELINK_SUBMISSION_STARTDATETIME, assignmentOpenDate); content_json.put(DeepLinkResponse.RESOURCELINK_AVAILABLE_ENDDATETIME, assignmentDueDate); @@ -2061,8 +2061,8 @@ private String build_view_external_tool_launch_context(VelocityPortlet portlet, } // Unlock this assignment for one launch... - String launch_code_key = SakaiBLTIUtil.getLaunchCodeKey(content); - String launch_code = SakaiBLTIUtil.getLaunchCode(content); + String launch_code_key = SakaiLTIUtil.getLaunchCodeKey(content); + String launch_code = SakaiLTIUtil.getLaunchCode(content); if ( launch_code_key != null && launch_code != null ) { Session session = sessionManager.getCurrentSession(); session.setAttribute(launch_code_key, launch_code); @@ -3634,7 +3634,7 @@ protected void setAssignmentFormContext(SessionState state, Context context) { } Placement placement = toolManager.getCurrentPlacement(); - // String contentReturn = SakaiBLTIUtil.getOurServerUrl() + "/portal/tool/" + placement.getId() + + // String contentReturn = SakaiLTIUtil.getOurServerUrl() + "/portal/tool/" + placement.getId() + String contentReturn = serverConfigurationService.getToolUrl() + "/" + placement.getId() + "/sakai.lti.admin.helper.helper" + "?panel=AssignmentsMain" @@ -10343,7 +10343,7 @@ public void doEdit_assignment(RunData data) { Map tool = ltiService.getTool(toolKey, site.getId()); String toolTitle = (String) tool.get(LTIService.LTI_TITLE); state.setAttribute(NEW_ASSIGNMENT_CONTENT_TITLE, toolTitle); - Long toolNewpage = SakaiBLTIUtil.getLong(tool.get(LTIService.LTI_NEWPAGE)); + Long toolNewpage = SakaiLTIUtil.getLong(tool.get(LTIService.LTI_NEWPAGE)); state.setAttribute(NEW_ASSIGNMENT_CONTENT_TOOL_NEWPAGE, toolNewpage); } } catch(org.sakaiproject.exception.IdUnusedException e ) { @@ -11199,7 +11199,7 @@ public void doRelease_grades(RunData data) { // UNGRADED and comments have been left on the submission // GRADED and a grade exists on the submission if (!s.getGradeReleased() - && (a.getTypeOfGrade() == Assignment.GradeType.UNGRADED_GRADE_TYPE && StringUtils.isNotBlank(s.getFeedbackComment()) + && (a.getTypeOfGrade() == Assignment.GradeType.UNGRADED_GRADE_TYPE && assignmentService.doesSubmissionHaveInstructorFeedback(s) || ((a.getTypeOfGrade() != Assignment.GradeType.UNGRADED_GRADE_TYPE) && StringUtils.isNotBlank(s.getGrade())))) { s.setGraded(true); s.setGradeReleased(true); diff --git a/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentToolUtils.java b/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentToolUtils.java index e30c4118f6fa..090c99a4ce24 100644 --- a/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentToolUtils.java +++ b/assignment/tool/src/java/org/sakaiproject/assignment/tool/AssignmentToolUtils.java @@ -37,6 +37,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.sakaiproject.assignment.api.AssignmentReferenceReckoner; @@ -304,7 +305,7 @@ public void gradeSubmission(AssignmentSubmission submission, String gradeOption, // determine if the submission is graded if (a.getTypeOfGrade().equals(Assignment.GradeType.UNGRADED_GRADE_TYPE)) { submission.setGrade(null); - submission.setGraded(submittedfeedbackComment != null); + submission.setGraded(submittedfeedbackComment != null || CollectionUtils.isNotEmpty(submission.getFeedbackAttachments())); } else { if (StringUtils.isNotBlank(grade)) { // if there is a grade then the submission is graded diff --git a/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_grading_submission.vm b/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_grading_submission.vm index 49cd261d462c..b1b9c3842288 100644 --- a/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_grading_submission.vm +++ b/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_grading_submission.vm @@ -396,7 +396,11 @@ #if($!rubricSelfReport)

$tlang.getString("autoevaluation")

+

+ $tlang.getString("studentrubric") +

-

- $tlang.getString("studentrubric") -


#end

$tlang.getString("instructor_grading")

-
$tlang.getString("allowResubmission.toggleall") - $tlang.getString( + @@ -434,7 +434,7 @@ function printView(url) { #if ($!userSubmission.hasVisibleAttachments) - $tlang.getString( + #end @@ -633,9 +633,7 @@ function printView(url) { #end #if ($submission.getGradeReleased()) - $tlang.getString( - #else - $tlang.getString( + #end diff --git a/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_new_edit_assignment.vm b/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_new_edit_assignment.vm index db6ce03b555b..e792cf669923 100644 --- a/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_new_edit_assignment.vm +++ b/assignment/tool/src/webapp/vm/assignment/chef_assignments_instructor_new_edit_assignment.vm @@ -157,7 +157,7 @@
#if ($alertMessage) -
+ #end @@ -579,7 +579,7 @@
diff --git a/meetings/ui/src/main/frontend/src/components/sakai-input.vue b/meetings/ui/src/main/frontend/src/components/sakai-input.vue index b020175e3be1..19672d918025 100644 --- a/meetings/ui/src/main/frontend/src/components/sakai-input.vue +++ b/meetings/ui/src/main/frontend/src/components/sakai-input.vue @@ -173,5 +173,13 @@ export default { color: var(--sakai-text-color-disabled); } } + .form-check-coorganizers { + appearance: none; + height: 15px; + width: 15px; + border-radius: 3px; + background-color: var(--sakai-background-color-1); + border: 1px solid var(--sakai-color-black) !important; + } } diff --git a/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue b/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue index 07c8887afe5e..8a5292f9aa04 100644 --- a/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue +++ b/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue @@ -104,6 +104,38 @@ +
+
+

{{ i18n.loading_report_data }}

+ +
+
+
+ âš ï¸ + {{ i18n.download_report_error_message }} + +
+
+
+ +
+

{{ i18n.no_preview_report }}

+
+ + + + + + + + + + + +
{{ header }}
{{ cell }}
+ +
+
+ diff --git a/meetings/ui/src/main/frontend/src/resources/icons.js b/meetings/ui/src/main/frontend/src/resources/icons.js index 4ee1d946b52d..9b4ce5453ccc 100644 --- a/meetings/ui/src/main/frontend/src/resources/icons.js +++ b/meetings/ui/src/main/frontend/src/resources/icons.js @@ -35,7 +35,10 @@ const iconsFontawsome = { presentation: "fa-desktop", remove: "fa-trash", refresh: "fa-refresh", - spinner: "fa-spinner fa-spin" + spinner: "fa-spinner fa-spin", + download: "fa fa-download", + fileCsv: "fa fa-file", + eye: "fa fa-eye" }; const iconsBootstrap = { }; diff --git a/meetings/ui/src/main/frontend/src/views/CreateMeeting.vue b/meetings/ui/src/main/frontend/src/views/CreateMeeting.vue index 070b4aa0a5ac..43da0304d96c 100644 --- a/meetings/ui/src/main/frontend/src/views/CreateMeeting.vue +++ b/meetings/ui/src/main/frontend/src/views/CreateMeeting.vue @@ -67,7 +67,7 @@
-->
+
+ {{ i18n.meeting_alert }} + +

{{ i18n.search_results }}

@@ -187,6 +201,7 @@ export default { searchString: '', editPermission: false, btnPress2: false, + showMeetingBanner: false, items: [ { id: 0, @@ -259,6 +274,19 @@ export default { this.showError(this.i18n.error_load_meetings); } }, + async loadDefaultProperties() { + try { + const response = await fetch(constants.toolPlacement + "/config"); + if (response.ok) { + const data = await response.json(); + this.showMeetingBanner = data.showMeetingBanner; + } else { + console.error('Error loading default configuration.'); + } + } catch (error) { + console.error('Error loading default configuration:', error); + } + }, meetingsComperator(a,b) { return dayjs(a.startDate).isBefore(b.startDate) ? -1 : 1; }, @@ -307,6 +335,7 @@ export default { mounted() { this.loadEditPermission(); this.loadMeetingsList(); + this.loadDefaultProperties(); }, }; diff --git a/message/message-util/util/src/java/org/sakaiproject/message/util/BaseMessage.java b/message/message-util/util/src/java/org/sakaiproject/message/util/BaseMessage.java index 074baadf0916..66793f2f36fa 100644 --- a/message/message-util/util/src/java/org/sakaiproject/message/util/BaseMessage.java +++ b/message/message-util/util/src/java/org/sakaiproject/message/util/BaseMessage.java @@ -4005,7 +4005,7 @@ public void clearGroupAccess() throws PermissionException } // verify that the user has permission to add in the channel context - boolean allowed = (m_message != null) && (((BaseMessageEdit) m_message).m_channel).allowAddChannelMessage(); + boolean allowed = ((BaseMessageEdit) m_message).getPropertiesEdit().get("selectedRoles") != null || ((m_message != null) && (((BaseMessageEdit) m_message).m_channel).allowAddChannelMessage()); if (!allowed) { throw new PermissionException(m_sessionManager.getCurrentSessionUserId(), "access:channel", ((BaseMessageEdit) m_message).getReference()); 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 40d5684479ec..5c9f2732d6a9 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 @@ -15,33 +15,28 @@ */ package org.sakaiproject.microsoft.controller; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.sakaiproject.microsoft.api.MicrosoftCommonService; import org.sakaiproject.microsoft.api.MicrosoftConfigurationService; +import org.sakaiproject.microsoft.api.MicrosoftLoggingService; import org.sakaiproject.microsoft.api.MicrosoftSynchronizationService; import org.sakaiproject.microsoft.api.SakaiProxy; +import org.sakaiproject.microsoft.api.data.AutoConfigProcessStatus; +import org.sakaiproject.microsoft.api.data.CreationStatus; import org.sakaiproject.microsoft.api.data.MicrosoftChannel; import org.sakaiproject.microsoft.api.data.MicrosoftCredentials; +import org.sakaiproject.microsoft.api.data.MicrosoftLogInvokers; import org.sakaiproject.microsoft.api.data.MicrosoftTeam; -import org.sakaiproject.microsoft.api.exceptions.MicrosoftCredentialsException; +import org.sakaiproject.microsoft.api.data.SynchronizationStatus; import org.sakaiproject.microsoft.api.model.GroupSynchronization; +import org.sakaiproject.microsoft.api.model.MicrosoftLog; import org.sakaiproject.microsoft.api.model.SiteSynchronization; -import org.sakaiproject.microsoft.controller.auxiliar.AutoConfigSessionBean; +import org.sakaiproject.microsoft.api.persistence.MicrosoftLoggingRepository; import org.sakaiproject.microsoft.controller.auxiliar.AutoConfigConfirmRequest; import org.sakaiproject.microsoft.controller.auxiliar.AutoConfigRequest; +import org.sakaiproject.microsoft.controller.auxiliar.AutoConfigSessionBean; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; import org.sakaiproject.util.ResourceLoader; @@ -49,12 +44,28 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; -import lombok.extern.slf4j.Slf4j; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.sakaiproject.microsoft.api.MicrosoftCommonService.MAX_ADD_CHANNELS; +import static org.sakaiproject.microsoft.api.MicrosoftCommonService.MAX_CHANNELS; /** @@ -68,7 +79,10 @@ public class AutoConfigController { private static ResourceLoader rb = new ResourceLoader("Messages"); - + @Setter + private MicrosoftLoggingRepository microsoftLoggingRepository; + @Autowired + private MicrosoftLoggingService microsoftLoggingService; @Autowired private MicrosoftSynchronizationService microsoftSynchronizationService; @@ -138,9 +152,9 @@ public String autoConfig( //just to display it in the confirmation table site.setTitle(title); } - final String finalTitle = title; - - //check if there is a, not-used Team that matched the Site + final String finalTitle = microsoftCommonService.processMicrosoftTeamName(title); + + //check if there is a, not-used Team that matched the Site MicrosoftTeam team = teamsMap.values().stream().filter(t -> t.getName().equalsIgnoreCase(finalTitle)).findAny().orElse(null); //match found @@ -203,9 +217,10 @@ public String autoConfig( public Boolean autoConfigConfirm( @RequestBody AutoConfigConfirmRequest payload, HttpServletRequest request, - Model model - ) throws Exception { - if(payload.getSiteIdList() == null || payload.getSiteIdList().size() == 0 || payload.getSyncDateFrom() == null || payload.getSyncDateTo() == null) { + Model model) throws Exception { + + if (CollectionUtils.isEmpty(payload.getSiteIdList()) || + payload.getSyncDateFrom() == null || payload.getSyncDateTo() == null) { return false; } @@ -214,159 +229,52 @@ public Boolean autoConfigConfirm( HttpSession session = request.getSession(); MicrosoftCredentials credentials = microsoftConfigurationService.getCredentials(); + new Thread(() -> { - AutoConfigSessionBean autoConfigSessionBean_aux = null; + AutoConfigSessionBean autoConfigSessionBean; synchronized (session) { - autoConfigSessionBean_aux = (AutoConfigSessionBean)session.getAttribute("AutoConfigSessionBean"); - if(autoConfigSessionBean_aux == null) { - autoConfigSessionBean_aux = new AutoConfigSessionBean(); - session.setAttribute("AutoConfigSessionBean", autoConfigSessionBean_aux); + autoConfigSessionBean = (AutoConfigSessionBean) session.getAttribute("AutoConfigSessionBean"); + if (autoConfigSessionBean == null) { + autoConfigSessionBean = new AutoConfigSessionBean(); + session.setAttribute("AutoConfigSessionBean", autoConfigSessionBean); } } - AutoConfigSessionBean autoConfigSessionBean = autoConfigSessionBean_aux; - - if(!autoConfigSessionBean.isRunning()) { - //start running + + if (!autoConfigSessionBean.isRunning()) { autoConfigSessionBean.startRunning(payload.getSiteIdList().size()); - + autoConfigSessionBean.addStatus(AutoConfigProcessStatus.START_RUNNING, ""); + Map map = autoConfigSessionBean.getConfirmMap(); - - for(String siteId : payload.getSiteIdList()) { - //get stored site from session bean + + for (String siteId : payload.getSiteIdList()) { Site site = autoConfigSessionBean.getSitesMap().get(siteId); - if(site != null) { - Object o = map.get(siteId); - if(o != null) { - if(o instanceof String) { - try { - //--> create NEW Team - String teamId = microsoftCommonService.createTeam((String)o, credentials.getEmail()); - if(teamId != null) { - //create relationship - SiteSynchronization ss = SiteSynchronization.builder() - .siteId(siteId) - .teamId(teamId) - .forced(false) - .syncDateFrom(syncDateFrom) - .syncDateTo(syncDateTo) - .build(); - - log.debug("saving NEW: siteId={}, teamId={}", siteId, teamId); - microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); - - //check if given site has groups and configuration allows it - if(autoConfigSessionBean.isNewChannel() && site.getGroups().size() > 0) { - for(Group g : site.getGroups()) { - try { - //exclude automatic lesson groups - if(g.getTitle().startsWith("Access:")) { - continue; - } - - //as Team is new, create all Channels - String createdChannelId = microsoftCommonService.createChannel(teamId, g.getTitle(), credentials.getEmail()); - - if(StringUtils.isNotBlank(createdChannelId)) { - //create relationship - GroupSynchronization gs = GroupSynchronization.builder() - .siteSynchronization(ss) - .groupId(g.getId()) - .channelId(createdChannelId) - .build(); - - log.debug("saving NEW: groupId={}, channelId={}, title={}", g.getId(), createdChannelId, g.getTitle()); - microsoftSynchronizationService.saveOrUpdateGroupSynchronization(gs); - } - }catch(Exception e) { - log.error("Unexpected exception creating channel: {}", e.getMessage()); - } - } - } - autoConfigSessionBean.increaseCounter(); - } else { - //mark this site as error - autoConfigSessionBean.addError(siteId, site.getTitle(), rb.getString("error.creating_team")); - } - - } catch (MicrosoftCredentialsException e) { - autoConfigSessionBean.addError(siteId, site.getTitle(), rb.getString(e.getMessage())); - } - //Team already exists and matches Site's title - } else if(o instanceof SiteSynchronization) { - SiteSynchronization ss = (SiteSynchronization)o; - - SiteSynchronization aux_ss = microsoftSynchronizationService.getSiteSynchronization(ss); - //check if ss already exists (this should never happen) - if(aux_ss != null) { - //mark this site as error - autoConfigSessionBean.addError(siteId, site.getTitle(), rb.getString("error.site_synchronization_already_exists")); - continue; - } - - //not forced -> check if exists some forced synchronization - if(microsoftSynchronizationService.countSiteSynchronizationsByTeamId(ss.getTeamId(), true) > 0) { - //mark this site as error - autoConfigSessionBean.addError(siteId, site.getTitle(), rb.getString("error.site_synchronization_already_forced")); - continue; - } - - //set dates - ss.setSyncDateFrom(syncDateFrom); - ss.setSyncDateTo(syncDateTo); - - log.debug("saving site-team: siteId={}, teamId={}", siteId, ss.getTeamId()); - microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); - - //check groups-channels - try { - if(site.getGroups().size() > 0) { - //get existing channels from Team - Map channelsMap = microsoftCommonService.getTeamPrivateChannels(ss.getTeamId(), true); - - //get existing groups from site - for(Group g : site.getGroups()) { - //exclude automatic lesson groups - if(g.getTitle().startsWith("Access:")) { - continue; - } - - //check if any group matches any channel - MicrosoftChannel channel = channelsMap.values().stream().filter(c -> c.getName().equalsIgnoreCase(g.getTitle())).findAny().orElse(null); - String channelId = (channel != null) ? channel.getId() : null; - - //match NOT found --> Create channel (if configuration allows it) - if(channel == null && autoConfigSessionBean.isNewChannel()) { - channelId = microsoftCommonService.createChannel(ss.getTeamId(), g.getTitle(), credentials.getEmail()); - } - - if(StringUtils.isNotBlank(channelId)) { - //create relationship - GroupSynchronization gs = GroupSynchronization.builder() - .siteSynchronization(ss) - .groupId(g.getId()) - .channelId(channelId) - .build(); - - //check if Group Synchronization does not exist - GroupSynchronization aux_gs = microsoftSynchronizationService.getGroupSynchronization(gs); - if(aux_gs == null) { - log.debug("saving group-channel: groupId={}, channelId={}", g.getId(), channelId); - microsoftSynchronizationService.saveOrUpdateGroupSynchronization(gs); - } - } - } - } - } catch (MicrosoftCredentialsException e) { - log.error("MicrosoftCredentialsException in confirm thread"); - } - autoConfigSessionBean.increaseCounter(); - } + if (site == null) { + continue; + } + + Object o = map.get(siteId); + if (o == null) { + continue; + } + + try { + if (o instanceof String) { + autoConfigSessionBean.addStatus(AutoConfigProcessStatus.CREATING_TEAM, site.getTitle()); + handleNewTeamCreation(autoConfigSessionBean, site, (String) o, syncDateFrom, syncDateTo, credentials); + } else if (o instanceof SiteSynchronization) { + autoConfigSessionBean.addStatus(AutoConfigProcessStatus.BINDING_TEAM, site.getTitle()); + handleExistingTeamBinding(autoConfigSessionBean, site, (SiteSynchronization) o, syncDateFrom, syncDateTo, credentials); } + } catch (Exception e) { + log.error("Error handling site " + siteId, e); + autoConfigSessionBean.addError(siteId, site.getTitle(), e.getMessage()); + } finally { + autoConfigSessionBean.increaseCounter(); } } - - //end running - if(autoConfigSessionBean.getCount() >= autoConfigSessionBean.getTotal()) { + + if (autoConfigSessionBean.getCount() >= autoConfigSessionBean.getTotal()) { + autoConfigSessionBean.addStatus(AutoConfigProcessStatus.END_RUNNING, ""); autoConfigSessionBean.finishRunning(); } } @@ -374,13 +282,206 @@ public Boolean autoConfigConfirm( return true; } - + + + public void handleNewTeamCreation(AutoConfigSessionBean autoConfigSessionBean, Site site, String teamName, ZonedDateTime syncDateFrom, ZonedDateTime syncDateTo, MicrosoftCredentials credentials) throws Exception { + String teamId = microsoftCommonService.createTeam(teamName, credentials.getEmail()); + SiteSynchronization ss = SiteSynchronization.builder() + .siteId(site.getId()) + .teamId(teamId != null ? teamId : "") + .forced(false) + .syncDateFrom(syncDateFrom) + .syncDateTo(syncDateTo) + .creationStatus(CreationStatus.OK) + .build(); + + if (teamId == null) { + ss.setStatus(SynchronizationStatus.NOT_AVAILABLE); + ss.setCreationStatus(CreationStatus.KO); + + autoConfigSessionBean.addError(site.getId(), site.getTitle(), rb.getString("error.creating_team")); + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); + microsoftLoggingService.saveLog(MicrosoftLog.builder() + .event(MicrosoftLog.ERROR_TEAM_ID_NULL) + .status(MicrosoftLog.Status.KO) + .addData("origin", MicrosoftLogInvokers.MANUAL.getCode()) + .addData("teamId", teamId) + .addData("siteId", site.getId()) + .addData("siteTitle", site.getTitle()) + .addData("teamTitle", teamName) + .build()); + return; + } + + microsoftLoggingService.saveLog(MicrosoftLog.builder() + .event(MicrosoftLog.EVENT_CREATE_TEAM_FROM_SITE) + .status(MicrosoftLog.Status.OK) + .addData("origin", MicrosoftLogInvokers.MANUAL.getCode()) + .addData("siteId", site.getId()) + .addData("siteTitle", site.getTitle()) + .addData("teamId", teamId) + .addData("teamTitle", teamName) + .build()); + + boolean limitExceeded = site.getGroups().size() > MAX_CHANNELS; + List groupsToProcess = limitGroups(site.getGroups().stream().filter(g -> !g.getTitle().startsWith("Access:")).collect(Collectors.toList())); + + if (limitExceeded) { + ss.setCreationStatus(CreationStatus.PARTIAL_OK); + } + + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); + + List channels = microsoftCommonService.createChannels(groupsToProcess, teamId, credentials.getEmail()); + + for (Group g : groupsToProcess) { + Optional channelOpt = channels.stream() + .filter(c -> c.getName().replace(" ", "").equalsIgnoreCase(microsoftCommonService.processMicrosoftChannelName(g.getTitle()).replace(" ", ""))).findFirst(); + + channelOpt.ifPresent(channel -> { + GroupSynchronization gs = GroupSynchronization.builder() + .siteSynchronization(ss) + .groupId(g.getId()) + .channelId(channel.getId()) + .build(); + microsoftSynchronizationService.saveOrUpdateGroupSynchronization(gs); + }); + } + String groupsIds = groupsToProcess.stream() + .map(group -> group.getId().toString()) + .collect(Collectors.joining(",")); + + String groupsNames = groupsToProcess.stream() + .map(group -> group.getTitle().toString()) + .collect(Collectors.joining(",")); + + String channelIds = channels.stream() + .map(channel -> channel.getId().toString()) + .collect(Collectors.joining(",")); + + String channelNames = channels.stream() + .map(channel -> channel.getName().toString()) + .collect(Collectors.joining(",")); + + microsoftLoggingService.saveLog(MicrosoftLog.builder() + .event(MicrosoftLog.EVENT_CHANNEL_PRESENT_ON_GROUP) + .status(MicrosoftLog.Status.OK) + .addData("origin", MicrosoftLogInvokers.MANUAL.getCode()) + .addData("siteId", site.getId()) + .addData("siteTitle", site.getTitle()) + .addData("processGroupsIds", groupsIds) + .addData("processGroupsNames", groupsNames) + .addData("numberGroupsOnSite", String.valueOf(site.getGroups().size())) + .addData("numberLimitedGroups", String.valueOf(groupsToProcess.size())) + .addData("numberNonProcessedGroups", String.valueOf(site.getGroups().size() - groupsToProcess.size())) + .addData("teamId", teamId) + .addData("teamTitle", teamName) + .addData("createChannelsId", channelIds) + .addData("createChannelsName", channelNames) + .build()); + + } + + public void handleExistingTeamBinding(AutoConfigSessionBean autoConfigSessionBean, Site site, SiteSynchronization ss, ZonedDateTime syncDateFrom, ZonedDateTime syncDateTo, MicrosoftCredentials credentials) throws Exception { + + microsoftLoggingService.saveLog(MicrosoftLog.builder() + .event(MicrosoftLog.BINDING_TEAM_FROM_SITE) + .status(!site.getId().isBlank() ? MicrosoftLog.Status.OK : MicrosoftLog.Status.KO) + .addData("origin", MicrosoftLogInvokers.MANUAL.getCode()) + .addData("siteId", site.getId()) + .addData("siteTitle", site.getTitle()) + .addData("teamId", ss.getTeamId()) + .build()); + + boolean limitExceeded = site.getGroups().size() > MAX_CHANNELS; + ss.setSyncDateFrom(syncDateFrom); + ss.setSyncDateTo(syncDateTo); + ss.setCreationStatus(limitExceeded ? CreationStatus.PARTIAL_OK : CreationStatus.OK); + + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); + + Map channelsMap = microsoftCommonService.getTeamPrivateChannels(ss.getTeamId(), true); + List groupsToProcess = limitGroups(site.getGroups().stream().filter(g -> !g.getTitle().startsWith("Access:")).collect(Collectors.toList())); + + List nonExistingGroups = groupsToProcess.stream() + .filter(g -> channelsMap.values().stream().noneMatch(c -> c.getName().equalsIgnoreCase(microsoftCommonService.processMicrosoftChannelName(g.getTitle())))) + .collect(Collectors.toList()); + + if (nonExistingGroups.size() > 0 && autoConfigSessionBean.isNewChannel()) { + List channels = microsoftCommonService.createChannels(nonExistingGroups, ss.getTeamId(), credentials.getEmail()); + channels.forEach(c -> channelsMap.put(c.getId(), c)); + } + + for (Group g : groupsToProcess) { + MicrosoftChannel channel = channelsMap.values().stream() + .filter(c -> c.getName().replace(" ", "").equalsIgnoreCase(microsoftCommonService.processMicrosoftChannelName(g.getTitle()).replace(" ", ""))) + .findAny() + .orElse(null); + + String channelId = (channel != null) ? channel.getId() : null; + + if (channel == null && autoConfigSessionBean.isNewChannel()) { + channelId = microsoftCommonService.createChannel(ss.getTeamId(), g.getTitle(), credentials.getEmail()); + } + + if (StringUtils.isNotBlank(channelId)) { + GroupSynchronization gs = GroupSynchronization.builder() + .siteSynchronization(ss) + .groupId(g.getId()) + .channelId(channelId) + .build(); + + if (microsoftSynchronizationService.getGroupSynchronization(gs) == null) { + microsoftSynchronizationService.saveOrUpdateGroupSynchronization(gs); + } + } + } + String groupsIds = groupsToProcess.stream() + .map(group -> group.getId().toString()) + .collect(Collectors.joining(",")); + + String groupsNames = groupsToProcess.stream() + .map(group -> group.getTitle().toString()) + .collect(Collectors.joining(",")); + + String channelIds = channelsMap.values().stream() + .map(channel -> channel.getId().toString()) + .collect(Collectors.joining(",")); + + String channelNames = channelsMap.values().stream() + .map(channel -> channel.getName().toString()) + .collect(Collectors.joining(",")); + + microsoftLoggingService.saveLog(MicrosoftLog.builder() + .event(MicrosoftLog.EVENT_CHANNEL_PRESENT_ON_GROUP) + .status(MicrosoftLog.Status.OK) + .addData("origin", MicrosoftLogInvokers.MANUAL.getCode()) + .addData("siteId", site.getId()) + .addData("siteTitle", site.getTitle()) + .addData("processGroupsIds", groupsIds) + .addData("processGroupsNames", groupsNames) + .addData("numberGroupsOnSite", String.valueOf(site.getGroups().size())) + .addData("numberLimitedGroups", String.valueOf(groupsToProcess.size())) + .addData("numberNonProcessedGroups", String.valueOf(site.getGroups().size() - groupsToProcess.size())) + .addData("teamId", ss.getTeamId()) + .addData("createChannelsId", channelIds) + .addData("createChannelsName", channelNames) + .build()); + } + + private List limitGroups(Collection groups) { + return groups.size() > MAX_CHANNELS ? + groups.stream().limit(MAX_ADD_CHANNELS).collect(Collectors.toList()) : + new ArrayList<>(groups); + } + + @GetMapping(value = {"/autoConfig-status"}, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public AutoConfigSessionBean autoConfigStatus(Model model, HttpServletRequest request) throws Exception { HttpSession session = request.getSession(); - AutoConfigSessionBean autoConfigSessionBean = (AutoConfigSessionBean)session.getAttribute("AutoConfigSessionBean"); - + AutoConfigSessionBean autoConfigSessionBean = (AutoConfigSessionBean) session.getAttribute("AutoConfigSessionBean"); + return autoConfigSessionBean; } } diff --git a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/GroupSynchronizationController.java b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/GroupSynchronizationController.java index c3d13c88cfe5..6b9927cd6869 100644 --- a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/GroupSynchronizationController.java +++ b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/GroupSynchronizationController.java @@ -15,15 +15,13 @@ */ package org.sakaiproject.microsoft.controller; -import java.text.MessageFormat; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.sakaiproject.microsoft.api.MicrosoftCommonService; import org.sakaiproject.microsoft.api.MicrosoftConfigurationService; import org.sakaiproject.microsoft.api.MicrosoftSynchronizationService; +import org.sakaiproject.microsoft.api.SakaiProxy; +import org.sakaiproject.microsoft.api.data.MicrosoftChannel; import org.sakaiproject.microsoft.api.exceptions.MicrosoftGenericException; import org.sakaiproject.microsoft.api.model.GroupSynchronization; import org.sakaiproject.microsoft.api.model.SiteSynchronization; @@ -38,10 +36,18 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.sakaiproject.microsoft.api.MicrosoftCommonService.MAX_CHANNELS; /** @@ -64,104 +70,133 @@ public class GroupSynchronizationController { @Autowired MicrosoftConfigurationService microsoftConfigurationService; - + + @Autowired + SakaiProxy sakaiProxy; + private static final String REDIRECT_INDEX = "redirect:/index"; private static final String REDIRECT_EDIT_GROUP_SYNCH = "redirect:/editGroupSynchronization"; private static final String EDIT_GROUP_SYNCH_TEMPLATE = "editGroupSynchronization"; private static final String NEW = "NEW"; - + @GetMapping(value = {"/editGroupSynchronization/{siteSynchronizationId}"}) public String editGroupSynchronization(@PathVariable String siteSynchronizationId, Model model, RedirectAttributes redirectAttributes) throws MicrosoftGenericException { SiteSynchronization ss = microsoftSynchronizationService.getSiteSynchronization(SiteSynchronization.builder().id(siteSynchronizationId).build(), true); - if(ss != null) { - model.addAttribute("siteSynchronizationId", siteSynchronizationId); - model.addAttribute("groupsMap", ss.getSite().getGroups().stream().collect(Collectors.toMap(Group::getId, Function.identity()))); - model.addAttribute("channelsMap", microsoftCommonService.getTeamPrivateChannels(ss.getTeamId())); - model.addAttribute("siteTitle", ss.getSite().getTitle()); - model.addAttribute("teamTitle", microsoftCommonService.getTeam(ss.getTeamId()).getName()); - - List list = microsoftSynchronizationService.getAllGroupSynchronizationsBySiteSynchronizationId(siteSynchronizationId); - if(list != null && list.size() > 0) { - model.addAttribute("groupSynchronizations", list); - } - - - return EDIT_GROUP_SYNCH_TEMPLATE; + + if (ss == null) { + redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.site_synchronization_not_found")); + return REDIRECT_INDEX; + } + + List groups = (List) sakaiProxy.getSite(ss.getSiteId()).getGroups(); + + model.addAttribute("siteSynchronizationId", siteSynchronizationId); + model.addAttribute("groupsMap", groups.stream().collect(Collectors.toMap(Group::getId, Function.identity()))); + model.addAttribute("channelsMap", microsoftCommonService.getTeamPrivateChannels(ss.getTeamId())); + model.addAttribute("siteTitle", ss.getSite().getTitle()); + model.addAttribute("teamTitle", microsoftCommonService.getTeam(ss.getTeamId()).getName()); + + List list = microsoftSynchronizationService.getAllGroupSynchronizationsBySiteSynchronizationId(siteSynchronizationId); + groups.stream() + .filter(g -> list.stream().noneMatch(item -> item.getGroupId().equals(g.getId()))) + .map(g -> GroupSynchronization.builder() + .groupId(g.getId()) + .channelId("") + .siteSynchronization(ss) + .build()) + .forEach(list::add); + + List sortedList = new ArrayList<>(); + groups.forEach( + g -> sortedList.addAll( + list.stream().filter(element -> element.getGroupId().equals(g.getId())).collect(Collectors.toList()) + ) + ); + + if (list.size() > 0) { + model.addAttribute("groupSynchronizations", sortedList); } - - redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.site_synchronization_not_found")); - return REDIRECT_INDEX; + + return EDIT_GROUP_SYNCH_TEMPLATE; } - + @PostMapping(path = {"/add-groupSynchronization/{siteSynchronizationId}"}, consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE}) - public String saveGroupSynchronization(@PathVariable String siteSynchronizationId, @ModelAttribute GroupSynchronizationRequest payload, Model model, RedirectAttributes redirectAttributes) throws MicrosoftGenericException { - String createdChannelId = null; - //get parent object + public String saveGroupSynchronization(@PathVariable String siteSynchronizationId, @ModelAttribute GroupSynchronizationRequest payload, Model model, RedirectAttributes redirectAttributes) throws MicrosoftGenericException { SiteSynchronization ss = microsoftSynchronizationService.getSiteSynchronization(SiteSynchronization.builder().id(siteSynchronizationId).build()); - if(ss != null) { - if(StringUtils.isNotBlank(payload.getSelectedGroupId()) && StringUtils.isNotBlank(payload.getSelectedChannelId())) { + if (ss != null) { + Map channelsMap = microsoftCommonService.getTeamPrivateChannels(ss.getTeamId()); + model.addAttribute("channelsMap", channelsMap); + + if (StringUtils.isNotBlank(payload.getSelectedGroupId()) && StringUtils.isNotBlank(payload.getSelectedChannelId())) { String groupId = payload.getSelectedGroupId(); String channelId = payload.getSelectedChannelId(); - - //TODO: do the same to create a site??? - if(channelId.equals(NEW) && createdChannelId == null) { - //create new channel - if(StringUtils.isBlank(payload.getNewChannelName())) { - redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.new_channel_empty")); - - return REDIRECT_EDIT_GROUP_SYNCH + "/" + siteSynchronizationId; - } - createdChannelId = microsoftCommonService.createChannel(ss.getTeamId(), payload.getNewChannelName(), microsoftConfigurationService.getCredentials().getEmail()); - - if(createdChannelId == null) { - redirectAttributes.addFlashAttribute("exception_error", MessageFormat.format(rb.getString("error.creating_channel"), payload.getNewChannelName())); - - return REDIRECT_EDIT_GROUP_SYNCH + "/" + siteSynchronizationId; - } - } - + GroupSynchronization gs = GroupSynchronization.builder() - .siteSynchronization(ss) - .groupId(groupId) - .channelId(channelId.equals(NEW) ? createdChannelId : channelId) - .build(); - + .siteSynchronization(ss) + .groupId(groupId) + .channelId(channelId) + .build(); + GroupSynchronization aux_gs = microsoftSynchronizationService.getGroupSynchronization(gs); - if(aux_gs != null) { + if (aux_gs != null) { redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.group_synchronization_already_exists")); - + return REDIRECT_EDIT_GROUP_SYNCH + "/" + siteSynchronizationId; } - + //check if parent is forced and selected channel is duplicated //TODO: at this point, if parent is not forcing, we will allow this relationship. But we don't check if the parent starts forcing after that - if(ss.isForced() && microsoftSynchronizationService.countGroupSynchronizationsByChannelId(gs.getChannelId()) > 0) { + if (ss.isForced() && microsoftSynchronizationService.countGroupSynchronizationsByChannelId(gs.getChannelId()) > 0) { redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.group_synchronization_already_forced")); - + return REDIRECT_EDIT_GROUP_SYNCH + "/" + siteSynchronizationId; } - - log.debug("saving: groupId={}, channelId={}", groupId, channelId.equals(NEW) ? createdChannelId : channelId); + + log.debug("saving: groupId={}, channelId={}", groupId, channelId); microsoftSynchronizationService.saveOrUpdateGroupSynchronization(gs); } } else { - redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.site_synchronization_not_found")); + redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.channel_number_more_than_30")); } - + return REDIRECT_EDIT_GROUP_SYNCH + "/" + siteSynchronizationId; } - - + + @GetMapping(path = {"/delete-groupSynchronization/{groupSynchronizationId}"}, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public Boolean deleteGroupSynchronization(@PathVariable String groupSynchronizationId, Model model, RedirectAttributes redirectAttributes) throws MicrosoftGenericException { boolean ok = false; GroupSynchronization gs = microsoftSynchronizationService.getGroupSynchronization(GroupSynchronization.builder().id(groupSynchronizationId).build()); - if(gs != null) { + if (gs != null) { ok = microsoftSynchronizationService.deleteGroupSynchronization(groupSynchronizationId); } - + return ok; } + + @PostMapping(value = {"/channel"}) + public String createNewChannel(@RequestParam String siteId, @RequestParam String name, RedirectAttributes redirectAttributes) throws MicrosoftGenericException { + log.debug("NEW channel creating"); + SiteSynchronization ss = microsoftSynchronizationService.getSiteSynchronization(SiteSynchronization.builder().id(siteId).build()); + try { + Map channelsMap = microsoftCommonService.getTeamPrivateChannels(ss.getTeamId()); + Collection channels = channelsMap.values(); + boolean channelExists = channels.stream() + .anyMatch(channel -> channel.getName().equalsIgnoreCase(name)); + if (!channelExists) { + if (channels.size() < MAX_CHANNELS) { + microsoftCommonService.createChannel(ss.getTeamId(), name, microsoftConfigurationService.getCredentials().getEmail()); + } else { + redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.channel_number_more_than_30")); + } + } else { + redirectAttributes.addFlashAttribute("exception_error", rb.getString("error.new_channel_with_same_name")); + } + } catch (NullPointerException e) { + log.error("MicrosoftCredentialsException in confirm thread"); + } + return REDIRECT_EDIT_GROUP_SYNCH + "/" + siteId; + } } 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 1208ce6f6c54..6c0cf23db3fe 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 @@ -15,28 +15,25 @@ */ package org.sakaiproject.microsoft.controller; -import java.time.LocalDate; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; - import org.sakaiproject.microsoft.api.MicrosoftCommonService; import org.sakaiproject.microsoft.api.MicrosoftConfigurationService; import org.sakaiproject.microsoft.api.MicrosoftSynchronizationService; import org.sakaiproject.microsoft.api.SakaiProxy; +import org.sakaiproject.microsoft.api.data.MicrosoftLogInvokers; import org.sakaiproject.microsoft.api.data.MicrosoftTeam; +import org.sakaiproject.microsoft.api.data.SakaiSiteFilter; +import org.sakaiproject.microsoft.api.data.SynchronizationStatus; import org.sakaiproject.microsoft.api.exceptions.MicrosoftCredentialsException; import org.sakaiproject.microsoft.api.exceptions.MicrosoftGenericException; import org.sakaiproject.microsoft.api.model.SiteSynchronization; import org.sakaiproject.microsoft.controller.auxiliar.AjaxResponse; +import org.sakaiproject.microsoft.controller.auxiliar.FilterRequest; import org.sakaiproject.microsoft.controller.auxiliar.MainSessionBean; import org.sakaiproject.site.api.Group; +import org.sakaiproject.tool.api.Session; import org.sakaiproject.util.ResourceLoader; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.MediaType; @@ -49,7 +46,17 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import lombok.extern.slf4j.Slf4j; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; /** @@ -87,7 +94,9 @@ public class MainController { @GetMapping(value = {"/", "/index"}) public String index(Model model) { - + Session session = sakaiProxy.getCurrentSession(); + session.setAttribute("origin", MicrosoftLogInvokers.MANUAL.getCode()); + return INDEX_TEMPLATE; } @@ -99,49 +108,72 @@ public String loadItems( @RequestParam(required = false) Integer pageNum, @RequestParam(required = false) Integer pageSize, @RequestParam(required = false) String search, + FilterRequest requestBody, Model model - ) throws MicrosoftGenericException { - if(sortBy == null) { + ) throws MicrosoftGenericException { + if (sortBy == null) { sortBy = mainSessionBean.getSortBy(); } - if(sortOrder == null) { + if (sortOrder == null) { sortOrder = mainSessionBean.getSortOrder(); } - if(pageNum == null) { + if (pageNum == null) { pageNum = mainSessionBean.getPageNum(); } - if(pageSize == null) { + if (pageSize == null) { pageSize = mainSessionBean.getPageSize(); } - if(search == null) { + if (search == null) { search = mainSessionBean.getSearch(); } + if (requestBody.getFromDate() == null || requestBody.getToDate() == null) { + requestBody = new FilterRequest(); + requestBody.setSiteProperty(mainSessionBean.getSiteProperty()); + requestBody.setFromDate(mainSessionBean.getFromDate()); + requestBody.setToDate(mainSessionBean.getToDate()); + } + mainSessionBean.setSortBy(sortBy); mainSessionBean.setSortOrder(sortOrder); mainSessionBean.setPageNum(pageNum); mainSessionBean.setPageSize(pageSize); mainSessionBean.setSearch(search); - - List list = microsoftSynchronizationService.getAllSiteSynchronizations(true); - Map map = microsoftCommonService.getTeams(); - + mainSessionBean.setFromDate(requestBody.getFromDate()); + mainSessionBean.setToDate(requestBody.getToDate()); + mainSessionBean.setSiteProperty(requestBody.getSiteProperty()); + + List list; + Map map; + ZonedDateTime fromDate = null; + ZonedDateTime toDate = null; + boolean filterByDate = !requestBody.getFromDate().isEmpty() && !requestBody.getToDate().isEmpty(); + + if (filterByDate) { + fromDate = LocalDate.parse(requestBody.getFromDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay(ZoneOffset.UTC); + toDate = LocalDate.parse(requestBody.getToDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay(ZoneOffset.UTC); + } + + list = microsoftSynchronizationService.getFilteredSiteSynchronizations(true, SakaiSiteFilter.builder().siteProperty(requestBody.getSiteProperty()).build(), fromDate, toDate); + + map = microsoftCommonService.retrieveCacheTeams(); + //filter elements - if(StringUtils.isNotBlank(search)) { + if (StringUtils.isNotBlank(search)) { String lcSearch = search.toLowerCase(); list = list.stream() .filter(ss -> ss.getSiteId().contains(lcSearch) || - ss.getTeamId().contains(lcSearch) || - ss.getSite().getTitle().toLowerCase().contains(lcSearch) || - map.get(ss.getTeamId()).getName().toLowerCase().contains(lcSearch)) + ss.getTeamId().contains(lcSearch) || + ss.getSite().getTitle().toLowerCase().contains(lcSearch) || + (Objects.nonNull(map.get(ss.getTeamId())) && map.get(ss.getTeamId()).getName().toLowerCase().contains(lcSearch))) .collect(Collectors.toList()); } //sort elements - if(StringUtils.isNotBlank(sortBy)) { + if (StringUtils.isNotBlank(sortBy)) { String finalSortBy = sortBy; String finalSortOrder = sortOrder; Collections.sort(list, (i1, i2) -> { - if("DESC".equals(finalSortOrder)) { + if ("DESC".equals(finalSortOrder)) { SiteSynchronization aux = i1; i1 = i2; i2 = aux; @@ -152,7 +184,9 @@ public String loadItems( case "teamId": return i1.getTeamId().compareTo(i2.getTeamId()); case "teamTitle": - return map.get(i1.getTeamId()).getName().compareToIgnoreCase(map.get(i2.getTeamId()).getName()); + String fromString = Objects.isNull(map.get(i1.getTeamId())) ? "_null" : map.get(i1.getTeamId()).getName(); + String toString = Objects.isNull(map.get(i2.getTeamId())) ? "_null" : map.get(i2.getTeamId()).getName(); + return fromString.compareToIgnoreCase(toString); case "siteTitle": return i1.getSite().getTitle().compareToIgnoreCase(i2.getSite().getTitle()); case "syncDateFrom": @@ -184,25 +218,35 @@ public String loadItems( model.addAttribute("siteSynchronizations", list); model.addAttribute("teamsMap", map); - model.addAttribute("sortBy", sortBy); model.addAttribute("sortOrder", sortOrder); model.addAttribute("pageSize", pageSize); model.addAttribute("pageNum", pageNum); model.addAttribute("maxPage", maxPage); model.addAttribute("search", search); + model.addAttribute("requestBody", requestBody); + model.addAttribute("fromDate", requestBody.getFromDate()); + model.addAttribute("toDate", requestBody.getToDate()); + model.addAttribute("siteProperty", requestBody.getSiteProperty()); + model.addAttribute("filterCount", requestBody.getFilterCount()); + + model.addAttribute("errorMembers", microsoftCommonService.getErrorUsers()); return BODY_TEMPLATE; } - + //called by AJAX @GetMapping(value = {"/listGroupSynchronizations/{siteSynchronizationId}"}) - public String groupSynchronizations(@PathVariable String siteSynchronizationId, Model model) throws MicrosoftGenericException { + public String groupSynchronizations(@PathVariable String siteSynchronizationId, Model model) throws MicrosoftGenericException { log.debug("List group synchronizations for siteSynchronizationId={}", siteSynchronizationId); SiteSynchronization ss = microsoftSynchronizationService.getSiteSynchronization(SiteSynchronization.builder().id(siteSynchronizationId).build(), true); - if(ss != null) { + if (ss != null) { model.addAttribute("groupsMap", ss.getSite().getGroups().stream().collect(Collectors.toMap(Group::getId, Function.identity()))); model.addAttribute("channelsMap", microsoftCommonService.getTeamPrivateChannels(ss.getTeamId())); + if (ss.getStatus().equals(SynchronizationStatus.ERROR) || ss.getStatus().equals(SynchronizationStatus.PARTIAL_OK)) { + model.addAttribute("errorMembers", microsoftCommonService.getErrorUsers()); + model.addAttribute("errorGroupMembers", microsoftCommonService.getErrorGroupsUsers()); + } model.addAttribute("siteRow", ss); } @@ -234,12 +278,38 @@ public String runSiteSynchronization(@PathVariable String id, Model model) throw if(ss != null) { microsoftSynchronizationService.runSiteSynchronization(ss); + if (ss.getGroupSynchronizationsList().stream().anyMatch(group -> group.getStatus().equals(SynchronizationStatus.OK)) && ss.getStatus().equals(SynchronizationStatus.ERROR)) { + ss.setStatus(SynchronizationStatus.PARTIAL_OK); + microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss); + } model.addAttribute("row", ss); model.addAttribute("teamsMap", microsoftCommonService.getTeams()); + + if (ss.getStatus().equals(SynchronizationStatus.ERROR)) { + model.addAttribute("errorMembers", microsoftCommonService.getErrorUsers()); + model.addAttribute("errorGroupMembers", microsoftCommonService.getErrorGroupsUsers()); + } else if (ss.getStatus().equals(SynchronizationStatus.PARTIAL_OK)) { + model.addAttribute("errorMembers", microsoftCommonService.getErrorUsers()); + } } return ROW_SITE_SYNCH_FRAGMENT; } + //called by AJAX - returns FRAGMENT + @GetMapping(value = {"/refreshSite/{id}"}) + public String refreshRow(@PathVariable String id, Model model) throws Exception { + SiteSynchronization ss = microsoftSynchronizationService.getSiteSynchronization(SiteSynchronization.builder().id(id).build(), true); + + if (ss != null) { + Map teams = model.getAttribute("teamsMap") == null ? new HashMap<>() : (Map) model.getAttribute("teamsMap"); + teams.put(ss.getTeamId(), microsoftCommonService.getTeam(ss.getTeamId(), true)); + model.addAttribute("row", ss); + model.addAttribute("teamsMap", teams); + } + + return ROW_SITE_SYNCH_FRAGMENT; + } + //called by AJAX - returns JSON @GetMapping(path = {"/setForced-siteSynchronization/{id}"}, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody diff --git a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigSessionBean.java b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigSessionBean.java index 472973904cbc..cc54f870eb13 100644 --- a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigSessionBean.java +++ b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigSessionBean.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.sakaiproject.microsoft.api.data.AutoConfigProcessStatus; import org.sakaiproject.site.api.Site; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -32,7 +33,9 @@ public class AutoConfigSessionBean { private int total = 0; private int count = -1; private List errorList = new ArrayList<>(); - + private List statusList = new ArrayList<>(); + private AutoConfigStatus status; + private boolean newChannel = false; @JsonIgnore @@ -45,6 +48,8 @@ public void startRunning(int total) { this.total = total; this.count = 0; this.errorList = new ArrayList<>(); + this.status = AutoConfigStatus.builder().status(AutoConfigProcessStatus.START_RUNNING).build(); + this.statusList = new ArrayList<>(); } public void finishRunning() { @@ -66,4 +71,9 @@ public void addError(String siteId, String siteTitle, String errorMessage) { errorList.add(error); increaseCounter(); } + + public void addStatus(AutoConfigProcessStatus status, String siteTitle) { + this.status = AutoConfigStatus.builder().status(status).siteTitle(siteTitle).build(); + statusList.add(this.status); + } } diff --git a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigStatus.java b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigStatus.java new file mode 100644 index 000000000000..08b8437cb068 --- /dev/null +++ b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigStatus.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024 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.microsoft.controller.auxiliar; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.sakaiproject.microsoft.api.data.AutoConfigProcessStatus; + +@Data +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class AutoConfigStatus { + private AutoConfigProcessStatus status; + private String siteTitle; +} diff --git a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/FilterRequest.java b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/FilterRequest.java new file mode 100644 index 000000000000..6ae519c55fb1 --- /dev/null +++ b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/FilterRequest.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 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.microsoft.controller.auxiliar; + +import lombok.Data; +@Data +public class FilterRequest { + private String siteProperty; + private String fromDate; + private String toDate; + + public int getFilterCount() { + int filterCount = 0; + if (!siteProperty.isEmpty()) { + filterCount++; + } + if (!fromDate.isEmpty() && !toDate.isEmpty()) { + filterCount++; + } + return filterCount; + } + +} diff --git a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/MainSessionBean.java b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/MainSessionBean.java index b4485c16b192..bce4c80754f5 100644 --- a/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/MainSessionBean.java +++ b/microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/MainSessionBean.java @@ -17,6 +17,9 @@ import lombok.Data; +import java.time.LocalDate; +import java.util.Calendar; + @Data public class MainSessionBean { private static final Integer DEFAULT_PAGE_SIZE = 50; @@ -26,4 +29,7 @@ public class MainSessionBean { private Integer pageNum = 0; private Integer pageSize = DEFAULT_PAGE_SIZE; private String search; + private String siteProperty = ""; + private String fromDate = ""; + private String toDate = ""; } diff --git a/microsoft-integration/admin-tool/src/main/resources/Messages.properties b/microsoft-integration/admin-tool/src/main/resources/Messages.properties index 362c307d390b..fb8c6721a8cf 100644 --- a/microsoft-integration/admin-tool/src/main/resources/Messages.properties +++ b/microsoft-integration/admin-tool/src/main/resources/Messages.properties @@ -33,6 +33,7 @@ status.ERROR=Error status.PARTIAL_OK=Partial OK status.PARTIAL_KO=Partial KO status.ERROR_GUEST=Error Guest +status.NOT_AVAILABLE=Not available empty_table = No active synchronizations forced=Forced save=Save @@ -43,11 +44,25 @@ delete=Delete back=Back confirm=Confirm refresh=Refresh +none=None start=Start new=NEW +new_channel= New Channel +new_group= New Group found=FOUND sync_start_date=Start date: sync_end_date=End date: +from=From +to=To +filter_by_period=Filter by period: +user_details=ID: {0}\nName: {1} {2}\nEmail: {3} +filters=Filters ({0}) +clean_filters=Clean + +creation.status_started=Process started +creation.status_create=Creating team's channels... +creation.status_bind=Binding existing team's channels... +creation.status_finished=Process finished ​ #Index index_title=Microsoft Admin Tool @@ -118,11 +133,13 @@ site_synch.filter.clear=Clear #New/Edit group synchronization messages group_synch.new_title=New Group Synchronization +group_synch.remove_title= Remove Group Synchronization group_synch.groups_from_site=Groups from: {0} group_synch.channels_from_team=Channels from: {0} group_synch.no_groups=No groups group_synch.no_channels=No private channels group_synch.add_synch=Add synchronization +group_synch.limit_reach=Limit of groups to create reached, please manage it here: #Config messages config_title=Microsoft Config @@ -199,6 +216,7 @@ error.creating_team=Error creating new Team. error.creating_team_param=Error creating new Team: {0}. error.creating_channel=Error creating new Channel: {0}. 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.deleting_site_synchronizations=Error removing one or more selected synchronizations @@ -206,3 +224,8 @@ error.cleaning_team=Error cleaning one or more selected synchronizations error.set_dates=Error setting dates error.dates=Invalid dates provided error.dates_order=Synchronization start date cannot be later than the end date +error.channel_number_more_than_30=You cannot add another channel because the team already has more than 30 channels. +error.general_failure=General failure +error.channel_number_limit_reached=Channel limitation has been reached +error.user_synchronization=The following users could not be found in Microsoft: +error.users_not_found=No users were found for this group, or Microsoft has not refreshed the data yet. Rerunning synchronization may resolve this issue. diff --git a/microsoft-integration/admin-tool/src/main/resources/Messages_ca.properties b/microsoft-integration/admin-tool/src/main/resources/Messages_ca.properties index 38925072e06e..06a06a46a8c5 100644 --- a/microsoft-integration/admin-tool/src/main/resources/Messages_ca.properties +++ b/microsoft-integration/admin-tool/src/main/resources/Messages_ca.properties @@ -1,12 +1,12 @@ # Common messages -common_danger= Necessites permisos d\u0027administrador per utilitzar aquesta eina, per favor contacte amb el seu Administrador +common_danger= Necessites permisos d\u2019administrador per utilitzar aquesta eina, per favor contacte amb el seu Administrador # Menu messages menu_main = Administrador menu_new = Nou -menu_new.title= Crear una nova sincronitzaci\u00F3 de l\u0027espai +menu_new.title= Crear una nova sincronitzaci\u00F3 de l\u2019espai menu_edit = Editar -menu_edit.title = Editar la sincronitzaci\u00F3 de l\u0027espai +menu_edit.title = Editar la sincronitzaci\u00F3 de l\u2019espai menu_config = Configuraci\u00F3 menu_config.title = Editar la configuraci\u00F3 de Microsoft menu_credentials = Credencials @@ -28,11 +28,12 @@ channel.id= Canal Id: {0} status= Estat status.OK=OK status.KO=KO -status.NONE= Res +status.NONE= Ning\u00FAn status.ERROR= Error status.PARTIAL_OK= Parcial OK status.PARTIAL_KO= Parcial KO status.ERROR_GUEST= Error de convidat +status.NOT_AVAILABLE=No disponible empty_table = Sincronitzaci\u00F3 no activada forced= For\u00E7at save= Guardar @@ -41,16 +42,30 @@ close= Cerrar search= Buscar delete= Eliminar back= Tornar +none=Ning\u00FAn confirm= Confirmar refresh= Recargar start= Comen\u00E7ar new= Nou +new_channel= Nou Canal +new_group= Nou Grup found= Trobar sync_start_date = Data inici: sync_end_date = Data fi: +from=Des de +to=Fins a +filter_by_period=Filtrar per per\u00EDode: +user_details=ID: {0}\nNom: {1} {2}\nEmail: {3} +filters=Filtres ({0}) +clean_filters=Netejar + +creation.status_started=Proc\u00E9s inicialitzat +creation.status_create=Creant canals dels teams... +creation.status_bind=Vinculant canals existents dels teams... +creation.status_finished=Proc\u00E9s finalitzat ​ #Index -index_title= Eina d\u0027Administrador de Microsoft +index_title= Eina d\u2019Administrador de Microsoft index.status_updated_at= Estat actualitzat al: {0} index.status_not_date= Estat no actualitzat index.run_updated_at= \u00DAltima execuci\u00F3: {0} @@ -77,9 +92,9 @@ index.end_date = Data fi #Auto-Config autoconfig_title= Auto-Configuraci\u00F3 autoconfig.count_sites= N\u00BA (filtered) espai disponibles -autoconfig.count_teams= N\u00BA disponible d\u0027equips -autoconfig.count_link= N\u00BA d\u0027espais i equips vinculats -autoconfig.count_new= N\u00BA d\u0027equips nous +autoconfig.count_teams= N\u00BA disponible d\u2019equips +autoconfig.count_link= N\u00BA d\u2019espais i equips vinculats +autoconfig.count_new= N\u00BA d\u2019equips nous autoconfig.confirm_title= Titol autoconfig.confirm_status= Estat autoconfig.confirm_confirm= Confirmar @@ -99,7 +114,7 @@ filter.site_property= Propietats del espai: filter.site_property_info= Valors acceptats:
  • SITE-PROPERTY-NAME
  • SITE-PROPERTY-NAME=VALUE
filter.new_team= Crear un nou Teams filter.new_channel= Crear un nou canal -filter.team_pattern= Patr\u00F3 del nom de l\u0027equip: +filter.team_pattern= Patr\u00F3 del nom de l\u2019equip: #New/Edit site synchronization messages @@ -118,27 +133,29 @@ site_synch.filter.clear= Netejar #New/Edit group synchronization messages group_synch.new_title= Nova sincronitzaci\u00F3 de grup +group_synch.remove_title= Eliminar sincronitzaci\u00F3 de grup group_synch.groups_from_site= Grups de: {0} group_synch.channels_from_team= Canals de: {0} group_synch.no_groups= No hi ha grups group_synch.no_channels=No hi ha canals privats group_synch.add_synch= Afegir sincronitzaci\u00F3 +group_synch.limit_reach=L\u00EDmit de grups a crear alcan\u00E7at, per favor gestiona-ho ac\u00ED: #Config messages config_title= Configuraci\u00F3 de Microsoft config.synchronization_config=Configuraci\u00F3 de sincronitzaci\u00F3 config.SYNCH\:CREATE_TEAM=Crear un nou Teams quan es crea un espai a Sakai -config.SYNCH\:DELETE_SYNCH = Elimina la sincronitzaci\u00F3 Espai-Teams quan s\u0027elimina l\u0027espai de Sakai -config.SYNCH\:DELETE_TEAM=Elimina el Teams quan s\u0027elimina l\u0027espai de Sakai (requereix una sincronitzaci\u00F3 espai-Teams v\u00E0lida) -config.SYNCH\:ADD_USER_TO_TEAM=Quan l\u0027usuari s\u0027agrega a un espai, afegiu-lo als Teams relacionats (requereix una sincronitzaci\u00F3 espai-Teams v\u00E0lida) -config.SYNCH\:REMOVE_USER_FROM_TEAM=Quan l\u0027usuari s\u0027elimina de l\u0027espai, suprimiu-lo dels Teams relacionats(requereix una sincronitzaci\u00F3 espai-Teams v\u00E0lida i for\u00E7ada) +config.SYNCH\:DELETE_SYNCH = Elimina la sincronitzaci\u00F3 Espai-Teams quan s\u2019elimina l\u2019espai de Sakai +config.SYNCH\:DELETE_TEAM=Elimina el Teams quan s\u2019elimina l\u2019espai de Sakai (requereix una sincronitzaci\u00F3 espai-Teams v\u00E0lida) +config.SYNCH\:ADD_USER_TO_TEAM=Quan l\u2019usuari s\u2019agrega a un espai, afegiu-lo als Teams relacionats (requereix una sincronitzaci\u00F3 espai-Teams v\u00E0lida) +config.SYNCH\:REMOVE_USER_FROM_TEAM=Quan l\u2019usuari s\u2019elimina de l\u2019espai, suprimiu-lo dels Teams relacionats(requereix una sincronitzaci\u00F3 espai-Teams v\u00E0lida i for\u00E7ada) config.SYNCH\:CREATE_CHANNEL=Crear un nou canal quan es crea un grup a Sakai (requereix una sincronitzaci\u00F3 espai-Teams v\u00E0lida) -config.SYNCH\:DELETE_CHANNEL=Eliminar canal quan s\u0027elimine un grup a Sakai (requereix una sincronitzaci\u00F3 grup-canal v\u00E0lida) -config.SYNCH\:ADD_USER_TO_CHANNEL=Quan s\u0027agrega un usuari a un grup, s\u0027agrega als canals relacionats (requereix una sincronitzaci\u00F3 grup-canal v\u00E0lida) -config.SYNCH\:REMOVE_USER_FROM_CHANNEL=Quan s\u0027elimine un usuari d\u0027un grup, eliminar-lo dels canals relacionats (requereix una sincronitzaci\u00F3 grup-canal v\u00E0lida i for\u00E7ada) -config.SYNCH\:REMOVE_USERS_WHEN_UNPUBLISH = Quan es despublica un espai, s\u0027eliminaran tots els usuaris dels Team(s) i canal(s) relacionats (requereix una sincronitzaci\u00F3 Espai-Teams v\u00E0lida) -config.SYNCH\:CREATE_INVITATION=Si l\u0027usuari de Microsoft no existeis, crea una nova invitaci\u00F3 +config.SYNCH\:DELETE_CHANNEL=Eliminar canal quan s\u2019elimine un grup a Sakai (requereix una sincronitzaci\u00F3 grup-canal v\u00E0lida) +config.SYNCH\:ADD_USER_TO_CHANNEL=Quan s\u2019agrega un usuari a un grup, s\u2019agrega als canals relacionats (requereix una sincronitzaci\u00F3 grup-canal v\u00E0lida) +config.SYNCH\:REMOVE_USER_FROM_CHANNEL=Quan s\u2019elimine un usuari d\u2019un grup, eliminar-lo dels canals relacionats (requereix una sincronitzaci\u00F3 grup-canal v\u00E0lida i for\u00E7ada) +config.SYNCH\:REMOVE_USERS_WHEN_UNPUBLISH = Quan es despublica un espai, s\u2019eliminaran tots els usuaris dels Team(s) i canal(s) relacionats (requereix una sincronitzaci\u00F3 Espai-Teams v\u00E0lida) +config.SYNCH\:CREATE_INVITATION=Si l\u2019usuari de Microsoft no existeis, crea una nova invitaci\u00F3 config.sync_duration=Duraci\u00F3 sincro (mesos): @@ -150,26 +167,26 @@ config.onedrive_enabled=OneDrive habilitat config.collaborative_documents_config=Configuraci\u00F3 dels Documents Col\u00B7laboratius config.max_upload_size=Grand\u00E0ria m\u00E0xima de pujada (MB) -config.id_mapping= Mapeig d\u0027Id -config.sakai_user_id=Id d\u0027usuari de Sakai -config.microsoft_user_id=Id d\u0027usuari de Microsoft -config.map_user_id=Id d\u0027usuari -config.map_user_property=Propietat de l\u0027usuari +config.id_mapping= Mapeig d\u2019Id +config.sakai_user_id=Id d\u2019usuari de Sakai +config.microsoft_user_id=Id d\u2019usuari de Microsoft +config.map_user_id=Id d\u2019usuari +config.map_user_property=Propietat de l\u2019usuari config.map_user_eid=Usuari Eid config.map_user_email=Correu electr\u00F2nic #Credentials messages credentials_title=Credencials de Microsoft credentials.test=Provar credencials -credentials.ok=Credentials d\u0027OK +credentials.ok=Credentials d\u2019OK credentials.authority=Autoritat credentials.clientId=Id de client credentials.secret=Secret credentials.scope=Scope credentials.delegated_scope = Scope Delegat credentials.scope_alt=Per defecte: {0} -credentials.email=Correu electr\u00F2nic d\u0027administrador de Microsoft -credentials.email_info=S\u0027utilitza en accions que requereixen un usuari. Per exemple: creeu un Teams nou (aquest usuari s\u0027inclour\u00E0 com a propietari inicial). +credentials.email=Correu electr\u00F2nic d\u2019administrador de Microsoft +credentials.email_info=S\u2019utilitza en accions que requereixen un usuari. Per exemple: creeu un Teams nou (aquest usuari s\u2019inclour\u00E0 com a propietari inicial). #Errors - frontend validation error.microsoft_teams=Selecciona almenys un equip de Microsoft @@ -178,31 +195,37 @@ error.groups=Selecciona almenys un grup error.channel=Selecciona almenys un canal #Errors - from exceptions -error.no_credentials_provided=No s\u0027han proporcionat credencials -error.invalid_credentials_provided=S\u0027ha proporcionat credencials inv\u00E0lides +error.no_credentials_provided=No s\u2019han proporcionat credencials +error.invalid_credentials_provided=S\u2019ha proporcionat credencials inv\u00E0lides error.invalid_token_provided=Token no v\u00E0lid -error.invalid_invitation=S\u0027ha enviat una invitaci\u00F3 inv\u00E0lida +error.invalid_invitation=S\u2019ha enviat una invitaci\u00F3 inv\u00E0lida error.invalid_team=Equip inv\u00E0lid -error.invalid_email=S\u0027ha proporcionat un correu electr\u00F2nic de Microsoft inv\u00E0lid +error.invalid_email=S\u2019ha proporcionat un correu electr\u00F2nic de Microsoft inv\u00E0lid error.no_admin=No sou administrador #Errors - backend -error.no_site_selected=No s\u0027ha seleccionat un espai -error.no_team_selected=No s\u0027ha seleccionat un equip -error.site_synchronization_not_found=No s\u0027ha trobat la sincronitzaci\u00F3 de l\u0027espai -error.site_synchronization_already_exists=Ja existeix una sincronitzaci\u00F3 amb el espai i l\u0027equip seleccionats. -error.site_synchronization_already_forced=Una sincronitzaci\u00F3 est\u00E0 for\u00E7ant l\u0027equip seleccionat. -error.site_synchronization_impossible_forced=Ja hi ha una sincronitzaci\u00F3 amb l\u0027equip seleccionat. Impossible for\u00E7ar l\u0027espai i l\u0027equip seleccionats. +error.no_site_selected=No s\u2019ha seleccionat un espai +error.no_team_selected=No s\u2019ha seleccionat un equip +error.site_synchronization_not_found=No s\u2019ha trobat la sincronitzaci\u00F3 de l\u2019espai +error.site_synchronization_already_exists=Ja existeix una sincronitzaci\u00F3 amb el espai i l\u2019equip seleccionats. +error.site_synchronization_already_forced=Una sincronitzaci\u00F3 est\u00E0 for\u00E7ant l\u2019equip seleccionat. +error.site_synchronization_impossible_forced=Ja hi ha una sincronitzaci\u00F3 amb l\u2019equip seleccionat. Impossible for\u00E7ar l\u2019espai i l\u2019equip seleccionats. error.group_synchronization_already_exists=La sincronitzaci\u00F3 del grup ja existeix amb el grup i el canal seleccionats. error.group_synchronization_already_forced= La sincronitzaci\u00F3 dels pares \u00E9s for\u00E7ada. No es pot enlla\u00E7ar el canal seleccionat. -error.creating_team= S\u0027ha produit un error creant l\u0027equip nou. -error.creating_team_param=S\u0027ha produit un error creant l\u0027equip nou: {0}. -error.creating_channel=s\u0027ha produit un error creant el nou canal: {0}. +error.creating_team= S\u2019ha produit un error creant l\u2019equip nou. +error.creating_team_param=S\u2019ha produit un error creant l\u2019equip nou: {0}. +error.creating_channel=s\u2019ha produit un error creant el nou canal: {0}. error.new_channel_empty=El nom del canal nou no pot estar buit -error.delete_group_synchronization=S\u0027ha produ\u00EFt un error en eliminar la sincronitzaci\u00F3 del grup seleccionat -error.set_forced_synchronization=Configuraci\u00F3 d\u0027error for\u00E7ada -error.deleting_site_synchronizations=S\u0027ha produ\u00EFt un error en eliminar una o m\u00E9s sincronitzacions seleccionades -error.cleaning_team=S\u0027ha produ\u00EFt un error en netejar una o m\u00E9s sincronitzacions seleccionades +error.delete_group_synchronization=S\u2019ha produ\u00EFt un error en eliminar la sincronitzaci\u00F3 del grup seleccionat +error.set_forced_synchronization=Configuraci\u00F3 d\u2019error for\u00E7ada +error.deleting_site_synchronizations=S\u2019ha produ\u00EFt un error en eliminar una o m\u00E9s sincronitzacions seleccionades +error.cleaning_team=S\u2019ha produ\u00EFt un error en netejar una o m\u00E9s sincronitzacions seleccionades error.set_dates = Error en establir les dates error.dates = Dates inv\u00E0lides -error.dates_order = La data d\u0027inici no pot ser posterior a la data de finalitzaci\u00F3 +error.dates_order = La data d\u2019inici no pot ser posterior a la data de finalitzaci\u00F3 +error.new_channel_with_same_name= = El nom del nou canal no pot tindr\u00E9 el mateix nom que un dels canals ja creats. +error.channel_number_more_than_30=No pots afegir altre canal perqu\u00E8 el equip ja t\u00E9 m\u00E9s de 30 canals. +error.general_failure=Error general +error.user_synchronization=No s\u2019han pogut trobar els seg\u00FCents usuaris a Microsoft: +error.channel_number_limit_reached=S\u2019ha aplegat al l\u00EDmit de canals +error.users_not_found=No s\u2019han trobat usuaris per a aquest grup o Microsoft no ha actualitzat les dades encara. Si torna a executar la sincronitzaci\u00F3, \u00E9s possible que es resolgui el problema. 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 012835f437b8..b014e1fd0d18 100644 --- a/microsoft-integration/admin-tool/src/main/resources/Messages_es.properties +++ b/microsoft-integration/admin-tool/src/main/resources/Messages_es.properties @@ -33,6 +33,7 @@ status.ERROR = Error status.PARTIAL_OK = Parcialmente OK status.PARTIAL_KO = Parcialmente KO status.ERROR_GUEST = Error de invitado +status.NOT_AVAILABLE=No disponible empty_table = No existen sincronizaciones activas forced = Forzado save = Guardar @@ -41,13 +42,27 @@ close = Cerrar search = Buscar delete = Borrar back = Atr\u00E1s +none=Ninguno confirm = Confirmar refresh = Actualizar start = Iniciar new = Nuevo +new_channel= Nuevo Canal +new_group= Nuevo Grupo found = Encontrado sync_start_date = Fecha inicio: sync_end_date = Fecha fin: +from=Desde +to=Hasta +filter_by_period=Filtrar por periodo: +user_details=ID: {0}\nNombre: {1} {2}\nEmail: {3} +filters=Filtros ({0}) +clean_filters=Limpiar + +creation.status_started=Proceso iniciado +creation.status_create=Creando canales de los teams... +creation.status_bind=Vinculando canales existentes de los teams... +creation.status_finished=Proceso finalizado ​ #Ãndice index_title = Herramienta de administraci\u00F3n de Microsoft @@ -118,11 +133,14 @@ site_synch.filter.clear = Borrar #Nuevo/Editar mensajes de sincronización de grupo group_synch.new_title = Sincronizaci\u00F3n de nuevo Grupo +group_synch.remove_title= Eliminar sincronizaci\u00F3n de Grupo group_synch.groups_from_site = Grupos de: {0} group_synch.channels_from_team = Canales de: {0} group_synch.no_groups = No hay grupos group_synch.no_channels = No hay canales privados group_synch.add_synch = A\u00F1adir sincronizaci\u00F3n +group_synch.limit_reach=L\u00EDmite de grupos a crear alcanzado, por favor gestionalo aqu\u00ED: + #Mensajes configurados config_title = Configuraci\u00F3n de Microsoft @@ -199,6 +217,7 @@ error.creating_team = Error al crear un nuevo Teams. error.creating_team_param = Error al crear un nuevo Teams: {0}. error.creating_channel = Error creando un canal nuevo: {0}. 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.deleting_site_synchronizations = Error al eliminar una o varias sincronizaciones seleccionadas @@ -206,3 +225,8 @@ error.cleaning_team = Error al limpiar una o m\u00E1s sincronizaciones seleccion error.set_dates = Error al establecer las fechas error.dates = Fechas inv\u00E1lidas error.dates_order = La fecha de inicio no puede ser posterior a la fecha de finalizaci\u00F3n +error.channel_number_more_than_30=No puedes agregar otro canal porque el equipo ya tiene más de 30 canales. +error.general_failure=Error general +error.channel_number_limit_reached=L\u00EDmite de grupos alcanzado +error.user_synchronization=No se han podido encontrar los siguientes usuarios en Microsoft: +error.users_not_found=No se han encontrado usuarios para este grupo o Microsoft a\u00fan no ha actualizado los datos. Si vuelve a ejecutar la sincronizaci\u00F3n, es posible que se resuelva el problema. diff --git a/microsoft-integration/admin-tool/src/main/resources/Messages_eu.properties b/microsoft-integration/admin-tool/src/main/resources/Messages_eu.properties index 062237eb50d4..e2fb9ff3332e 100644 --- a/microsoft-integration/admin-tool/src/main/resources/Messages_eu.properties +++ b/microsoft-integration/admin-tool/src/main/resources/Messages_eu.properties @@ -33,6 +33,7 @@ status.ERROR=Error status.PARTIAL_OK=Partial OK status.PARTIAL_KO=Partial KO status.ERROR_GUEST=Error Guest +status.NOT_AVAILABLE=Not available empty_table = No active synchronizations forced=Forced save=Save @@ -43,11 +44,25 @@ delete=Delete back=Back confirm=Confirm refresh=Refresh +none=None start=Start new=NEW +new_channel= New Channel +new_group= New Group found=FOUND sync_start_date=Start date: sync_end_date=End date: +from=From +to=To +filter_by_period=Filter by period: +user_details=ID: {0}\nName: {1} {2}\nEmail: {3} +filters=Filters ({0}) +clean_filters=Clean + +creation.status_started=Process started +creation.status_create=Creating team's channels... +creation.status_bind=Binding existing team's channels... +creation.status_finished=Process finished ​ #Index index_title=Microsoft Admin Tool @@ -118,11 +133,14 @@ site_synch.filter.clear=Clear #New/Edit group synchronization messages group_synch.new_title=New Group Synchronization +group_synch.remove_title= Remove Group Synchronization group_synch.groups_from_site=Groups from: {0} group_synch.channels_from_team=Channels from: {0} group_synch.no_groups=No groups group_synch.no_channels=No private channels group_synch.add_synch=Add synchronization +group_synch.limit_reach=Limit of groups to create reached, please manage it here: + #Config messages config_title=Microsoft Config @@ -199,7 +217,13 @@ error.creating_team=Error creating new Team. error.creating_team_param=Error creating new Team: {0}. error.creating_channel=Error creating new Channel: {0}. 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.deleting_site_synchronizations=Error removing one or more selected synchronizations error.cleaning_team=Error cleaning one or more selected synchronizations +error.channel_number_more_than_30=Ezin duzu beste kanalik gehitu gailuak dagoeneko 30 kanal baino gehiago dituelako. +error.general_failure=General failure +error.channel_number_limit_reached=Channel limitation has been reached +error.user_synchronization=The following users could not be found in Microsoft: +error.users_not_found=No users were found for this group, or Microsoft has not refreshed the data yet. Rerunning synchronization may resolve this issue. diff --git a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/body.html b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/body.html index 7147d0747050..6a3386c35fd6 100644 --- a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/body.html +++ b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/body.html @@ -1,11 +1,14 @@
-
+
+ + +
@@ -20,7 +23,7 @@
- -
@@ -52,7 +55,7 @@ - +
@@ -114,4 +117,64 @@
-
[[#{empty_table}]]
\ No newline at end of file +
[[#{empty_table}]]
+ + +
+ +
+
diff --git a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/editGroupSynchronization.html b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/editGroupSynchronization.html index b192ddd2a07b..32afbe547e19 100644 --- a/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/editGroupSynchronization.html +++ b/microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/editGroupSynchronization.html @@ -8,14 +8,27 @@
- +
- +
- +
+ +
- +
+ +
@@ -129,6 +133,15 @@