Skip to content

Commit 11154a6

Browse files
Display Popular Vote Leaderboard (#1066)
* 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 <34370238+RichDom2185@users.noreply.github.com>
1 parent 3135f48 commit 11154a6

File tree

6 files changed

+139
-6
lines changed

6 files changed

+139
-6
lines changed

lib/cadet/assessments/answer.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ defmodule Cadet.Assessments.Answer do
1616
schema "answers" do
1717
# used to compare answers with others
1818
field(:relative_score, :float, default: 0.0)
19+
field(:popular_score, :float, default: 0.0)
1920
field(:xp, :integer, default: 0)
2021
field(:xp_adjustment, :integer, default: 0)
2122
field(:comments, :string)
@@ -122,4 +123,12 @@ defmodule Cadet.Assessments.Answer do
122123
answer
123124
|> cast(contest_score_param, [:relative_score])
124125
end
126+
127+
@doc """
128+
Used to update popular_score of answer to contest_score
129+
"""
130+
def popular_score_update_changeset(answer, popular_score_param) do
131+
answer
132+
|> cast(popular_score_param, [:popular_score])
133+
end
125134
end

lib/cadet/assessments/assessments.ex

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,18 @@ defmodule Cadet.Assessments do
10061006
# fetch top 10 contest voting entries with the contest question id
10071007
question_id = fetch_associated_contest_question_id(course_id, q)
10081008

1009+
# fetch top 10 contest coting entries with contest question id based on popular score
1010+
popular_results =
1011+
if is_nil(question_id) do
1012+
[]
1013+
else
1014+
if leaderboard_open?(assessment, q) or role in @open_all_assessment_roles do
1015+
fetch_top_popular_score_answers(question_id, 10)
1016+
else
1017+
[]
1018+
end
1019+
end
1020+
10091021
leaderboard_results =
10101022
if is_nil(question_id) do
10111023
[]
@@ -1025,6 +1037,10 @@ defmodule Cadet.Assessments do
10251037
:contest_leaderboard,
10261038
leaderboard_results
10271039
)
1040+
|> Map.put(
1041+
:popular_leaderboard,
1042+
popular_results
1043+
)
10281044

10291045
Map.put(q, :question, voting_question)
10301046
else
@@ -1097,6 +1113,37 @@ defmodule Cadet.Assessments do
10971113
|> Repo.all()
10981114
end
10991115

1116+
@doc """
1117+
Fetches top answers for the given question, based on the contest popular_score
1118+
1119+
Used for contest leaderboard fetching
1120+
"""
1121+
def fetch_top_popular_score_answers(question_id, number_of_answers) do
1122+
Answer
1123+
|> where(question_id: ^question_id)
1124+
|> where(
1125+
[a],
1126+
fragment(
1127+
"?->>'code' like ?",
1128+
a.answer,
1129+
"%return%"
1130+
)
1131+
)
1132+
|> order_by(desc: :popular_score)
1133+
|> join(:left, [a], s in assoc(a, :submission))
1134+
|> join(:left, [a, s], student in assoc(s, :student))
1135+
|> join(:inner, [a, s, student], student_user in assoc(student, :user))
1136+
|> where([a, s, student], student.role == "student")
1137+
|> select([a, s, student, student_user], %{
1138+
submission_id: a.submission_id,
1139+
answer: a.answer,
1140+
popular_score: a.popular_score,
1141+
student_name: student_user.name
1142+
})
1143+
|> limit(^number_of_answers)
1144+
|> Repo.all()
1145+
end
1146+
11001147
@doc """
11011148
Computes rolling leaderboard for contest votes that are still open.
11021149
"""
@@ -1181,6 +1228,7 @@ defmodule Cadet.Assessments do
11811228
|> Repo.get_by(id: contest_voting_question_id)
11821229

11831230
entry_scores = map_eligible_votes_to_entry_score(eligible_votes, token_divider)
1231+
normalized_scores = map_eligible_votes_to_popular_score(eligible_votes, token_divider)
11841232

11851233
entry_scores
11861234
|> Enum.map(fn {ans_id, relative_score} ->
@@ -1195,6 +1243,20 @@ defmodule Cadet.Assessments do
11951243
end)
11961244
|> Enum.reduce(Multi.new(), &Multi.append/2)
11971245
|> Repo.transaction()
1246+
1247+
normalized_scores
1248+
|> Enum.map(fn {ans_id, popular_score} ->
1249+
%Answer{id: ans_id}
1250+
|> Answer.popular_score_update_changeset(%{
1251+
popular_score: popular_score
1252+
})
1253+
end)
1254+
|> Enum.map(fn changeset ->
1255+
op_key = "answer_#{changeset.data.id}"
1256+
Multi.update(Multi.new(), op_key, changeset)
1257+
end)
1258+
|> Enum.reduce(Multi.new(), &Multi.append/2)
1259+
|> Repo.transaction()
11981260
end
11991261

12001262
defp map_eligible_votes_to_entry_score(eligible_votes, token_divider) do
@@ -1220,14 +1282,46 @@ defmodule Cadet.Assessments do
12201282
)
12211283
end
12221284

1285+
defp map_eligible_votes_to_popular_score(eligible_votes, token_divider) do
1286+
# converts eligible votes to the {total cumulative score, number of votes, tokens}
1287+
entry_vote_data =
1288+
Enum.reduce(eligible_votes, %{}, fn %{ans_id: ans_id, score: score, ans: ans}, tracker ->
1289+
{prev_score, prev_count, _ans_tokens} = Map.get(tracker, ans_id, {0, 0, 0})
1290+
1291+
Map.put(
1292+
tracker,
1293+
ans_id,
1294+
# assume each voter is assigned 10 entries which will make it fair.
1295+
{prev_score + score, prev_count + 1, Lexer.count_tokens(ans)}
1296+
)
1297+
end)
1298+
1299+
# calculate the score based on formula {ans_id, score}
1300+
Enum.map(
1301+
entry_vote_data,
1302+
fn {ans_id, {sum_of_scores, number_of_voters, tokens}} ->
1303+
{ans_id,
1304+
calculate_normalized_score(sum_of_scores, number_of_voters, tokens, token_divider)}
1305+
end
1306+
)
1307+
end
1308+
12231309
# Calculate the score based on formula
12241310
# score(v,t) = v - 2^(t/token_divider) where v is the normalized_voting_score
12251311
# normalized_voting_score = sum_of_scores / number_of_voters / 10 * 100
12261312
defp calculate_formula_score(sum_of_scores, number_of_voters, tokens, token_divider) do
1227-
normalized_voting_score = sum_of_scores / number_of_voters / 10 * 100
1313+
normalized_voting_score =
1314+
calculate_normalized_score(sum_of_scores, number_of_voters, tokens, token_divider)
1315+
12281316
normalized_voting_score - :math.pow(2, min(1023.5, tokens / token_divider))
12291317
end
12301318

1319+
# Calculate the normalized score based on formula
1320+
# normalized_voting_score = sum_of_scores / number_of_voters / 10 * 100
1321+
defp calculate_normalized_score(sum_of_scores, number_of_voters, _tokens, _token_divider) do
1322+
sum_of_scores / number_of_voters / 10 * 100
1323+
end
1324+
12311325
@doc """
12321326
Function returning submissions under a grader. This function returns only the
12331327
fields that are exposed in the /grading endpoint.
@@ -1509,7 +1603,8 @@ defmodule Cadet.Assessments do
15091603
|> Enum.map(fn ans ->
15101604
if ans.question.type == :voting do
15111605
empty_contest_entries = Map.put(ans.question.question, :contest_entries, [])
1512-
empty_contest_leaderboard = Map.put(empty_contest_entries, :contest_leaderboard, [])
1606+
empty_popular_leaderboard = Map.put(empty_contest_entries, :popular_leaderboard, [])
1607+
empty_contest_leaderboard = Map.put(empty_popular_leaderboard, :contest_leaderboard, [])
15131608
question = Map.put(ans.question, :question, empty_contest_leaderboard)
15141609
Map.put(ans, :question, question)
15151610
else

lib/cadet_web/helpers/assessments_helpers.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ defmodule CadetWeb.AssessmentsHelpers do
113113
)
114114
end
115115

116+
defp build_popular_leaderboard_entry(leaderboard_ans) do
117+
Map.put(
118+
transform_map_for_view(leaderboard_ans, %{
119+
submission_id: :submission_id,
120+
answer: :answer,
121+
student_name: :student_name
122+
}),
123+
"final_score",
124+
Float.round(leaderboard_ans.popular_score, 2)
125+
)
126+
end
127+
116128
defp build_choice(choice) do
117129
transform_map_for_view(choice, %{
118130
id: "choice_id",
@@ -183,6 +195,10 @@ defmodule CadetWeb.AssessmentsHelpers do
183195
scoreLeaderboard:
184196
&Enum.map(&1[:contest_leaderboard], fn entry ->
185197
build_contest_leaderboard_entry(entry)
198+
end),
199+
popularVoteLeaderboard:
200+
&Enum.map(&1[:popular_leaderboard], fn entry ->
201+
build_popular_leaderboard_entry(entry)
186202
end)
187203
})
188204
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule Cadet.Repo.Migrations.AnswersAddPopularScoreColumn do
2+
use Ecto.Migration
3+
4+
def change do
5+
alter table("answers") do
6+
add(:popular_score, :float, default: 0.0)
7+
end
8+
end
9+
end

test/cadet_web/admin_controllers/admin_grading_controller_test.exs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,8 @@ defmodule CadetWeb.AdminGradingControllerTest do
400400
"autogradingResults" => &1.autograding_results,
401401
"answer" => nil,
402402
"contestEntries" => [],
403-
"scoreLeaderboard" => []
403+
"scoreLeaderboard" => [],
404+
"popularVoteLeaderboard" => []
404405
},
405406
"grade" => %{
406407
"xp" => &1.xp,
@@ -1075,7 +1076,8 @@ defmodule CadetWeb.AdminGradingControllerTest do
10751076
"autogradingResults" => &1.autograding_results,
10761077
"answer" => nil,
10771078
"contestEntries" => [],
1078-
"scoreLeaderboard" => []
1079+
"scoreLeaderboard" => [],
1080+
"popularVoteLeaderboard" => []
10791081
},
10801082
"grade" => %{
10811083
"xp" => &1.xp,

test/cadet_web/controllers/assessments_controller_test.exs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,8 +444,10 @@ defmodule CadetWeb.AssessmentsControllerTest do
444444
expected_voting_questions
445445
|> Enum.zip(contests_entries)
446446
|> Enum.map(fn {question, contest_entries} ->
447-
question = Map.put(question, "contestEntries", contest_entries)
448-
Map.put(question, "scoreLeaderboard", [])
447+
question
448+
|> Map.put("contestEntries", contest_entries)
449+
|> Map.put("scoreLeaderboard", [])
450+
|> Map.put("popularVoteLeaderboard", [])
449451
end)
450452

451453
expected_questions =

0 commit comments

Comments
 (0)