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 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
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
505 changes: 451 additions & 54 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
49 changes: 44 additions & 5 deletions lib/cadet_web/admin_controllers/admin_assessments_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -146,15 +150,19 @@ 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)

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)
Expand All @@ -165,14 +173,45 @@ 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)

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 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

defp check_dates(open_at, close_at, assessment) do
if is_nil(open_at) and is_nil(close_at) do
{:ok, assessment}
Expand Down Expand Up @@ -288,7 +327,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
42 changes: 42 additions & 0 deletions 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

import Ecto.Query, only: [where: 2]

alias Cadet.Assessments
alias Cadet.Assessments.{Question, Assessment}
alias Cadet.{Assessments, Repo}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if you need to alias Assessments again


# These roles can save and finalise answers for closed assessments and
# submitted answers
Expand Down Expand Up @@ -67,6 +71,44 @@ defmodule CadetWeb.AssessmentsController do
end
end

def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why we require all users? Could we possible paginate this. It feels like it would be an expensive query.

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()
Comment on lines +97 to +100
Copy link
Contributor

Choose a reason for hiding this comment

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

I know contests are supposed to have one question, but can we handle the error raised in case for some reason there was more than one question? i.e. returning a proper error message.


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")
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
6 changes: 4 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,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)
Expand All @@ -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)
Expand Down
32 changes: 30 additions & 2 deletions lib/cadet_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ defmodule CadetWeb.Router do
put("/user/game_states", UserController, :update_game_states)
put("/user/research_agreement", UserController, :update_research_agreement)

get("/all_users_xp", AssessmentsController, :combined_total_xp_for_all_users)

get(
"/leaderboard/contests/:assessmentid/get_score_leaderboard",
AssessmentsController,
:get_contest_relative_scores
)

get(
"/leaderboard/contests/:assessmentid/get_popular_vote_leaderboard",
AssessmentsController,
:get_contest_popular_scores
)

get("/all_contests", AssessmentsController, :get_all_contests)

get("/config", CoursesController, :index)

get("/team/:assessmentid", TeamController, :index)
Expand Down Expand Up @@ -179,14 +195,26 @@ defmodule CadetWeb.Router do

resources("/sourcecast", AdminSourcecastController, only: [:create, :delete])

post(
"/assessments/:assessmentid/calculateContestScore",
AdminAssessmentsController,
:calculate_contest_score
)

post(
"/assessments/:assessmentid/dispatchContestXp",
AdminAssessmentsController,
:dispatch_contest_xp
)

get(
"/assessments/:assessmentid/popularVoteLeaderboard",
"/assessments/:assessmentid/:visibleentries/popularVoteLeaderboard",
AdminAssessmentsController,
:get_popular_leaderboard
)

get(
"/assessments/:assessmentid/scoreLeaderboard",
"/assessments/:assessmentid/:visibleentries/scoreLeaderboard",
AdminAssessmentsController,
:get_score_leaderboard
)
Expand Down
3 changes: 2 additions & 1 deletion lib/cadet_web/views/assessments_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ defmodule CadetWeb.AssessmentsView do
%{
student_name: :student_name,
answer: & &1.answer["code"],
final_score: "final_score"
final_score: "final_score",
rank: :rank
}
)
end
Expand Down
Loading
Loading