Skip to content

Commit f85c676

Browse files
plukasewjonespm
authored andcommitted
SAK-33108: Provide visual indicators for dropped scores in categories
1 parent 662e06a commit f85c676

File tree

17 files changed

+392
-84
lines changed

17 files changed

+392
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.sakaiproject.service.gradebook.shared;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
6+
/**
7+
* Immutable category score and list of gradebook items dropped (highest/lowest) from the score calculation
8+
* @author plukasew
9+
*/
10+
public final class CategoryScoreData
11+
{
12+
public final double score;
13+
public final List<Long> droppedItems;
14+
15+
public CategoryScoreData(double score, List<Long> droppedItems)
16+
{
17+
this.score = score;
18+
this.droppedItems = Collections.unmodifiableList(droppedItems);
19+
}
20+
}

edu-services/gradebook-service/api/src/java/org/sakaiproject/service/gradebook/shared/GradebookService.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Date;
2828
import java.util.List;
2929
import java.util.Map;
30+
import java.util.Optional;
3031
import java.util.Set;
3132

3233
import org.apache.commons.lang3.StringUtils;
@@ -804,10 +805,10 @@ public void finalizeGrades(String gradebookUid)
804805
* @param gradebookId Id of the gradebook
805806
* @param studentUuid uuid of the student
806807
* @param categoryId id of category
807-
* @return percentage or null if no calculations were made
808+
* @return percentage and dropped items, or empty if no calculations were made
808809
*
809810
*/
810-
Double calculateCategoryScore(Long gradebookId, String studentUuid, Long categoryId);
811+
Optional<CategoryScoreData> calculateCategoryScore(Long gradebookId, String studentUuid, Long categoryId);
811812

812813
/**
813814
* Calculate the category score for the given gradebook, category, assignments in the category and grade map. This doesn't do any
@@ -816,12 +817,12 @@ public void finalizeGrades(String gradebookUid)
816817
* @param gradebook the gradebook. As this method is called for every student at once, this is passed in to save additional lookups by
817818
* id.
818819
* @param studentUuid uuid of the student
819-
* @param categoryId id of category
820+
* @param category the category
820821
* @param categoryAssignments list of assignments the student can view, and are in the category
821822
* @param gradeMap map of assignmentId to grade, to use for the calculations
822-
* @return percentage or null if no calculations were made
823+
* @return percentage and dropped items, or empty if no calculations were made
823824
*/
824-
Double calculateCategoryScore(Object gradebook, String studentUuid, CategoryDefinition category,
825+
Optional<CategoryScoreData> calculateCategoryScore(Object gradebook, String studentUuid, CategoryDefinition category,
825826
final List<Assignment> categoryAssignments, Map<Long, String> gradeMap);
826827

827828
/**

edu-services/gradebook-service/impl/src/java/org/sakaiproject/component/gradebook/GradebookServiceHibernateImpl.java

+20-10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.List;
3333
import java.util.Map;
3434
import java.util.Map.Entry;
35+
import java.util.Optional;
3536
import java.util.Set;
3637
import java.util.stream.Collectors;
3738

@@ -58,6 +59,7 @@
5859
import org.sakaiproject.service.gradebook.shared.AssessmentNotFoundException;
5960
import org.sakaiproject.service.gradebook.shared.AssignmentHasIllegalPointsException;
6061
import org.sakaiproject.service.gradebook.shared.CategoryDefinition;
62+
import org.sakaiproject.service.gradebook.shared.CategoryScoreData;
6163
import org.sakaiproject.service.gradebook.shared.CommentDefinition;
6264
import org.sakaiproject.service.gradebook.shared.ConflictingAssignmentNameException;
6365
import org.sakaiproject.service.gradebook.shared.ConflictingCategoryNameException;
@@ -3132,7 +3134,7 @@ public List<GradingEvent> getGradingEvents(final String studentId, final long as
31323134
}
31333135

31343136
@Override
3135-
public Double calculateCategoryScore(final Object gradebook, final String studentUuid, final CategoryDefinition category, final List<org.sakaiproject.service.gradebook.shared.Assignment> categoryAssignments, final Map<Long,String> gradeMap) {
3137+
public Optional<CategoryScoreData> calculateCategoryScore(final Object gradebook, final String studentUuid, final CategoryDefinition category, final List<org.sakaiproject.service.gradebook.shared.Assignment> categoryAssignments, final Map<Long,String> gradeMap) {
31363138

31373139
final Gradebook gb = (Gradebook) gradebook;
31383140

@@ -3176,6 +3178,7 @@ public Double calculateCategoryScore(final Object gradebook, final String studen
31763178
a.setRemoved(false); //shared.GradebookAssignment doesn't include removed so this will always be false
31773179
a.setGradebook(gb);
31783180
a.setCategory(c);
3181+
a.setId(assignment.getId()); // store the id so we can find out later which grades were dropped, if any
31793182

31803183
//create the AGR
31813184
final AssignmentGradeRecord gradeRecord = new AssignmentGradeRecord(a, studentUuid, grade);
@@ -3187,7 +3190,7 @@ public Double calculateCategoryScore(final Object gradebook, final String studen
31873190
}
31883191

31893192
@Override
3190-
public Double calculateCategoryScore(final Long gradebookId, final String studentUuid, final Long categoryId) {
3193+
public Optional<CategoryScoreData> calculateCategoryScore(final Long gradebookId, final String studentUuid, final Long categoryId) {
31913194

31923195
//get all grade records for the student
31933196
@SuppressWarnings({ "unchecked", "rawtypes"})
@@ -3208,22 +3211,22 @@ public Object doInHibernate(final Session session) throws HibernateException {
32083211
/**
32093212
* Does the heavy lifting for the category calculations.
32103213
* Requires the List of AssignmentGradeRecord so that we can applyDropScores.
3211-
* @param studentUuid the studnet uuid
3212-
* @param categoryId the cateogry id we are interested in
3214+
* @param studentUuid the student uuid
3215+
* @param categoryId the category id we are interested in
32133216
* @param gradeRecords all grade records for the student
32143217
* @return
32153218
*/
3216-
private Double calculateCategoryScore(final String studentUuid, final Long categoryId, final List<AssignmentGradeRecord> gradeRecords) {
3219+
private Optional<CategoryScoreData> calculateCategoryScore(final String studentUuid, final Long categoryId, final List<AssignmentGradeRecord> gradeRecords) {
32173220

32183221
//validate
32193222
if(gradeRecords == null) {
32203223
log.debug("No grade records for student: {}. Nothing to do.", studentUuid);
3221-
return null;
3224+
return Optional.empty();
32223225
}
32233226

32243227
if (categoryId == null) {
32253228
log.debug("No category supplied, nothing to do.");
3226-
return null;
3229+
return Optional.empty();
32273230
}
32283231

32293232
//setup
@@ -3235,6 +3238,13 @@ private Double calculateCategoryScore(final String studentUuid, final Long categ
32353238
// apply any drop/keep settings for this category
32363239
applyDropScores(gradeRecords);
32373240

3241+
// find the records marked as dropped (highest/lowest) before continuing,
3242+
// as gradeRecords will be modified in place after this and these records will be removed
3243+
List<Long> droppedItemIds = gradeRecords.stream()
3244+
.filter(AssignmentGradeRecord::getDroppedFromGrade)
3245+
.map(agr -> agr.getAssignment().getId())
3246+
.collect(Collectors.toList());
3247+
32383248
// Since all gradeRecords for the student are passed in, not just for this category,
32393249
// plus they may not meet the criteria for including in the calculation,
32403250
// this list is filtered down according to the following rules:
@@ -3271,7 +3281,7 @@ private Double calculateCategoryScore(final String studentUuid, final Long categ
32713281
// pre-calculation
32723282
// Rule 1. If category only has a single EC item, don't try to calculate category total.
32733283
if(gradeRecords.size() == 1 && gradeRecords.get(0).getAssignment().isExtraCredit()) {
3274-
return null;
3284+
return Optional.empty();
32753285
}
32763286

32773287
//iterate the filtered list and set the variables for the calculation
@@ -3295,11 +3305,11 @@ private Double calculateCategoryScore(final String studentUuid, final Long categ
32953305
}
32963306

32973307
if (numScored == 0 || numOfAssignments == 0 || totalPossible.doubleValue() == 0) {
3298-
return null;
3308+
return Optional.empty();
32993309
}
33003310

33013311
final BigDecimal mean = totalEarned.divide(new BigDecimal(numScored), GradebookService.MATH_CONTEXT).divide((totalPossible.divide(new BigDecimal(numOfAssignments), GradebookService.MATH_CONTEXT)), GradebookService.MATH_CONTEXT).multiply(new BigDecimal("100"));
3302-
return mean.doubleValue();
3312+
return Optional.of(new CategoryScoreData(mean.doubleValue(), droppedItemIds));
33033313
}
33043314

33053315
@Override

gradebookng/tool/src/java/org/sakaiproject/gradebookng/GradebookNgApplication.properties

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ label.noduedate=-
2929
label.percentage.valued={0}%
3030
label.percentage.plain=%
3131

32+
label.category.drophighest=Drop Highest: {0}
33+
label.category.droplowest=Drop Lowest: {0}
34+
label.category.keephighest=Keep Highest: {0}
35+
label.category.dropSeparator=&
36+
3237
label.coursegrade.released=Course grade has been released to students.
3338
label.coursegrade.notreleased=Course grade has not been released to students.
3439
label.coursegrade.nopermission=-

gradebookng/tool/src/java/org/sakaiproject/gradebookng/business/GradebookNgBusinessService.java

+50-8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import org.apache.commons.lang.StringUtils;
4343

4444
import org.sakaiproject.authz.api.Member;
45+
import org.sakaiproject.authz.api.SecurityAdvisor;
46+
import org.sakaiproject.authz.api.SecurityAdvisor.SecurityAdvice;
4547
import org.sakaiproject.authz.api.SecurityService;
4648
import org.sakaiproject.component.cover.ComponentManager;
4749
import org.sakaiproject.coursemanagement.api.Membership;
@@ -67,6 +69,7 @@
6769
import org.sakaiproject.service.gradebook.shared.AssessmentNotFoundException;
6870
import org.sakaiproject.service.gradebook.shared.Assignment;
6971
import org.sakaiproject.service.gradebook.shared.CategoryDefinition;
72+
import org.sakaiproject.service.gradebook.shared.CategoryScoreData;
7073
import org.sakaiproject.service.gradebook.shared.CommentDefinition;
7174
import org.sakaiproject.service.gradebook.shared.CourseGrade;
7275
import org.sakaiproject.service.gradebook.shared.GradeDefinition;
@@ -515,6 +518,38 @@ public List<CategoryDefinition> getGradebookCategories(final String siteId) {
515518
return rval;
516519
}
517520

521+
/**
522+
* Retrieve the categories visible to the given student.
523+
*
524+
* This should only be called if you are wanting to view the categories that a student would see (ie if you ARE a student, or if you
525+
* are an instructor using the student review mode)
526+
*
527+
* @param studentUuid
528+
* @return list of visible categories
529+
*/
530+
public List<CategoryDefinition> getGradebookCategoriesForStudent(String studentUuid) {
531+
// find the categories that this student's visible assignments belong to
532+
List<Assignment> viewableAssignments = getGradebookAssignmentsForStudent(studentUuid);
533+
final List<Long> catIds = new ArrayList<>();
534+
for (Assignment a : viewableAssignments) {
535+
Long catId = a.getCategoryId();
536+
if (catId != null && !catIds.contains(catId)) {
537+
catIds.add(a.getCategoryId());
538+
}
539+
}
540+
541+
// get all the categories in the gradebook, use a security advisor in case the current user is the student
542+
SecurityAdvisor gbAdvisor = (String userId, String function, String reference)
543+
-> "gradebook.gradeAll".equals(function) ? SecurityAdvice.ALLOWED : SecurityAdvice.PASS;
544+
securityService.pushAdvisor(gbAdvisor);
545+
List<CategoryDefinition> catDefs = gradebookService.getCategoryDefinitions(getGradebook().getUid());
546+
securityService.popAdvisor(gbAdvisor);
547+
548+
// filter out the categories that don't match the categories of the viewable assignments
549+
return catDefs.stream().filter(def -> catIds.contains(def.getId())).collect(Collectors.toList());
550+
551+
}
552+
518553
/**
519554
* Get a map of course grades for all students in the given site.
520555
*
@@ -1110,10 +1145,17 @@ public void putAssignmentsAndCategoryItemsInMatrix(Map<String, GbStudentGradeInf
11101145
}
11111146
}
11121147

1113-
final Double categoryScore = this.gradebookService.calculateCategoryScore(gradebook, student.getUserUuid(), category, category.getAssignmentList(), gradeMap);
1114-
1115-
// add to GbStudentGradeInfo
1116-
sg.addCategoryAverage(category.getId(), categoryScore);
1148+
final Optional<CategoryScoreData> categoryScore = gradebookService.calculateCategoryScore(gradebook,
1149+
student.getUserUuid(), category, category.getAssignmentList(), gradeMap);
1150+
categoryScore.ifPresent(data -> {
1151+
for (Long item : gradeMap.keySet()) {
1152+
if (data.droppedItems.contains(item)) {
1153+
grades.get(item).setDroppedFromCategoryScore(true);
1154+
}
1155+
}
1156+
// add to GbStudentGradeInfo
1157+
sg.addCategoryAverage(category.getId(), data.score);
1158+
});
11171159

11181160
// TODO the TA permission check could reuse this iteration... check performance.
11191161
}
@@ -2094,14 +2136,14 @@ public Map<Long, GbGradeInfo> getGradesForStudent(final String studentUuid) {
20942136
* @param studentUuid uuid of student
20952137
* @return
20962138
*/
2097-
public Double getCategoryScoreForStudent(final Long categoryId, final String studentUuid) {
2139+
public Optional<CategoryScoreData> getCategoryScoreForStudent(final Long categoryId, final String studentUuid) {
20982140

20992141
final Gradebook gradebook = getGradebook();
21002142

2101-
final Double score = this.gradebookService.calculateCategoryScore(gradebook.getId(), studentUuid, categoryId);
2102-
log.info("Category score for category: {}, student: {}:{}", categoryId, studentUuid, score);
2143+
final Optional<CategoryScoreData> result = gradebookService.calculateCategoryScore(gradebook.getId(), studentUuid, categoryId);
2144+
log.info("Category score for category: {}, student: {}:{}", categoryId, studentUuid, result.map(r -> r.score).orElse(null));
21032145

2104-
return score;
2146+
return result;
21052147
}
21062148

21072149
/**

gradebookng/tool/src/java/org/sakaiproject/gradebookng/business/model/GbGradeInfo.java

+7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ public class GbGradeInfo implements Serializable, Comparable<GbGradeInfo> {
4747
@Setter
4848
private boolean gradeable;
4949

50+
/**
51+
* Whether this grade has been dropped from the category score calculation
52+
*/
53+
@Getter
54+
@Setter
55+
private boolean droppedFromCategoryScore = false;
56+
5057
/**
5158
* Constructor. Takes a GradeDefinition or null. If null, a stub is created.
5259
*

gradebookng/tool/src/java/org/sakaiproject/gradebookng/business/util/FormatHelper.java

+37
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
import java.text.NumberFormat;
2525
import java.text.ParseException;
2626
import java.text.SimpleDateFormat;
27+
import java.util.ArrayList;
28+
import java.util.Collections;
2729
import java.util.Date;
30+
import java.util.List;
2831
import java.util.Locale;
2932

3033
import org.apache.commons.lang.StringUtils;
3134
import org.apache.commons.validator.routines.DoubleValidator;
3235
import org.sakaiproject.util.ResourceLoader;
3336

3437
import lombok.extern.slf4j.Slf4j;
38+
import org.sakaiproject.service.gradebook.shared.CategoryDefinition;
3539

3640
@Slf4j
3741
public class FormatHelper {
@@ -322,4 +326,37 @@ public static String decode(final String s) {
322326
throw new AssertionError("UTF-8 not supported");
323327
}
324328
}
329+
330+
/**
331+
* Returns a list of drop highest/lowest labels based on the settings of the given category.
332+
* @param category the category
333+
* @return a list of 1 or 2 labels indicating that drop highest/lowest is in use, or an empty list if not in use.
334+
*/
335+
public static List<String> formatCategoryDropInfo(CategoryDefinition category) {
336+
337+
if (category == null) {
338+
return Collections.emptyList();
339+
}
340+
341+
int dropHighest = category.getDropHighest() == null ? 0 : category.getDropHighest();
342+
int dropLowest = category.getDropLowest() == null ? 0 : category.getDropLowest();
343+
int keepHighest = category.getKeepHighest() == null ? 0 : category.getKeepHighest();
344+
345+
if (dropHighest == 0 && dropLowest == 0 && keepHighest == 0) {
346+
return Collections.emptyList();
347+
}
348+
349+
List<String> info = new ArrayList<>(2);
350+
if (dropHighest > 0) {
351+
info.add(MessageHelper.getString("label.category.drophighest", dropHighest));
352+
}
353+
if (dropLowest > 0) {
354+
info.add(MessageHelper.getString("label.category.droplowest", dropLowest));
355+
}
356+
if (keepHighest > 0) {
357+
info.add(MessageHelper.getString("label.category.keephighest", keepHighest));
358+
}
359+
360+
return info;
361+
}
325362
}

0 commit comments

Comments
 (0)