Skip to content

Commit

Permalink
Add contest leaderboard hiding (#825)
Browse files Browse the repository at this point in the history
  • Loading branch information
YaleChen299 authored Sep 23, 2021
1 parent d0ae948 commit 02557b7
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 29 deletions.
31 changes: 24 additions & 7 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ defmodule Cadet.Assessments do
{q, a, nil, _} -> %{q | answer: %Answer{a | grader: nil}}
{q, a, g, u} -> %{q | answer: %Answer{a | grader: %CourseRegistration{g | user: u}}}
end)
|> load_contest_voting_entries(course_reg.course_id, course_reg.id)
|> load_contest_voting_entries(course_reg, assessment)

assessment = assessment |> Map.put(:questions, questions)
{:ok, assessment}
Expand Down Expand Up @@ -734,6 +734,7 @@ defmodule Cadet.Assessments do
|> preload([_, a], assessment: a)
|> Repo.get(submission_id)

# allows staff to unsubmit own assessment
bypass = role in @bypass_closed_roles and submission.student_id == course_reg_id

with {:submission_found?, true} <- {:submission_found?, is_map(submission)},
Expand Down Expand Up @@ -884,7 +885,11 @@ defmodule Cadet.Assessments do
end
end

defp load_contest_voting_entries(questions, course_id, voter_id) do
defp load_contest_voting_entries(
questions,
%CourseRegistration{role: role, course_id: course_id, id: voter_id},
assessment
) do
Enum.map(
questions,
fn q ->
Expand All @@ -893,11 +898,16 @@ 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)

leaderboard_results = []
# temporary fix to hide the leaderboard
# if is_nil(question_id),
# do: [],
# else: fetch_top_relative_score_answers(question_id, 10)
leaderboard_results =
if is_nil(question_id) do
[]
else
if leaderboard_open?(assessment, q) or role in @open_all_assessment_roles do
fetch_top_relative_score_answers(question_id, 10)
else
[]
end
end

# populate entries to vote for and leaderboard data into the question
voting_question =
Expand Down Expand Up @@ -941,6 +951,13 @@ defmodule Cadet.Assessments do
end
end

defp leaderboard_open?(assessment, voting_question) do
Timex.before?(
Timex.now(),
Timex.shift(assessment.close_at, hours: voting_question.question["reveal_hours"])
)
end

@doc """
Fetches top answers for the given question, based on the contest relative_score
Expand Down
3 changes: 2 additions & 1 deletion lib/cadet/assessments/question_types/voting_question.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ defmodule Cadet.Assessments.QuestionTypes.VotingQuestion do
field(:prepend, :string, default: "")
field(:template, :string)
field(:contest_number, :string)
field(:reveal_hours, :integer)
end

@required_fields ~w(content contest_number)a
@required_fields ~w(content contest_number reveal_hours)a
@optional_fields ~w(prepend template)a

def changeset(question, params \\ %{}) do
Expand Down
4 changes: 2 additions & 2 deletions lib/cadet/jobs/xml_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ defmodule Cadet.Updater.XMLParser do
|> xpath(
~x"//TASK"e,
access: ~x"./@access"s |> transform_by(&process_access/1),
# type: ~x"./@kind"s |> transform_by(&change_quest_to_sidequest/1),
title: ~x"./@title"s,
number: ~x"./@number"s,
story: ~x"./@story"s,
Expand Down Expand Up @@ -259,7 +258,8 @@ defmodule Cadet.Updater.XMLParser do
entity
|> xpath(
~x"./VOTING"e,
contest_number: ~x"./@assessment_number"s
contest_number: ~x"./@assessment_number"s,
reveal_hours: ~x"./@reveal_hours"i
)
)
end
Expand Down
7 changes: 7 additions & 0 deletions priv/repo/migrations/20210915125021_add_reveal_hours.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Cadet.Repo.Migrations.AddRevealHours do
use Ecto.Migration

def change do
execute("update questions set question = question || jsonb_build_object('reveal_hours', 48)")
end
end
3 changes: 2 additions & 1 deletion test/cadet/assessments/assessments_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ defmodule Cadet.AssessmentsTest do
library: build(:library),
question: %{
content: Faker.Pokemon.name(),
contest_number: assessment.number
contest_number: assessment.number,
reveal_hours: 48
}
},
assessment.id
Expand Down
3 changes: 2 additions & 1 deletion test/cadet/assessments/question_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ defmodule Cadet.Assessments.QuestionTest do
library: build(:library),
question: %{
content: Faker.Pokemon.name(),
contest_number: assessment.number
contest_number: assessment.number,
reveal_hours: 48
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ defmodule Cadet.Assessments.QuestionTypes.VotingQuestionTest do
assert_changeset(
%{
content: "content",
contest_number: "C4"
contest_number: "C4",
reveal_hours: 48
},
:valid
)
Expand Down
156 changes: 144 additions & 12 deletions test/cadet_web/controllers/assessments_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
end
end

test "it renders contest leaderboards", %{
test "renders open leaderboard for all roles", %{
conn: conn,
course_regs: course_regs,
courses: %{course1: course1},
Expand Down Expand Up @@ -510,17 +510,16 @@ defmodule CadetWeb.AssessmentsControllerTest do
})
end

expected_leaderboard = []
# temporary fix to hide the leaderboard
# for answer <- contest_answers do
# %{
# "answer" => %{"code" => answer.answer.code},
# "score" => answer.relative_score,
# "student_name" => answer.submission.student.user.name,
# "submission_id" => answer.submission.id
# }
# end
# |> Enum.sort_by(& &1["score"], &>=/2)
expected_leaderboard =
for answer <- contest_answers do
%{
"answer" => %{"code" => answer.answer.code},
"final_score" => answer.relative_score,
"student_name" => answer.submission.student.user.name,
"submission_id" => answer.submission.id
}
end
|> Enum.sort_by(& &1["final_score"], &>=/2)

for role <- Role.__enum_map__() do
course_reg = Map.get(role_crs, role)
Expand All @@ -538,6 +537,139 @@ defmodule CadetWeb.AssessmentsControllerTest do
end
end

test "renders close leaderboard for staff and admin", %{
conn: conn,
course_regs: course_regs,
courses: %{course1: course1},
role_crs: role_crs,
assessments: assessments
} do
voting_assessment = assessments["practical"].assessment

voting_assessment
|> Assessment.changeset(%{
open_at: Timex.shift(Timex.now(), days: -30),
close_at: Timex.shift(Timex.now(), days: -20)
})
|> Repo.update()

voting_question = assessments["practical"].voting_questions |> List.first()
contest_assessment_number = voting_question.question.contest_number

contest_assessment = Repo.get_by(Assessment, number: contest_assessment_number)

# insert contest question
contest_question =
insert(:programming_question, %{
display_order: 1,
assessment: contest_assessment,
max_xp: 1000
})

# insert contest submissions and answers
contest_submissions =
for student <- Enum.take(course_regs.students, 5) do
insert(:submission, %{assessment: contest_assessment, student: student})
end

contest_answers =
for {submission, score} <- Enum.with_index(contest_submissions, 1) do
insert(:answer, %{
xp: 1000,
question: contest_question,
submission: submission,
answer: build(:programming_answer),
relative_score: score / 1
})
end

expected_leaderboard =
for answer <- contest_answers do
%{
"answer" => %{"code" => answer.answer.code},
"final_score" => answer.relative_score,
"student_name" => answer.submission.student.user.name,
"submission_id" => answer.submission.id
}
end
|> Enum.sort_by(& &1["final_score"], &>=/2)

for role <- [:admin, :staff] do
course_reg = Map.get(role_crs, role)

resp_leaderboard =
conn
|> sign_in(course_reg.user)
|> get(build_url(course1.id, voting_question.assessment.id))
|> json_response(200)
|> Map.get("questions", [])
|> Enum.find(&(&1["id"] == voting_question.id))
|> Map.get("contestLeaderboard")

assert resp_leaderboard == expected_leaderboard
end
end

test "does not render close leaderboard for students", %{
conn: conn,
course_regs: course_regs,
courses: %{course1: course1},
role_crs: %{student: course_reg},
assessments: assessments
} do
voting_assessment = assessments["practical"].assessment

voting_assessment
|> Assessment.changeset(%{
open_at: Timex.shift(Timex.now(), days: -30),
close_at: Timex.shift(Timex.now(), days: -20)
})
|> Repo.update()

voting_question = assessments["practical"].voting_questions |> List.first()
contest_assessment_number = voting_question.question.contest_number

contest_assessment = Repo.get_by(Assessment, number: contest_assessment_number)

# insert contest question
contest_question =
insert(:programming_question, %{
display_order: 1,
assessment: contest_assessment,
max_xp: 1000
})

# insert contest submissions and answers
contest_submissions =
for student <- Enum.take(course_regs.students, 5) do
insert(:submission, %{assessment: contest_assessment, student: student})
end

_contest_answers =
for {submission, score} <- Enum.with_index(contest_submissions, 1) do
insert(:answer, %{
xp: 1000,
question: contest_question,
submission: submission,
answer: build(:programming_answer),
relative_score: score / 1
})
end

expected_leaderboard = []

resp_leaderboard =
conn
|> sign_in(course_reg.user)
|> get(build_url(course1.id, voting_question.assessment.id))
|> json_response(200)
|> Map.get("questions", [])
|> Enum.find(&(&1["id"] == voting_question.id))
|> Map.get("contestLeaderboard")

assert resp_leaderboard == expected_leaderboard
end

test "it renders assessment question libraries", %{
conn: conn,
courses: %{course1: course1},
Expand Down
6 changes: 4 additions & 2 deletions test/factories/assessments/question_factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ defmodule Cadet.Assessments.QuestionFactory do
content: Faker.Pokemon.name(),
prepend: Faker.Pokemon.location(),
template: Faker.Lorem.Shakespeare.as_you_like_it(),
contest_number: contest_assessment.number
contest_number: contest_assessment.number,
reveal_hours: 48
}
}
end
Expand All @@ -104,7 +105,8 @@ defmodule Cadet.Assessments.QuestionFactory do
content: Faker.Pokemon.name(),
prepend: Faker.Pokemon.location(),
template: Faker.Lorem.Shakespeare.as_you_like_it(),
contest_number: contest_assessment.number
contest_number: contest_assessment.number,
reveal_hours: 48
}
end
end
Expand Down
8 changes: 6 additions & 2 deletions test/support/xml_generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ defmodule Cadet.Test.XMLGenerator do

template_field = [template(question.question.template)]

voting_field = voting(%{assessment_number: question.question.contest_number})
voting_field =
voting(%{
reveal_hours: question.question.reveal_hours,
assessment_number: question.question.contest_number
})

[
snippet(prepend_field ++ template_field)
Expand All @@ -163,7 +167,7 @@ defmodule Cadet.Test.XMLGenerator do
end

defp voting(raw_attr) do
{"VOTING", map_permit_keys(raw_attr, ~w(assessment_number)a)}
{"VOTING", map_permit_keys(raw_attr, ~w(assessment_number reveal_hours)a)}
end

defp deployment(raw_attrs, children) do
Expand Down

0 comments on commit 02557b7

Please sign in to comment.