Skip to content

Commit fdeb111

Browse files
authored
Fix issue with contest voting pooling (#1000)
1 parent 04ec74b commit fdeb111

File tree

3 files changed

+387
-98
lines changed

3 files changed

+387
-98
lines changed

config/config.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ config :cadet, Cadet.Jobs.Scheduler,
2323
# Compute contest leaderboard that close in the previous day at 00:01
2424
{"1 0 * * *", {Cadet.Assessments, :update_final_contest_leaderboards, []}},
2525
# Compute rolling leaderboard every 2 hours
26-
{"0 */2 * * *", {Cadet.Assessments, :update_rolling_contest_leaderboards, []}}
26+
{"0 */2 * * *", {Cadet.Assessments, :update_rolling_contest_leaderboards, []}},
27+
# Collate contest entries that close in the previous day at 00:01
28+
{"1 0 * * *", {Cadet.Assessments, :update_final_contest_entries, []}}
2729
]
2830

2931
# Configures the endpoint

lib/cadet/assessments/assessments.ex

Lines changed: 135 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ defmodule Cadet.Assessments do
2323
alias Cadet.ProgramAnalysis.Lexer
2424
alias Ecto.Multi
2525
alias Cadet.Incentives.Achievements
26+
alias Timex.Duration
2627

2728
require Decimal
2829

@@ -500,6 +501,35 @@ defmodule Cadet.Assessments do
500501
Question.changeset(%Question{}, params_with_assessment_id)
501502
end
502503

504+
def update_final_contest_entries do
505+
# 1435 = 1 day - 5 minutes
506+
if Log.log_execution("update_final_contest_entries", Duration.from_minutes(1435)) do
507+
Logger.info("Started update of contest entry pools")
508+
questions = fetch_unassigned_voting_questions()
509+
510+
for q <- questions do
511+
insert_voting(q.course_id, q.question["contest_number"], q.question_id)
512+
end
513+
514+
Logger.info("Successfully update contest entry pools")
515+
end
516+
end
517+
518+
# fetch voting questions where entries have not been assigned
519+
def fetch_unassigned_voting_questions do
520+
voting_assigned_question_ids =
521+
SubmissionVotes
522+
|> select([v], v.question_id)
523+
|> Repo.all()
524+
525+
Question
526+
|> where(type: :voting)
527+
|> where([q], q.id not in ^voting_assigned_question_ids)
528+
|> join(:inner, [q], asst in assoc(q, :assessment))
529+
|> select([q, asst], %{course_id: asst.course_id, question: q.question, question_id: q.id})
530+
|> Repo.all()
531+
end
532+
503533
@doc """
504534
Generates and assigns contest entries for users with given usernames.
505535
"""
@@ -522,102 +552,119 @@ defmodule Cadet.Assessments do
522552

523553
{:error, error_changeset}
524554
else
525-
# Returns contest submission ids with answers that contain "return"
526-
contest_submission_ids =
527-
Submission
528-
|> join(:inner, [s], ans in assoc(s, :answers))
529-
|> join(:inner, [s, ans], cr in assoc(s, :student))
530-
|> where([s, ans, cr], cr.role == "student")
531-
|> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted")
532-
|> where(
533-
[_, ans, cr],
534-
fragment(
535-
"?->>'code' like ?",
536-
ans.answer,
537-
"%return%"
538-
)
555+
if Timex.compare(contest_assessment.close_at, Timex.now()) < 0 do
556+
compile_entries(course_id, contest_assessment, question_id)
557+
else
558+
# contest has not closed, do nothing
559+
{:ok, nil}
560+
end
561+
end
562+
end
563+
564+
def compile_entries(
565+
course_id,
566+
contest_assessment,
567+
question_id
568+
) do
569+
# Returns contest submission ids with answers that contain "return"
570+
contest_submission_ids =
571+
Submission
572+
|> join(:inner, [s], ans in assoc(s, :answers))
573+
|> join(:inner, [s, ans], cr in assoc(s, :student))
574+
|> where([s, ans, cr], cr.role == "student")
575+
|> where([s, _], s.assessment_id == ^contest_assessment.id and s.status == "submitted")
576+
|> where(
577+
[_, ans, cr],
578+
fragment(
579+
"?->>'code' like ?",
580+
ans.answer,
581+
"%return%"
539582
)
540-
|> select([s, _ans], {s.student_id, s.id})
541-
|> Repo.all()
542-
|> Enum.into(%{})
583+
)
584+
|> select([s, _ans], {s.student_id, s.id})
585+
|> Repo.all()
586+
|> Enum.into(%{})
543587

544-
contest_submission_ids_length = Enum.count(contest_submission_ids)
588+
contest_submission_ids_length = Enum.count(contest_submission_ids)
545589

546-
voter_ids =
547-
CourseRegistration
548-
|> where(role: "student", course_id: ^course_id)
549-
|> select([cr], cr.id)
550-
|> Repo.all()
590+
voter_ids =
591+
CourseRegistration
592+
|> where(role: "student", course_id: ^course_id)
593+
|> select([cr], cr.id)
594+
|> Repo.all()
551595

552-
votes_per_user = min(contest_submission_ids_length, 10)
596+
votes_per_user = min(contest_submission_ids_length, 10)
553597

554-
votes_per_submission =
555-
if Enum.empty?(contest_submission_ids) do
556-
0
557-
else
558-
trunc(Float.ceil(votes_per_user * length(voter_ids) / contest_submission_ids_length))
559-
end
598+
votes_per_submission =
599+
if Enum.empty?(contest_submission_ids) do
600+
0
601+
else
602+
trunc(Float.ceil(votes_per_user * length(voter_ids) / contest_submission_ids_length))
603+
end
560604

561-
submission_id_list =
562-
contest_submission_ids
563-
|> Enum.map(fn {_, s_id} -> s_id end)
564-
|> Enum.shuffle()
565-
|> List.duplicate(votes_per_submission)
566-
|> List.flatten()
567-
568-
{_submission_map, submission_votes_changesets} =
569-
voter_ids
570-
|> Enum.reduce({submission_id_list, []}, fn voter_id, acc ->
571-
{submission_list, submission_votes} = acc
572-
573-
user_contest_submission_id = Map.get(contest_submission_ids, voter_id)
574-
575-
{votes, rest} =
576-
submission_list
577-
|> Enum.reduce_while({MapSet.new(), submission_list}, fn s_id, acc ->
578-
{user_votes, submissions} = acc
579-
580-
max_votes =
581-
if votes_per_user == contest_submission_ids_length and
582-
not is_nil(user_contest_submission_id) do
583-
# no. of submssions is less than 10. Unable to find
584-
votes_per_user - 1
585-
else
586-
votes_per_user
587-
end
588-
589-
if MapSet.size(user_votes) < max_votes do
590-
if s_id != user_contest_submission_id and not MapSet.member?(user_votes, s_id) do
591-
new_user_votes = MapSet.put(user_votes, s_id)
592-
new_submissions = List.delete(submissions, s_id)
593-
{:cont, {new_user_votes, new_submissions}}
594-
else
595-
{:cont, {user_votes, submissions}}
596-
end
605+
submission_id_list =
606+
contest_submission_ids
607+
|> Enum.map(fn {_, s_id} -> s_id end)
608+
|> Enum.shuffle()
609+
|> List.duplicate(votes_per_submission)
610+
|> List.flatten()
611+
612+
{_submission_map, submission_votes_changesets} =
613+
voter_ids
614+
|> Enum.reduce({submission_id_list, []}, fn voter_id, acc ->
615+
{submission_list, submission_votes} = acc
616+
617+
user_contest_submission_id = Map.get(contest_submission_ids, voter_id)
618+
619+
{votes, rest} =
620+
submission_list
621+
|> Enum.reduce_while({MapSet.new(), submission_list}, fn s_id, acc ->
622+
{user_votes, submissions} = acc
623+
624+
max_votes =
625+
if votes_per_user == contest_submission_ids_length and
626+
not is_nil(user_contest_submission_id) do
627+
# no. of submssions is less than 10. Unable to find
628+
votes_per_user - 1
597629
else
598-
{:halt, acc}
630+
votes_per_user
599631
end
600-
end)
601632

602-
votes = MapSet.to_list(votes)
603-
604-
new_submission_votes =
605-
votes
606-
|> Enum.map(fn s_id ->
607-
%SubmissionVotes{voter_id: voter_id, submission_id: s_id, question_id: question_id}
608-
end)
609-
|> Enum.concat(submission_votes)
610-
611-
{rest, new_submission_votes}
612-
end)
613-
614-
submission_votes_changesets
615-
|> Enum.with_index()
616-
|> Enum.reduce(Multi.new(), fn {changeset, index}, multi ->
617-
Multi.insert(multi, Integer.to_string(index), changeset)
633+
if MapSet.size(user_votes) < max_votes do
634+
if s_id != user_contest_submission_id and not MapSet.member?(user_votes, s_id) do
635+
new_user_votes = MapSet.put(user_votes, s_id)
636+
new_submissions = List.delete(submissions, s_id)
637+
{:cont, {new_user_votes, new_submissions}}
638+
else
639+
{:cont, {user_votes, submissions}}
640+
end
641+
else
642+
{:halt, acc}
643+
end
644+
end)
645+
646+
votes = MapSet.to_list(votes)
647+
648+
new_submission_votes =
649+
votes
650+
|> Enum.map(fn s_id ->
651+
%SubmissionVotes{
652+
voter_id: voter_id,
653+
submission_id: s_id,
654+
question_id: question_id
655+
}
656+
end)
657+
|> Enum.concat(submission_votes)
658+
659+
{rest, new_submission_votes}
618660
end)
619-
|> Repo.transaction()
620-
end
661+
662+
submission_votes_changesets
663+
|> Enum.with_index()
664+
|> Enum.reduce(Multi.new(), fn {changeset, index}, multi ->
665+
Multi.insert(multi, Integer.to_string(index), changeset)
666+
end)
667+
|> Repo.transaction()
621668
end
622669

623670
def update_assessment(id, params) when is_ecto_id(id) do
@@ -1026,7 +1073,7 @@ defmodule Cadet.Assessments do
10261073
"""
10271074
def update_rolling_contest_leaderboards do
10281075
# 115 = 2 hours - 5 minutes is default.
1029-
if Log.log_execution("update_rolling_contest_leaderboards", Timex.Duration.from_minutes(115)) do
1076+
if Log.log_execution("update_rolling_contest_leaderboards", Duration.from_minutes(115)) do
10301077
Logger.info("Started update_rolling_contest_leaderboards")
10311078

10321079
voting_questions_to_update = fetch_active_voting_questions()
@@ -1053,7 +1100,7 @@ defmodule Cadet.Assessments do
10531100
"""
10541101
def update_final_contest_leaderboards do
10551102
# 1435 = 24 hours - 5 minutes
1056-
if Log.log_execution("update_final_contest_leaderboards", Timex.Duration.from_minutes(1435)) do
1103+
if Log.log_execution("update_final_contest_leaderboards", Duration.from_minutes(1435)) do
10571104
Logger.info("Started update_final_contest_leaderboards")
10581105

10591106
voting_questions_to_update = fetch_voting_questions_due_yesterday()

0 commit comments

Comments
 (0)