From e80c289170db47cc436259a99e3f0fd522b5bf1c Mon Sep 17 00:00:00 2001 From: Blerargh Date: Fri, 14 Feb 2025 02:02:02 +0800 Subject: [PATCH 01/30] added 'enable_leaderboard' columns in courses table --- lib/cadet/courses/course.ex | 6 +++++- lib/cadet_web/admin_controllers/admin_courses_controller.ex | 2 ++ lib/cadet_web/controllers/courses_controller.ex | 6 ++++++ lib/cadet_web/controllers/user_controller.ex | 4 ++++ lib/cadet_web/views/courses_view.ex | 2 ++ lib/cadet_web/views/user_view.ex | 2 ++ priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 4 ++++ 7 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index c74d23bd7..b187ad16a 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -12,6 +12,8 @@ defmodule Cadet.Courses.Course do viewable: boolean(), enable_game: boolean(), enable_achievements: boolean(), + enable_overall_leaderboard: boolean(), + enable_contest_leaderboard: boolean(), enable_sourcecast: boolean(), enable_stories: boolean(), source_chapter: integer(), @@ -26,6 +28,8 @@ defmodule Cadet.Courses.Course do field(:viewable, :boolean, default: true) field(:enable_game, :boolean, default: true) field(:enable_achievements, :boolean, default: true) + field(:enable_overall_leaderboard, :boolean, default: false) + field(:enable_contest_leaderboard, :boolean, default: true) field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:source_chapter, :integer) @@ -41,7 +45,7 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - enable_achievements enable_sourcecast enable_stories source_chapter source_variant)a + enable_achievements enable_overall_leaderboard enable_contest_leaderboard enable_sourcecast enable_stories source_chapter source_variant)a @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 7220a4d80..5f9939aec 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -106,6 +106,8 @@ defmodule CadetWeb.AdminCoursesController do viewable(:body, :boolean, "Course viewability") enable_game(:body, :boolean, "Enable game") enable_achievements(:body, :boolean, "Enable achievements") + enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard") + enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard") enable_sourcecast(:body, :boolean, "Enable sourcecast") enable_stories(:body, :boolean, "Enable stories") sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index e6555bd7a..8d705d30f 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -54,6 +54,8 @@ defmodule CadetWeb.CoursesController do viewable(:body, :boolean, "Course viewability", required: true) enable_game(:body, :boolean, "Enable game", required: true) enable_achievements(:body, :boolean, "Enable achievements", required: true) + enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard", required: true) + enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard", required: true) enable_sourcecast(:body, :boolean, "Enable sourcecast", required: true) enable_stories(:body, :boolean, "Enable stories", required: true) source_chapter(:body, :number, "Default source chapter", required: true) @@ -95,6 +97,8 @@ defmodule CadetWeb.CoursesController do viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) + enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) + enable_contest_leaderboard(:boolean, "Enable contest leaderboard", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -109,6 +113,8 @@ defmodule CadetWeb.CoursesController do viewable: true, enable_game: true, enable_achievements: true, + enable_overall_leaderboard: false, + enable_contest_leaderboard: true, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index cfc162652..0314b4a9c 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -315,6 +315,8 @@ defmodule CadetWeb.UserController do viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) + enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) + enable_contest_leaderboard(:boolean, "Enable contest leadeboard", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -330,6 +332,8 @@ defmodule CadetWeb.UserController do viewable: true, enable_game: true, enable_achievements: true, + enable_overall_leaderboard: false, + enable_contest_leaderboard: true, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index a6ae9c4fa..afea1c27e 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -10,6 +10,8 @@ defmodule CadetWeb.CoursesView do viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, + enableOverallLeaderboard: :enable_overall_leaderboard, + enableContestLeaderboard: :enable_contest_leaderboard, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index b547e2440..f35f44057 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -103,6 +103,8 @@ defmodule CadetWeb.UserView do viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, + enableOverallLeaderboard: :enable_overall_leaderboard, + enableContestLeaderboard: :enable_contest_leaderboard, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 9181bf4c2..ce32f7249 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -10,6 +10,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:viewable, :boolean, null: false, default: true) add(:enable_game, :boolean, null: false, default: true) add(:enable_achievements, :boolean, null: false, default: true) + add(:enable_overall_leaderboard, :boolean, null: false, default: false) + add(:enable_contest_leaderboard, :boolean, null: false, default: true) add(:enable_sourcecast, :boolean, null: false, default: true) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) @@ -143,6 +145,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do viewable: true, enable_game: true, enable_achievements: true, + enable_overall_leaderboard: false, + enable_contest_leaderboard: true, enable_sourcecast: true, source_chapter: 1, source_variant: "default", From 2464ad43f8499dad17da6b76f91095bbe945b828 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Sat, 15 Feb 2025 01:29:49 +0800 Subject: [PATCH 02/30] Leaderboard create course config, leaderboard page routing, leaderboard sql query for all users in course --- lib/cadet/assessments/assessments.ex | 125 +++++++++++++++++- lib/cadet/courses/course.ex | 6 +- .../admin_courses_controller.ex | 2 + .../admin_user_controller.ex | 5 + .../controllers/courses_controller.ex | 6 + lib/cadet_web/controllers/user_controller.ex | 4 + lib/cadet_web/router.ex | 2 + lib/cadet_web/views/courses_view.ex | 2 + lib/cadet_web/views/user_view.ex | 2 + .../20210531155751_multitenant_upgrade.exs | 4 + 10 files changed, 156 insertions(+), 2 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 244ebc742..652081681 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -4,6 +4,7 @@ defmodule Cadet.Assessments do missions, sidequests, paths, etc. """ use Cadet, [:context, :display] + alias Cadet.Incentives.{Achievement, GoalProgress, Goal} import Ecto.Query require Logger @@ -25,7 +26,7 @@ defmodule Cadet.Assessments do alias Cadet.Jobs.Log alias Cadet.ProgramAnalysis.Lexer alias Ecto.Multi - alias Cadet.Incentives.Achievements + alias Cadet.Incentives.{Achievements, AchievementToGoal} alias Timex.Duration require Decimal @@ -146,6 +147,128 @@ defmodule Cadet.Assessments do total_achievement_xp + total_assessment_xp end + def all_user_total_xp(course_id) do + base_user_query = + from( + cr in CourseRegistration, + full_join: u in User, + on: cr.user_id == u.id, + where: cr.course_id == ^course_id, + select: %{ + user_id: u.id, + name: u.name, + username: u.username + } + ) + + achievements_xp_query = + from(u in User, + full_join: cr in CourseRegistration, + on: cr.user_id == u.id and cr.course_id == ^course_id, + left_join: a in Achievement, + on: a.course_id == cr.course_id, + left_join: j in assoc(a, :goals), + left_join: g in assoc(j, :goal), + left_join: p in GoalProgress, + on: p.goal_uuid == g.uuid and p.course_reg_id == cr.id, + where: a.course_id == ^course_id or not is_nil(cr.id), + group_by: [u.id, u.name, u.username, cr.id], + having: + fragment( + "bool_and(?)", + p.completed and p.count == g.target_count and not is_nil(p.course_reg_id) + ), + select_merge: %{ + user_id: u.id, + name: u.name, + username: u.username, + is_variable_xp: fragment("bool_and(is_variable_xp)"), + xp_count_sum: sum(p.count), + xp_max: max(a.xp) + } + ) + + achievements_xp_final_query = + from(ax in subquery(achievements_xp_query), + select: %{ + user_id: ax.user_id, + name: ax.name, + username: ax.username, + achievements_xp: + fragment( + "CASE WHEN ? THEN ? ELSE ? END", + ax.is_variable_xp, + ax.xp_count_sum, + ax.xp_max + ) + } + ) + + submissions_xp_query = + from( + sub_xp in subquery( + from(cr in CourseRegistration, + full_join: u in User, + on: cr.user_id == u.id, + full_join: tm in TeamMember, + on: cr.id == tm.student_id, + full_join: s in Submission, + on: tm.team_id == s.team_id or s.student_id == cr.id, + full_join: a in Answer, + on: s.id == a.submission_id, + where: s.is_grading_published == true and cr.course_id == ^course_id, + group_by: [cr.id, u.name, u.username, s.id], + select: %{ + user_id: cr.id, + name: u.name, + username: u.username, + submission_xp: sum(a.xp) + sum(a.xp_adjustment) + max(s.xp_bonus) + } + ) + ), + group_by: [sub_xp.user_id, sub_xp.name, sub_xp.username], + select: %{ + user_id: sub_xp.user_id, + name: sub_xp.name, + username: sub_xp.username, + submission_xp: sum(sub_xp.submission_xp) + } + ) + + total_xp_query = + from(bu in subquery(base_user_query), + left_join: ax in subquery(achievements_xp_final_query), + on: bu.user_id == ax.user_id, + left_join: sx in subquery(submissions_xp_query), + on: bu.user_id == sx.user_id, + select: %{ + user_id: bu.user_id, + name: bu.name, + username: bu.username, + total_xp: + fragment( + "COALESCE(?, 0) + COALESCE(?, 0)", + ax.achievements_xp, + sx.submission_xp + ) + }, + order_by: [desc: fragment("total_xp")] + ) + + ranked_xp_query = + from(t in subquery(total_xp_query), + select: %{ + rank: fragment("ROW_NUMBER() OVER (ORDER BY total_xp DESC)"), + user_id: t.user_id, + name: t.name, + username: t.username, + total_xp: t.total_xp + } + ) + + Repo.all(ranked_xp_query) + end + defp decimal_to_integer(decimal) do if Decimal.is_decimal(decimal) do Decimal.to_integer(decimal) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index c74d23bd7..51a23bf75 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -12,6 +12,8 @@ defmodule Cadet.Courses.Course do viewable: boolean(), enable_game: boolean(), enable_achievements: boolean(), + enable_contest_leaderboard: boolean(), + enable_overall_leaderboard: boolean(), enable_sourcecast: boolean(), enable_stories: boolean(), source_chapter: integer(), @@ -26,6 +28,8 @@ defmodule Cadet.Courses.Course do field(:viewable, :boolean, default: true) field(:enable_game, :boolean, default: true) field(:enable_achievements, :boolean, default: true) + field(:enable_contest_leaderboard, :boolean, default: true) + field(:enable_overall_leaderboard, :boolean, default: true) field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:source_chapter, :integer) @@ -41,7 +45,7 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - enable_achievements enable_sourcecast enable_stories source_chapter source_variant)a + enable_achievements enable_sourcecast enable_stories source_chapter source_variant enable_contest_leaderboard enable_overall_leaderboard)a @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 7220a4d80..7f671095e 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -106,6 +106,8 @@ defmodule CadetWeb.AdminCoursesController do viewable(:body, :boolean, "Course viewability") enable_game(:body, :boolean, "Enable game") enable_achievements(:body, :boolean, "Enable achievements") + enable_contest_leaderboard(:body, :boolean, "Enable Contest Leaderboard") + enable_overall_leaderboard(:body, :boolean, "Enable Overall Leaderboard") enable_sourcecast(:body, :boolean, "Enable sourcecast") enable_stories(:body, :boolean, "Enable stories") sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 53add9133..7734f47f4 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -28,6 +28,11 @@ defmodule CadetWeb.AdminUserController do json(conn, %{totalXp: total_xp}) end + def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do + users_with_xp = Assessments.all_user_total_xp(course_id) + json(conn, %{users: users_with_xp}) + end + @add_users_role ~w(admin)a def get_students(conn, filter) do users = diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index e6555bd7a..71adda14c 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -54,6 +54,8 @@ defmodule CadetWeb.CoursesController do viewable(:body, :boolean, "Course viewability", required: true) enable_game(:body, :boolean, "Enable game", required: true) enable_achievements(:body, :boolean, "Enable achievements", required: true) + enable_contest_leaderboard(:body, :boolean, "Enable Contest Leaderboard", required: true) + enable_overall_leaderboard(:body, :boolean, "Enable Overall Leaderboard", required: true) enable_sourcecast(:body, :boolean, "Enable sourcecast", required: true) enable_stories(:body, :boolean, "Enable stories", required: true) source_chapter(:body, :number, "Default source chapter", required: true) @@ -95,6 +97,8 @@ defmodule CadetWeb.CoursesController do viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) + enable_contest_leaderboard(:boolean, "Enable Contest Leaderboard", required: true) + enable_overall_leaderboard(:boolean, "Enable Overall Leaderboard", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -109,6 +113,8 @@ defmodule CadetWeb.CoursesController do viewable: true, enable_game: true, enable_achievements: true, + enable_contest_leaderboard: true, + enable_overall_leaderboard: true, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index cfc162652..434560f1c 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -315,6 +315,8 @@ defmodule CadetWeb.UserController do viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) + enable_contest_leaderboard(:boolean, "Enable Contest Leaderboard", required: true) + enable_overall_leaderboard(:boolean, "Enable Overall Leaderboard", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -330,6 +332,8 @@ defmodule CadetWeb.UserController do viewable: true, enable_game: true, enable_achievements: true, + enable_contest_leaderboard: true, + enable_overall_leaderboard: true, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1ee304c01..208b7a663 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -118,6 +118,8 @@ defmodule CadetWeb.Router do put("/user/game_states", UserController, :update_game_states) put("/user/research_agreement", UserController, :update_research_agreement) + get("/all_users_xp", AdminUserController, :combined_total_xp_for_all_users) + get("/config", CoursesController, :index) get("/team/:assessmentid", TeamController, :index) diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index a6ae9c4fa..4939a5ff4 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -10,6 +10,8 @@ defmodule CadetWeb.CoursesView do viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, + enableContestLeaderboard: :enable_contest_leaderboard, + enableOverallLeaderboard: :enable_overall_leaderboard, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index b547e2440..ebf4d6bf6 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -103,6 +103,8 @@ defmodule CadetWeb.UserView do viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, + enableContestLeaderboard: :enable_contest_leaderboard, + enableOverallLeaderboard: :enable_overall_leaderboard, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 9181bf4c2..dfc39402e 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -10,6 +10,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:viewable, :boolean, null: false, default: true) add(:enable_game, :boolean, null: false, default: true) add(:enable_achievements, :boolean, null: false, default: true) + add(:enable_contest_leaderboard, :boolean, null: false, default: true) + add(:enable_overall_leaderboard, :boolean, null: false, default: true) add(:enable_sourcecast, :boolean, null: false, default: true) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) @@ -143,6 +145,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do viewable: true, enable_game: true, enable_achievements: true, + enable_contest_leaderboard: true, + enable_overall_leaderboard: true, enable_sourcecast: true, source_chapter: 1, source_variant: "default", From e8403852def5b5b688af173bca9e013c41be0942 Mon Sep 17 00:00:00 2001 From: Blerargh Date: Sat, 15 Feb 2025 01:33:13 +0800 Subject: [PATCH 03/30] added 'top_leaderboard_display' columns in courses table --- lib/cadet/courses/course.ex | 4 +++- lib/cadet_web/admin_controllers/admin_courses_controller.ex | 1 + lib/cadet_web/controllers/courses_controller.ex | 3 +++ lib/cadet_web/controllers/user_controller.ex | 2 ++ lib/cadet_web/views/courses_view.ex | 1 + lib/cadet_web/views/user_view.ex | 1 + priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 2 ++ 7 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index b187ad16a..94ab6580c 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -14,6 +14,7 @@ defmodule Cadet.Courses.Course do enable_achievements: boolean(), enable_overall_leaderboard: boolean(), enable_contest_leaderboard: boolean(), + top_leaderboard_display: integer(), enable_sourcecast: boolean(), enable_stories: boolean(), source_chapter: integer(), @@ -30,6 +31,7 @@ defmodule Cadet.Courses.Course do field(:enable_achievements, :boolean, default: true) field(:enable_overall_leaderboard, :boolean, default: false) field(:enable_contest_leaderboard, :boolean, default: true) + field(:top_leaderboard_display, :integer) field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:source_chapter, :integer) @@ -45,7 +47,7 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - enable_achievements enable_overall_leaderboard enable_contest_leaderboard enable_sourcecast enable_stories source_chapter source_variant)a + enable_achievements enable_overall_leaderboard enable_contest_leaderboard top_leaderboard_display enable_sourcecast enable_stories source_chapter source_variant)a @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 5f9939aec..ac152115e 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -108,6 +108,7 @@ defmodule CadetWeb.AdminCoursesController do enable_achievements(:body, :boolean, "Enable achievements") enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard") enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard") + top_leaderboard_display(:body, :integer, "Top Leaderboard Display") enable_sourcecast(:body, :boolean, "Enable sourcecast") enable_stories(:body, :boolean, "Enable stories") sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 8d705d30f..ca216f43f 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -56,6 +56,7 @@ defmodule CadetWeb.CoursesController do enable_achievements(:body, :boolean, "Enable achievements", required: true) enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard", required: true) enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard", required: true) + top_leaderboard_display(:body, :number, "Top leaderboard display", required: true) enable_sourcecast(:body, :boolean, "Enable sourcecast", required: true) enable_stories(:body, :boolean, "Enable stories", required: true) source_chapter(:body, :number, "Default source chapter", required: true) @@ -99,6 +100,7 @@ defmodule CadetWeb.CoursesController do enable_achievements(:boolean, "Enable achievements", required: true) enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) enable_contest_leaderboard(:boolean, "Enable contest leaderboard", required: true) + top_leaderboard_display(:boolean, "Top leaderboard display", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -115,6 +117,7 @@ defmodule CadetWeb.CoursesController do enable_achievements: true, enable_overall_leaderboard: false, enable_contest_leaderboard: true, + top_leaderboard_display: "100", enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 0314b4a9c..a1f5f1342 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -317,6 +317,7 @@ defmodule CadetWeb.UserController do enable_achievements(:boolean, "Enable achievements", required: true) enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) enable_contest_leaderboard(:boolean, "Enable contest leadeboard", required: true) + top_leaderboard_display(:integer, "Top leaderboard display", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -334,6 +335,7 @@ defmodule CadetWeb.UserController do enable_achievements: true, enable_overall_leaderboard: false, enable_contest_leaderboard: true, + top_leaderboard_display: 100, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index afea1c27e..80c8bf3ef 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -12,6 +12,7 @@ defmodule CadetWeb.CoursesView do enableAchievements: :enable_achievements, enableOverallLeaderboard: :enable_overall_leaderboard, enableContestLeaderboard: :enable_contest_leaderboard, + topLeaderboardDisplay: :top_leaderboard_display, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index f35f44057..091e8d5ae 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -105,6 +105,7 @@ defmodule CadetWeb.UserView do enableAchievements: :enable_achievements, enableOverallLeaderboard: :enable_overall_leaderboard, enableContestLeaderboard: :enable_contest_leaderboard, + topLeaderboardDisplay: :top_leaderboard_display, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index ce32f7249..16f32d98e 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -12,6 +12,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:enable_achievements, :boolean, null: false, default: true) add(:enable_overall_leaderboard, :boolean, null: false, default: false) add(:enable_contest_leaderboard, :boolean, null: false, default: true) + add(:top_leaderboard_display, :integer, default: 100) add(:enable_sourcecast, :boolean, null: false, default: true) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) @@ -147,6 +148,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do enable_achievements: true, enable_overall_leaderboard: false, enable_contest_leaderboard: true, + top_leaderboard_display: 100, enable_sourcecast: true, source_chapter: 1, source_variant: "default", From 0a11acd220fe8062371637a49e9b44b4a6448e30 Mon Sep 17 00:00:00 2001 From: Blerargh Date: Sat, 15 Feb 2025 01:34:55 +0800 Subject: [PATCH 04/30] added 'all_user_total_xp' function for leaderboard display --- lib/cadet/assessments/assessments.ex | 125 +++++++++++++++++- .../admin_user_controller.ex | 7 +- lib/cadet_web/router.ex | 2 + 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 244ebc742..652081681 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -4,6 +4,7 @@ defmodule Cadet.Assessments do missions, sidequests, paths, etc. """ use Cadet, [:context, :display] + alias Cadet.Incentives.{Achievement, GoalProgress, Goal} import Ecto.Query require Logger @@ -25,7 +26,7 @@ defmodule Cadet.Assessments do alias Cadet.Jobs.Log alias Cadet.ProgramAnalysis.Lexer alias Ecto.Multi - alias Cadet.Incentives.Achievements + alias Cadet.Incentives.{Achievements, AchievementToGoal} alias Timex.Duration require Decimal @@ -146,6 +147,128 @@ defmodule Cadet.Assessments do total_achievement_xp + total_assessment_xp end + def all_user_total_xp(course_id) do + base_user_query = + from( + cr in CourseRegistration, + full_join: u in User, + on: cr.user_id == u.id, + where: cr.course_id == ^course_id, + select: %{ + user_id: u.id, + name: u.name, + username: u.username + } + ) + + achievements_xp_query = + from(u in User, + full_join: cr in CourseRegistration, + on: cr.user_id == u.id and cr.course_id == ^course_id, + left_join: a in Achievement, + on: a.course_id == cr.course_id, + left_join: j in assoc(a, :goals), + left_join: g in assoc(j, :goal), + left_join: p in GoalProgress, + on: p.goal_uuid == g.uuid and p.course_reg_id == cr.id, + where: a.course_id == ^course_id or not is_nil(cr.id), + group_by: [u.id, u.name, u.username, cr.id], + having: + fragment( + "bool_and(?)", + p.completed and p.count == g.target_count and not is_nil(p.course_reg_id) + ), + select_merge: %{ + user_id: u.id, + name: u.name, + username: u.username, + is_variable_xp: fragment("bool_and(is_variable_xp)"), + xp_count_sum: sum(p.count), + xp_max: max(a.xp) + } + ) + + achievements_xp_final_query = + from(ax in subquery(achievements_xp_query), + select: %{ + user_id: ax.user_id, + name: ax.name, + username: ax.username, + achievements_xp: + fragment( + "CASE WHEN ? THEN ? ELSE ? END", + ax.is_variable_xp, + ax.xp_count_sum, + ax.xp_max + ) + } + ) + + submissions_xp_query = + from( + sub_xp in subquery( + from(cr in CourseRegistration, + full_join: u in User, + on: cr.user_id == u.id, + full_join: tm in TeamMember, + on: cr.id == tm.student_id, + full_join: s in Submission, + on: tm.team_id == s.team_id or s.student_id == cr.id, + full_join: a in Answer, + on: s.id == a.submission_id, + where: s.is_grading_published == true and cr.course_id == ^course_id, + group_by: [cr.id, u.name, u.username, s.id], + select: %{ + user_id: cr.id, + name: u.name, + username: u.username, + submission_xp: sum(a.xp) + sum(a.xp_adjustment) + max(s.xp_bonus) + } + ) + ), + group_by: [sub_xp.user_id, sub_xp.name, sub_xp.username], + select: %{ + user_id: sub_xp.user_id, + name: sub_xp.name, + username: sub_xp.username, + submission_xp: sum(sub_xp.submission_xp) + } + ) + + total_xp_query = + from(bu in subquery(base_user_query), + left_join: ax in subquery(achievements_xp_final_query), + on: bu.user_id == ax.user_id, + left_join: sx in subquery(submissions_xp_query), + on: bu.user_id == sx.user_id, + select: %{ + user_id: bu.user_id, + name: bu.name, + username: bu.username, + total_xp: + fragment( + "COALESCE(?, 0) + COALESCE(?, 0)", + ax.achievements_xp, + sx.submission_xp + ) + }, + order_by: [desc: fragment("total_xp")] + ) + + ranked_xp_query = + from(t in subquery(total_xp_query), + select: %{ + rank: fragment("ROW_NUMBER() OVER (ORDER BY total_xp DESC)"), + user_id: t.user_id, + name: t.name, + username: t.username, + total_xp: t.total_xp + } + ) + + Repo.all(ranked_xp_query) + end + defp decimal_to_integer(decimal) do if Decimal.is_decimal(decimal) do Decimal.to_integer(decimal) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 53add9133..f480aad0d 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -6,7 +6,7 @@ defmodule CadetWeb.AdminUserController do alias Cadet.Repo alias Cadet.{Accounts, Assessments, Courses} - alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role} + alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role, User} # This controller is used to find all users of a course @@ -28,6 +28,11 @@ defmodule CadetWeb.AdminUserController do json(conn, %{totalXp: total_xp}) end + def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do + users_with_xp = Assessments.all_user_total_xp(course_id) + json(conn, %{users: users_with_xp}) + end + @add_users_role ~w(admin)a def get_students(conn, filter) do users = diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1ee304c01..208b7a663 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -118,6 +118,8 @@ defmodule CadetWeb.Router do put("/user/game_states", UserController, :update_game_states) put("/user/research_agreement", UserController, :update_research_agreement) + get("/all_users_xp", AdminUserController, :combined_total_xp_for_all_users) + get("/config", CoursesController, :index) get("/team/:assessmentid", TeamController, :index) From 309354440cd89b8463f8c4aa0084abfc35f95af3 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Sat, 8 Mar 2025 20:38:17 +0800 Subject: [PATCH 05/30] add top leaderboard display options to course settings (select how many to display in leaderboard) --- lib/cadet/courses/course.ex | 8 +++++--- .../admin_controllers/admin_courses_controller.ex | 5 +++-- lib/cadet_web/controllers/courses_controller.ex | 13 ++++++++----- lib/cadet_web/controllers/user_controller.ex | 8 +++++--- lib/cadet_web/views/courses_view.ex | 3 ++- lib/cadet_web/views/user_view.ex | 3 ++- .../20210531155751_multitenant_upgrade.exs | 6 ++++-- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 51a23bf75..283b2f4bf 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -12,8 +12,9 @@ defmodule Cadet.Courses.Course do viewable: boolean(), enable_game: boolean(), enable_achievements: boolean(), - enable_contest_leaderboard: boolean(), enable_overall_leaderboard: boolean(), + enable_contest_leaderboard: boolean(), + top_leaderboard_display: integer(), enable_sourcecast: boolean(), enable_stories: boolean(), source_chapter: integer(), @@ -28,8 +29,9 @@ defmodule Cadet.Courses.Course do field(:viewable, :boolean, default: true) field(:enable_game, :boolean, default: true) field(:enable_achievements, :boolean, default: true) - field(:enable_contest_leaderboard, :boolean, default: true) field(:enable_overall_leaderboard, :boolean, default: true) + field(:enable_contest_leaderboard, :boolean, default: true) + field(:top_leaderboard_display, :integer) field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:source_chapter, :integer) @@ -45,7 +47,7 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - enable_achievements enable_sourcecast enable_stories source_chapter source_variant enable_contest_leaderboard enable_overall_leaderboard)a + enable_achievements enable_overall_leaderboard enable_contest_leaderboard top_leaderboard_display enable_sourcecast enable_stories source_chapter source_variant)a @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 7f671095e..ac152115e 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -106,8 +106,9 @@ defmodule CadetWeb.AdminCoursesController do viewable(:body, :boolean, "Course viewability") enable_game(:body, :boolean, "Enable game") enable_achievements(:body, :boolean, "Enable achievements") - enable_contest_leaderboard(:body, :boolean, "Enable Contest Leaderboard") - enable_overall_leaderboard(:body, :boolean, "Enable Overall Leaderboard") + enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard") + enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard") + top_leaderboard_display(:body, :integer, "Top Leaderboard Display") enable_sourcecast(:body, :boolean, "Enable sourcecast") enable_stories(:body, :boolean, "Enable stories") sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 71adda14c..65a1e4870 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -54,8 +54,9 @@ defmodule CadetWeb.CoursesController do viewable(:body, :boolean, "Course viewability", required: true) enable_game(:body, :boolean, "Enable game", required: true) enable_achievements(:body, :boolean, "Enable achievements", required: true) - enable_contest_leaderboard(:body, :boolean, "Enable Contest Leaderboard", required: true) - enable_overall_leaderboard(:body, :boolean, "Enable Overall Leaderboard", required: true) + enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard", required: true) + enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard", required: true) + top_leaderboard_display(:body, :number, "Top leaderboard display", required: true) enable_sourcecast(:body, :boolean, "Enable sourcecast", required: true) enable_stories(:body, :boolean, "Enable stories", required: true) source_chapter(:body, :number, "Default source chapter", required: true) @@ -97,8 +98,9 @@ defmodule CadetWeb.CoursesController do viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) - enable_contest_leaderboard(:boolean, "Enable Contest Leaderboard", required: true) - enable_overall_leaderboard(:boolean, "Enable Overall Leaderboard", required: true) + enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) + enable_contest_leaderboard(:boolean, "Enable contest leaderboard", required: true) + top_leaderboard_display(:boolean, "Top leaderboard display", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -113,8 +115,9 @@ defmodule CadetWeb.CoursesController do viewable: true, enable_game: true, enable_achievements: true, - enable_contest_leaderboard: true, enable_overall_leaderboard: true, + enable_contest_leaderboard: true, + top_leaderboard_display: "100", enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 434560f1c..e12f97452 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -315,8 +315,9 @@ defmodule CadetWeb.UserController do viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) - enable_contest_leaderboard(:boolean, "Enable Contest Leaderboard", required: true) - enable_overall_leaderboard(:boolean, "Enable Overall Leaderboard", required: true) + enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) + enable_contest_leaderboard(:boolean, "Enable contest leadeboard", required: true) + top_leaderboard_display(:integer, "Top leaderboard display", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -332,8 +333,9 @@ defmodule CadetWeb.UserController do viewable: true, enable_game: true, enable_achievements: true, - enable_contest_leaderboard: true, enable_overall_leaderboard: true, + enable_contest_leaderboard: true, + top_leaderboard_display: 100, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 4939a5ff4..80c8bf3ef 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -10,8 +10,9 @@ defmodule CadetWeb.CoursesView do viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, - enableContestLeaderboard: :enable_contest_leaderboard, enableOverallLeaderboard: :enable_overall_leaderboard, + enableContestLeaderboard: :enable_contest_leaderboard, + topLeaderboardDisplay: :top_leaderboard_display, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index ebf4d6bf6..091e8d5ae 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -103,8 +103,9 @@ defmodule CadetWeb.UserView do viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, - enableContestLeaderboard: :enable_contest_leaderboard, enableOverallLeaderboard: :enable_overall_leaderboard, + enableContestLeaderboard: :enable_contest_leaderboard, + topLeaderboardDisplay: :top_leaderboard_display, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index dfc39402e..b5e41fd78 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -10,8 +10,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:viewable, :boolean, null: false, default: true) add(:enable_game, :boolean, null: false, default: true) add(:enable_achievements, :boolean, null: false, default: true) - add(:enable_contest_leaderboard, :boolean, null: false, default: true) add(:enable_overall_leaderboard, :boolean, null: false, default: true) + add(:enable_contest_leaderboard, :boolean, null: false, default: true) + add(:top_leaderboard_display, :integer, default: 100) add(:enable_sourcecast, :boolean, null: false, default: true) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) @@ -145,8 +146,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do viewable: true, enable_game: true, enable_achievements: true, - enable_contest_leaderboard: true, enable_overall_leaderboard: true, + enable_contest_leaderboard: true, + top_leaderboard_display: 100, enable_sourcecast: true, source_chapter: 1, source_variant: "default", From fe9b9bf01c6d30c39d7fd3d079af1b67f1bd21c3 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Sat, 8 Mar 2025 20:39:30 +0800 Subject: [PATCH 06/30] added contest scores fetching and contest score calculation --- lib/cadet/assessments/assessments.ex | 96 +++++++++++++++++++ .../admin_assessments_controller.ex | 43 +++++++++ lib/cadet_web/router.ex | 18 ++++ 3 files changed, 157 insertions(+) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 652081681..fba720da2 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -148,6 +148,7 @@ defmodule Cadet.Assessments do end def all_user_total_xp(course_id) do + # get all users even if they have 0 xp base_user_query = from( cr in CourseRegistration, @@ -255,6 +256,7 @@ defmodule Cadet.Assessments do order_by: [desc: fragment("total_xp")] ) + # add rank index ranked_xp_query = from(t in subquery(total_xp_query), select: %{ @@ -1800,6 +1802,100 @@ defmodule Cadet.Assessments do ) end + def fetch_contest_voting_assesment_id(assessment_id) do + contest_number = + Assessment + |> where(id: ^assessment_id) + |> select([a], a.number) + |> Repo.one() + + if is_nil(contest_number) do + nil + else + Assessment + |> join(:inner, [a], q in assoc(a, :questions)) + |> where([a, q], q.question["contest_number"] == ^contest_number) + |> select([a], a.id) + |> Repo.one() + end + end + + @doc """ + Fetches all contest scores for the given question, sorted by relative score + + Used for contest leaderboard fetching + """ + def fetch_contest_relative_scores(question_id) do + subquery = + Answer + |> where(question_id: ^question_id) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) + |> order_by(desc: :relative_score) + |> join(:left, [a], s in assoc(a, :submission)) + |> join(:left, [a, s], student in assoc(s, :student)) + |> join(:inner, [a, s, student], student_user in assoc(student, :user)) + # |> where([a, s, student], student.role == "student") + |> select([a, s, student, student_user], %{ + submission_id: a.submission_id, + code: a.answer["code"], + score: a.relative_score, + name: student_user.name, + username: student_user.username + }) + + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) + } + ) + |> Repo.all() + end + + @doc """ + Fetches all contest scores for the given question, sorted by popular score + + Used for contest leaderboard fetching + """ + def fetch_contest_popular_scores(question_id) do + subquery = + Answer + |> where(question_id: ^question_id) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) + |> order_by(desc: :popular_score) + |> join(:left, [a], s in assoc(a, :submission)) + |> join(:left, [a, s], student in assoc(s, :student)) + |> join(:inner, [a, s, student], student_user in assoc(student, :user)) + # |> where([a, s, student], student.role == "student") + |> select([a, s, student, student_user], %{ + submission_id: a.submission_id, + code: a.answer["code"], + score: a.popular_score, + name: student_user.name, + username: student_user.username + }) + + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) + } + ) + |> Repo.all() + end + @doc """ Fetches top answers for the given question, based on the contest relative_score diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 9263195f2..02770de20 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -173,6 +173,49 @@ defmodule CadetWeb.AdminAssessmentsController do render(conn, "leaderboard.json", leaderboard: result) end + def calculate_contest_score(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do + voting_questions = + Question + |> where(type: :voting) + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + if voting_questions do + Assessments.compute_relative_score(voting_questions.id) + text(conn, "CONTEST SCORE CALCULATED") + else + text(conn, "No voting questions found for the given assessment") + end + end + + def get_contest_popular_scores(conn, %{ + "assessmentid" => assessment_id, + "course_id" => course_id + }) do + contest_id = + Question + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + contestpop = Assessments.fetch_contest_popular_scores(contest_id.id) + voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id) + json(conn, %{contest_popular: contestpop, voting_id: voting_id}) + end + + def get_contest_relative_scores(conn, %{ + "assessmentid" => assessment_id, + "course_id" => course_id + }) do + contest_id = + Question + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + contestscore = Assessments.fetch_contest_relative_scores(contest_id.id) + voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id) + json(conn, %{contest_score: contestscore, voting_id: voting_id}) + end + defp check_dates(open_at, close_at, assessment) do if is_nil(open_at) and is_nil(close_at) do {:ok, assessment} diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 208b7a663..40d3e1900 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -120,6 +120,18 @@ defmodule CadetWeb.Router do get("/all_users_xp", AdminUserController, :combined_total_xp_for_all_users) + get( + "/leaderboard/contests/:assessmentid/get_score_leaderboard", + AdminAssessmentsController, + :get_contest_relative_scores + ) + + get( + "/leaderboard/contests/:assessmentid/get_popular_vote_leaderboard", + AdminAssessmentsController, + :get_contest_popular_scores + ) + get("/config", CoursesController, :index) get("/team/:assessmentid", TeamController, :index) @@ -181,6 +193,12 @@ defmodule CadetWeb.Router do resources("/sourcecast", AdminSourcecastController, only: [:create, :delete]) + post( + "/assessments/:assessmentid/calculateContestScore", + AdminAssessmentsController, + :calculate_contest_score + ) + get( "/assessments/:assessmentid/popularVoteLeaderboard", AdminAssessmentsController, From 595bdeaabcbdf06b311571d1afc7bfa1ce3b0a42 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Sun, 9 Mar 2025 00:27:18 +0800 Subject: [PATCH 07/30] Refactor query execution in assessments module for improved readability --- lib/cadet/assessments/assessments.ex | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index fba720da2..d5435dcce 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1850,12 +1850,14 @@ defmodule Cadet.Assessments do username: student_user.username }) - from(sub in subquery(subquery), - select_merge: %{ - rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) - } - ) - |> Repo.all() + query = + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) + } + ) + + Repo.all(query) end @doc """ @@ -1888,12 +1890,14 @@ defmodule Cadet.Assessments do username: student_user.username }) - from(sub in subquery(subquery), - select_merge: %{ - rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) - } - ) - |> Repo.all() + query = + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) + } + ) + + Repo.all(query) end @doc """ From c806f63f33c1de940dc17bde1f40dac574ba5fd2 Mon Sep 17 00:00:00 2001 From: Blerargh Date: Sun, 9 Mar 2025 16:27:43 +0800 Subject: [PATCH 08/30] added functions to fetch contest scoring and voting --- lib/cadet/assessments/assessments.ex | 84 +++++++++++++++++++ .../admin_assessments_controller.ex | 45 ++++++++++ lib/cadet_web/router.ex | 18 ++++ 3 files changed, 147 insertions(+) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 652081681..fe251b86f 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -148,6 +148,7 @@ defmodule Cadet.Assessments do end def all_user_total_xp(course_id) do + # get all users even if they have 0 xp base_user_query = from( cr in CourseRegistration, @@ -255,6 +256,7 @@ defmodule Cadet.Assessments do order_by: [desc: fragment("total_xp")] ) + # add rank index ranked_xp_query = from(t in subquery(total_xp_query), select: %{ @@ -269,6 +271,12 @@ defmodule Cadet.Assessments do Repo.all(ranked_xp_query) end + def calculate_contest_score_update_table(course_id, assessment_id) do + # calculate contest score for all users + + # update table + end + defp decimal_to_integer(decimal) do if Decimal.is_decimal(decimal) do Decimal.to_integer(decimal) @@ -1800,6 +1808,82 @@ defmodule Cadet.Assessments do ) end + @doc """ + Fetches all contest scores for the given question, sorted by relative score + + Used for contest leaderboard fetching + """ + def fetch_contest_relative_scores(question_id) do + subquery = + Answer + |> where(question_id: ^question_id) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) + |> order_by(desc: :relative_score) + |> join(:left, [a], s in assoc(a, :submission)) + |> join(:left, [a, s], student in assoc(s, :student)) + |> join(:inner, [a, s, student], student_user in assoc(student, :user)) + |> where([a, s, student], student.role == "student") + |> select([a, s, student, student_user], %{ + submission_id: a.submission_id, + code: a.answer, + score: a.relative_score, + name: student_user.name, + username: student_user.username + }) + + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("RANK() OVER (ORDER BY ? DESC)", sub.relative_score) + } + ) + |> Repo.all() + end + + @doc """ + Fetches all contest scores for the given question, sorted by popular score + + Used for contest leaderboard fetching + """ + def fetch_contest_popular_scores(question_id) do + subquery = + Answer + |> where(question_id: ^question_id) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) + |> order_by(desc: :popular_score) + |> join(:left, [a], s in assoc(a, :submission)) + |> join(:left, [a, s], student in assoc(s, :student)) + |> join(:inner, [a, s, student], student_user in assoc(student, :user)) + |> where([a, s, student], student.role == "student") + |> select([a, s, student, student_user], %{ + submission_id: a.submission_id, + code: a.answer, + score: a.popular_score, + name: student_user.name, + username: student_user.username + }) + + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("RANK() OVER (ORDER BY ? DESC)", sub.popular_score) + } + ) + |> Repo.all() + end + @doc """ Fetches top answers for the given question, based on the contest relative_score diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 9263195f2..0c9df4ad1 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -173,6 +173,51 @@ defmodule CadetWeb.AdminAssessmentsController do render(conn, "leaderboard.json", leaderboard: result) end + def calculate_contest_score(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do + voting_questions = + Question + |> where(type: :voting) + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions) + + Assessments.compute_relative_score(contest_id) + text(conn, "CONTEST SCORE CALCULATED") + end + + def get_contest_popular_scores(conn, %{ + "assessmentid" => assessment_id, + "course_id" => course_id + }) do + voting_questions = + Question + |> where(type: :voting) + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions) + + contestpop = Assessments.fetch_contest_popular_scores(contest_id) + json(conn, %{contest_popular: contestpop}) + end + + def get_contest_relative_scores(conn, %{ + "assessmentid" => assessment_id, + "course_id" => course_id + }) do + voting_questions = + Question + |> where(type: :voting) + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions) + + contestscore = Assessments.fetch_contest_relative_scores(contest_id) + json(conn, %{contest_score: contestscore}) + end + defp check_dates(open_at, close_at, assessment) do if is_nil(open_at) and is_nil(close_at) do {:ok, assessment} diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 208b7a663..40d3e1900 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -120,6 +120,18 @@ defmodule CadetWeb.Router do get("/all_users_xp", AdminUserController, :combined_total_xp_for_all_users) + get( + "/leaderboard/contests/:assessmentid/get_score_leaderboard", + AdminAssessmentsController, + :get_contest_relative_scores + ) + + get( + "/leaderboard/contests/:assessmentid/get_popular_vote_leaderboard", + AdminAssessmentsController, + :get_contest_popular_scores + ) + get("/config", CoursesController, :index) get("/team/:assessmentid", TeamController, :index) @@ -181,6 +193,12 @@ defmodule CadetWeb.Router do resources("/sourcecast", AdminSourcecastController, only: [:create, :delete]) + post( + "/assessments/:assessmentid/calculateContestScore", + AdminAssessmentsController, + :calculate_contest_score + ) + get( "/assessments/:assessmentid/popularVoteLeaderboard", AdminAssessmentsController, From f99a898e3e5d93da7c9fbc00d78cec217206aa81 Mon Sep 17 00:00:00 2001 From: Blerargh Date: Sun, 9 Mar 2025 16:28:12 +0800 Subject: [PATCH 09/30] changes to default values --- lib/cadet/courses/course.ex | 2 +- lib/cadet_web/admin_controllers/admin_user_controller.ex | 2 +- lib/cadet_web/controllers/courses_controller.ex | 2 +- lib/cadet_web/controllers/user_controller.ex | 2 +- lib/mix/tasks/token.ex | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 94ab6580c..283b2f4bf 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -29,7 +29,7 @@ defmodule Cadet.Courses.Course do field(:viewable, :boolean, default: true) field(:enable_game, :boolean, default: true) field(:enable_achievements, :boolean, default: true) - field(:enable_overall_leaderboard, :boolean, default: false) + field(:enable_overall_leaderboard, :boolean, default: true) field(:enable_contest_leaderboard, :boolean, default: true) field(:top_leaderboard_display, :integer) field(:enable_sourcecast, :boolean, default: true) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index f480aad0d..7734f47f4 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -6,7 +6,7 @@ defmodule CadetWeb.AdminUserController do alias Cadet.Repo alias Cadet.{Accounts, Assessments, Courses} - alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role, User} + alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role} # This controller is used to find all users of a course diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index ca216f43f..65a1e4870 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -115,7 +115,7 @@ defmodule CadetWeb.CoursesController do viewable: true, enable_game: true, enable_achievements: true, - enable_overall_leaderboard: false, + enable_overall_leaderboard: true, enable_contest_leaderboard: true, top_leaderboard_display: "100", enable_sourcecast: true, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index a1f5f1342..e12f97452 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -333,7 +333,7 @@ defmodule CadetWeb.UserController do viewable: true, enable_game: true, enable_achievements: true, - enable_overall_leaderboard: false, + enable_overall_leaderboard: true, enable_contest_leaderboard: true, top_leaderboard_display: 100, enable_sourcecast: true, diff --git a/lib/mix/tasks/token.ex b/lib/mix/tasks/token.ex index 808ded384..3a225062c 100644 --- a/lib/mix/tasks/token.ex +++ b/lib/mix/tasks/token.ex @@ -62,7 +62,7 @@ defmodule Mix.Tasks.Cadet.Token do if Cadet.Env.env() in @env_allow_mock do user = User - |> where(role: ^role) + |> where(username: ^role) |> first |> Repo.one() From f42c4cd90536e000fff5738f74531897584e88fd Mon Sep 17 00:00:00 2001 From: Blerargh Date: Sun, 9 Mar 2025 17:12:14 +0800 Subject: [PATCH 10/30] updated tests --- lib/cadet/courses/course.ex | 2 +- .../controllers/courses_controller.ex | 2 +- test/cadet/courses/course_test.exs | 68 ++++++++++++++----- test/cadet/courses/courses_test.exs | 3 + .../controllers/courses_controller_test.exs | 6 ++ .../controllers/user_controller_test.exs | 6 ++ 6 files changed, 69 insertions(+), 18 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 283b2f4bf..e4032fb05 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -31,7 +31,7 @@ defmodule Cadet.Courses.Course do field(:enable_achievements, :boolean, default: true) field(:enable_overall_leaderboard, :boolean, default: true) field(:enable_contest_leaderboard, :boolean, default: true) - field(:top_leaderboard_display, :integer) + field(:top_leaderboard_display, :integer, default: 100) field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:source_chapter, :integer) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 65a1e4870..ca6c4fb63 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -117,7 +117,7 @@ defmodule CadetWeb.CoursesController do enable_achievements: true, enable_overall_leaderboard: true, enable_contest_leaderboard: true, - top_leaderboard_display: "100", + top_leaderboard_display: 100, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/test/cadet/courses/course_test.exs b/test/cadet/courses/course_test.exs index 4e852596e..2de56a73a 100644 --- a/test/cadet/courses/course_test.exs +++ b/test/cadet/courses/course_test.exs @@ -9,7 +9,8 @@ defmodule Cadet.Courses.CourseTest do %{ course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -19,7 +20,8 @@ defmodule Cadet.Courses.CourseTest do course_short_name: "CS2040S", course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -29,7 +31,8 @@ defmodule Cadet.Courses.CourseTest do viewable: false, course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -39,7 +42,8 @@ defmodule Cadet.Courses.CourseTest do enable_game: false, course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -49,7 +53,8 @@ defmodule Cadet.Courses.CourseTest do enable_achievements: false, course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -59,7 +64,8 @@ defmodule Cadet.Courses.CourseTest do enable_sourcecast: false, course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -69,7 +75,8 @@ defmodule Cadet.Courses.CourseTest do module_help_text: "", course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -79,7 +86,8 @@ defmodule Cadet.Courses.CourseTest do module_help_text: "Module help text", course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -91,7 +99,8 @@ defmodule Cadet.Courses.CourseTest do enable_sourcecast: true, course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -104,7 +113,8 @@ defmodule Cadet.Courses.CourseTest do enable_stories: false, course_name: "Data Structures and Algorithms", source_chapter: 1, - source_variant: "default" + source_variant: "default", + top_leaderboard_display: 100 }, :valid ) @@ -113,7 +123,8 @@ defmodule Cadet.Courses.CourseTest do %{ source_chapter: 1, source_variant: "wasm", - course_name: "Data Structures and Algorithms" + course_name: "Data Structures and Algorithms", + top_leaderboard_display: 100 }, :valid ) @@ -122,7 +133,8 @@ defmodule Cadet.Courses.CourseTest do %{ source_chapter: 2, source_variant: "lazy", - course_name: "Data Structures and Algorithms" + course_name: "Data Structures and Algorithms", + top_leaderboard_display: 100 }, :valid ) @@ -131,7 +143,8 @@ defmodule Cadet.Courses.CourseTest do %{ source_chapter: 3, source_variant: "non-det", - course_name: "Data Structures and Algorithms" + course_name: "Data Structures and Algorithms", + top_leaderboard_display: 100 }, :valid ) @@ -140,7 +153,8 @@ defmodule Cadet.Courses.CourseTest do %{ source_chapter: 3, source_variant: "native", - course_name: "Data Structures and Algorithms" + course_name: "Data Structures and Algorithms", + top_leaderboard_display: 100 }, :valid ) @@ -149,7 +163,8 @@ defmodule Cadet.Courses.CourseTest do %{ source_chapter: 2, source_variant: "typed", - course_name: "Data Structures and Algorithms" + course_name: "Data Structures and Algorithms", + top_leaderboard_display: 100 }, :valid ) @@ -159,7 +174,28 @@ defmodule Cadet.Courses.CourseTest do source_chapter: 4, source_variant: "default", enable_achievements: true, - course_name: "Data Structures and Algorithms" + course_name: "Data Structures and Algorithms", + top_leaderboard_display: 100 + }, + :valid + ) + + assert_changeset( + %{ + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default", + top_leaderboard_display: 200 + }, + :valid + ) + + assert_changeset( + %{ + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default", + top_leaderboard_display: 350 }, :valid ) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index fc8880f79..5d8ce728a 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -19,6 +19,9 @@ defmodule Cadet.CoursesTest do viewable: true, enable_game: true, enable_achievements: true, + enable_overall_leaderboard: true, + enable_contest_leaderboard: true, + top_leaderboard_display: 100, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 1a24caebf..b1de9af37 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -26,6 +26,9 @@ defmodule CadetWeb.CoursesControllerTest do "viewable" => "true", "enable_game" => "true", "enable_achievements" => "true", + "enable_overall_leaderboard" => "true", + "enable_contest_leaderboard" => "true", + "top_leaderboard_display" => "100", "enable_sourcecast" => "true", "enable_stories" => "true", "source_chapter" => "1", @@ -118,6 +121,9 @@ defmodule CadetWeb.CoursesControllerTest do "viewable" => "true", "enable_game" => "true", "enable_achievements" => "true", + "enable_overall_leaderboard" => "true", + "enable_contest_leaderboard" => "true", + "top_leaderboard_display" => "100", "enable_sourcecast" => "true", "enable_stories" => "true", "source_chapter" => "1", diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 18264bd1b..7c4a2c0f2 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -114,6 +114,9 @@ defmodule CadetWeb.UserControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true, + "enableContestLeaderboard" => true, + "enableOverallLeaderboard" => true, + "topLeaderboardDisplay" => 100, "assetsPrefix" => Courses.assets_prefix(course) }, "assessmentConfigurations" => [ @@ -325,6 +328,9 @@ defmodule CadetWeb.UserControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true, + "enableContestLeaderboard" => true, + "enableOverallLeaderboard" => true, + "topLeaderboardDisplay" => 100, "assetsPrefix" => Courses.assets_prefix(course) }, "assessmentConfigurations" => [] From 82e633f58ecf5a683c408fa8cb0acc645b7bd133 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 12 Mar 2025 12:22:26 +0800 Subject: [PATCH 11/30] Fixed xp fetching for all users --- lib/cadet/assessments/assessments.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index d5435dcce..fd2b83a3c 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -218,12 +218,12 @@ defmodule Cadet.Assessments do full_join: a in Answer, on: s.id == a.submission_id, where: s.is_grading_published == true and cr.course_id == ^course_id, - group_by: [cr.id, u.name, u.username, s.id], + group_by: [cr.id, u.name, u.username, s.id, a.xp, a.xp_adjustment], select: %{ user_id: cr.id, name: u.name, username: u.username, - submission_xp: sum(a.xp) + sum(a.xp_adjustment) + max(s.xp_bonus) + submission_xp: a.xp + a.xp_adjustment + max(s.xp_bonus) } ) ), From 64e812b1e1e43547eebe080b6e9f3a76fc4a3351 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Mon, 17 Mar 2025 21:53:25 +0800 Subject: [PATCH 12/30] Add top contest leaderboard display configuration and update related tests. Updated leaderboard fetching and exporting for assessment workspace leaderboard. Added Leaderboard Dropdown contests fetching. --- lib/cadet/assessments/assessments.ex | 92 ++++++++++++------- lib/cadet/courses/course.ex | 4 +- .../admin_assessments_controller.ex | 23 ++++- .../admin_courses_controller.ex | 1 + .../controllers/courses_controller.ex | 11 +++ lib/cadet_web/controllers/user_controller.ex | 6 ++ lib/cadet_web/router.ex | 6 +- lib/cadet_web/views/courses_view.ex | 1 + lib/cadet_web/views/user_view.ex | 1 + .../20210531155751_multitenant_upgrade.exs | 2 + test/cadet/courses/course_test.exs | 54 +++++++---- test/cadet/courses/courses_test.exs | 1 + .../controllers/courses_controller_test.exs | 2 + .../controllers/user_controller_test.exs | 2 + 14 files changed, 148 insertions(+), 58 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index fd2b83a3c..2000ff973 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -412,6 +412,13 @@ defmodule Cadet.Assessments do |> join(:inner, [a], s in assoc(a, :submission)) |> where([_, s], s.student_id == ^course_reg.id or s.team_id == ^team_id) + visible_entries = + Assessment + |> join(:inner, [a], c in assoc(a, :course)) + |> where([a, c], a.id == ^id) + |> select([a, c], c.top_contest_leaderboard_display) + |> Repo.one() + questions = Question |> where(assessment_id: ^id) @@ -426,7 +433,7 @@ defmodule Cadet.Assessments do {q, a, nil, _} -> %{q | answer: %Answer{a | grader: nil}} {q, a, g, u} -> %{q | answer: %Answer{a | grader: %CourseRegistration{g | user: u}}} end) - |> load_contest_voting_entries(course_reg, assessment) + |> load_contest_voting_entries(course_reg, assessment, visible_entries) is_grading_published = Submission @@ -900,16 +907,16 @@ defmodule Cadet.Assessments do Submission |> join(:inner, [s], ans in assoc(s, :answers)) |> join(:inner, [s, ans], cr in assoc(s, :student)) - |> where([s, ans, cr], cr.role == "student") + # |> where([s, ans, cr], cr.role == "student") |> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted") - |> where( - [_, ans, cr], - fragment( - "?->>'code' like ?", - ans.answer, - "%return%" - ) - ) + # |> where( + # [_, ans, cr], + # fragment( + # "?->>'code' like ?", + # ans.answer, + # "%return%" + # ) + # ) |> select([s, _ans], {s.student_id, s.id}) |> Repo.all() |> Enum.into(%{}) @@ -918,7 +925,8 @@ defmodule Cadet.Assessments do voter_ids = CourseRegistration - |> where(role: "student", course_id: ^course_id) + # |> where(role: "student", course_id: ^course_id) + |> where(course_id: ^course_id) |> select([cr], cr.id) |> Repo.all() @@ -1716,7 +1724,8 @@ defmodule Cadet.Assessments do defp load_contest_voting_entries( questions, %CourseRegistration{role: role, course_id: course_id, id: voter_id}, - assessment + assessment, + visibleEntries ) do Enum.map( questions, @@ -1732,7 +1741,7 @@ defmodule Cadet.Assessments do [] else if leaderboard_open?(assessment, q) or role in @open_all_assessment_roles do - fetch_top_popular_score_answers(question_id, 10) + fetch_top_popular_score_answers(question_id, visibleEntries) else [] end @@ -1743,7 +1752,7 @@ defmodule Cadet.Assessments do [] else if leaderboard_open?(assessment, q) or role in @open_all_assessment_roles do - fetch_top_relative_score_answers(question_id, 10) + fetch_top_relative_score_answers(question_id, visibleEntries) else [] end @@ -1860,6 +1869,25 @@ defmodule Cadet.Assessments do Repo.all(query) end + @doc """ + Fetches all contests for the course id + + Used for contest leaderboard dropdown fetching + """ + def fetch_all_contests(course_id) do + assessments = + Assessment + |> where(course_id: ^course_id) + |> where(is_published: true) + |> join(:inner, [a], q in assoc(a, :questions)) + |> where([a, q], q.type != "voting") + |> join(:inner, [a, q], ac in AssessmentConfig, on: a.config_id == ac.id) + |> where([a, q, ac], ac.type == "Contests") + |> select([a], %{contest_id: a.id, title: a.title, published: a.is_published}) + + Repo.all(assessments) + end + @doc """ Fetches all contest scores for the given question, sorted by popular score @@ -1908,19 +1936,19 @@ defmodule Cadet.Assessments do def fetch_top_relative_score_answers(question_id, number_of_answers) do Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, @@ -1939,19 +1967,19 @@ defmodule Cadet.Assessments do def fetch_top_popular_score_answers(question_id, number_of_answers) do Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index e4032fb05..7ddd80a49 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -15,6 +15,7 @@ defmodule Cadet.Courses.Course do enable_overall_leaderboard: boolean(), enable_contest_leaderboard: boolean(), top_leaderboard_display: integer(), + top_contest_leaderboard_display: integer(), enable_sourcecast: boolean(), enable_stories: boolean(), source_chapter: integer(), @@ -32,6 +33,7 @@ defmodule Cadet.Courses.Course do field(:enable_overall_leaderboard, :boolean, default: true) field(:enable_contest_leaderboard, :boolean, default: true) field(:top_leaderboard_display, :integer, default: 100) + field(:top_contest_leaderboard_display, :integer, default: 10) field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:source_chapter, :integer) @@ -47,7 +49,7 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - enable_achievements enable_overall_leaderboard enable_contest_leaderboard top_leaderboard_display enable_sourcecast enable_stories source_chapter source_variant)a + enable_achievements enable_overall_leaderboard enable_contest_leaderboard top_leaderboard_display top_contest_leaderboard_display enable_sourcecast enable_stories source_chapter source_variant)a @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 02770de20..3b0280639 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -135,7 +135,11 @@ defmodule CadetWeb.AdminAssessmentsController do end end - def get_score_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do + def get_score_leaderboard(conn, %{ + "assessmentid" => assessment_id, + "visibleentries" => visible_entries, + "course_id" => course_id + }) do voting_questions = Question |> where(type: :voting) @@ -146,7 +150,7 @@ defmodule CadetWeb.AdminAssessmentsController do result = contest_id - |> Assessments.fetch_top_relative_score_answers(10) + |> Assessments.fetch_top_relative_score_answers(visible_entries) |> Enum.map(fn entry -> AssessmentsHelpers.build_contest_leaderboard_entry(entry) end) @@ -154,7 +158,11 @@ defmodule CadetWeb.AdminAssessmentsController do render(conn, "leaderboard.json", leaderboard: result) end - def get_popular_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do + def get_popular_leaderboard(conn, %{ + "assessmentid" => assessment_id, + "visibleentries" => visible_entries, + "course_id" => course_id + }) do voting_questions = Question |> where(type: :voting) @@ -165,7 +173,7 @@ defmodule CadetWeb.AdminAssessmentsController do result = contest_id - |> Assessments.fetch_top_popular_score_answers(10) + |> Assessments.fetch_top_popular_score_answers(visible_entries) |> Enum.map(fn entry -> AssessmentsHelpers.build_popular_leaderboard_entry(entry) end) @@ -216,6 +224,11 @@ defmodule CadetWeb.AdminAssessmentsController do json(conn, %{contest_score: contestscore, voting_id: voting_id}) end + def get_all_contests(conn, %{"course_id" => course_id}) do + contests = Assessments.fetch_all_contests(course_id) + json(conn, contests) + end + defp check_dates(open_at, close_at, assessment) do if is_nil(open_at) and is_nil(close_at) do {:ok, assessment} @@ -331,7 +344,7 @@ defmodule CadetWeb.AdminAssessmentsController do swagger_path :get_score_leaderboard do get("/courses/{course_id}/admin/assessments/:assessmentid/scoreLeaderboard") - summary("get the top 10 contest entries based on score") + summary("get the top X contest entries based on score") security([%{JWT: []}]) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index ac152115e..830cb06f9 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -109,6 +109,7 @@ defmodule CadetWeb.AdminCoursesController do enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard") enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard") top_leaderboard_display(:body, :integer, "Top Leaderboard Display") + top_contest_leaderboard_display(:body, :integer, "Top Contest Leaderboard Display") enable_sourcecast(:body, :boolean, "Enable sourcecast") enable_stories(:body, :boolean, "Enable stories") sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index ca6c4fb63..cb8cf68dd 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -57,6 +57,11 @@ defmodule CadetWeb.CoursesController do enable_overall_leaderboard(:body, :boolean, "Enable overall leaderboard", required: true) enable_contest_leaderboard(:body, :boolean, "Enable contest leaderboard", required: true) top_leaderboard_display(:body, :number, "Top leaderboard display", required: true) + + top_contest_leaderboard_display(:body, :number, "Top contest leaderboard display", + required: true + ) + enable_sourcecast(:body, :boolean, "Enable sourcecast", required: true) enable_stories(:body, :boolean, "Enable stories", required: true) source_chapter(:body, :number, "Default source chapter", required: true) @@ -101,6 +106,11 @@ defmodule CadetWeb.CoursesController do enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) enable_contest_leaderboard(:boolean, "Enable contest leaderboard", required: true) top_leaderboard_display(:boolean, "Top leaderboard display", required: true) + + top_contest_leaderboard_display(:boolean, "Top contest leaderboard display", + required: true + ) + enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -118,6 +128,7 @@ defmodule CadetWeb.CoursesController do enable_overall_leaderboard: true, enable_contest_leaderboard: true, top_leaderboard_display: 100, + top_contest_leaderboard_display: 10, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index e12f97452..02abbc05f 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -318,6 +318,11 @@ defmodule CadetWeb.UserController do enable_overall_leaderboard(:boolean, "Enable overall leaderboard", required: true) enable_contest_leaderboard(:boolean, "Enable contest leadeboard", required: true) top_leaderboard_display(:integer, "Top leaderboard display", required: true) + + top_contest_leaderboard_display(:integer, "Top contest leaderboard display", + required: true + ) + enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) @@ -336,6 +341,7 @@ defmodule CadetWeb.UserController do enable_overall_leaderboard: true, enable_contest_leaderboard: true, top_leaderboard_display: 100, + top_contest_leaderboard_display: 10, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 40d3e1900..569f12421 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -132,6 +132,8 @@ defmodule CadetWeb.Router do :get_contest_popular_scores ) + get("/all_contests", AdminAssessmentsController, :get_all_contests) + get("/config", CoursesController, :index) get("/team/:assessmentid", TeamController, :index) @@ -200,13 +202,13 @@ defmodule CadetWeb.Router do ) get( - "/assessments/:assessmentid/popularVoteLeaderboard", + "/assessments/:assessmentid/:visibleentries/popularVoteLeaderboard", AdminAssessmentsController, :get_popular_leaderboard ) get( - "/assessments/:assessmentid/scoreLeaderboard", + "/assessments/:assessmentid/:visibleentries/scoreLeaderboard", AdminAssessmentsController, :get_score_leaderboard ) diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 80c8bf3ef..b1db95a07 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -13,6 +13,7 @@ defmodule CadetWeb.CoursesView do enableOverallLeaderboard: :enable_overall_leaderboard, enableContestLeaderboard: :enable_contest_leaderboard, topLeaderboardDisplay: :top_leaderboard_display, + topContestLeaderboardDisplay: :top_contest_leaderboard_display, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 091e8d5ae..2529139ee 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -106,6 +106,7 @@ defmodule CadetWeb.UserView do enableOverallLeaderboard: :enable_overall_leaderboard, enableContestLeaderboard: :enable_contest_leaderboard, topLeaderboardDisplay: :top_leaderboard_display, + topContestLeaderboardDisplay: :top_contest_leaderboard_display, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, sourceChapter: :source_chapter, diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index b5e41fd78..654c16199 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -13,6 +13,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:enable_overall_leaderboard, :boolean, null: false, default: true) add(:enable_contest_leaderboard, :boolean, null: false, default: true) add(:top_leaderboard_display, :integer, default: 100) + add(:top_contest_leaderboard_display, :integer, default: 10) add(:enable_sourcecast, :boolean, null: false, default: true) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) @@ -149,6 +150,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do enable_overall_leaderboard: true, enable_contest_leaderboard: true, top_leaderboard_display: 100, + top_contest_leaderboard_display: 10, enable_sourcecast: true, source_chapter: 1, source_variant: "default", diff --git a/test/cadet/courses/course_test.exs b/test/cadet/courses/course_test.exs index 2de56a73a..74bfed6fe 100644 --- a/test/cadet/courses/course_test.exs +++ b/test/cadet/courses/course_test.exs @@ -10,7 +10,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -21,7 +22,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -32,7 +34,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -43,7 +46,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -54,7 +58,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -65,7 +70,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -76,7 +82,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -87,7 +94,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -100,7 +108,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -114,7 +123,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -124,7 +134,8 @@ defmodule Cadet.Courses.CourseTest do source_chapter: 1, source_variant: "wasm", course_name: "Data Structures and Algorithms", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -134,7 +145,8 @@ defmodule Cadet.Courses.CourseTest do source_chapter: 2, source_variant: "lazy", course_name: "Data Structures and Algorithms", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -144,7 +156,8 @@ defmodule Cadet.Courses.CourseTest do source_chapter: 3, source_variant: "non-det", course_name: "Data Structures and Algorithms", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -154,7 +167,8 @@ defmodule Cadet.Courses.CourseTest do source_chapter: 3, source_variant: "native", course_name: "Data Structures and Algorithms", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -164,7 +178,8 @@ defmodule Cadet.Courses.CourseTest do source_chapter: 2, source_variant: "typed", course_name: "Data Structures and Algorithms", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -175,7 +190,8 @@ defmodule Cadet.Courses.CourseTest do source_variant: "default", enable_achievements: true, course_name: "Data Structures and Algorithms", - top_leaderboard_display: 100 + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 }, :valid ) @@ -185,7 +201,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 200 + top_leaderboard_display: 200, + top_contest_leaderboard_display: 10 }, :valid ) @@ -195,7 +212,8 @@ defmodule Cadet.Courses.CourseTest do course_name: "Data Structures and Algorithms", source_chapter: 1, source_variant: "default", - top_leaderboard_display: 350 + top_leaderboard_display: 350, + top_contest_leaderboard_display: 10 }, :valid ) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 5d8ce728a..0a0e3a91c 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -22,6 +22,7 @@ defmodule Cadet.CoursesTest do enable_overall_leaderboard: true, enable_contest_leaderboard: true, top_leaderboard_display: 100, + top_contest_leaderboard_display: 10, enable_sourcecast: true, enable_stories: false, source_chapter: 1, diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index b1de9af37..876c0166e 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -29,6 +29,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_overall_leaderboard" => "true", "enable_contest_leaderboard" => "true", "top_leaderboard_display" => "100", + "top_contest_leaderboard_display" => "10", "enable_sourcecast" => "true", "enable_stories" => "true", "source_chapter" => "1", @@ -124,6 +125,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_overall_leaderboard" => "true", "enable_contest_leaderboard" => "true", "top_leaderboard_display" => "100", + "top_contest_leaderboard_display" => "10", "enable_sourcecast" => "true", "enable_stories" => "true", "source_chapter" => "1", diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 7c4a2c0f2..f74626b9f 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -117,6 +117,7 @@ defmodule CadetWeb.UserControllerTest do "enableContestLeaderboard" => true, "enableOverallLeaderboard" => true, "topLeaderboardDisplay" => 100, + "topContestLeaderboardDisplay" => 10, "assetsPrefix" => Courses.assets_prefix(course) }, "assessmentConfigurations" => [ @@ -331,6 +332,7 @@ defmodule CadetWeb.UserControllerTest do "enableContestLeaderboard" => true, "enableOverallLeaderboard" => true, "topLeaderboardDisplay" => 100, + "topContestLeaderboardDisplay" => 10, "assetsPrefix" => Courses.assets_prefix(course) }, "assessmentConfigurations" => [] From 731bf0939cbcf144c8dc199265fc348c0caaa3d3 Mon Sep 17 00:00:00 2001 From: Blerargh Date: Tue, 18 Mar 2025 16:06:05 +0800 Subject: [PATCH 13/30] Added automatic XP assignment for winning contest entries --- lib/cadet/assessments/assessments.ex | 84 ++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 2000ff973..a6ec72682 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -907,16 +907,16 @@ defmodule Cadet.Assessments do Submission |> join(:inner, [s], ans in assoc(s, :answers)) |> join(:inner, [s, ans], cr in assoc(s, :student)) - # |> where([s, ans, cr], cr.role == "student") + |> where([s, ans, cr], cr.role == "student") |> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted") - # |> where( - # [_, ans, cr], - # fragment( - # "?->>'code' like ?", - # ans.answer, - # "%return%" - # ) - # ) + |> where( + [_, ans, cr], + fragment( + "?->>'code' like ?", + ans.answer, + "%return%" + ) + ) |> select([s, _ans], {s.student_id, s.id}) |> Repo.all() |> Enum.into(%{}) @@ -925,8 +925,7 @@ defmodule Cadet.Assessments do voter_ids = CourseRegistration - # |> where(role: "student", course_id: ^course_id) - |> where(course_id: ^course_id) + |> where(role: "student", course_id: ^course_id) |> select([cr], cr.id) |> Repo.all() @@ -2029,7 +2028,10 @@ defmodule Cadet.Assessments do _ = voting_questions_to_update - |> Enum.map(fn qn -> compute_relative_score(qn.id) end) + |> Enum.each(fn qn -> + compute_relative_score(qn.id) + assign_winning_contest_entries_xp(qn.id) + end) Logger.info("Successfully update_final_contest_leaderboards") end @@ -2048,6 +2050,64 @@ defmodule Cadet.Assessments do |> Repo.all() end + @doc """ + Automatically assigns XP to the winning contest entries + """ + def assign_winning_contest_entries_xp(contest_voting_question_id) do + contest_id = + SubmissionVotes + |> where(question_id: ^contest_voting_question_id) + |> join(:inner, [sv], ans in Answer, on: sv.submission_id == ans.submission_id) + |> select([sv, ans], ans.question_id) + |> limit(1) + |> Repo.one() + + Repo.transaction(fn -> + winning_popular_entries = + Answer + |> where(question_id: ^contest_id) + |> order_by([a], desc: a.popular_score) + |> Repo.all() + + winning_popular_entries + |> Enum.with_index() + |> Enum.each(fn {entry, index} -> + increment = + case index do + 0 -> 300 + 1 -> 200 + 2 -> 100 + _ -> 0 + end + + Repo.update!(Ecto.Changeset.change(entry, %{xp_adjustment: increment})) + end) + + winning_score_entries = + Answer + |> where(question_id: ^contest_id) + |> order_by([a], desc: a.relative_score) + |> Repo.all() + + winning_score_entries + |> Enum.with_index() + |> Enum.each(fn {entry, index} -> + increment = + case index do + 0 -> 300 + 1 -> 200 + 2 -> 100 + _ -> 0 + end + + new_value = entry.xp_adjustment + increment + Repo.update!(Ecto.Changeset.change(entry, %{xp_adjustment: new_value})) + end) + end) + + Logger.info("XP assigned to winning contest entries") + end + @doc """ Computes the current relative_score of each voting submission answer based on current submitted votes. From 0e89b531a12639ea6765199bdc31c3c3b48a57f5 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Tue, 18 Mar 2025 22:08:03 +0800 Subject: [PATCH 14/30] Implement XP assignment for winning contest entries based on contest voting XML and added dispatch endpoint for XP customisation --- lib/cadet/assessments/assessments.ex | 77 ++++++++++--------- .../question_types/voting_question.ex | 3 +- lib/cadet/jobs/xml_parser.ex | 39 ++++++---- .../admin_assessments_controller.ex | 16 ++++ lib/cadet_web/router.ex | 6 ++ 5 files changed, 87 insertions(+), 54 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index a6ec72682..554947912 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -907,16 +907,16 @@ defmodule Cadet.Assessments do Submission |> join(:inner, [s], ans in assoc(s, :answers)) |> join(:inner, [s, ans], cr in assoc(s, :student)) - |> where([s, ans, cr], cr.role == "student") + # |> where([s, ans, cr], cr.role == "student") |> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted") - |> where( - [_, ans, cr], - fragment( - "?->>'code' like ?", - ans.answer, - "%return%" - ) - ) + # |> where( + # [_, ans, cr], + # fragment( + # "?->>'code' like ?", + # ans.answer, + # "%return%" + # ) + # ) |> select([s, _ans], {s.student_id, s.id}) |> Repo.all() |> Enum.into(%{}) @@ -925,7 +925,8 @@ defmodule Cadet.Assessments do voter_ids = CourseRegistration - |> where(role: "student", course_id: ^course_id) + # |> where(role: "student", course_id: ^course_id) + |> where(course_id: ^course_id) |> select([cr], cr.id) |> Repo.all() @@ -2054,7 +2055,15 @@ defmodule Cadet.Assessments do Automatically assigns XP to the winning contest entries """ def assign_winning_contest_entries_xp(contest_voting_question_id) do - contest_id = + voting_questions = + Question + |> where(type: :voting) + |> where(id: ^contest_voting_question_id) + |> Repo.one() + + scores = voting_questions.question["xp_values"] + + contest_question_id = SubmissionVotes |> where(question_id: ^contest_voting_question_id) |> join(:inner, [sv], ans in Answer, on: sv.submission_id == ans.submission_id) @@ -2065,43 +2074,35 @@ defmodule Cadet.Assessments do Repo.transaction(fn -> winning_popular_entries = Answer - |> where(question_id: ^contest_id) - |> order_by([a], desc: a.popular_score) + |> where(question_id: ^contest_question_id) + |> select([a], %{ + id: a.id, + rank: fragment("rank() OVER (ORDER BY ? DESC)", a.popular_score) + }) |> Repo.all() winning_popular_entries - |> Enum.with_index() - |> Enum.each(fn {entry, index} -> - increment = - case index do - 0 -> 300 - 1 -> 200 - 2 -> 100 - _ -> 0 - end - - Repo.update!(Ecto.Changeset.change(entry, %{xp_adjustment: increment})) + |> Enum.each(fn %{id: answer_id, rank: rank} -> + increment = Enum.at(scores, rank - 1, 0) + answer = Repo.get!(Answer, answer_id) + Repo.update!(Ecto.Changeset.change(answer, %{xp: increment})) end) winning_score_entries = Answer - |> where(question_id: ^contest_id) - |> order_by([a], desc: a.relative_score) + |> where(question_id: ^contest_question_id) + |> select([a], %{ + id: a.id, + rank: fragment("rank() OVER (ORDER BY ? DESC)", a.relative_score) + }) |> Repo.all() winning_score_entries - |> Enum.with_index() - |> Enum.each(fn {entry, index} -> - increment = - case index do - 0 -> 300 - 1 -> 200 - 2 -> 100 - _ -> 0 - end - - new_value = entry.xp_adjustment + increment - Repo.update!(Ecto.Changeset.change(entry, %{xp_adjustment: new_value})) + |> Enum.each(fn %{id: answer_id, rank: rank} -> + increment = Enum.at(scores, rank - 1, 0) + answer = Repo.get!(Answer, answer_id) + new_value = answer.xp + increment + Repo.update!(Ecto.Changeset.change(answer, %{xp: new_value})) end) end) diff --git a/lib/cadet/assessments/question_types/voting_question.ex b/lib/cadet/assessments/question_types/voting_question.ex index 95c762fcd..809cc4277 100644 --- a/lib/cadet/assessments/question_types/voting_question.ex +++ b/lib/cadet/assessments/question_types/voting_question.ex @@ -12,10 +12,11 @@ defmodule Cadet.Assessments.QuestionTypes.VotingQuestion do field(:contest_number, :string) field(:reveal_hours, :integer) field(:token_divider, :integer) + field(:xp_values, {:array, :integer}) end @required_fields ~w(content contest_number reveal_hours token_divider)a - @optional_fields ~w(prepend template)a + @optional_fields ~w(prepend template xp_values)a def changeset(question, params \\ %{}) do question diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index b49128506..d01c7a974 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -246,22 +246,31 @@ defmodule Cadet.Updater.XMLParser do end defp process_question_entity_by_type(entity, "voting") do - Map.merge( - entity - |> xpath( - ~x"."e, - content: ~x"./TEXT/text()" |> transform_by(&process_charlist/1), - prepend: ~x"./SNIPPET/PREPEND/text()" |> transform_by(&process_charlist/1), - template: ~x"./SNIPPET/TEMPLATE/text()" |> transform_by(&process_charlist/1) - ), - entity - |> xpath( - ~x"./VOTING"e, - contest_number: ~x"./@assessment_number"s, - reveal_hours: ~x"./@reveal_hours"i, - token_divider: ~x"./@token_divider"i + question_data = + Map.merge( + entity + |> xpath( + ~x"."e, + content: ~x"./TEXT/text()" |> transform_by(&process_charlist/1), + prepend: ~x"./SNIPPET/PREPEND/text()" |> transform_by(&process_charlist/1), + template: ~x"./SNIPPET/TEMPLATE/text()" |> transform_by(&process_charlist/1) + ), + entity + |> xpath( + ~x"./VOTING"e, + contest_number: ~x"./@assessment_number"s, + reveal_hours: ~x"./@reveal_hours"i, + token_divider: ~x"./@token_divider"i + ) ) - ) + + xp_values = + entity + |> xpath(~x"./VOTING/XP_ARRAY/XP"el, value: ~x"./@value"i) + |> Enum.map(& &1[:value]) + + IO.inspect(xp_values, label: "xp_values") + Map.merge(question_data, %{xp_values: xp_values}) end defp process_question_entity_by_type(_, _) do diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 3b0280639..29711abae 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -196,6 +196,22 @@ defmodule CadetWeb.AdminAssessmentsController do end end + def dispatch_contest_xp(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do + voting_questions = + Question + |> where(type: :voting) + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + if voting_questions do + Assessments.assign_winning_contest_entries_xp(voting_questions.id) + + text(conn, "XP Dispatched") + else + text(conn, "No voting questions found for the given assessment") + end + end + def get_contest_popular_scores(conn, %{ "assessmentid" => assessment_id, "course_id" => course_id diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 569f12421..90f1f8676 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -201,6 +201,12 @@ defmodule CadetWeb.Router do :calculate_contest_score ) + post( + "/assessments/:assessmentid/dispatchContestXp", + AdminAssessmentsController, + :dispatch_contest_xp + ) + get( "/assessments/:assessmentid/:visibleentries/popularVoteLeaderboard", AdminAssessmentsController, From 31d821d30d24d04da9cbe5c51edbf67d003896ea Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Tue, 18 Mar 2025 22:34:12 +0800 Subject: [PATCH 15/30] Add default value for XP values and improve XP assignment logic for contest entries --- lib/cadet/assessments/assessments.ex | 67 ++++++++++--------- .../question_types/voting_question.ex | 2 +- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 554947912..72ebba3e1 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -2071,42 +2071,47 @@ defmodule Cadet.Assessments do |> limit(1) |> Repo.one() - Repo.transaction(fn -> - winning_popular_entries = - Answer - |> where(question_id: ^contest_question_id) - |> select([a], %{ - id: a.id, - rank: fragment("rank() OVER (ORDER BY ? DESC)", a.popular_score) - }) - |> Repo.all() + if scores == [] or is_nil(contest_question_id) do + Logger.warn("No XP values provided or contest question ID is missing. Terminating.") + :ok + else + Repo.transaction(fn -> + winning_popular_entries = + Answer + |> where(question_id: ^contest_question_id) + |> select([a], %{ + id: a.id, + rank: fragment("rank() OVER (ORDER BY ? DESC)", a.popular_score) + }) + |> Repo.all() - winning_popular_entries - |> Enum.each(fn %{id: answer_id, rank: rank} -> - increment = Enum.at(scores, rank - 1, 0) - answer = Repo.get!(Answer, answer_id) - Repo.update!(Ecto.Changeset.change(answer, %{xp: increment})) - end) + winning_popular_entries + |> Enum.each(fn %{id: answer_id, rank: rank} -> + increment = Enum.at(scores, rank - 1, 0) + answer = Repo.get!(Answer, answer_id) + Repo.update!(Ecto.Changeset.change(answer, %{xp: increment})) + end) - winning_score_entries = - Answer - |> where(question_id: ^contest_question_id) - |> select([a], %{ - id: a.id, - rank: fragment("rank() OVER (ORDER BY ? DESC)", a.relative_score) - }) - |> Repo.all() + winning_score_entries = + Answer + |> where(question_id: ^contest_question_id) + |> select([a], %{ + id: a.id, + rank: fragment("rank() OVER (ORDER BY ? DESC)", a.relative_score) + }) + |> Repo.all() - winning_score_entries - |> Enum.each(fn %{id: answer_id, rank: rank} -> - increment = Enum.at(scores, rank - 1, 0) - answer = Repo.get!(Answer, answer_id) - new_value = answer.xp + increment - Repo.update!(Ecto.Changeset.change(answer, %{xp: new_value})) + winning_score_entries + |> Enum.each(fn %{id: answer_id, rank: rank} -> + increment = Enum.at(scores, rank - 1, 0) + answer = Repo.get!(Answer, answer_id) + new_value = answer.xp + increment + Repo.update!(Ecto.Changeset.change(answer, %{xp: new_value})) + end) end) - end) - Logger.info("XP assigned to winning contest entries") + Logger.info("XP assigned to winning contest entries") + end end @doc """ diff --git a/lib/cadet/assessments/question_types/voting_question.ex b/lib/cadet/assessments/question_types/voting_question.ex index 809cc4277..038c344ab 100644 --- a/lib/cadet/assessments/question_types/voting_question.ex +++ b/lib/cadet/assessments/question_types/voting_question.ex @@ -12,7 +12,7 @@ defmodule Cadet.Assessments.QuestionTypes.VotingQuestion do field(:contest_number, :string) field(:reveal_hours, :integer) field(:token_divider, :integer) - field(:xp_values, {:array, :integer}) + field(:xp_values, {:array, :integer}, default: []) end @required_fields ~w(content contest_number reveal_hours token_divider)a From 9410c4f0ca85c9797fbe4e244d56a3269113c244 Mon Sep 17 00:00:00 2001 From: Blerargh Date: Wed, 19 Mar 2025 00:16:25 +0800 Subject: [PATCH 16/30] No tiebreak for contest scoring --- lib/cadet/assessments/assessments.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 72ebba3e1..993b54ef5 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1862,7 +1862,7 @@ defmodule Cadet.Assessments do query = from(sub in subquery(subquery), select_merge: %{ - rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) + rank: fragment("RANK() OVER (ORDER BY ? DESC)", sub.score) } ) @@ -1921,7 +1921,7 @@ defmodule Cadet.Assessments do query = from(sub in subquery(subquery), select_merge: %{ - rank: fragment("ROW_NUMBER() OVER (ORDER BY ? DESC)", sub.score) + rank: fragment("RANK() OVER (ORDER BY ? DESC)", sub.score) } ) From 4e78a246ead41e3e496e48a992e2c65031668f19 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 19 Mar 2025 00:44:04 +0800 Subject: [PATCH 17/30] Refactor contest scoring endpoints for authentication errors --- lib/cadet/jobs/xml_parser.ex | 1 - .../admin_assessments_controller.ex | 33 --------------- .../admin_user_controller.ex | 5 --- .../controllers/assessments_controller.ex | 42 +++++++++++++++++++ lib/cadet_web/router.ex | 8 ++-- 5 files changed, 46 insertions(+), 43 deletions(-) diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index d01c7a974..6907be6d0 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -269,7 +269,6 @@ defmodule Cadet.Updater.XMLParser do |> xpath(~x"./VOTING/XP_ARRAY/XP"el, value: ~x"./@value"i) |> Enum.map(& &1[:value]) - IO.inspect(xp_values, label: "xp_values") Map.merge(question_data, %{xp_values: xp_values}) end diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 29711abae..5ad008963 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -212,39 +212,6 @@ defmodule CadetWeb.AdminAssessmentsController do end end - def get_contest_popular_scores(conn, %{ - "assessmentid" => assessment_id, - "course_id" => course_id - }) do - contest_id = - Question - |> where(assessment_id: ^assessment_id) - |> Repo.one() - - contestpop = Assessments.fetch_contest_popular_scores(contest_id.id) - voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id) - json(conn, %{contest_popular: contestpop, voting_id: voting_id}) - end - - def get_contest_relative_scores(conn, %{ - "assessmentid" => assessment_id, - "course_id" => course_id - }) do - contest_id = - Question - |> where(assessment_id: ^assessment_id) - |> Repo.one() - - contestscore = Assessments.fetch_contest_relative_scores(contest_id.id) - voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id) - json(conn, %{contest_score: contestscore, voting_id: voting_id}) - end - - def get_all_contests(conn, %{"course_id" => course_id}) do - contests = Assessments.fetch_all_contests(course_id) - json(conn, contests) - end - defp check_dates(open_at, close_at, assessment) do if is_nil(open_at) and is_nil(close_at) do {:ok, assessment} diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 7734f47f4..53add9133 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -28,11 +28,6 @@ defmodule CadetWeb.AdminUserController do json(conn, %{totalXp: total_xp}) end - def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do - users_with_xp = Assessments.all_user_total_xp(course_id) - json(conn, %{users: users_with_xp}) - end - @add_users_role ~w(admin)a def get_students(conn, filter) do users = diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index bb9a563f2..994381c9c 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -3,7 +3,11 @@ defmodule CadetWeb.AssessmentsController do use PhoenixSwagger + import Ecto.Query, only: [where: 2] + alias Cadet.Assessments + alias Cadet.Assessments.{Question, Assessment} + alias Cadet.{Assessments, Repo} # These roles can save and finalise answers for closed assessments and # submitted answers @@ -67,6 +71,44 @@ defmodule CadetWeb.AssessmentsController do end end + def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do + users_with_xp = Assessments.all_user_total_xp(course_id) + json(conn, %{users: users_with_xp}) + end + + def get_contest_popular_scores(conn, %{ + "assessmentid" => assessment_id, + "course_id" => course_id + }) do + contest_id = + Question + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + contestpop = Assessments.fetch_contest_popular_scores(contest_id.id) + voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id) + json(conn, %{contest_popular: contestpop, voting_id: voting_id}) + end + + def get_contest_relative_scores(conn, %{ + "assessmentid" => assessment_id, + "course_id" => course_id + }) do + contest_id = + Question + |> where(assessment_id: ^assessment_id) + |> Repo.one() + + contestscore = Assessments.fetch_contest_relative_scores(contest_id.id) + voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id) + json(conn, %{contest_score: contestscore, voting_id: voting_id}) + end + + def get_all_contests(conn, %{"course_id" => course_id}) do + contests = Assessments.fetch_all_contests(course_id) + json(conn, contests) + end + swagger_path :submit do post("/courses/{course_id}/assessments/{assessmentId}/submit") summary("Finalise submission for an assessment") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 90f1f8676..fc687fa91 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -118,21 +118,21 @@ defmodule CadetWeb.Router do put("/user/game_states", UserController, :update_game_states) put("/user/research_agreement", UserController, :update_research_agreement) - get("/all_users_xp", AdminUserController, :combined_total_xp_for_all_users) + get("/all_users_xp", AssessmentsController, :combined_total_xp_for_all_users) get( "/leaderboard/contests/:assessmentid/get_score_leaderboard", - AdminAssessmentsController, + AssessmentsController, :get_contest_relative_scores ) get( "/leaderboard/contests/:assessmentid/get_popular_vote_leaderboard", - AdminAssessmentsController, + AssessmentsController, :get_contest_popular_scores ) - get("/all_contests", AdminAssessmentsController, :get_all_contests) + get("/all_contests", AssessmentsController, :get_all_contests) get("/config", CoursesController, :index) From 04f9d20eed8b579ecb8e827e8d48d046183238bb Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 19 Mar 2025 02:37:16 +0800 Subject: [PATCH 18/30] Enhance leaderboard update logic and improve error handling for voting questions --- lib/cadet/assessments/assessments.ex | 24 ++++++++++++++----- .../admin_assessments_controller_test.exs | 16 ++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 993b54ef5..5ec57ff71 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -2025,14 +2025,21 @@ defmodule Cadet.Assessments do if Log.log_execution("update_final_contest_leaderboards", Duration.from_minutes(1435)) do Logger.info("Started update_final_contest_leaderboards") - voting_questions_to_update = fetch_voting_questions_due_yesterday() + voting_questions_to_update = fetch_voting_questions_due_yesterday() || [] - _ = + voting_questions_to_update = + if is_nil(voting_questions_to_update), do: [], else: voting_questions_to_update + + if Enum.empty?(voting_questions_to_update) do + Logger.warn("No voting questions to update.") + else + # Process each voting question voting_questions_to_update |> Enum.each(fn qn -> compute_relative_score(qn.id) assign_winning_contest_entries_xp(qn.id) end) + end Logger.info("Successfully update_final_contest_leaderboards") end @@ -2061,8 +2068,6 @@ defmodule Cadet.Assessments do |> where(id: ^contest_voting_question_id) |> Repo.one() - scores = voting_questions.question["xp_values"] - contest_question_id = SubmissionVotes |> where(question_id: ^contest_voting_question_id) @@ -2071,8 +2076,14 @@ defmodule Cadet.Assessments do |> limit(1) |> Repo.one() - if scores == [] or is_nil(contest_question_id) do - Logger.warn("No XP values provided or contest question ID is missing. Terminating.") + if is_nil(contest_question_id) do + Logger.warn("Contest question ID is missing. Terminating.") + :ok + else + scores = voting_questions.question["xp_values"] + + if scores == [] do + Logger.warn("No XP values provided. Terminating.") :ok else Repo.transaction(fn -> @@ -2111,6 +2122,7 @@ defmodule Cadet.Assessments do end) Logger.info("XP assigned to winning contest entries") + end end end diff --git a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs index cfd590925..0601f4eaf 100644 --- a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs @@ -159,7 +159,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end - describe "GET /:assessment_id/popularVoteLeaderboard, unauthenticated" do + describe "GET /:assessment_id/:visibleentries/popularVoteLeaderboard, unauthenticated" do test "unauthorized", %{conn: conn, courses: %{course1: course1}} do config = insert(:assessment_config, %{course: course1}) assessment = insert(:assessment, %{course: course1, config: config}) @@ -170,7 +170,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end - describe "GET /:assessment_id/popularVoteLeaderboard, student only" do + describe "GET /:assessment_id/:visibleentries/popularVoteLeaderboard, student only" do @tag authenticate: :student test "Forbidden", %{conn: conn} do test_cr = conn.assigns.test_cr @@ -184,7 +184,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end - describe "GET /:assessment_id/popularVoteLeaderboard" do + describe "GET /:assessment_id/:visibleentries/popularVoteLeaderboard" do @tag authenticate: :staff test "successful", %{conn: conn} do test_cr = conn.assigns.test_cr @@ -239,7 +239,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end - describe "GET /:assessment_id/scoreLeaderboard, unauthenticated" do + describe "GET /:assessment_id/:visibleentries/scoreLeaderboard, unauthenticated" do test "unauthorized", %{conn: conn, courses: %{course1: course1}} do config = insert(:assessment_config, %{course: course1}) assessment = insert(:assessment, %{course: course1, config: config}) @@ -250,7 +250,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end - describe "GET /:assessment_id/scoreLeaderboard, student only" do + describe "GET /:assessment_id/:visibleentries/scoreLeaderboard, student only" do @tag authenticate: :student test "Forbidden", %{conn: conn} do test_cr = conn.assigns.test_cr @@ -264,7 +264,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end - describe "GET /:assessment_id/scoreLeaderboard" do + describe "GET /:assessment_id/:visibleentries/scoreLeaderboard" do @tag authenticate: :staff test "successful", %{conn: conn} do test_cr = conn.assigns.test_cr @@ -985,10 +985,10 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do do: "/v2/courses/#{course_id}/admin/users/#{course_reg_id}/assessments" defp build_popular_leaderboard_url(course_id, assessment_id), - do: "#{build_url(course_id, assessment_id)}/popularVoteLeaderboard" + do: "#{build_url(course_id, assessment_id)}/10/popularVoteLeaderboard" defp build_score_leaderboard_url(course_id, assessment_id), - do: "#{build_url(course_id, assessment_id)}/scoreLeaderboard" + do: "#{build_url(course_id, assessment_id)}/10/scoreLeaderboard" defp open_at_asc_comparator(x, y), do: Timex.before?(x.open_at, y.open_at) From 9500bcb87abd5ef67a7892658ca25de14c867cd7 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 19 Mar 2025 13:18:16 +0800 Subject: [PATCH 19/30] Refactor XP assignment logic for voting questions and set default XP values. Refactor Score calculation logic to reset to 0 before calculating. --- lib/cadet/assessments/assessments.ex | 163 ++++++++++-------- .../question_types/voting_question.ex | 2 +- lib/cadet/jobs/xml_parser.ex | 2 +- 3 files changed, 97 insertions(+), 70 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 5ec57ff71..2445c92a6 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -27,6 +27,7 @@ defmodule Cadet.Assessments do alias Cadet.ProgramAnalysis.Lexer alias Ecto.Multi alias Cadet.Incentives.{Achievements, AchievementToGoal} + alias Ecto.Changeset alias Timex.Duration require Decimal @@ -907,16 +908,16 @@ defmodule Cadet.Assessments do Submission |> join(:inner, [s], ans in assoc(s, :answers)) |> join(:inner, [s, ans], cr in assoc(s, :student)) - # |> where([s, ans, cr], cr.role == "student") + |> where([s, ans, cr], cr.role == "student") |> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted") - # |> where( - # [_, ans, cr], - # fragment( - # "?->>'code' like ?", - # ans.answer, - # "%return%" - # ) - # ) + |> where( + [_, ans, cr], + fragment( + "?->>'code' like ?", + ans.answer, + "%return%" + ) + ) |> select([s, _ans], {s.student_id, s.id}) |> Repo.all() |> Enum.into(%{}) @@ -925,7 +926,7 @@ defmodule Cadet.Assessments do voter_ids = CourseRegistration - # |> where(role: "student", course_id: ^course_id) + |> where(role: "student", course_id: ^course_id) |> where(course_id: ^course_id) |> select([cr], cr.id) |> Repo.all() @@ -1838,19 +1839,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1897,19 +1898,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -2030,18 +2031,23 @@ defmodule Cadet.Assessments do voting_questions_to_update = if is_nil(voting_questions_to_update), do: [], else: voting_questions_to_update + scores = + Enum.map(voting_questions_to_update, fn qn -> + compute_relative_score(qn.id) + end) + if Enum.empty?(voting_questions_to_update) do Logger.warn("No voting questions to update.") else # Process each voting question - voting_questions_to_update - |> Enum.each(fn qn -> - compute_relative_score(qn.id) + Enum.each(voting_questions_to_update, fn qn -> assign_winning_contest_entries_xp(qn.id) end) + + Logger.info("Successfully update_final_contest_leaderboards") end - Logger.info("Successfully update_final_contest_leaderboards") + scores end end @@ -2080,48 +2086,49 @@ defmodule Cadet.Assessments do Logger.warn("Contest question ID is missing. Terminating.") :ok else - scores = voting_questions.question["xp_values"] + default_xp_values = %Cadet.Assessments.QuestionTypes.VotingQuestion{} |> Map.get(:xp_values) + scores = voting_questions.question["xp_values"] || default_xp_values if scores == [] do Logger.warn("No XP values provided. Terminating.") - :ok - else - Repo.transaction(fn -> - winning_popular_entries = - Answer - |> where(question_id: ^contest_question_id) - |> select([a], %{ - id: a.id, - rank: fragment("rank() OVER (ORDER BY ? DESC)", a.popular_score) - }) - |> Repo.all() - - winning_popular_entries - |> Enum.each(fn %{id: answer_id, rank: rank} -> - increment = Enum.at(scores, rank - 1, 0) - answer = Repo.get!(Answer, answer_id) - Repo.update!(Ecto.Changeset.change(answer, %{xp: increment})) - end) - - winning_score_entries = - Answer - |> where(question_id: ^contest_question_id) - |> select([a], %{ - id: a.id, - rank: fragment("rank() OVER (ORDER BY ? DESC)", a.relative_score) - }) - |> Repo.all() + :ok + else + Repo.transaction(fn -> + winning_popular_entries = + Answer + |> where(question_id: ^contest_question_id) + |> select([a], %{ + id: a.id, + rank: fragment("rank() OVER (ORDER BY ? DESC)", a.popular_score) + }) + |> Repo.all() + + winning_popular_entries + |> Enum.each(fn %{id: answer_id, rank: rank} -> + increment = Enum.at(scores, rank - 1, 0) + answer = Repo.get!(Answer, answer_id) + Repo.update!(Changeset.change(answer, %{xp: increment})) + end) - winning_score_entries - |> Enum.each(fn %{id: answer_id, rank: rank} -> - increment = Enum.at(scores, rank - 1, 0) - answer = Repo.get!(Answer, answer_id) - new_value = answer.xp + increment - Repo.update!(Ecto.Changeset.change(answer, %{xp: new_value})) + winning_score_entries = + Answer + |> where(question_id: ^contest_question_id) + |> select([a], %{ + id: a.id, + rank: fragment("rank() OVER (ORDER BY ? DESC)", a.relative_score) + }) + |> Repo.all() + + winning_score_entries + |> Enum.each(fn %{id: answer_id, rank: rank} -> + increment = Enum.at(scores, rank - 1, 0) + answer = Repo.get!(Answer, answer_id) + new_value = answer.xp + increment + Repo.update!(Changeset.change(answer, %{xp: new_value})) + end) end) - end) - Logger.info("XP assigned to winning contest entries") + Logger.info("XP assigned to winning contest entries") end end end @@ -2131,6 +2138,26 @@ defmodule Cadet.Assessments do based on current submitted votes. """ def compute_relative_score(contest_voting_question_id) do + # reset all scores to 0 first + voting_questions = + Question + |> where(type: :voting) + |> where(id: ^contest_voting_question_id) + |> Repo.one() + + course_id = + Assessment + |> where(id: ^voting_questions.assessment_id) + |> select([a], a.course_id) + |> Repo.one() + + contest_question_id = fetch_associated_contest_question_id(course_id, voting_questions) + + Answer + |> where([ans], ans.question_id == ^contest_question_id) + |> update([ans], set: [popular_score: 0.0, relative_score: 0.0]) + |> Repo.update_all([]) + # query all records from submission votes tied to the question id -> # map score to user id -> # store as grade -> diff --git a/lib/cadet/assessments/question_types/voting_question.ex b/lib/cadet/assessments/question_types/voting_question.ex index 038c344ab..52aae9d51 100644 --- a/lib/cadet/assessments/question_types/voting_question.ex +++ b/lib/cadet/assessments/question_types/voting_question.ex @@ -12,7 +12,7 @@ defmodule Cadet.Assessments.QuestionTypes.VotingQuestion do field(:contest_number, :string) field(:reveal_hours, :integer) field(:token_divider, :integer) - field(:xp_values, {:array, :integer}, default: []) + field(:xp_values, {:array, :integer}, default: [500, 400, 300]) end @required_fields ~w(content contest_number reveal_hours token_divider)a diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 6907be6d0..f12408236 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -269,7 +269,7 @@ defmodule Cadet.Updater.XMLParser do |> xpath(~x"./VOTING/XP_ARRAY/XP"el, value: ~x"./@value"i) |> Enum.map(& &1[:value]) - Map.merge(question_data, %{xp_values: xp_values}) + if xp_values == [], do: question_data, else: Map.merge(question_data, %{xp_values: xp_values}) end defp process_question_entity_by_type(_, _) do From ab255e056ed0a8feb8f42a5e39ec6279a17fb62e Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 19 Mar 2025 13:20:58 +0800 Subject: [PATCH 20/30] Temporary Assessment Workspace leaderboard fix for testing --- lib/cadet/assessments/assessments.ex | 61 ++++++++++++++++------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 2445c92a6..12e34beb1 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -910,14 +910,15 @@ defmodule Cadet.Assessments do |> join(:inner, [s, ans], cr in assoc(s, :student)) |> where([s, ans, cr], cr.role == "student") |> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted") - |> where( - [_, ans, cr], - fragment( - "?->>'code' like ?", - ans.answer, - "%return%" - ) - ) + # TODO: uncomment when Leaderboard testing is finished + # |> where( + # [_, ans, cr], + # fragment( + # "?->>'code' like ?", + # ans.answer, + # "%return%" + # ) + # ) |> select([s, _ans], {s.student_id, s.id}) |> Repo.all() |> Enum.into(%{}) @@ -926,7 +927,9 @@ defmodule Cadet.Assessments do voter_ids = CourseRegistration - |> where(role: "student", course_id: ^course_id) + # TODO: uncomment when Leaderboard testing is finished + # |> where(role: "student", course_id: ^course_id) + # TODO: delete when Leaderboard testing is finished |> where(course_id: ^course_id) |> select([cr], cr.id) |> Repo.all() @@ -1839,19 +1842,21 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # TODO: uncomment when Leaderboard testing is finished + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # TODO: uncomment when Leaderboard testing is finished + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1898,19 +1903,21 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # TODO: uncomment when Leaderboard testing is finished + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # TODO: uncomment when Leaderboard testing is finished + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], From 116618985a0c200c37765bf5f39d7aca11385368 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 19 Mar 2025 17:51:30 +0800 Subject: [PATCH 21/30] Fixed tests for assessments (default XP to award for contests) --- test/factories/assessments/question_factory.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/factories/assessments/question_factory.ex b/test/factories/assessments/question_factory.ex index cfd088dd1..783db1180 100644 --- a/test/factories/assessments/question_factory.ex +++ b/test/factories/assessments/question_factory.ex @@ -94,7 +94,8 @@ defmodule Cadet.Assessments.QuestionFactory do template: Faker.Lorem.Shakespeare.as_you_like_it(), contest_number: contest_assessment.number, reveal_hours: 48, - token_divider: 50 + token_divider: 50, + xp_values: [500, 400, 300] } } end @@ -108,7 +109,8 @@ defmodule Cadet.Assessments.QuestionFactory do template: Faker.Lorem.Shakespeare.as_you_like_it(), contest_number: contest_assessment.number, reveal_hours: 48, - token_divider: 50 + token_divider: 50, + xp_values: [500, 400, 300] } end end From 1096a050d0f1898a834b15cba2b7551bbf90c96c Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 19 Mar 2025 23:00:30 +0800 Subject: [PATCH 22/30] Refactor contest fetching logic to filter by voting question contest numbers --- lib/cadet/assessments/assessments.ex | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 12e34beb1..0dea60fa5 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1881,17 +1881,23 @@ defmodule Cadet.Assessments do Used for contest leaderboard dropdown fetching """ def fetch_all_contests(course_id) do - assessments = + contest_numbers = + Question + |> where(type: :voting) + |> select([q], q.question["contest_number"]) + |> Repo.all() + |> Enum.reject(&is_nil/1) + + if contest_numbers == [] do + [] + else Assessment - |> where(course_id: ^course_id) - |> where(is_published: true) - |> join(:inner, [a], q in assoc(a, :questions)) - |> where([a, q], q.type != "voting") - |> join(:inner, [a, q], ac in AssessmentConfig, on: a.config_id == ac.id) - |> where([a, q, ac], ac.type == "Contests") + |> where([a], a.number in ^contest_numbers and a.course_id == ^course_id) + |> join(:inner, [a], ac in AssessmentConfig, on: a.config_id == ac.id) + |> where([a, ac], ac.type == "Contests") |> select([a], %{contest_id: a.id, title: a.title, published: a.is_published}) - - Repo.all(assessments) + |> Repo.all() + end end @doc """ From 61a456182fe182f24b6ee3ed3d30f3483305d6bc Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Thu, 20 Mar 2025 20:02:38 +0800 Subject: [PATCH 23/30] Refactor leaderboard query logic to use RANK() and improve code readability. Uncommented leaderboard portions after finalising testing --- lib/cadet/assessments/assessments.ex | 132 ++++++++++++++------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 0dea60fa5..e3b060674 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -261,7 +261,7 @@ defmodule Cadet.Assessments do ranked_xp_query = from(t in subquery(total_xp_query), select: %{ - rank: fragment("ROW_NUMBER() OVER (ORDER BY total_xp DESC)"), + rank: fragment("RANK() OVER (ORDER BY total_xp DESC)"), user_id: t.user_id, name: t.name, username: t.username, @@ -910,15 +910,14 @@ defmodule Cadet.Assessments do |> join(:inner, [s, ans], cr in assoc(s, :student)) |> where([s, ans, cr], cr.role == "student") |> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted") - # TODO: uncomment when Leaderboard testing is finished - # |> where( - # [_, ans, cr], - # fragment( - # "?->>'code' like ?", - # ans.answer, - # "%return%" - # ) - # ) + |> where( + [_, ans, cr], + fragment( + "?->>'code' like ?", + ans.answer, + "%return%" + ) + ) |> select([s, _ans], {s.student_id, s.id}) |> Repo.all() |> Enum.into(%{}) @@ -927,10 +926,7 @@ defmodule Cadet.Assessments do voter_ids = CourseRegistration - # TODO: uncomment when Leaderboard testing is finished - # |> where(role: "student", course_id: ^course_id) - # TODO: delete when Leaderboard testing is finished - |> where(course_id: ^course_id) + |> where(role: "student", course_id: ^course_id) |> select([cr], cr.id) |> Repo.all() @@ -1842,21 +1838,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # TODO: uncomment when Leaderboard testing is finished - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # TODO: uncomment when Leaderboard testing is finished - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1876,7 +1870,7 @@ defmodule Cadet.Assessments do end @doc """ - Fetches all contests for the course id + Fetches all contests for the course id where the voting assessment has been published Used for contest leaderboard dropdown fetching """ @@ -1909,21 +1903,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # TODO: uncomment when Leaderboard testing is finished - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # TODO: uncomment when Leaderboard testing is finished - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1950,19 +1942,19 @@ defmodule Cadet.Assessments do def fetch_top_relative_score_answers(question_id, number_of_answers) do Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, @@ -1981,19 +1973,19 @@ defmodule Cadet.Assessments do def fetch_top_popular_score_answers(question_id, number_of_answers) do Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, @@ -2158,18 +2150,28 @@ defmodule Cadet.Assessments do |> where(id: ^contest_voting_question_id) |> Repo.one() - course_id = - Assessment - |> where(id: ^voting_questions.assessment_id) - |> select([a], a.course_id) - |> Repo.one() + if is_nil(voting_questions) do + IO.puts("Voting question not found, skipping score computation.") + :ok + else + course_id = + Assessment + |> where(id: ^voting_questions.assessment_id) + |> select([a], a.course_id) + |> Repo.one() - contest_question_id = fetch_associated_contest_question_id(course_id, voting_questions) + if is_nil(course_id) do + IO.puts("Course ID not found, skipping score computation.") + :ok + else + contest_question_id = fetch_associated_contest_question_id(course_id, voting_questions) - Answer - |> where([ans], ans.question_id == ^contest_question_id) - |> update([ans], set: [popular_score: 0.0, relative_score: 0.0]) - |> Repo.update_all([]) + Answer + |> where([ans], ans.question_id == ^contest_question_id) + |> update([ans], set: [popular_score: 0.0, relative_score: 0.0]) + |> Repo.update_all([]) + end + end # query all records from submission votes tied to the question id -> # map score to user id -> From 0e1ed6179f059149cf7ecbd0952d5b635c7af5d9 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Tue, 15 Apr 2025 23:43:08 +0800 Subject: [PATCH 24/30] temporary fix for STePS --- lib/cadet/assessments/assessments.ex | 72 ++++++++++++++-------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index e3b060674..fc5089b00 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1838,19 +1838,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1903,19 +1903,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1942,19 +1942,19 @@ defmodule Cadet.Assessments do def fetch_top_relative_score_answers(question_id, number_of_answers) do Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, @@ -1973,19 +1973,19 @@ defmodule Cadet.Assessments do def fetch_top_popular_score_answers(question_id, number_of_answers) do Answer |> where(question_id: ^question_id) - |> where( - [a], - fragment( - "?->>'code' like ?", - a.answer, - "%return%" - ) - ) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - |> where([a, s, student], student.role == "student") + # |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, From 80fcb0522cf4b4c7708cbe11a9a0fc6d7d909429 Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Wed, 16 Apr 2025 01:59:44 +0800 Subject: [PATCH 25/30] Add ranking to assessment workspace leaderboard queries and update view helpers to include rank --- lib/cadet/assessments/assessments.ex | 118 +++++++++++-------- lib/cadet_web/helpers/assessments_helpers.ex | 6 +- lib/cadet_web/views/assessments_view.ex | 3 +- 3 files changed, 78 insertions(+), 49 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index fc5089b00..bab7bf367 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1940,29 +1940,42 @@ defmodule Cadet.Assessments do Used for contest leaderboard fetching """ def fetch_top_relative_score_answers(question_id, number_of_answers) do - Answer - |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) - |> order_by(desc: :relative_score) - |> join(:left, [a], s in assoc(a, :submission)) - |> join(:left, [a, s], student in assoc(s, :student)) - |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") - |> select([a, s, student, student_user], %{ - submission_id: a.submission_id, - answer: a.answer, - relative_score: a.relative_score, - student_name: student_user.name - }) - |> limit(^number_of_answers) - |> Repo.all() + subquery = + Answer + |> where(question_id: ^question_id) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) + |> order_by(desc: :relative_score) + |> join(:left, [a], s in assoc(a, :submission)) + |> join(:left, [a, s], student in assoc(s, :student)) + |> join(:inner, [a, s, student], student_user in assoc(student, :user)) + # |> where([a, s, student], student.role == "student") + |> select([a, s, student, student_user], %{ + submission_id: a.submission_id, + answer: a.answer, + relative_score: a.relative_score, + student_name: student_user.name + }) + + ranked_subquery = + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("RANK() OVER (ORDER BY ? DESC)", sub.relative_score) + } + ) + + final_query = + from(r in subquery(ranked_subquery), + where: r.rank <= ^number_of_answers + ) + + Repo.all(final_query) end @doc """ @@ -1971,29 +1984,42 @@ defmodule Cadet.Assessments do Used for contest leaderboard fetching """ def fetch_top_popular_score_answers(question_id, number_of_answers) do - Answer - |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) - |> order_by(desc: :popular_score) - |> join(:left, [a], s in assoc(a, :submission)) - |> join(:left, [a, s], student in assoc(s, :student)) - |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") - |> select([a, s, student, student_user], %{ - submission_id: a.submission_id, - answer: a.answer, - popular_score: a.popular_score, - student_name: student_user.name - }) - |> limit(^number_of_answers) - |> Repo.all() + subquery = + Answer + |> where(question_id: ^question_id) + # |> where( + # [a], + # fragment( + # "?->>'code' like ?", + # a.answer, + # "%return%" + # ) + # ) + |> order_by(desc: :popular_score) + |> join(:left, [a], s in assoc(a, :submission)) + |> join(:left, [a, s], student in assoc(s, :student)) + |> join(:inner, [a, s, student], student_user in assoc(student, :user)) + # |> where([a, s, student], student.role == "student") + |> select([a, s, student, student_user], %{ + submission_id: a.submission_id, + answer: a.answer, + popular_score: a.popular_score, + student_name: student_user.name + }) + + ranked_subquery = + from(sub in subquery(subquery), + select_merge: %{ + rank: fragment("RANK() OVER (ORDER BY ? DESC)", sub.popular_score) + } + ) + + final_query = + from(r in subquery(ranked_subquery), + where: r.rank <= ^number_of_answers + ) + + Repo.all(final_query) end @doc """ diff --git a/lib/cadet_web/helpers/assessments_helpers.ex b/lib/cadet_web/helpers/assessments_helpers.ex index 967df1131..310e69083 100644 --- a/lib/cadet_web/helpers/assessments_helpers.ex +++ b/lib/cadet_web/helpers/assessments_helpers.ex @@ -107,7 +107,8 @@ defmodule CadetWeb.AssessmentsHelpers do transform_map_for_view(leaderboard_ans, %{ submission_id: :submission_id, answer: :answer, - student_name: :student_name + student_name: :student_name, + rank: :rank }), "final_score", Float.round(leaderboard_ans.relative_score, 2) @@ -119,7 +120,8 @@ defmodule CadetWeb.AssessmentsHelpers do transform_map_for_view(leaderboard_ans, %{ submission_id: :submission_id, answer: :answer, - student_name: :student_name + student_name: :student_name, + rank: :rank }), "final_score", Float.round(leaderboard_ans.popular_score, 2) diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index a00c61aa3..70539600d 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -76,7 +76,8 @@ defmodule CadetWeb.AssessmentsView do %{ student_name: :student_name, answer: & &1.answer["code"], - final_score: "final_score" + final_score: "final_score", + rank: :rank } ) end From db2888a1251e4f25503a8b604bc4391848b0ae8c Mon Sep 17 00:00:00 2001 From: Yi Zhong Date: Fri, 25 Apr 2025 23:14:45 +0800 Subject: [PATCH 26/30] Post-STePS fixes --- lib/cadet/assessments/assessments.ex | 72 ++++++++++++++-------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index bab7bf367..79552d4b0 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1838,19 +1838,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1903,19 +1903,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, code: a.answer["code"], @@ -1943,19 +1943,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, @@ -1987,19 +1987,19 @@ defmodule Cadet.Assessments do subquery = Answer |> where(question_id: ^question_id) - # |> where( - # [a], - # fragment( - # "?->>'code' like ?", - # a.answer, - # "%return%" - # ) - # ) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :popular_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) |> join(:inner, [a, s, student], student_user in assoc(student, :user)) - # |> where([a, s, student], student.role == "student") + |> where([a, s, student], student.role == "student") |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, From d067879f29534764f11f4373d387bc7c29b90bfc Mon Sep 17 00:00:00 2001 From: Blerargh Date: Tue, 29 Apr 2025 16:41:51 +0800 Subject: [PATCH 27/30] Shifted schema updates to a new migration file --- .../20210531155751_multitenant_upgrade.exs | 8 ------- ...081534_add_leaderboard_display_columns.exs | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 priv/repo/migrations/20250429081534_add_leaderboard_display_columns.exs diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 654c16199..9181bf4c2 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -10,10 +10,6 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:viewable, :boolean, null: false, default: true) add(:enable_game, :boolean, null: false, default: true) add(:enable_achievements, :boolean, null: false, default: true) - add(:enable_overall_leaderboard, :boolean, null: false, default: true) - add(:enable_contest_leaderboard, :boolean, null: false, default: true) - add(:top_leaderboard_display, :integer, default: 100) - add(:top_contest_leaderboard_display, :integer, default: 10) add(:enable_sourcecast, :boolean, null: false, default: true) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) @@ -147,10 +143,6 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do viewable: true, enable_game: true, enable_achievements: true, - enable_overall_leaderboard: true, - enable_contest_leaderboard: true, - top_leaderboard_display: 100, - top_contest_leaderboard_display: 10, enable_sourcecast: true, source_chapter: 1, source_variant: "default", diff --git a/priv/repo/migrations/20250429081534_add_leaderboard_display_columns.exs b/priv/repo/migrations/20250429081534_add_leaderboard_display_columns.exs new file mode 100644 index 000000000..6dedc2abc --- /dev/null +++ b/priv/repo/migrations/20250429081534_add_leaderboard_display_columns.exs @@ -0,0 +1,21 @@ +defmodule Cadet.Repo.Migrations.AddLeaderboardDisplayColumns do + use Ecto.Migration + + def change do + alter table(:courses) do + add(:enable_overall_leaderboard, :boolean, null: false, default: true) + add(:enable_contest_leaderboard, :boolean, null: false, default: true) + add(:top_leaderboard_display, :integer, default: 100) + add(:top_contest_leaderboard_display, :integer, default: 10) + end + + execute(fn -> + repo().update_all("courses", set: [ + enable_overall_leaderboard: true, + enable_contest_leaderboard: true, + top_leaderboard_display: 100, + top_contest_leaderboard_display: 10 + ]) + end) + end +end From 0761323a457c6ceb905cadcd31ccd12cc251cb3d Mon Sep 17 00:00:00 2001 From: Blerargh Date: Tue, 29 Apr 2025 18:40:21 +0800 Subject: [PATCH 28/30] Added support to retrieve only relevant rows for leaderboard pages --- lib/cadet/assessments/assessments.ex | 6 ++++-- lib/cadet_web/controllers/assessments_controller.ex | 6 ++++++ lib/cadet_web/router.ex | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 79552d4b0..aa532789b 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -148,7 +148,7 @@ defmodule Cadet.Assessments do total_achievement_xp + total_assessment_xp end - def all_user_total_xp(course_id) do + def all_user_total_xp(course_id, offset \\ nil, limit \\ nil) do # get all users even if they have 0 xp base_user_query = from( @@ -266,7 +266,9 @@ defmodule Cadet.Assessments do name: t.name, username: t.username, total_xp: t.total_xp - } + }, + limit: ^limit, + offset: ^offset ) Repo.all(ranked_xp_query) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index 994381c9c..fc556ca4d 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -76,6 +76,12 @@ defmodule CadetWeb.AssessmentsController do json(conn, %{users: users_with_xp}) end + def paginated_total_xp_for_leaderboard_display(conn, %{"course_id" => course_id, "page" => page, "page_size" => page_size}) do + offset = (String.to_integer(page) - 1) * String.to_integer(page_size) + paginated_display = Assessments.all_user_total_xp(course_id, offset, page_size) + json(conn, %{users: paginated_display}) + end + def get_contest_popular_scores(conn, %{ "assessmentid" => assessment_id, "course_id" => course_id diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index fc687fa91..de48f5ee4 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -119,6 +119,7 @@ defmodule CadetWeb.Router do put("/user/research_agreement", UserController, :update_research_agreement) get("/all_users_xp", AssessmentsController, :combined_total_xp_for_all_users) + get("/get_paginated_display/:page/:page_size", AssessmentsController, :paginated_total_xp_for_leaderboard_display) get( "/leaderboard/contests/:assessmentid/get_score_leaderboard", From c33a441fbe93deade77e66b862bba56813a3175e Mon Sep 17 00:00:00 2001 From: Blerargh Date: Tue, 29 Apr 2025 23:47:19 +0800 Subject: [PATCH 29/30] Updated all_user_total_xp to include row count --- lib/cadet/assessments/assessments.ex | 13 ++++++++++++- lib/cadet_web/controllers/assessments_controller.ex | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index aa532789b..e1986dff8 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -271,7 +271,18 @@ defmodule Cadet.Assessments do offset: ^offset ) - Repo.all(ranked_xp_query) + count_query = + from(t in subquery(total_xp_query), + select: count("*") + ) + + rows = Repo.all(ranked_xp_query) + total_count = Repo.one(count_query) + + %{ + users: rows, + total_count: total_count + } end defp decimal_to_integer(decimal) do diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index fc556ca4d..febd59067 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -73,13 +73,13 @@ defmodule CadetWeb.AssessmentsController do def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do users_with_xp = Assessments.all_user_total_xp(course_id) - json(conn, %{users: users_with_xp}) + json(conn, %{users: users_with_xp.users}) end def paginated_total_xp_for_leaderboard_display(conn, %{"course_id" => course_id, "page" => page, "page_size" => page_size}) do offset = (String.to_integer(page) - 1) * String.to_integer(page_size) paginated_display = Assessments.all_user_total_xp(course_id, offset, page_size) - json(conn, %{users: paginated_display}) + json(conn, paginated_display) end def get_contest_popular_scores(conn, %{ From 52bf87ed575d0382f9d90d2e056e4036b388ffa6 Mon Sep 17 00:00:00 2001 From: Blerargh Date: Tue, 29 Apr 2025 23:54:11 +0800 Subject: [PATCH 30/30] Combined aliases and removed unused ones --- lib/cadet/assessments/assessments.ex | 7 +++---- lib/cadet_web/controllers/assessments_controller.ex | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index e1986dff8..b2d36e382 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -4,7 +4,7 @@ defmodule Cadet.Assessments do missions, sidequests, paths, etc. """ use Cadet, [:context, :display] - alias Cadet.Incentives.{Achievement, GoalProgress, Goal} + alias Cadet.Incentives.{Achievement, GoalProgress} import Ecto.Query require Logger @@ -25,9 +25,8 @@ defmodule Cadet.Assessments do alias Cadet.Courses.{Group, AssessmentConfig} alias Cadet.Jobs.Log alias Cadet.ProgramAnalysis.Lexer - alias Ecto.Multi - alias Cadet.Incentives.{Achievements, AchievementToGoal} - alias Ecto.Changeset + alias Ecto.{Multi, Changeset} + alias Cadet.Incentives.Achievements alias Timex.Duration require Decimal diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index febd59067..dc3c67602 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -5,9 +5,8 @@ defmodule CadetWeb.AssessmentsController do import Ecto.Query, only: [where: 2] - alias Cadet.Assessments - alias Cadet.Assessments.{Question, Assessment} alias Cadet.{Assessments, Repo} + alias Cadet.Assessments.Question # These roles can save and finalise answers for closed assessments and # submitted answers