Skip to content

SAK-52310 SelectActionListener refactor for performance while accessing the assessment list as a student#14350

Open
jumarub wants to merge 2 commits intosakaiproject:masterfrom
jumarub:SAK-52310
Open

SAK-52310 SelectActionListener refactor for performance while accessing the assessment list as a student#14350
jumarub wants to merge 2 commits intosakaiproject:masterfrom
jumarub:SAK-52310

Conversation

@jumarub
Copy link
Contributor

@jumarub jumarub commented Jan 28, 2026

https://sakaiproject.atlassian.net/browse/SAK-52310

Summary by CodeRabbit

  • Refactor

    • Modernized assessment processing for safer typing, clearer item flows, and more consistent null handling.
    • Streamlined preparation of "takeable", "submitted", and "reviewable" lists for more reliable display.
  • Bug Fixes

    • Improved average-score aggregation and tracking for assessments with multiple submissions.
    • More accurate past-due, updated, and resubmission status determination; refined sorting and filtering for final display.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 28, 2026 13:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request refactors the SelectActionListener class for performance improvements when students access the assessment list. The refactoring focuses on code modernization, improved type safety, and cleaner logic.

Changes:

  • Added generic type parameters to collections (Map, List) for improved type safety
  • Replaced traditional for-loops with enhanced for-loops and Stream API operations
  • Simplified conditional logic by consolidating if-else blocks into single-line assignments
  • Improved code formatting and removed trailing whitespace

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

Walkthrough

Refactors SelectActionListener.java to use typed generics, enhanced for-loops and streams; consolidates past-due/updated/multiple-submission logic; adds average-score aggregation and reviewable/recorded grouping; updates secure-delivery URL resolution and introduces a new public setter on SelectAssessmentBean for average submission tracking.

Changes

Cohort / File(s) Summary
Action listener iteration & state refactoring
samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java
Rewrites action processing: replaces raw collections and index loops with typed List/Map, enhanced for-loops and streams; initializes and propagates average-submission flags; consolidates PastDue/Updated/Resubmit and multiple-submission/highest-score logic; rebuilds DeliveryBeanie construction; adds averageScoreMap aggregation and refactors reviewable/recorded grouping and final sorting/filtering; changes secure delivery URL resolution to use explicit Optional handling.
Assessment bean state tracking
samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/bean/select/SelectAssessmentBean.java
Adds public setter setHasAverageMultipleSubmissions(boolean) to expose average-multiple-submission state.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: a performance-focused refactor of SelectActionListener for student assessment list access, matching the substantial refactoring work detailed in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java`:
- Around line 293-310: DeliveryBeanie.getFinalScore() can be null causing
Double.parseDouble(null) to NPE; before parsing inside the loop over
averageScoreAssessmentGradingList check if db.getFinalScore() is null or empty
and skip that entry (continue) or treat it as 0 per desired semantics, then
parse the non-null value (e.g., Double.parseDouble(db.getFinalScore())); also
when computing averageScore for lastPublishedAssessmentId ensure
totalSubmissions > 0 before dividing and only put an average into
averageScoreMap when totalSubmissions is positive; update references in this
block (DeliveryBeanie, getFinalScore, averageScoreAssessmentGradingList,
averageScoreMap, lastPublishedAssessmentId, totalScores, totalSubmissions)
accordingly.
- Around line 359-365: The lookup averageScoreMap.get(assessmentIdNew) can be
null; update the block in SelectActionListener where processRecordedAvg is true
(around variables assessmentIdNew, beanie.getAssessmentId(), averageScoreMap and
recorded.setFinalScore/setGrade/setRawScore) to first retrieve the value into a
local (e.g., avg) and test for null before calling toString(); if avg is
non-null convert to string and call recorded.setFinalScore/setGrade/setRawScore,
otherwise avoid calling toString() and either leave the recorded fields unset or
explicitly set them to null/empty as appropriate for downstream logic.
🧹 Nitpick comments (1)
samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java (1)

332-357: Consider extracting a copy/clone utility for DeliveryBeanie.

The manual field-by-field copying is verbose and error-prone if new fields are added to DeliveryBeanie. A copy constructor or static factory method would centralize this logic.

Comment on lines +293 to 310
for (DeliveryBeanie db : averageScoreAssessmentGradingList) {
String currentId = db.getAssessmentId();
double score = Double.parseDouble(db.getFinalScore());

if (currentId.equals(lastPublishedAssessmentId)) {
totalScores += score;
totalSubmissions++;
} else {
if (lastPublishedAssessmentId != null) {
double averageScore = totalScores / totalSubmissions;
averageScoreMap.put(lastPublishedAssessmentId, averageScore);
}

lastPublishedAssessmentId = currentId;
totalScores = score;
totalSubmissions = 1;
}
}
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 | 🟠 Major

Potential NPE when parsing finalScore.

If AssessmentGradingData.getFinalScore() was null (see lines 242-249 where setFinalScore is conditional), then db.getFinalScore() returns null and Double.parseDouble(null) throws NullPointerException.

🐛 Proposed fix with null guard
 for (DeliveryBeanie db : averageScoreAssessmentGradingList) {
   String currentId = db.getAssessmentId();
-  double score = Double.parseDouble(db.getFinalScore());
+  String finalScoreStr = db.getFinalScore();
+  if (finalScoreStr == null) {
+    continue;
+  }
+  double score = Double.parseDouble(finalScoreStr);

   if (currentId.equals(lastPublishedAssessmentId)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (DeliveryBeanie db : averageScoreAssessmentGradingList) {
String currentId = db.getAssessmentId();
double score = Double.parseDouble(db.getFinalScore());
if (currentId.equals(lastPublishedAssessmentId)) {
totalScores += score;
totalSubmissions++;
} else {
if (lastPublishedAssessmentId != null) {
double averageScore = totalScores / totalSubmissions;
averageScoreMap.put(lastPublishedAssessmentId, averageScore);
}
lastPublishedAssessmentId = currentId;
totalScores = score;
totalSubmissions = 1;
}
}
for (DeliveryBeanie db : averageScoreAssessmentGradingList) {
String currentId = db.getAssessmentId();
String finalScoreStr = db.getFinalScore();
if (finalScoreStr == null) {
continue;
}
double score = Double.parseDouble(finalScoreStr);
if (currentId.equals(lastPublishedAssessmentId)) {
totalScores += score;
totalSubmissions++;
} else {
if (lastPublishedAssessmentId != null) {
double averageScore = totalScores / totalSubmissions;
averageScoreMap.put(lastPublishedAssessmentId, averageScore);
}
lastPublishedAssessmentId = currentId;
totalScores = score;
totalSubmissions = 1;
}
}
🤖 Prompt for AI Agents
In
`@samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java`
around lines 293 - 310, DeliveryBeanie.getFinalScore() can be null causing
Double.parseDouble(null) to NPE; before parsing inside the loop over
averageScoreAssessmentGradingList check if db.getFinalScore() is null or empty
and skip that entry (continue) or treat it as 0 per desired semantics, then
parse the non-null value (e.g., Double.parseDouble(db.getFinalScore())); also
when computing averageScore for lastPublishedAssessmentId ensure
totalSubmissions > 0 before dividing and only put an average into
averageScoreMap when totalSubmissions is positive; update references in this
block (DeliveryBeanie, getFinalScore, averageScoreAssessmentGradingList,
averageScoreMap, lastPublishedAssessmentId, totalScores, totalSubmissions)
accordingly.

Comment on lines +359 to +365
if (processRecordedAvg) {
assessmentIdNew = beanie.getAssessmentId();
String avgScoreStr = averageScoreMap.get(assessmentIdNew).toString();

recorded.setFinalScore(avgScoreStr);
recorded.setGrade(avgScoreStr);
recorded.setRawScore(avgScoreStr);
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

Add defensive null check for map lookup.

If an assessment's finalScore was null and skipped during average calculation, averageScoreMap.get(assessmentIdNew) would return null, causing NPE on .toString().

🛡️ Proposed defensive fix
 if (processRecordedAvg) {
   assessmentIdNew = beanie.getAssessmentId();
-  String avgScoreStr = averageScoreMap.get(assessmentIdNew).toString();
+  Double avgScore = averageScoreMap.get(assessmentIdNew);
+  if (avgScore == null) {
+    continue;
+  }
+  String avgScoreStr = avgScore.toString();

   recorded.setFinalScore(avgScoreStr);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (processRecordedAvg) {
assessmentIdNew = beanie.getAssessmentId();
String avgScoreStr = averageScoreMap.get(assessmentIdNew).toString();
recorded.setFinalScore(avgScoreStr);
recorded.setGrade(avgScoreStr);
recorded.setRawScore(avgScoreStr);
if (processRecordedAvg) {
assessmentIdNew = beanie.getAssessmentId();
Double avgScore = averageScoreMap.get(assessmentIdNew);
if (avgScore == null) {
continue;
}
String avgScoreStr = avgScore.toString();
recorded.setFinalScore(avgScoreStr);
recorded.setGrade(avgScoreStr);
recorded.setRawScore(avgScoreStr);
🤖 Prompt for AI Agents
In
`@samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java`
around lines 359 - 365, The lookup averageScoreMap.get(assessmentIdNew) can be
null; update the block in SelectActionListener where processRecordedAvg is true
(around variables assessmentIdNew, beanie.getAssessmentId(), averageScoreMap and
recorded.setFinalScore/setGrade/setRawScore) to first retrieve the value into a
local (e.g., avg) and test for null before calling toString(); if avg is
non-null convert to string and call recorded.setFinalScore/setGrade/setRawScore,
otherwise avoid calling toString() and either leave the recorded fields unset or
explicitly set them to null/empty as appropriate for downstream logic.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java`:
- Around line 284-315: The current aggregation in SelectActionListener uses a
sequential scan over averageScoreAssessmentGradingList and relies on consecutive
grouping by assessment id (lastPublishedAssessmentId), which breaks when the
list is not pre-sorted; change the logic to be order-independent by grouping
submissions by assessment id and computing averages per group: use a Map keyed
by DeliveryBeanie.getAssessmentId() (e.g., via
stream().collect(Collectors.groupingBy(DeliveryBeanie::getAssessmentId))) or
iterate and accumulate totals/counts into a Map<String, double[]> or holder
object, then compute averageScoreMap entries from those aggregates and populate
averageScoreMap accordingly (keep variable names
averageScoreAssessmentGradingList, averageScoreMap, DeliveryBeanie,
getAssessmentId, getFinalScore).
- Around line 236-259: The call to hasFeedback(p, g.getFinalScore()) can NPE
because g.getFinalScore() may be null; before invoking hasFeedback (just after
PublishedAssessmentFacade p = ... and prior to
delivery.setFeedback(hasFeedback)), add a null guard that checks
g.getFinalScore() and only calls hasFeedback when non-null (or call an
overload/alternate logic when null), then set delivery.setFeedback to the safe
result (e.g., a default "no feedback" value or result of hasFeedback when score
exists); ensure you reference g.getFinalScore(), hasFeedback(...),
PublishedAssessmentFacade p, and delivery.setFeedback(...) when making the
change.

Comment on lines +236 to +259
delivery.setFeedbackDelivery(getFeedbackDelivery(g.getPublishedAssessmentId(), publishedAssessmentHash));
delivery.setFeedbackComponentOption(getFeedbackComponentOption(g.getPublishedAssessmentId(), publishedAssessmentHash));
delivery.setFeedbackDate(getFeedbackDate(g.getPublishedAssessmentId(), publishedAssessmentHash));
delivery.setFeedbackEndDate(getFeedbackEndDate(g.getPublishedAssessmentId(), publishedAssessmentHash));
delivery.setFeedbackScoreThreshold(getFeedbackScoreThreshold(g.getPublishedAssessmentId(), publishedAssessmentHash));

delivery.setIsRecordedAssessment(g.getIsRecorded());

// to do: set statistics and time for delivery here.
submittedAssessmentGradingList.add(delivery);
if (g.getFinalScore() != null) {
String scoreStr = g.getFinalScore().toString();

delivery.setFinalScore(scoreStr);
delivery.setGrade(scoreStr);
delivery.setRawScore(scoreStr); // Bug 318 fix. It seems raw score should also be based on final score.
delivery.setRaw(g.getFinalScore().longValue());
}

delivery.setTimeElapse(getTimeElapsed(g.getTimeElapsed()));
delivery.setSubmissionDate(g.getSubmittedDate());
delivery.setHasAssessmentBeenModified(getHasAssessmentBeenModified(select, g, publishedAssessmentHash));
delivery.setSubmitted(true); // records are all submitted for grade

PublishedAssessmentFacade p = (PublishedAssessmentFacade)publishedAssessmentHash.get(g.getPublishedAssessmentId());
// check is feedback is available
String hasFeedback = hasFeedback(p, g.getFinalScore());
delivery.setFeedback(hasFeedback);
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 | 🟠 Major

Guard hasFeedback against null finalScore.
g.getFinalScore() can be null, and the feedback‑threshold path dereferences it, causing NPEs. Add a null guard before calling hasFeedback.

🐛 Proposed fix
-      delivery.setFeedbackScoreThreshold(getFeedbackScoreThreshold(g.getPublishedAssessmentId(), publishedAssessmentHash));
+      Double feedbackScoreThreshold = getFeedbackScoreThreshold(g.getPublishedAssessmentId(), publishedAssessmentHash);
+      delivery.setFeedbackScoreThreshold(feedbackScoreThreshold);
@@
-      String hasFeedback = hasFeedback(p, g.getFinalScore());
+      Double finalScore = g.getFinalScore();
+      String hasFeedback = (finalScore == null && feedbackScoreThreshold != null)
+          ? "blank"
+          : hasFeedback(p, finalScore);
       delivery.setFeedback(hasFeedback);
🤖 Prompt for AI Agents
In
`@samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java`
around lines 236 - 259, The call to hasFeedback(p, g.getFinalScore()) can NPE
because g.getFinalScore() may be null; before invoking hasFeedback (just after
PublishedAssessmentFacade p = ... and prior to
delivery.setFeedback(hasFeedback)), add a null guard that checks
g.getFinalScore() and only calls hasFeedback when non-null (or call an
overload/alternate logic when null), then set delivery.setFeedback to the safe
result (e.g., a default "no feedback" value or result of hasFeedback when score
exists); ensure you reference g.getFinalScore(), hasFeedback(...),
PublishedAssessmentFacade p, and delivery.setFeedback(...) when making the
change.

Comment on lines +284 to +315
List<DeliveryBeanie> averageScoreAssessmentGradingList = submittedAssessmentGradingList.stream()
.filter(db -> EvaluationModelIfc.AVERAGE_SCORE.toString().equals(db.getScoringOption()))
.collect(Collectors.toList());

String lastPublishedAssessmentId = null;
Map<String, Double> averageScoreMap = new HashMap<>();
double totalScores = 0d;
int totalSubmissions = 0;

for (DeliveryBeanie db : averageScoreAssessmentGradingList) {
String currentId = db.getAssessmentId();
double score = Double.parseDouble(db.getFinalScore());

if (currentId.equals(lastPublishedAssessmentId)) {
totalScores += score;
totalSubmissions++;
} else {
if (lastPublishedAssessmentId != null) {
double averageScore = totalScores / totalSubmissions;
averageScoreMap.put(lastPublishedAssessmentId, averageScore);
}

lastPublishedAssessmentId = currentId;
totalScores = score;
totalSubmissions = 1;
}
}

String lastPublishedAssessmentId = "";
Map averageScoreMap = new HashMap();
double totalScores= 0d;
int totalSubmissions= 0;
double averageScore = 0d;

for (int i = 0; i < averageScoreAssessmentGradingList.size(); i++)
{
DeliveryBeanie db = (DeliveryBeanie) averageScoreAssessmentGradingList.get(i);
if ((lastPublishedAssessmentId != null && lastPublishedAssessmentId.equals(db.getAssessmentId())) || averageScoreAssessmentGradingList.size() == 1) {
totalScores += Double.parseDouble(db.getFinalScore());
totalSubmissions++;
if (i == averageScoreAssessmentGradingList.size() - 1) {
averageScore = totalScores/totalSubmissions;
averageScoreMap.put(db.getAssessmentId(), averageScore);
}
}
else {
if (i > 0) {
averageScore = totalScores/totalSubmissions;
averageScoreMap.put(lastPublishedAssessmentId, averageScore);
}
lastPublishedAssessmentId = db.getAssessmentId();
totalScores = Double.parseDouble(db.getFinalScore());
totalSubmissions = 1;

if (i == averageScoreAssessmentGradingList.size() - 1) {
averageScore = totalScores/totalSubmissions;
averageScoreMap.put(db.getAssessmentId(), averageScore);
}
}
}


if (lastPublishedAssessmentId != null) {
double averageScore = totalScores / totalSubmissions;
averageScoreMap.put(lastPublishedAssessmentId, averageScore);
}
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

Average‑score aggregation currently depends on list ordering.
Sorting by title can interleave assessments that share a title, which can skew averages. Aggregate by assessmentId directly to make results order‑independent.

🔧 Proposed fix
-    String lastPublishedAssessmentId = null;
-    Map<String, Double> averageScoreMap = new HashMap<>();
-    double totalScores = 0d;
-    int totalSubmissions = 0;
-
-    for (DeliveryBeanie db : averageScoreAssessmentGradingList) {
-      String currentId = db.getAssessmentId();
-      double score = Double.parseDouble(db.getFinalScore());
-
-      if (currentId.equals(lastPublishedAssessmentId)) {
-        totalScores += score;
-        totalSubmissions++;
-      } else {
-        if (lastPublishedAssessmentId != null) {
-            double averageScore = totalScores / totalSubmissions;
-            averageScoreMap.put(lastPublishedAssessmentId, averageScore);
-        }
-
-        lastPublishedAssessmentId = currentId;
-        totalScores = score;
-        totalSubmissions = 1;
-      }
-    }
-
-    if (lastPublishedAssessmentId != null) {
-      double averageScore = totalScores / totalSubmissions;
-      averageScoreMap.put(lastPublishedAssessmentId, averageScore);
-    }
+    Map<String, Double> averageScoreMap = averageScoreAssessmentGradingList.stream()
+        .collect(Collectors.groupingBy(
+            DeliveryBeanie::getAssessmentId,
+            Collectors.averagingDouble(db -> Double.parseDouble(db.getFinalScore()))
+        ));
🤖 Prompt for AI Agents
In
`@samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/listener/select/SelectActionListener.java`
around lines 284 - 315, The current aggregation in SelectActionListener uses a
sequential scan over averageScoreAssessmentGradingList and relies on consecutive
grouping by assessment id (lastPublishedAssessmentId), which breaks when the
list is not pre-sorted; change the logic to be order-independent by grouping
submissions by assessment id and computing averages per group: use a Map keyed
by DeliveryBeanie.getAssessmentId() (e.g., via
stream().collect(Collectors.groupingBy(DeliveryBeanie::getAssessmentId))) or
iterate and accumulate totals/counts into a Map<String, double[]> or holder
object, then compute averageScoreMap entries from those aggregates and populate
averageScoreMap accordingly (keep variable names
averageScoreAssessmentGradingList, averageScoreMap, DeliveryBeanie,
getAssessmentId, getFinalScore).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant