Skip to content
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

Display Popular Vote Leaderboard #1066

Merged
merged 10 commits into from
Feb 24, 2024
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
Loading