Skip to content

Add contest leaderboard hiding #825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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