From 2c8918c7b05a3715d000fb6027783f977853bbd0 Mon Sep 17 00:00:00 2001 From: Gjergj Kadriu Date: Fri, 12 Dec 2025 15:10:28 +0200 Subject: [PATCH 1/8] SAK-52090 Add filter by item option to Gradebook --- .../src/main/bundle/gradebookng.properties | 2 + .../main/bundle/gradebookng_fr_FR.properties | 2 + .../gradebookng/tool/pages/GradebookPage.html | 32 ++++ .../gradebookng/tool/pages/GradebookPage.java | 8 +- .../webapp/scripts/gradebook-gbgrade-table.js | 150 ++++++++++++++---- 5 files changed, 163 insertions(+), 31 deletions(-) mode change 100644 => 100755 gradebookng/bundle/src/main/bundle/gradebookng.properties mode change 100644 => 100755 gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties mode change 100644 => 100755 gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html mode change 100644 => 100755 gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.java mode change 100644 => 100755 gradebookng/tool/src/webapp/scripts/gradebook-gbgrade-table.js diff --git a/gradebookng/bundle/src/main/bundle/gradebookng.properties b/gradebookng/bundle/src/main/bundle/gradebookng.properties old mode 100644 new mode 100755 index 07661d4070bb..66856698c706 --- a/gradebookng/bundle/src/main/bundle/gradebookng.properties +++ b/gradebookng/bundle/src/main/bundle/gradebookng.properties @@ -119,6 +119,8 @@ column.header.coursegrade = Course Grade filter.students = Filter students filter.studentsclear = Clear student filter +filter.items = Filter items +filter.itemsclear = Clear item filter filter.groups = Filter by group/section button.addgradeitem = Add Gradebook Item diff --git a/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties b/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties old mode 100644 new mode 100755 index 915945e6682e..760fc59b4e16 --- a/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties +++ b/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties @@ -89,6 +89,8 @@ column.header.coursegrade=Note pour le cours filter.students=Filtrer les \u00e9tudiants filter.studentsclear=Clear student filter +filter.items=Filtrer les items +filter.itemsclear=Effacer le filtre d'items filter.groups=Filtrer par section/groupe button.addgradeitem=Ajouter \u00e9l\u00e9ment du bulletin de notes diff --git a/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html b/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html old mode 100644 new mode 100755 index 3e1bd3397626..fc7ed4c45e6e --- a/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html +++ b/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html @@ -5,6 +5,29 @@ +
+ +
+ +
+
+ + +
diff --git a/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/panels/StudentGradeSummaryGradesPanel.java b/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/panels/StudentGradeSummaryGradesPanel.java old mode 100644 new mode 100755 index 2ced255fdb85..1f12f516b490 --- a/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/panels/StudentGradeSummaryGradesPanel.java +++ b/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/panels/StudentGradeSummaryGradesPanel.java @@ -23,6 +23,10 @@ import java.util.Objects; import java.util.stream.Collectors; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; import org.apache.commons.lang3.StringUtils; import org.apache.wicket.AttributeModifier; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -37,11 +41,16 @@ import org.sakaiproject.gradebookng.business.util.CourseGradeFormatter; import org.sakaiproject.gradebookng.tool.component.GbAjaxLink; import org.sakaiproject.gradebookng.tool.pages.GradebookPage; +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.handler.TextRequestHandler; +import org.apache.wicket.util.string.StringValue; import org.sakaiproject.grading.api.Assignment; import org.sakaiproject.grading.api.CategoryDefinition; import org.sakaiproject.grading.api.CategoryScoreData; import org.sakaiproject.grading.api.CourseGradeTransferBean; import org.sakaiproject.grading.api.GradebookInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sakaiproject.grading.api.GradingConstants; import org.sakaiproject.grading.api.SortType; import org.sakaiproject.grading.api.model.Gradebook; @@ -52,6 +61,7 @@ public class StudentGradeSummaryGradesPanel extends BasePanel { private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger(StudentGradeSummaryGradesPanel.class); Integer configuredCategoryType; @@ -61,6 +71,8 @@ public class StudentGradeSummaryGradesPanel extends BasePanel { boolean categoriesEnabled = false; boolean isAssignmentsDisplayed = false; private boolean courseGradeStatsEnabled; + private AbstractDefaultAjaxBehavior whatIfCalculationBehavior; + private String studentUuid; public StudentGradeSummaryGradesPanel(final String id, final IModel> model) { super(id, model); @@ -70,6 +82,16 @@ public StudentGradeSummaryGradesPanel(final String id, final IModel modelData = (Map) getDefaultModelObject(); @@ -93,6 +115,7 @@ public void onBeforeRender() { // unpack model final Map modelData = (Map) getDefaultModelObject(); final String userId = (String) modelData.get("studentUuid"); + this.studentUuid = userId; final Gradebook gradebook = getGradebook(); String studentCourseGradeComment = this.businessService.getAssignmentGradeComment(getCurrentSiteId(), this.businessService.getCourseGradeId(gradebook.getId()), userId); @@ -232,6 +255,84 @@ public boolean isVisible() { courseGradePanel.add(courseGradeStatsLink); courseGradePanel.addOrReplace(new Label("studentCourseGradeComment", studentCourseGradeComment)); add(new AttributeModifier("data-studentid", userId)); + add(new AttributeModifier("data-whatif-url", this.whatIfCalculationBehavior.getCallbackUrl().toString())); + add(new AttributeModifier("data-whatif-error", getString("whatif.error.generic"))); + + final boolean whatIfEnabled = this.someAssignmentsReleased && courseGrade != null; + add(new AttributeModifier("data-whatif-enabled", String.valueOf(whatIfEnabled))); + } + + /** + * Handle a what-if calculation request from the client. + */ + private void handleWhatIfRequest() { + final RequestCycle requestCycle = RequestCycle.get(); + final StringValue payload = requestCycle.getRequest().getRequestParameters().getParameterValue("whatIf"); + + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode result = mapper.createObjectNode(); + + try { + Map whatIfMap = Collections.emptyMap(); + if (payload != null && !payload.isNull() && StringUtils.isNotBlank(payload.toString())) { + final Map raw = mapper.readValue(payload.toString(), new TypeReference>() {}); + if (raw != null) { + final Map parsed = new HashMap<>(); + for (final Map.Entry entry : raw.entrySet()) { + if (StringUtils.isBlank(entry.getKey())) { + continue; + } + try { + parsed.put(Long.valueOf(entry.getKey()), entry.getValue()); + } catch (NumberFormatException nfe) { + log.warn("Ignoring what-if entry with invalid assignment id {}", entry.getKey()); + } + } + whatIfMap = parsed; + } + } + + final CourseGradeTransferBean preview = this.businessService.calculateWhatIfCourseGrade(currentGradebookUid, currentSiteId, + this.studentUuid, whatIfMap, true); + + final Gradebook gradebook = this.businessService.getGradebook(currentGradebookUid, currentSiteId); + final CourseGradeFormatter formatter = new CourseGradeFormatter( + gradebook, + GbRole.STUDENT, + gradebook.getCourseGradeDisplayed(), + gradebook.getCoursePointsDisplayed(), + true, + this.businessService.getShowCalculatedGrade()); + + if (preview != null) { + result.put("courseGrade", formatter.format(preview)); + if (preview.getMappedGrade() != null) { + result.put("mappedGrade", preview.getMappedGrade()); + } + if (preview.getCalculatedGrade() != null) { + result.put("calculatedGrade", preview.getCalculatedGrade()); + } + if (preview.getPointsEarned() != null) { + result.put("pointsEarned", preview.getPointsEarned()); + } + if (preview.getTotalPointsPossible() != null) { + result.put("totalPointsPossible", preview.getTotalPointsPossible()); + } + } else { + result.put("courseGrade", formatter.format(null)); + } + } catch (IllegalArgumentException e) { + if ("invalidGrade".equals(e.getMessage())) { + result.put("error", getString("whatif.error.invalid")); + } else { + result.put("error", getString("whatif.error.generic")); + } + } catch (Exception e) { + log.error("Error calculating what-if course grade preview", e); + result.put("error", getString("whatif.error.generic")); + } + + requestCycle.scheduleRequestHandlerAfterCurrent(new TextRequestHandler("application/json", "UTF-8", result.toString())); } /** diff --git a/gradebookng/tool/src/webapp/scripts/gradebook-grade-summary.js b/gradebookng/tool/src/webapp/scripts/gradebook-grade-summary.js old mode 100644 new mode 100755 index 65ee8a438928..c0138a81a660 --- a/gradebookng/tool/src/webapp/scripts/gradebook-grade-summary.js +++ b/gradebookng/tool/src/webapp/scripts/gradebook-grade-summary.js @@ -254,6 +254,7 @@ GradebookGradeSummary.prototype.setupModalPrint = function() { GradebookGradeSummary.prototype.setupStudentView = function() { var self = this; self.setupTableSorting(); + self.setupWhatIfCalculator(); var $button = $("body").find(".portletBody .gb-summary-print"); $button.off("click").on("click", function() { @@ -265,6 +266,127 @@ GradebookGradeSummary.prototype.setupStudentView = function() { }; +GradebookGradeSummary.prototype.setupWhatIfCalculator = function() { + const $toggle = $("body").find(".portletBody .gb-whatif-toggle"); + const callbackUrl = this.$content.attr("data-whatif-url"); + const enabled = this.$content.attr("data-whatif-enabled") === "true"; + + if (!$toggle.length || !callbackUrl || !enabled) { + $toggle.hide(); + return; + } + + $toggle.attr("aria-pressed", "false").removeClass("active"); + + const $help = this.$content.find(".gb-whatif-note"); + const $result = this.$content.find(".gb-whatif-result"); + const $gradeTarget = this.$content.find(".gb-whatif-grade"); + const $error = this.$content.find(".gb-whatif-error"); + + const showError = message => { + if (!$error.length) { + return; + } + $error.text(message).removeClass("d-none"); + }; + + const clearError = () => { + if (!$error.length) { + return; + } + $error.addClass("d-none").text(""); + }; + + const ensureInputs = () => { + this.$content.find(".gb-summary-grade-row[data-assignment-id]").each(function() { + const $row = $(this); + let $input = $row.find(".gb-whatif-input"); + if ($input.length === 0) { + const starting = $row.data("grade") ? ("" + $row.data("grade")).trim() + : $row.find(".gb-summary-grade-score-raw").text().trim(); + $input = $(''); + if (starting) { + $input.val(starting); + } + $row.find(".gb-summary-grade-score").append($input); + } + }); + }; + + const collectScores = () => { + const scores = {}; + this.$content.find(".gb-whatif-input").each(function() { + const $input = $(this); + const assignmentId = $input.closest(".gb-summary-grade-row").data("assignment-id"); + if (assignmentId !== undefined && assignmentId !== null) { + scores[assignmentId] = $input.val().trim(); + } + }); + return scores; + }; + + let debounceTimer = null; + const requestPreview = () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + const payload = collectScores(); + fetch(callbackUrl, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, + body: "whatIf=" + encodeURIComponent(JSON.stringify(payload)) + }) + .then(response => response.ok ? response.json() : Promise.reject()) + .then(data => { + if (data.error) { + showError(data.error); + $result.addClass("d-none"); + return; + } + clearError(); + if (data.courseGrade) { + $gradeTarget.text(data.courseGrade); + $result.removeClass("d-none"); + } else { + showError(this.$content.attr("data-whatif-error") || "Unable to calculate what-if grade right now."); + $result.addClass("d-none"); + } + }) + .catch(() => { + showError(this.$content.attr("data-whatif-error") || "Unable to calculate what-if grade right now."); + }); + }, 250); + }; + + const activate = () => { + ensureInputs(); + this.$content.find(".gb-whatif-input").removeClass("d-none"); + $help.removeClass("d-none"); + requestPreview(); + }; + + const deactivate = () => { + this.$content.find(".gb-whatif-input").addClass("d-none"); + $help.addClass("d-none"); + $result.addClass("d-none"); + clearError(); + }; + + this.$content.on("input", ".gb-whatif-input", requestPreview); + + $toggle.off("click").on("click", function(event) { + event.preventDefault(); + const active = $(this).attr("aria-pressed") === "true"; + const nextState = !active; + $(this).attr("aria-pressed", nextState); + $(this).toggleClass("active", nextState); + if (nextState) { + activate(); + } else { + deactivate(); + } + }); +}; + GradebookGradeSummary.prototype._print = function(headerHTML, contentHTML) { const printWindow = window.open("", "_blank"); From ee794008a56bcfa217ed3122eecbbf94eb035c3b Mon Sep 17 00:00:00 2001 From: Gjergj Kadriu Date: Sat, 13 Dec 2025 20:11:49 +0200 Subject: [PATCH 3/8] Revert "SAK-52090 Add filter by item option to Gradebook" This reverts commit 2c8918c7b05a3715d000fb6027783f977853bbd0. --- .../src/main/bundle/gradebookng.properties | 2 - .../main/bundle/gradebookng_fr_FR.properties | 2 - .../gradebookng/tool/pages/GradebookPage.html | 32 ---- .../gradebookng/tool/pages/GradebookPage.java | 8 +- .../webapp/scripts/gradebook-gbgrade-table.js | 150 ++++-------------- 5 files changed, 31 insertions(+), 163 deletions(-) mode change 100755 => 100644 gradebookng/bundle/src/main/bundle/gradebookng.properties mode change 100755 => 100644 gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties mode change 100755 => 100644 gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html mode change 100755 => 100644 gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.java mode change 100755 => 100644 gradebookng/tool/src/webapp/scripts/gradebook-gbgrade-table.js diff --git a/gradebookng/bundle/src/main/bundle/gradebookng.properties b/gradebookng/bundle/src/main/bundle/gradebookng.properties old mode 100755 new mode 100644 index c6ac3934e29d..d60bc1ea12a1 --- a/gradebookng/bundle/src/main/bundle/gradebookng.properties +++ b/gradebookng/bundle/src/main/bundle/gradebookng.properties @@ -119,8 +119,6 @@ column.header.coursegrade = Course Grade filter.students = Filter students filter.studentsclear = Clear student filter -filter.items = Filter items -filter.itemsclear = Clear item filter filter.groups = Filter by group/section button.addgradeitem = Add Gradebook Item diff --git a/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties b/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties old mode 100755 new mode 100644 index 6197a1e83611..341cdb08f2ba --- a/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties +++ b/gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties @@ -89,8 +89,6 @@ column.header.coursegrade=Note pour le cours filter.students=Filtrer les \u00e9tudiants filter.studentsclear=Clear student filter -filter.items=Filtrer les items -filter.itemsclear=Effacer le filtre d'items filter.groups=Filtrer par section/groupe button.addgradeitem=Ajouter \u00e9l\u00e9ment du bulletin de notes diff --git a/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html b/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html old mode 100755 new mode 100644 index fc7ed4c45e6e..3e1bd3397626 --- a/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html +++ b/gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html @@ -5,29 +5,6 @@ -