Skip to content

Leaderboard #1238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e80c289
added 'enable_leaderboard' columns in courses table
Blerargh Feb 13, 2025
2464ad4
Leaderboard create course config, leaderboard page routing, leaderboa…
tohyzhong Feb 14, 2025
e840385
added 'top_leaderboard_display' columns in courses table
Blerargh Feb 14, 2025
0a11acd
added 'all_user_total_xp' function for leaderboard display
Blerargh Feb 14, 2025
3093544
add top leaderboard display options to course settings (select how ma…
tohyzhong Mar 8, 2025
fe9b9bf
added contest scores fetching and contest score calculation
tohyzhong Mar 8, 2025
595bdea
Refactor query execution in assessments module for improved readability
tohyzhong Mar 8, 2025
e760976
Merge remote-tracking branch 'origin/master' into leaderboard
Blerargh Mar 9, 2025
c806f63
added functions to fetch contest scoring and voting
Blerargh Mar 9, 2025
f99a898
changes to default values
Blerargh Mar 9, 2025
f42c4cd
updated tests
Blerargh Mar 9, 2025
05f8e80
Merge branch 'leaderboard' of https://github.com/source-academy/backe…
tohyzhong Mar 9, 2025
82e633f
Fixed xp fetching for all users
tohyzhong Mar 12, 2025
64e812b
Add top contest leaderboard display configuration and update related …
tohyzhong Mar 17, 2025
731bf09
Added automatic XP assignment for winning contest entries
Blerargh Mar 18, 2025
0e89b53
Implement XP assignment for winning contest entries based on contest …
tohyzhong Mar 18, 2025
31d821d
Add default value for XP values and improve XP assignment logic for c…
tohyzhong Mar 18, 2025
9410c4f
No tiebreak for contest scoring
Blerargh Mar 18, 2025
4e78a24
Refactor contest scoring endpoints for authentication errors
tohyzhong Mar 18, 2025
04f9d20
Enhance leaderboard update logic and improve error handling for votin…
tohyzhong Mar 18, 2025
9500bcb
Refactor XP assignment logic for voting questions and set default XP …
tohyzhong Mar 19, 2025
ab255e0
Temporary Assessment Workspace leaderboard fix for testing
tohyzhong Mar 19, 2025
1166189
Fixed tests for assessments (default XP to award for contests)
tohyzhong Mar 19, 2025
1096a05
Refactor contest fetching logic to filter by voting question contest …
tohyzhong Mar 19, 2025
61a4561
Refactor leaderboard query logic to use RANK() and improve code reada…
tohyzhong Mar 20, 2025
0e1ed61
temporary fix for STePS
tohyzhong Apr 15, 2025
80fcb05
Add ranking to assessment workspace leaderboard queries and update vi…
tohyzhong Apr 15, 2025
97345b0
Merge branch 'master' into leaderboard
tohyzhong Apr 16, 2025
db2888a
Post-STePS fixes
tohyzhong Apr 25, 2025
d067879
Shifted schema updates to a new migration file
Blerargh Apr 29, 2025
0761323
Added support to retrieve only relevant rows for leaderboard pages
Blerargh Apr 29, 2025
c33a441
Updated all_user_total_xp to include row count
Blerargh Apr 29, 2025
52bf87e
Combined aliases and removed unused ones
Blerargh Apr 29, 2025
e089c64
Refactor leaderboard endpoints and update response structure for cont…
tohyzhong Apr 30, 2025
19eb5c7
Refactor contest score retrieval to handle multiple questions and imp…
tohyzhong Apr 30, 2025
2eaa06a
Remove contest score retrieval endpoints from AdminAssessmentsControl…
tohyzhong May 1, 2025
f92f87c
Removed unnecessary execute command in migration file
Blerargh May 1, 2025
b244e1b
Updated leaderboard display routes to use query parameters; Offset ca…
Blerargh May 1, 2025
8b5ddc1
Added default values if query parameters for leaderboard display are …
Blerargh May 1, 2025
f46e354
Refactor leaderboard logic to reset scores conditionally and enhance …
tohyzhong May 1, 2025
0c5254b
Fix XP assignment logic for contest winners and added test coverage f…
Blerargh May 4, 2025
19c9319
Add tests for all_user_total_xp pagination
Blerargh May 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
423 changes: 368 additions & 55 deletions lib/cadet/assessments/assessments.ex

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion lib/cadet/assessments/question_types/voting_question.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}, default: [500, 400, 300])
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
Expand Down
10 changes: 9 additions & 1 deletion lib/cadet/courses/course.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ defmodule Cadet.Courses.Course do
viewable: boolean(),
enable_game: boolean(),
enable_achievements: boolean(),
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(),
Expand All @@ -26,6 +30,10 @@ 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: 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)
Expand All @@ -41,7 +49,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 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
Expand Down
38 changes: 23 additions & 15 deletions lib/cadet/jobs/xml_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -246,22 +246,30 @@ 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])

if xp_values == [], do: question_data, else: Map.merge(question_data, %{xp_values: xp_values})
end

defp process_question_entity_by_type(_, _) do
Expand Down
37 changes: 15 additions & 22 deletions lib/cadet_web/admin_controllers/admin_assessments_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,42 +135,35 @@ defmodule CadetWeb.AdminAssessmentsController do
end
end

def get_score_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
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)

result =
contest_id
|> Assessments.fetch_top_relative_score_answers(10)
|> Enum.map(fn entry ->
AssessmentsHelpers.build_contest_leaderboard_entry(entry)
end)

render(conn, "leaderboard.json", leaderboard: result)
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_popular_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
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()

contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)
if voting_questions do
Assessments.assign_winning_contest_entries_xp(voting_questions.id)

result =
contest_id
|> Assessments.fetch_top_popular_score_answers(10)
|> Enum.map(fn entry ->
AssessmentsHelpers.build_popular_leaderboard_entry(entry)
end)

render(conn, "leaderboard.json", leaderboard: result)
text(conn, "XP Dispatched")
else
text(conn, "No voting questions found for the given assessment")
end
end

defp check_dates(open_at, close_at, assessment) do
Expand Down Expand Up @@ -288,7 +281,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: []}])

Expand Down
4 changes: 4 additions & 0 deletions lib/cadet_web/admin_controllers/admin_courses_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ 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")
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")
Expand Down
103 changes: 102 additions & 1 deletion lib/cadet_web/controllers/assessments_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ defmodule CadetWeb.AssessmentsController do

use PhoenixSwagger

alias Cadet.Assessments
import Ecto.Query, only: [where: 2]

alias Cadet.{Assessments, Repo}
alias Cadet.Assessments.Question
alias CadetWeb.AssessmentsHelpers

# These roles can save and finalise answers for closed assessments and
# submitted answers
Expand Down Expand Up @@ -67,6 +71,103 @@ 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.users})
end

def paginated_total_xp_for_leaderboard_display(conn, %{"course_id" => course_id}) do
offset = String.to_integer(conn.params["offset"] || "1");
page_size = String.to_integer(conn.params["page_size"] || "25")
paginated_display = Assessments.all_user_total_xp(course_id, offset, page_size)
json(conn, paginated_display)
end

def get_score_leaderboard(conn, %{
"assessmentid" => assessment_id,
"course_id" => course_id
}) do
visible_entries = String.to_integer(conn.params["visible_entries"] || "10")
voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id)

voting_questions =
Question
|> where(type: :voting)
|> where(assessment_id: ^assessment_id)
|> Repo.one()
|> case do
nil ->
Question
|> where(type: :voting)
|> where(assessment_id: ^voting_id)
|> Repo.one()

question ->
question
end

contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)

result =
contest_id
|> Assessments.fetch_top_relative_score_answers(visible_entries)
|> Enum.map(fn entry ->
updated_entry = %{
entry
| answer: entry.answer["code"]
}

AssessmentsHelpers.build_contest_leaderboard_entry(updated_entry)
end)

json(conn, %{leaderboard: result, voting_id: voting_id})
end

def get_popular_leaderboard(conn, %{
"assessmentid" => assessment_id,
"course_id" => course_id
}) do
visible_entries = String.to_integer(conn.params["visible_entries"] || "10")
voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id)

voting_questions =
Question
|> where(type: :voting)
|> where(assessment_id: ^assessment_id)
|> Repo.one()
|> case do
nil ->
Question
|> where(type: :voting)
|> where(assessment_id: ^voting_id)
|> Repo.one()

question ->
question
end

contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)

result =
contest_id
|> Assessments.fetch_top_popular_score_answers(visible_entries)
|> Enum.map(fn entry ->
updated_entry = %{
entry
| answer: entry.answer["code"]
}

AssessmentsHelpers.build_popular_leaderboard_entry(updated_entry)
end)

json(conn, %{leaderboard: result, 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")
Expand Down
20 changes: 20 additions & 0 deletions lib/cadet_web/controllers/courses_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ 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)
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)
Expand Down Expand Up @@ -95,6 +103,14 @@ 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)
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)
Expand All @@ -109,6 +125,10 @@ defmodule CadetWeb.CoursesController 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,
enable_stories: false,
source_chapter: 1,
Expand Down
12 changes: 12 additions & 0 deletions lib/cadet_web/controllers/user_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,14 @@ 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)
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)
Expand All @@ -330,6 +338,10 @@ defmodule CadetWeb.UserController 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,
enable_stories: false,
source_chapter: 1,
Expand Down
8 changes: 6 additions & 2 deletions lib/cadet_web/helpers/assessments_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ defmodule CadetWeb.AssessmentsHelpers do
transform_map_for_view(leaderboard_ans, %{
submission_id: :submission_id,
answer: :answer,
student_name: :student_name
student_name: :student_name,
student_username: :student_username,
rank: :rank
}),
"final_score",
Float.round(leaderboard_ans.relative_score, 2)
Expand All @@ -119,7 +121,9 @@ defmodule CadetWeb.AssessmentsHelpers do
transform_map_for_view(leaderboard_ans, %{
submission_id: :submission_id,
answer: :answer,
student_name: :student_name
student_name: :student_name,
student_username: :student_username,
rank: :rank
}),
"final_score",
Float.round(leaderboard_ans.popular_score, 2)
Expand Down
Loading
Loading