Skip to content

Commit

Permalink
Merge branch 'master' into sv
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-henz authored Sep 19, 2023
2 parents 686c135 + fdeb111 commit 9d6ff07
Show file tree
Hide file tree
Showing 4 changed files with 389 additions and 100 deletions.
4 changes: 3 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ config :cadet, Cadet.Jobs.Scheduler,
# Compute contest leaderboard that close in the previous day at 00:01
{"1 0 * * *", {Cadet.Assessments, :update_final_contest_leaderboards, []}},
# Compute rolling leaderboard every 2 hours
{"0 */2 * * *", {Cadet.Assessments, :update_rolling_contest_leaderboards, []}}
{"0 */2 * * *", {Cadet.Assessments, :update_rolling_contest_leaderboards, []}},
# Collate contest entries that close in the previous day at 00:01
{"1 0 * * *", {Cadet.Assessments, :update_final_contest_entries, []}}
]

# Configures the endpoint
Expand Down
223 changes: 135 additions & 88 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule Cadet.Assessments do
alias Cadet.ProgramAnalysis.Lexer
alias Ecto.Multi
alias Cadet.Incentives.Achievements
alias Timex.Duration

require Decimal

Expand Down Expand Up @@ -500,6 +501,35 @@ defmodule Cadet.Assessments do
Question.changeset(%Question{}, params_with_assessment_id)
end

def update_final_contest_entries do
# 1435 = 1 day - 5 minutes
if Log.log_execution("update_final_contest_entries", Duration.from_minutes(1435)) do
Logger.info("Started update of contest entry pools")
questions = fetch_unassigned_voting_questions()

for q <- questions do
insert_voting(q.course_id, q.question["contest_number"], q.question_id)
end

Logger.info("Successfully update contest entry pools")
end
end

# fetch voting questions where entries have not been assigned
def fetch_unassigned_voting_questions do
voting_assigned_question_ids =
SubmissionVotes
|> select([v], v.question_id)
|> Repo.all()

Question
|> where(type: :voting)
|> where([q], q.id not in ^voting_assigned_question_ids)
|> join(:inner, [q], asst in assoc(q, :assessment))
|> select([q, asst], %{course_id: asst.course_id, question: q.question, question_id: q.id})
|> Repo.all()
end

@doc """
Generates and assigns contest entries for users with given usernames.
"""
Expand All @@ -522,102 +552,119 @@ defmodule Cadet.Assessments do

{:error, error_changeset}
else
# Returns contest submission ids with answers that contain "return"
contest_submission_ids =
Submission
|> join(:inner, [s], ans in assoc(s, :answers))
|> join(:inner, [s, ans], cr in assoc(s, :student))
|> where([s, ans, cr], cr.role == "student")
|> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted")
|> where(
[_, ans, cr],
fragment(
"?->>'code' like ?",
ans.answer,
"%return%"
)
if Timex.compare(contest_assessment.close_at, Timex.now()) < 0 do
compile_entries(course_id, contest_assessment, question_id)
else
# contest has not closed, do nothing
{:ok, nil}
end
end
end

def compile_entries(
course_id,
contest_assessment,
question_id
) do
# Returns contest submission ids with answers that contain "return"
contest_submission_ids =
Submission
|> join(:inner, [s], ans in assoc(s, :answers))
|> join(:inner, [s, ans], cr in assoc(s, :student))
|> where([s, ans, cr], cr.role == "student")
|> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted")
|> where(
[_, ans, cr],
fragment(
"?->>'code' like ?",
ans.answer,
"%return%"
)
|> select([s, _ans], {s.student_id, s.id})
|> Repo.all()
|> Enum.into(%{})
)
|> select([s, _ans], {s.student_id, s.id})
|> Repo.all()
|> Enum.into(%{})

contest_submission_ids_length = Enum.count(contest_submission_ids)
contest_submission_ids_length = Enum.count(contest_submission_ids)

voter_ids =
CourseRegistration
|> where(role: "student", course_id: ^course_id)
|> select([cr], cr.id)
|> Repo.all()
voter_ids =
CourseRegistration
|> where(role: "student", course_id: ^course_id)
|> select([cr], cr.id)
|> Repo.all()

votes_per_user = min(contest_submission_ids_length, 10)
votes_per_user = min(contest_submission_ids_length, 10)

votes_per_submission =
if Enum.empty?(contest_submission_ids) do
0
else
trunc(Float.ceil(votes_per_user * length(voter_ids) / contest_submission_ids_length))
end
votes_per_submission =
if Enum.empty?(contest_submission_ids) do
0
else
trunc(Float.ceil(votes_per_user * length(voter_ids) / contest_submission_ids_length))
end

submission_id_list =
contest_submission_ids
|> Enum.map(fn {_, s_id} -> s_id end)
|> Enum.shuffle()
|> List.duplicate(votes_per_submission)
|> List.flatten()

{_submission_map, submission_votes_changesets} =
voter_ids
|> Enum.reduce({submission_id_list, []}, fn voter_id, acc ->
{submission_list, submission_votes} = acc

user_contest_submission_id = Map.get(contest_submission_ids, voter_id)

{votes, rest} =
submission_list
|> Enum.reduce_while({MapSet.new(), submission_list}, fn s_id, acc ->
{user_votes, submissions} = acc

max_votes =
if votes_per_user == contest_submission_ids_length and
not is_nil(user_contest_submission_id) do
# no. of submssions is less than 10. Unable to find
votes_per_user - 1
else
votes_per_user
end

if MapSet.size(user_votes) < max_votes do
if s_id != user_contest_submission_id and not MapSet.member?(user_votes, s_id) do
new_user_votes = MapSet.put(user_votes, s_id)
new_submissions = List.delete(submissions, s_id)
{:cont, {new_user_votes, new_submissions}}
else
{:cont, {user_votes, submissions}}
end
submission_id_list =
contest_submission_ids
|> Enum.map(fn {_, s_id} -> s_id end)
|> Enum.shuffle()
|> List.duplicate(votes_per_submission)
|> List.flatten()

{_submission_map, submission_votes_changesets} =
voter_ids
|> Enum.reduce({submission_id_list, []}, fn voter_id, acc ->
{submission_list, submission_votes} = acc

user_contest_submission_id = Map.get(contest_submission_ids, voter_id)

{votes, rest} =
submission_list
|> Enum.reduce_while({MapSet.new(), submission_list}, fn s_id, acc ->
{user_votes, submissions} = acc

max_votes =
if votes_per_user == contest_submission_ids_length and
not is_nil(user_contest_submission_id) do
# no. of submssions is less than 10. Unable to find
votes_per_user - 1
else
{:halt, acc}
votes_per_user
end
end)

votes = MapSet.to_list(votes)

new_submission_votes =
votes
|> Enum.map(fn s_id ->
%SubmissionVotes{voter_id: voter_id, submission_id: s_id, question_id: question_id}
end)
|> Enum.concat(submission_votes)

{rest, new_submission_votes}
end)

submission_votes_changesets
|> Enum.with_index()
|> Enum.reduce(Multi.new(), fn {changeset, index}, multi ->
Multi.insert(multi, Integer.to_string(index), changeset)
if MapSet.size(user_votes) < max_votes do
if s_id != user_contest_submission_id and not MapSet.member?(user_votes, s_id) do
new_user_votes = MapSet.put(user_votes, s_id)
new_submissions = List.delete(submissions, s_id)
{:cont, {new_user_votes, new_submissions}}
else
{:cont, {user_votes, submissions}}
end
else
{:halt, acc}
end
end)

votes = MapSet.to_list(votes)

new_submission_votes =
votes
|> Enum.map(fn s_id ->
%SubmissionVotes{
voter_id: voter_id,
submission_id: s_id,
question_id: question_id
}
end)
|> Enum.concat(submission_votes)

{rest, new_submission_votes}
end)
|> Repo.transaction()
end

submission_votes_changesets
|> Enum.with_index()
|> Enum.reduce(Multi.new(), fn {changeset, index}, multi ->
Multi.insert(multi, Integer.to_string(index), changeset)
end)
|> Repo.transaction()
end

def update_assessment(id, params) when is_ecto_id(id) do
Expand Down Expand Up @@ -1026,7 +1073,7 @@ defmodule Cadet.Assessments do
"""
def update_rolling_contest_leaderboards do
# 115 = 2 hours - 5 minutes is default.
if Log.log_execution("update_rolling_contest_leaderboards", Timex.Duration.from_minutes(115)) do
if Log.log_execution("update_rolling_contest_leaderboards", Duration.from_minutes(115)) do
Logger.info("Started update_rolling_contest_leaderboards")

voting_questions_to_update = fetch_active_voting_questions()
Expand All @@ -1053,7 +1100,7 @@ defmodule Cadet.Assessments do
"""
def update_final_contest_leaderboards do
# 1435 = 24 hours - 5 minutes
if Log.log_execution("update_final_contest_leaderboards", Timex.Duration.from_minutes(1435)) do
if Log.log_execution("update_final_contest_leaderboards", Duration.from_minutes(1435)) do
Logger.info("Started update_final_contest_leaderboards")

voting_questions_to_update = fetch_voting_questions_due_yesterday()
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@
"gen_stage": {:hex, :gen_stage, "1.1.2", "b1656cd4ba431ed02c5656fe10cb5423820847113a07218da68eae5d6a260c23", [:mix], [], "hexpm", "9e39af23140f704e2b07a3e29d8f05fd21c2aaf4088ff43cb82be4b9e3148d02"},
"gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
"git_hooks": {:hex, :git_hooks, "0.7.3", "09489e94d88dfc767662e22aff2b6208bd7cf555a19dd0e1477cca4683ce0701", [:mix], [{:blankable, "~> 1.0.0", [hex: :blankable, repo: "hexpm", optional: false]}, {:recase, "~> 0.7.0", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "d6ddedeb4d3a8602bc3f84e087a38f6150a86d9e790628ed8bc70e6d90681659"},
"guardian": {:hex, :guardian, "2.3.1", "2b2d78dc399a7df182d739ddc0e566d88723299bfac20be36255e2d052fd215d", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bbe241f9ca1b09fad916ad42d6049d2600bbc688aba5b3c4a6c82592a54274c3"},
"guardian": {:hex, :guardian, "2.3.2", "78003504b987f2b189d76ccf9496ceaa6a454bb2763627702233f31eb7212881", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b189ff38cd46a22a8a824866a6867ca8722942347f13c33f7d23126af8821b52"},
"guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"},
"hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inch_ex": {:hex, :inch_ex, "2.1.0-rc.1", "7642a8902c0d2ed5d9b5754b2fc88fedf630500d630fc03db7caca2e92dedb36", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4ceee988760f9382d1c1d0b93ea5875727f6071693e89a0a3c49c456ef1be75d"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
"jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"},
"mail": {:hex, :mail, "0.2.3", "2c6bb5f8a5f74845fa50ecd0fb45ea16b164026f285f45104f1c4c078cd616d4", [:mix], [], "hexpm", "932b398fa9c69fdf290d7ff63175826e0f1e24414d5b0763bb00a2acfc6c6bf5"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
Expand Down
Loading

0 comments on commit 9d6ff07

Please sign in to comment.