Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions gradebookng/api/src/main/java/org/sakaiproject/grading/api/GradingService.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,19 @@ Map<String, Map<Long, CategoryScoreData>> calculateAllCategoryScoresForStudents(
*/
Map<String, CourseGradeTransferBean> getCourseGradeForStudents(String gradebookUid, String siteId, List<String> userUuids, Map<String, Double> schema);

/**
* Calculate an on-the-fly course grade preview using supplied what-if scores. No data is persisted.
*
* @param gradebookUid the gradebook id
* @param siteId the site id
* @param studentUuid the student to calculate for
* @param whatIfScores assignment id to raw score map (points/percent/letter based on gradebook type)
* @param includeNonReleasedItems whether to include items that are not released
* @return the calculated course grade, or null if the user cannot access it
*/
CourseGradeTransferBean calculateCourseGradePreview(String gradebookUid, String siteId, String studentUuid,
Map<Long, String> whatIfScores, boolean includeNonReleasedItems);

/**
* Get a list of CourseSections that the current user has access to in the given gradebook. This is a combination of sections and groups
* and is permission filtered.
Expand Down
5 changes: 5 additions & 0 deletions gradebookng/bundle/src/main/bundle/gradebookng.properties
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ button.settingsexpandall = Expand All
button.settingscollapseall = Collapse All
button.saveoverride = Save Course Grade Override
button.print = Print
button.whatif.toggle = What-if calculator

heading.addgradeitem = Add Gradebook Item
heading.editgradeitem = Edit Gradebook Item
Expand Down Expand Up @@ -235,6 +236,10 @@ label.studentsummary.next = Next Student
label.studentsummary.coursegradenotreleased = Not Yet Released
label.studentsummary.coursegradenotreleasedflag = Not released to students*
label.studentsummary.coursegradenotreleasedmessage = * To release final course grade to students, go to Settings and select "Display Final Course Grades to Students".
whatif.help = Enter hypothetical scores to estimate your course grade. These values are not saved.
whatif.projected = Projected course grade:
whatif.error.generic = Unable to calculate a what-if grade right now.
whatif.error.invalid = One or more scores could not be read. Please enter valid scores.
label.studentsummary.categoryweight=({0})
label.studentsummary.closeconfirmation.title=You are about to leave Student Review mode.
label.studentsummary.closeconfirmation.content=Unreleased grades and other students' data will become visible.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ button.settingsexpandall=D\u00e9velopper tout
button.settingscollapseall=Replier tout
button.saveoverride=Enregistrez le remplacement de note de cours
button.print=Imprimer
button.whatif.toggle=Calculateur hypoth\u00e9tique

Comment on lines +110 to 111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep property formatting consistent (spaces around =).

This file mostly uses key=value, but the new whatif.* entries use key = value. Consider standardizing to the existing style to reduce diff noise.

Also applies to: 205-208

🤖 Prompt for AI Agents
In gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties around lines
110-111 (and also apply the same change to lines 205-208), the new whatif.*
entries use "key = value" spacing while the rest of the file uses "key=value";
update those lines to remove the spaces around the equal sign so they match the
existing "key=value" format (standardize formatting only, do not change keys or
translated values).

heading.addgradeitem=Ajouter \u00e9l\u00e9ment du bulletin de notes
heading.editgradeitem=Modifier \u00e9l\u00e9ment du bulletin de notes
Expand Down Expand Up @@ -201,6 +202,10 @@ label.studentsummary.previous=\u00c9tudiant pr\u00e9c\u00e9dent
label.studentsummary.next=\u00c9tudiant suivant
label.studentsummary.coursegradenotreleased=Pas encore diffus\u00e9
label.studentsummary.coursegradenotreleasedflag=Pas diffus\u00e9 aux \u00e9tudiants*
whatif.help = Saisissez des notes hypoth\u00e9tiques pour estimer votre note finale. Ces valeurs ne sont pas enregistr\u00e9es.
whatif.projected = Note de cours simul\u00e9e \:
whatif.error.generic = Impossible de calculer la note hypoth\u00e9tique pour le moment.
whatif.error.invalid = Une ou plusieurs notes sont invalides. Merci de saisir des valeurs valides.
label.studentsummary.coursegradenotreleasedmessage=* To release final course grade to students, go to Settings and select "Display Final Course Grades to Students".
label.studentsummary.categoryweight=({0})
label.studentsummary.closeconfirmation.title=Vous \u00eates sur le point de quitter le mode de r\u00e9vision \u00e9tudiant.
Expand Down
134 changes: 134 additions & 0 deletions gradebookng/impl/src/main/java/org/sakaiproject/grading/impl/GradingServiceImpl.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,22 @@ public Double convertStringToDouble(final String doubleAsString) {
return scoreAsDouble;
}

private Double convertWhatIfGrade(final String rawGrade, final GradebookAssignment assignment, final Gradebook gradebook,
final Map<String, Double> sortedGradeMap) {

if (StringUtils.isBlank(rawGrade)) {
return null;
}

if (Objects.equals(GradingConstants.GRADE_TYPE_PERCENTAGE, gradebook.getGradeType())) {
return calculateEquivalentPointValueForPercent(assignment.getPointsPossible(), NumberUtils.createDouble(rawGrade));
} else if (Objects.equals(GradingConstants.GRADE_TYPE_LETTER, gradebook.getGradeType())) {
return sortedGradeMap.get(rawGrade);
}

return convertStringToDouble(rawGrade);
}

/**
* Get a list of assignments in the gradebook attached to the given category. Note that each assignment only knows the category by name.
*
Expand Down Expand Up @@ -3433,6 +3449,124 @@ public Map<String, CourseGradeTransferBean> getCourseGradeForStudents(final Stri
return rval;
}

@Override
public CourseGradeTransferBean calculateCourseGradePreview(final String gradebookUid, final String siteId,
final String studentUuid, final Map<Long, String> whatIfScores, final boolean includeNonReleasedItems) {

if (gradebookUid == null || siteId == null || studentUuid == null) {
throw new IllegalArgumentException("gradebookUid, siteId and studentUuid are required");
}

final String currentUser = sessionManager.getCurrentSessionUserId();
final boolean isSelf = StringUtils.equals(studentUuid, currentUser);
final boolean canView =
currentUserHasEditPerm(siteId)
|| currentUserHasGradingPerm(siteId)
|| (currentUserHasViewOwnGradesPerm(siteId) && isSelf);

if (!canView) {
throw new GradingSecurityException("You do not have permission to preview this course grade");
}

final Gradebook gradebook = getGradebook(gradebookUid);
if (gradebook == null) {
throw new IllegalArgumentException("Invalid gradebook uid");
}

if (!gradebook.getCourseGradeDisplayed() && !(currentUserHasEditPerm(siteId) || currentUserHasGradingPerm(siteId))) {
return null;
}

final CourseGrade courseGrade = getCourseGrade(gradebook.getId());
final Map<String, Double> sortedGradeMap = GradeMappingDefinition
.sortGradeMapping(gradebook.getSelectedGradeMapping().getGradeMap());

final List<AssignmentGradeRecord> existingRecords = getAllAssignmentGradeRecords(gradebook.getId(), Collections.singletonList(studentUuid));
final Map<Long, AssignmentGradeRecord> recordByAssignment = new HashMap<>();
final List<AssignmentGradeRecord> workingRecords = new ArrayList<>();

for (final AssignmentGradeRecord agr : existingRecords) {
final AssignmentGradeRecord copy = agr.clone();
copy.setExcludedFromGrade(agr.getExcludedFromGrade());
copy.setDroppedFromGrade(Boolean.FALSE);
recordByAssignment.put(copy.getAssignment().getId(), copy);
workingRecords.add(copy);
}

if (whatIfScores != null && !whatIfScores.isEmpty()) {
for (final Map.Entry<Long, String> entry : whatIfScores.entrySet()) {
final Long assignmentId = entry.getKey();
if (assignmentId == null) {
continue;
}

final GradebookAssignment assignment = getAssignmentWithoutStatsByID(gradebookUid, assignmentId);
if (assignment == null) {
continue;
}

AssignmentGradeRecord record = recordByAssignment.get(assignmentId);
if (record == null) {
record = new AssignmentGradeRecord(assignment, studentUuid, null);
record.setExcludedFromGrade(Boolean.FALSE);
recordByAssignment.put(assignmentId, record);
workingRecords.add(record);
}

final String rawGrade = StringUtils.trimToEmpty(entry.getValue());
final Double convertedGrade = convertWhatIfGrade(rawGrade, assignment, gradebook, sortedGradeMap);

if (StringUtils.isNotBlank(rawGrade) && convertedGrade == null) {
throw new IllegalArgumentException("invalidGrade");
}

record.setPointsEarned(convertedGrade);
if (StringUtils.isNotBlank(rawGrade)) {
record.setExcludedFromGrade(Boolean.FALSE);
}
record.setDroppedFromGrade(Boolean.FALSE);
record.setGradableObject(assignment);
}
}

if (!includeNonReleasedItems) {
workingRecords.removeIf(rec -> rec.getAssignment() != null && !rec.getAssignment().getReleased());
}

applyDropScores(workingRecords, gradebook.getCategoryType());

final List<Category> categories = getCategories(gradebook.getId());
final List<GradebookAssignment> countedAssignments = getCountedAssignments(gradebook.getId()).stream()
.filter(GradebookAssignment::isIncludedInCalculations)
.collect(Collectors.toList());

final List<Double> earnedTotals = getTotalPointsEarnedInternal(studentUuid, gradebook, categories, workingRecords, countedAssignments);
final double totalPointsEarned = earnedTotals.get(0);
final double literalTotalPointsEarned = earnedTotals.get(1);
final double extraPointsEarned = earnedTotals.get(2);
final double totalPointsPossible = getTotalPointsInternal(gradebook, categories, studentUuid, workingRecords, countedAssignments, false);

final CourseGradeRecord previewRecord = new CourseGradeRecord(courseGrade, studentUuid);
previewRecord.initNonpersistentFields(totalPointsPossible, totalPointsEarned, literalTotalPointsEarned, extraPointsEarned);

final CourseGradeTransferBean cg = new CourseGradeTransferBean();
cg.setId(courseGrade.getId());

Double calculatedGrade = previewRecord.getAutoCalculatedGrade();
if (calculatedGrade != null) {
final BigDecimal rounded = new BigDecimal(calculatedGrade)
.setScale(10, RoundingMode.HALF_UP)
.setScale(2, RoundingMode.HALF_UP);
calculatedGrade = rounded.doubleValue();
cg.setCalculatedGrade(calculatedGrade.toString());
cg.setMappedGrade(GradeMapping.getMappedGrade(sortedGradeMap, calculatedGrade));
}

cg.setPointsEarned(previewRecord.getCalculatedPointsEarned());
cg.setTotalPointsPossible(previewRecord.getTotalPointsPossible());
return cg;
}

@Override
public List<CourseSection> getViewableSections(final String gradebookUid, final String siteId) {

Expand Down
15 changes: 15 additions & 0 deletions ...ookng/tool/src/java/org/sakaiproject/gradebookng/business/GradebookNgBusinessService.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,21 @@ public Long getCourseGradeId(Long gradebookId){
return this.gradingService.getCourseGradeId(gradebookId);
}

/**
* Calculate a course grade preview for the given student with hypothetical scores.
*
* @param gradebookUid the gradebook id
* @param siteId the site id
* @param studentUuid the student
* @param whatIfScores map of assignment id to raw score string
* @param includeNonReleasedItems whether to include items that are not released
* @return preview of the course grade, or null if it cannot be calculated for the user
*/
public CourseGradeTransferBean calculateWhatIfCourseGrade(final String gradebookUid, final String siteId, final String studentUuid,
final Map<Long, String> whatIfScores, final boolean includeNonReleasedItems) {
return this.gradingService.calculateCourseGradePreview(gradebookUid, siteId, studentUuid, whatIfScores, includeNonReleasedItems);
}

/**
* Save the grade and comment for a student's assignment and do concurrency checking
*
Expand Down
1 change: 1 addition & 0 deletions gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/StudentPage.html
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<div class="row">
<div class="col-sm-12">
<h2 wicket:id="heading">Grade Report for Tony Stark</h2>
<button type="button" class="btn btn-link btn-xs gb-whatif-toggle pull-right"><wicket:message key="button.whatif.toggle" /></button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this element is missing an accessible name or label. That makes it hard for people using screen readers or voice control to use the control.

<button type="button" class="btn btn-link btn-xs gb-summary-print pull-right"><wicket:message key="button.print" /></button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ protected void populateItem(final ListItem<Assignment> assignmentItem) {
rawGrade = "";
comment = "";
}
assignmentItem.add(new AttributeModifier("data-assignment-id", assignment.getId()));
assignmentItem.add(new AttributeModifier("data-points", assignment.getPoints()));
assignmentItem.add(new AttributeModifier("data-grade", StringUtils.defaultString(rawGrade)));

final Label title = new Label("title", assignment.getName());
assignmentItem.add(title);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
</span>
</div>
<a href="javascript:void(0);" class="gb-summary-grade-stats float-end" wicket:message="title:studentsummary.gradebookitem.gradebookstats" wicket:id="courseGradeStatsLink"></a>
<div class="gb-whatif-error alert alert-warning d-none" role="alert"></div>
<div class="gb-whatif-note alert alert-info d-none" role="status">
<wicket:message key="whatif.help" />
</div>
<div class="gb-whatif-result alert alert-secondary d-none" role="status">
<strong><wicket:message key="whatif.projected" /></strong>
<span class="gb-whatif-grade ms-1"></span>
</div>
</div>
</div>
</div>
Expand Down
Loading