From bb928b3fc63f58ecf72d37fbfa5749115a70a3c2 Mon Sep 17 00:00:00 2001 From: Xiang Yu <156517235+DesSnowy@users.noreply.github.com> Date: Tue, 13 Feb 2024 18:54:19 +0800 Subject: [PATCH 1/6] Added new col for popular vote score and compatible with frontend --- lib/cadet/assessments/answer.ex | 9 ++ lib/cadet/assessments/assessments.ex | 97 ++++++++++++++++++- lib/cadet_web/helpers/assessments_helpers.ex | 18 +++- ...81500_answers_add_popular_score_column.exs | 10 ++ 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs diff --git a/lib/cadet/assessments/answer.ex b/lib/cadet/assessments/answer.ex index 1035591ab..bb867eced 100644 --- a/lib/cadet/assessments/answer.ex +++ b/lib/cadet/assessments/answer.ex @@ -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) @@ -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 diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 6bb932160..4c43e1a81 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -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 [] @@ -1017,15 +1029,19 @@ defmodule Cadet.Assessments do end end + # populate entries to vote for and leaderboard data into the question voting_question = q.question |> Map.put(:contest_entries, submission_votes) |> Map.put( - :contest_leaderboard, + :score_leaderboard, leaderboard_results ) - + |> Map.put( + :popular_leaderboard, + popular_results + ) Map.put(q, :question, voting_question) else q @@ -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. """ @@ -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} -> @@ -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 @@ -1220,14 +1282,43 @@ 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. The reason we select only diff --git a/lib/cadet_web/helpers/assessments_helpers.ex b/lib/cadet_web/helpers/assessments_helpers.ex index a77e0a618..14be0d482 100644 --- a/lib/cadet_web/helpers/assessments_helpers.ex +++ b/lib/cadet_web/helpers/assessments_helpers.ex @@ -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", @@ -181,8 +193,12 @@ defmodule CadetWeb.AssessmentsHelpers do contestEntries: &Enum.map(&1[:contest_entries], fn entry -> build_contest_entry(entry) end), scoreLeaderboard: - &Enum.map(&1[:contest_leaderboard], fn entry -> + &Enum.map(&1[:score_leaderboard], fn entry -> build_contest_leaderboard_entry(entry) + end), + popularVoteLeaderboard: + &Enum.map(&1[:popular_leaderboard], fn entry -> + build_popular_leaderboard_entry(entry) end) }) end diff --git a/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs b/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs new file mode 100644 index 000000000..651947f2d --- /dev/null +++ b/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs @@ -0,0 +1,10 @@ +defmodule Cadet.Repo.Migrations.AnswersAddPopularScoreColumn do + use Ecto.Migration + + def change do + alter table("answers") do + add(:relative_score, :float, default: 0.0) + end + + end +end From e408ac2643a3d202150ef4215a4186f89556f71b Mon Sep 17 00:00:00 2001 From: Xiang Yu <156517235+DesSnowy@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:30:22 +0800 Subject: [PATCH 2/6] rename new table col from relative_score to popular_score --- .../20240213081500_answers_add_popular_score_column.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs b/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs index 651947f2d..92ce498ea 100644 --- a/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs +++ b/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs @@ -3,7 +3,7 @@ defmodule Cadet.Repo.Migrations.AnswersAddPopularScoreColumn do def change do alter table("answers") do - add(:relative_score, :float, default: 0.0) + add(:popular_score, :float, default: 0.0) end end From f58ebd9b1b844e46743bf2e01845dcfdc943bd46 Mon Sep 17 00:00:00 2001 From: Xiang Yu <156517235+DesSnowy@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:57:16 +0800 Subject: [PATCH 3/6] Fix an issue which causes test cases to fail due to nil --- lib/cadet/assessments/assessments.ex | 5 +++-- lib/cadet_web/helpers/assessments_helpers.ex | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 4c43e1a81..8f26dd87e 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1035,7 +1035,7 @@ defmodule Cadet.Assessments do q.question |> Map.put(:contest_entries, submission_votes) |> Map.put( - :score_leaderboard, + :contest_leaderboard, leaderboard_results ) |> Map.put( @@ -1462,7 +1462,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 diff --git a/lib/cadet_web/helpers/assessments_helpers.ex b/lib/cadet_web/helpers/assessments_helpers.ex index 14be0d482..57d99f0a9 100644 --- a/lib/cadet_web/helpers/assessments_helpers.ex +++ b/lib/cadet_web/helpers/assessments_helpers.ex @@ -193,7 +193,7 @@ defmodule CadetWeb.AssessmentsHelpers do contestEntries: &Enum.map(&1[:contest_entries], fn entry -> build_contest_entry(entry) end), scoreLeaderboard: - &Enum.map(&1[:score_leaderboard], fn entry -> + &Enum.map(&1[:contest_leaderboard], fn entry -> build_contest_leaderboard_entry(entry) end), popularVoteLeaderboard: From 5fdd36ac8fbd1a2780c9c594ce3abe4136609c93 Mon Sep 17 00:00:00 2001 From: Xiang Yu <156517235+DesSnowy@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:58:12 +0800 Subject: [PATCH 4/6] changed test cases to include popularVoteLeaderboard --- .../admin_controllers/admin_grading_controller_test.exs | 6 ++++-- test/cadet_web/controllers/assessments_controller_test.exs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index e2491051f..a99c1bde7 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -372,7 +372,8 @@ defmodule CadetWeb.AdminGradingControllerTest do "autogradingResults" => &1.autograding_results, "answer" => nil, "contestEntries" => [], - "scoreLeaderboard" => [] + "scoreLeaderboard" => [], + "popularVoteLeaderboard" => [] }, "grade" => %{ "xp" => &1.xp, @@ -1019,7 +1020,8 @@ defmodule CadetWeb.AdminGradingControllerTest do "autogradingResults" => &1.autograding_results, "answer" => nil, "contestEntries" => [], - "scoreLeaderboard" => [] + "scoreLeaderboard" => [], + "popularVoteLeaderboard" => [] }, "grade" => %{ "xp" => &1.xp, diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index 02bfc4603..dc5cf71f2 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -446,6 +446,7 @@ defmodule CadetWeb.AssessmentsControllerTest do |> Enum.map(fn {question, contest_entries} -> question = Map.put(question, "contestEntries", contest_entries) Map.put(question, "scoreLeaderboard", []) + |> Map.put("popularVoteLeaderboard", []) end) expected_questions = From 4b4a45d847ad8924add0f50affa5289d692b0f2b Mon Sep 17 00:00:00 2001 From: Xiang Yu <156517235+DesSnowy@users.noreply.github.com> Date: Fri, 16 Feb 2024 00:14:33 +0800 Subject: [PATCH 5/6] Fixed formatting --- lib/cadet/assessments/answer.ex | 2 +- lib/cadet/assessments/assessments.ex | 11 +++++++---- ...0240213081500_answers_add_popular_score_column.exs | 1 - .../controllers/assessments_controller_test.exs | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/cadet/assessments/answer.ex b/lib/cadet/assessments/answer.ex index bb867eced..933c9603a 100644 --- a/lib/cadet/assessments/answer.ex +++ b/lib/cadet/assessments/answer.ex @@ -124,7 +124,7 @@ defmodule Cadet.Assessments.Answer do |> cast(contest_score_param, [:relative_score]) end - @doc """ + @doc """ Used to update popular_score of answer to contest_score """ def popular_score_update_changeset(answer, popular_score_param) do diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 8f26dd87e..2f138b177 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1029,7 +1029,6 @@ defmodule Cadet.Assessments do end end - # populate entries to vote for and leaderboard data into the question voting_question = q.question @@ -1042,6 +1041,7 @@ defmodule Cadet.Assessments do :popular_leaderboard, popular_results ) + Map.put(q, :question, voting_question) else q @@ -1113,7 +1113,7 @@ defmodule Cadet.Assessments do |> Repo.all() end - @doc """ + @doc """ Fetches top answers for the given question, based on the contest popular_score Used for contest leaderboard fetching @@ -1300,7 +1300,8 @@ defmodule Cadet.Assessments do 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)} + {ans_id, + calculate_normalized_score(sum_of_scores, number_of_voters, tokens, token_divider)} end ) end @@ -1309,7 +1310,9 @@ defmodule Cadet.Assessments do # 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 = calculate_normalized_score(sum_of_scores, number_of_voters, tokens, token_divider) + 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 diff --git a/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs b/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs index 92ce498ea..ffeae3cbe 100644 --- a/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs +++ b/priv/repo/migrations/20240213081500_answers_add_popular_score_column.exs @@ -5,6 +5,5 @@ defmodule Cadet.Repo.Migrations.AnswersAddPopularScoreColumn do alter table("answers") do add(:popular_score, :float, default: 0.0) end - end end diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index dc5cf71f2..dff618a80 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -445,6 +445,7 @@ defmodule CadetWeb.AssessmentsControllerTest do |> Enum.zip(contests_entries) |> Enum.map(fn {question, contest_entries} -> question = Map.put(question, "contestEntries", contest_entries) + Map.put(question, "scoreLeaderboard", []) |> Map.put("popularVoteLeaderboard", []) end) From f65def411700882919aa32fd2925c43ebc4c77f6 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 18 Feb 2024 15:38:43 +0800 Subject: [PATCH 6/6] Fix credo error --- test/cadet_web/controllers/assessments_controller_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index dff618a80..f9251d09b 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -444,9 +444,9 @@ 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)