From bf7ac037dc5351157afd053f6b63edbac8ef35a6 Mon Sep 17 00:00:00 2001 From: EllMoorby <99148447+EllMoorby@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:02:09 +0100 Subject: [PATCH 1/4] feat: Highest achievable score --- app/models/semester.rb | 12 ++++++++++++ app/models/uni_module.rb | 11 +++++++++++ app/models/user.rb | 15 +++++++++++++-- app/models/year.rb | 14 ++++++++++++++ app/views/semesters/show.html.haml | 3 +++ app/views/shared/_dashboard_stats.html.haml | 3 +++ app/views/uni_modules/show.html.haml | 3 +++ app/views/users/show.html.haml | 3 +++ app/views/years/show.html.haml | 3 +++ 9 files changed, 65 insertions(+), 2 deletions(-) diff --git a/app/models/semester.rb b/app/models/semester.rb index 75c6925..d836c33 100644 --- a/app/models/semester.rb +++ b/app/models/semester.rb @@ -60,6 +60,18 @@ def achieved_score(user) total_weight.zero? ? 0 : (weighted_sum / total_weight) end + # Gets the highest possible percentage grade of the semester, if all uncompleted assessments were 100% + def highest_achievable_score(user) + return final_score if final_score.present? + return 0 if uni_modules.empty? + + total_credits = credits + return 0 if total_credits.zero? + + weighted_sum = uni_modules.sum { |m| m.highest_achievable_score(user) * m.credit_share } + (weighted_sum / total_credits).round(2) + end + def progress(user) return 100 if final_score.present? diff --git a/app/models/uni_module.rb b/app/models/uni_module.rb index df597f1..b46cfc7 100644 --- a/app/models/uni_module.rb +++ b/app/models/uni_module.rb @@ -78,6 +78,17 @@ def completion_percentage(user) exams_with_results(user).sum(:weight) end + # Gets the highest possible percentage grade of the module, if all uncompleted assessments were 100% + def highest_achievable_score(user) + return final_score if final_score.present? + + current = achieved_score(user) + done = completion_percentage(user) + return 100 if done == 0 && current == 0 + + ((current * done) + (100 * (100 - done))) / 100.0 + end + def target(user) target = UniModuleTarget.find_by(user: user, uni_module: self) return nil if target.nil? || target.score.nil? diff --git a/app/models/user.rb b/app/models/user.rb index d22c6a1..440d0d6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -74,14 +74,25 @@ def predicted_score total_weight = valid_years.sum(&:weighting_non_null) return 0 if total_weight.zero? - weighted_sum = valid_years.sum do |year| + weighted_sum = valid_years.sum { |year| year_score = year.predicted_score(self) year_score * year.weighting_non_null - end + } weighted_sum / total_weight end + # Gets the highest possible percentage grade of the user, if all uncompleted assessments were 100% + def highest_achievable_score + return 0 if years.empty? + + total_credits = years.sum(&:credits) + return 0 if total_credits.zero? + + weighted_sum = years.sum { |year| year.highest_achievable_score(self) * year.credits } + (weighted_sum / total_credits).round(2) + end + def required_score_for_threshold(threshold) p = progress / 100.0 a = achieved_score diff --git a/app/models/year.rb b/app/models/year.rb index 47edf84..d7727c7 100644 --- a/app/models/year.rb +++ b/app/models/year.rb @@ -47,6 +47,18 @@ def weighting_non_null weighting end + # Gets the highest possible percentage grade of the year, if all uncompleted assessments were 100% + def highest_achievable_score(user) + return final_score if final_score.present? + return 0 if semesters.empty? + + total_credits = credits + return 0 if total_credits.zero? + + weighted_sum = semesters.sum { |s| s.highest_achievable_score(user) * s.credits } + (weighted_sum / total_credits).round(2) + end + def completed_credits(user) return 0 if uni_modules.empty? @@ -84,6 +96,8 @@ def achieved_score(user) achieved_score_by_module(user) end + + # Good enough with weighted average TODO: use exam results instead def average_score(_user) return 0 if exam_results.empty? diff --git a/app/views/semesters/show.html.haml b/app/views/semesters/show.html.haml index 8bf7bb5..f742924 100644 --- a/app/views/semesters/show.html.haml +++ b/app/views/semesters/show.html.haml @@ -24,6 +24,9 @@ = link_to edit_semester_path(@semester, anchor: 'final_score') do Achieved Score .stat-content= number_to_percentage(@semester.achieved_score(current_user), precision: 2) + .stat + .stat-header Highest Achievable Score + .stat-content= number_to_percentage(@semester.highest_achievable_score(current_user), precision: 2) = turbo_frame_tag "modules" do .gt-empty-card diff --git a/app/views/shared/_dashboard_stats.html.haml b/app/views/shared/_dashboard_stats.html.haml index 971fb64..de00648 100644 --- a/app/views/shared/_dashboard_stats.html.haml +++ b/app/views/shared/_dashboard_stats.html.haml @@ -15,6 +15,9 @@ .stat .stat-header Achieved Score .stat-content= number_to_percentage(current_user.achieved_score, precision: 2) + .stat + .stat-header Highest Achievable Score + .stat-content= number_to_percentage(current_user.highest_achievable_score, precision: 2) .stat .stat-header Predicted .stat-content= number_to_percentage(current_user.predicted_score, precision: 2) \ No newline at end of file diff --git a/app/views/uni_modules/show.html.haml b/app/views/uni_modules/show.html.haml index a553405..563897f 100644 --- a/app/views/uni_modules/show.html.haml +++ b/app/views/uni_modules/show.html.haml @@ -29,6 +29,9 @@ = link_to edit_uni_module_path(@uni_module, anchor: 'final_score') do Achieved Score .stat-content= number_to_percentage(@uni_module.achieved_score(current_user), precision: 2) + .stat + .stat-header Highest Achievable Score + .stat-content= number_to_percentage(@uni_module.highest_achievable_score(current_user), precision: 2) .exams-container .exams diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 5eebaa0..56a9b7a 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -28,6 +28,9 @@ .stat .stat-header Achieved Score .stat-content= number_to_percentage(@user.achieved_score, precision: 2) + .stat + .stat-header Highest Achievable Score + .stat-content= number_to_percentage(@user.highest_achievable_score, precision: 2) .stat .stat-header Predicted .stat-content= number_to_percentage(@user.predicted_score, precision: 2) diff --git a/app/views/years/show.html.haml b/app/views/years/show.html.haml index c6cc166..3d5f979 100644 --- a/app/views/years/show.html.haml +++ b/app/views/years/show.html.haml @@ -20,6 +20,9 @@ = link_to edit_year_path(@year, anchor: 'final_score') do Achieved Score .stat-content= number_to_percentage(@year.achieved_score(current_user), precision: 2) + .stat + .stat-header Highest Achievable Score + .stat-content= number_to_percentage(@year.highest_achievable_score(current_user), precision: 2) .stat .stat-header Predicted Score .stat-content= number_to_percentage(@year.predicted_score(current_user), precision: 2) From b0fd5dcb5f33bfa3a73b98ec782a20c1ff19f9c2 Mon Sep 17 00:00:00 2001 From: Elliott Moorby <99148447+EllMoorby@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:49:08 +0100 Subject: [PATCH 2/4] Fix: potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/models/user.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 440d0d6..d236e2b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -84,13 +84,14 @@ def predicted_score # Gets the highest possible percentage grade of the user, if all uncompleted assessments were 100% def highest_achievable_score - return 0 if years.empty? + valid_years = years.select { |year| year.weighting_non_null.positive? } + return 0 if valid_years.empty? - total_credits = years.sum(&:credits) - return 0 if total_credits.zero? + total_weight = valid_years.sum(&:weighting_non_null) + return 0 if total_weight.zero? - weighted_sum = years.sum { |year| year.highest_achievable_score(self) * year.credits } - (weighted_sum / total_credits).round(2) + weighted_sum = valid_years.sum { |year| year.highest_achievable_score(self) * year.weighting_non_null } + (weighted_sum / total_weight).round(2) end def required_score_for_threshold(threshold) From c75b9bcd4f8ef9b3fcf3300a68eb3141b798c923 Mon Sep 17 00:00:00 2001 From: EllMoorby <99148447+EllMoorby@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:52:49 +0100 Subject: [PATCH 3/4] fix: Fixed highest_achievable_score calculation --- app/models/uni_module.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/uni_module.rb b/app/models/uni_module.rb index 0ad0080..07b1a00 100644 --- a/app/models/uni_module.rb +++ b/app/models/uni_module.rb @@ -77,11 +77,11 @@ def completion_percentage(user) def highest_achievable_score(user) return final_score if final_score.present? - current = achieved_score(user) + achieved = achieved_score(user) done = completion_percentage(user) - return 100 if done == 0 && current == 0 + return 100 if done.zero? && achieved.zero? - ((current * done) + (100 * (100 - done))) / 100.0 + (achieved + (100 - done)).clamp(0.0, 100.0) end def target(user) From afc4ade7edcc5901d7a46cfed3f3a1e3ff891fe5 Mon Sep 17 00:00:00 2001 From: EllMoorby <99148447+EllMoorby@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:00:19 +0100 Subject: [PATCH 4/4] refactor: removed excess spaces and updated summation for conciseness --- app/models/user.rb | 5 +---- app/models/year.rb | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index d236e2b..8ec9e6c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -74,10 +74,7 @@ def predicted_score total_weight = valid_years.sum(&:weighting_non_null) return 0 if total_weight.zero? - weighted_sum = valid_years.sum { |year| - year_score = year.predicted_score(self) - year_score * year.weighting_non_null - } + weighted_sum = valid_years.sum { |year| year.predicted_score(self) * year.weighting_non_null } weighted_sum / total_weight end diff --git a/app/models/year.rb b/app/models/year.rb index d7727c7..08fffb4 100644 --- a/app/models/year.rb +++ b/app/models/year.rb @@ -96,8 +96,6 @@ def achieved_score(user) achieved_score_by_module(user) end - - # Good enough with weighted average TODO: use exam results instead def average_score(_user) return 0 if exam_results.empty?