Skip to content

Commit

Permalink
Display Popular Vote Leaderboard (#1066)
Browse files Browse the repository at this point in the history
* Added new col for popular vote score and compatible with frontend

* rename new table col from relative_score to popular_score

* Fix an issue which causes test cases to fail due to nil

* changed test cases to include popularVoteLeaderboard

* Fixed formatting

* Fix credo error

---------

Co-authored-by: Richard Dominick <[email protected]>
  • Loading branch information
DesSnowy and RichDom2185 authored Feb 24, 2024
1 parent 3135f48 commit 11154a6
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 6 deletions.
9 changes: 9 additions & 0 deletions lib/cadet/assessments/answer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule Cadet.Assessments.Answer do
schema "answers" do
# used to compare answers with others
field(:relative_score, :float, default: 0.0)
field(:popular_score, :float, default: 0.0)
field(:xp, :integer, default: 0)
field(:xp_adjustment, :integer, default: 0)
field(:comments, :string)
Expand Down Expand Up @@ -122,4 +123,12 @@ defmodule Cadet.Assessments.Answer do
answer
|> cast(contest_score_param, [:relative_score])
end

@doc """
Used to update popular_score of answer to contest_score
"""
def popular_score_update_changeset(answer, popular_score_param) do
answer
|> cast(popular_score_param, [:popular_score])
end
end
99 changes: 97 additions & 2 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,18 @@ defmodule Cadet.Assessments do
# fetch top 10 contest voting entries with the contest question id
question_id = fetch_associated_contest_question_id(course_id, q)

# fetch top 10 contest coting entries with contest question id based on popular score
popular_results =
if is_nil(question_id) do
[]
else
if leaderboard_open?(assessment, q) or role in @open_all_assessment_roles do
fetch_top_popular_score_answers(question_id, 10)
else
[]
end
end

leaderboard_results =
if is_nil(question_id) do
[]
Expand All @@ -1025,6 +1037,10 @@ defmodule Cadet.Assessments do
:contest_leaderboard,
leaderboard_results
)
|> Map.put(
:popular_leaderboard,
popular_results
)

Map.put(q, :question, voting_question)
else
Expand Down Expand Up @@ -1097,6 +1113,37 @@ defmodule Cadet.Assessments do
|> Repo.all()
end

@doc """
Fetches top answers for the given question, based on the contest popular_score
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()
end

@doc """
Computes rolling leaderboard for contest votes that are still open.
"""
Expand Down Expand Up @@ -1181,6 +1228,7 @@ defmodule Cadet.Assessments do
|> Repo.get_by(id: contest_voting_question_id)

entry_scores = map_eligible_votes_to_entry_score(eligible_votes, token_divider)
normalized_scores = map_eligible_votes_to_popular_score(eligible_votes, token_divider)

entry_scores
|> Enum.map(fn {ans_id, relative_score} ->
Expand All @@ -1195,6 +1243,20 @@ defmodule Cadet.Assessments do
end)
|> Enum.reduce(Multi.new(), &Multi.append/2)
|> Repo.transaction()

normalized_scores
|> Enum.map(fn {ans_id, popular_score} ->
%Answer{id: ans_id}
|> Answer.popular_score_update_changeset(%{
popular_score: popular_score
})
end)
|> Enum.map(fn changeset ->
op_key = "answer_#{changeset.data.id}"
Multi.update(Multi.new(), op_key, changeset)
end)
|> Enum.reduce(Multi.new(), &Multi.append/2)
|> Repo.transaction()
end

defp map_eligible_votes_to_entry_score(eligible_votes, token_divider) do
Expand All @@ -1220,14 +1282,46 @@ defmodule Cadet.Assessments do
)
end

defp map_eligible_votes_to_popular_score(eligible_votes, token_divider) do
# converts eligible votes to the {total cumulative score, number of votes, tokens}
entry_vote_data =
Enum.reduce(eligible_votes, %{}, fn %{ans_id: ans_id, score: score, ans: ans}, tracker ->
{prev_score, prev_count, _ans_tokens} = Map.get(tracker, ans_id, {0, 0, 0})

Map.put(
tracker,
ans_id,
# assume each voter is assigned 10 entries which will make it fair.
{prev_score + score, prev_count + 1, Lexer.count_tokens(ans)}
)
end)

# calculate the score based on formula {ans_id, score}
Enum.map(
entry_vote_data,
fn {ans_id, {sum_of_scores, number_of_voters, tokens}} ->
{ans_id,
calculate_normalized_score(sum_of_scores, number_of_voters, tokens, token_divider)}
end
)
end

# Calculate the score based on formula
# score(v,t) = v - 2^(t/token_divider) where v is the normalized_voting_score
# normalized_voting_score = sum_of_scores / number_of_voters / 10 * 100
defp calculate_formula_score(sum_of_scores, number_of_voters, tokens, token_divider) do
normalized_voting_score = sum_of_scores / number_of_voters / 10 * 100
normalized_voting_score =
calculate_normalized_score(sum_of_scores, number_of_voters, tokens, token_divider)

normalized_voting_score - :math.pow(2, min(1023.5, tokens / token_divider))
end

# Calculate the normalized score based on formula
# normalized_voting_score = sum_of_scores / number_of_voters / 10 * 100
defp calculate_normalized_score(sum_of_scores, number_of_voters, _tokens, _token_divider) do
sum_of_scores / number_of_voters / 10 * 100
end

@doc """
Function returning submissions under a grader. This function returns only the
fields that are exposed in the /grading endpoint.
Expand Down Expand Up @@ -1509,7 +1603,8 @@ defmodule Cadet.Assessments do
|> Enum.map(fn ans ->
if ans.question.type == :voting do
empty_contest_entries = Map.put(ans.question.question, :contest_entries, [])
empty_contest_leaderboard = Map.put(empty_contest_entries, :contest_leaderboard, [])
empty_popular_leaderboard = Map.put(empty_contest_entries, :popular_leaderboard, [])
empty_contest_leaderboard = Map.put(empty_popular_leaderboard, :contest_leaderboard, [])
question = Map.put(ans.question, :question, empty_contest_leaderboard)
Map.put(ans, :question, question)
else
Expand Down
16 changes: 16 additions & 0 deletions lib/cadet_web/helpers/assessments_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ defmodule CadetWeb.AssessmentsHelpers do
)
end

defp build_popular_leaderboard_entry(leaderboard_ans) do
Map.put(
transform_map_for_view(leaderboard_ans, %{
submission_id: :submission_id,
answer: :answer,
student_name: :student_name
}),
"final_score",
Float.round(leaderboard_ans.popular_score, 2)
)
end

defp build_choice(choice) do
transform_map_for_view(choice, %{
id: "choice_id",
Expand Down Expand Up @@ -183,6 +195,10 @@ defmodule CadetWeb.AssessmentsHelpers do
scoreLeaderboard:
&Enum.map(&1[:contest_leaderboard], fn entry ->
build_contest_leaderboard_entry(entry)
end),
popularVoteLeaderboard:
&Enum.map(&1[:popular_leaderboard], fn entry ->
build_popular_leaderboard_entry(entry)
end)
})
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Cadet.Repo.Migrations.AnswersAddPopularScoreColumn do
use Ecto.Migration

def change do
alter table("answers") do
add(:popular_score, :float, default: 0.0)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@ defmodule CadetWeb.AdminGradingControllerTest do
"autogradingResults" => &1.autograding_results,
"answer" => nil,
"contestEntries" => [],
"scoreLeaderboard" => []
"scoreLeaderboard" => [],
"popularVoteLeaderboard" => []
},
"grade" => %{
"xp" => &1.xp,
Expand Down Expand Up @@ -1075,7 +1076,8 @@ defmodule CadetWeb.AdminGradingControllerTest do
"autogradingResults" => &1.autograding_results,
"answer" => nil,
"contestEntries" => [],
"scoreLeaderboard" => []
"scoreLeaderboard" => [],
"popularVoteLeaderboard" => []
},
"grade" => %{
"xp" => &1.xp,
Expand Down
6 changes: 4 additions & 2 deletions test/cadet_web/controllers/assessments_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,10 @@ defmodule CadetWeb.AssessmentsControllerTest do
expected_voting_questions
|> Enum.zip(contests_entries)
|> Enum.map(fn {question, contest_entries} ->
question = Map.put(question, "contestEntries", contest_entries)
Map.put(question, "scoreLeaderboard", [])
question
|> Map.put("contestEntries", contest_entries)
|> Map.put("scoreLeaderboard", [])
|> Map.put("popularVoteLeaderboard", [])
end)

expected_questions =
Expand Down

0 comments on commit 11154a6

Please sign in to comment.