Skip to content

Commit 02557b7

Browse files
authored
Add contest leaderboard hiding (#825)
1 parent d0ae948 commit 02557b7

File tree

10 files changed

+195
-29
lines changed

10 files changed

+195
-29
lines changed

lib/cadet/assessments/assessments.ex

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ defmodule Cadet.Assessments do
244244
{q, a, nil, _} -> %{q | answer: %Answer{a | grader: nil}}
245245
{q, a, g, u} -> %{q | answer: %Answer{a | grader: %CourseRegistration{g | user: u}}}
246246
end)
247-
|> load_contest_voting_entries(course_reg.course_id, course_reg.id)
247+
|> load_contest_voting_entries(course_reg, assessment)
248248

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

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

739740
with {:submission_found?, true} <- {:submission_found?, is_map(submission)},
@@ -884,7 +885,11 @@ defmodule Cadet.Assessments do
884885
end
885886
end
886887

887-
defp load_contest_voting_entries(questions, course_id, voter_id) do
888+
defp load_contest_voting_entries(
889+
questions,
890+
%CourseRegistration{role: role, course_id: course_id, id: voter_id},
891+
assessment
892+
) do
888893
Enum.map(
889894
questions,
890895
fn q ->
@@ -893,11 +898,16 @@ defmodule Cadet.Assessments do
893898
# fetch top 10 contest voting entries with the contest question id
894899
question_id = fetch_associated_contest_question_id(course_id, q)
895900

896-
leaderboard_results = []
897-
# temporary fix to hide the leaderboard
898-
# if is_nil(question_id),
899-
# do: [],
900-
# else: fetch_top_relative_score_answers(question_id, 10)
901+
leaderboard_results =
902+
if is_nil(question_id) do
903+
[]
904+
else
905+
if leaderboard_open?(assessment, q) or role in @open_all_assessment_roles do
906+
fetch_top_relative_score_answers(question_id, 10)
907+
else
908+
[]
909+
end
910+
end
901911

902912
# populate entries to vote for and leaderboard data into the question
903913
voting_question =
@@ -941,6 +951,13 @@ defmodule Cadet.Assessments do
941951
end
942952
end
943953

954+
defp leaderboard_open?(assessment, voting_question) do
955+
Timex.before?(
956+
Timex.now(),
957+
Timex.shift(assessment.close_at, hours: voting_question.question["reveal_hours"])
958+
)
959+
end
960+
944961
@doc """
945962
Fetches top answers for the given question, based on the contest relative_score
946963

lib/cadet/assessments/question_types/voting_question.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ defmodule Cadet.Assessments.QuestionTypes.VotingQuestion do
1010
field(:prepend, :string, default: "")
1111
field(:template, :string)
1212
field(:contest_number, :string)
13+
field(:reveal_hours, :integer)
1314
end
1415

15-
@required_fields ~w(content contest_number)a
16+
@required_fields ~w(content contest_number reveal_hours)a
1617
@optional_fields ~w(prepend template)a
1718

1819
def changeset(question, params \\ %{}) do

lib/cadet/jobs/xml_parser.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ defmodule Cadet.Updater.XMLParser do
8585
|> xpath(
8686
~x"//TASK"e,
8787
access: ~x"./@access"s |> transform_by(&process_access/1),
88-
# type: ~x"./@kind"s |> transform_by(&change_quest_to_sidequest/1),
8988
title: ~x"./@title"s,
9089
number: ~x"./@number"s,
9190
story: ~x"./@story"s,
@@ -259,7 +258,8 @@ defmodule Cadet.Updater.XMLParser do
259258
entity
260259
|> xpath(
261260
~x"./VOTING"e,
262-
contest_number: ~x"./@assessment_number"s
261+
contest_number: ~x"./@assessment_number"s,
262+
reveal_hours: ~x"./@reveal_hours"i
263263
)
264264
)
265265
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule Cadet.Repo.Migrations.AddRevealHours do
2+
use Ecto.Migration
3+
4+
def change do
5+
execute("update questions set question = question || jsonb_build_object('reveal_hours', 48)")
6+
end
7+
end

test/cadet/assessments/assessments_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ defmodule Cadet.AssessmentsTest do
7474
library: build(:library),
7575
question: %{
7676
content: Faker.Pokemon.name(),
77-
contest_number: assessment.number
77+
contest_number: assessment.number,
78+
reveal_hours: 48
7879
}
7980
},
8081
assessment.id

test/cadet/assessments/question_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ defmodule Cadet.Assessments.QuestionTest do
3838
library: build(:library),
3939
question: %{
4040
content: Faker.Pokemon.name(),
41-
contest_number: assessment.number
41+
contest_number: assessment.number,
42+
reveal_hours: 48
4243
}
4344
}
4445

test/cadet/assessments/question_types/voting_question_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ defmodule Cadet.Assessments.QuestionTypes.VotingQuestionTest do
88
assert_changeset(
99
%{
1010
content: "content",
11-
contest_number: "C4"
11+
contest_number: "C4",
12+
reveal_hours: 48
1213
},
1314
:valid
1415
)

test/cadet_web/controllers/assessments_controller_test.exs

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
473473
end
474474
end
475475

476-
test "it renders contest leaderboards", %{
476+
test "renders open leaderboard for all roles", %{
477477
conn: conn,
478478
course_regs: course_regs,
479479
courses: %{course1: course1},
@@ -510,17 +510,16 @@ defmodule CadetWeb.AssessmentsControllerTest do
510510
})
511511
end
512512

513-
expected_leaderboard = []
514-
# temporary fix to hide the leaderboard
515-
# for answer <- contest_answers do
516-
# %{
517-
# "answer" => %{"code" => answer.answer.code},
518-
# "score" => answer.relative_score,
519-
# "student_name" => answer.submission.student.user.name,
520-
# "submission_id" => answer.submission.id
521-
# }
522-
# end
523-
# |> Enum.sort_by(& &1["score"], &>=/2)
513+
expected_leaderboard =
514+
for answer <- contest_answers do
515+
%{
516+
"answer" => %{"code" => answer.answer.code},
517+
"final_score" => answer.relative_score,
518+
"student_name" => answer.submission.student.user.name,
519+
"submission_id" => answer.submission.id
520+
}
521+
end
522+
|> Enum.sort_by(& &1["final_score"], &>=/2)
524523

525524
for role <- Role.__enum_map__() do
526525
course_reg = Map.get(role_crs, role)
@@ -538,6 +537,139 @@ defmodule CadetWeb.AssessmentsControllerTest do
538537
end
539538
end
540539

540+
test "renders close leaderboard for staff and admin", %{
541+
conn: conn,
542+
course_regs: course_regs,
543+
courses: %{course1: course1},
544+
role_crs: role_crs,
545+
assessments: assessments
546+
} do
547+
voting_assessment = assessments["practical"].assessment
548+
549+
voting_assessment
550+
|> Assessment.changeset(%{
551+
open_at: Timex.shift(Timex.now(), days: -30),
552+
close_at: Timex.shift(Timex.now(), days: -20)
553+
})
554+
|> Repo.update()
555+
556+
voting_question = assessments["practical"].voting_questions |> List.first()
557+
contest_assessment_number = voting_question.question.contest_number
558+
559+
contest_assessment = Repo.get_by(Assessment, number: contest_assessment_number)
560+
561+
# insert contest question
562+
contest_question =
563+
insert(:programming_question, %{
564+
display_order: 1,
565+
assessment: contest_assessment,
566+
max_xp: 1000
567+
})
568+
569+
# insert contest submissions and answers
570+
contest_submissions =
571+
for student <- Enum.take(course_regs.students, 5) do
572+
insert(:submission, %{assessment: contest_assessment, student: student})
573+
end
574+
575+
contest_answers =
576+
for {submission, score} <- Enum.with_index(contest_submissions, 1) do
577+
insert(:answer, %{
578+
xp: 1000,
579+
question: contest_question,
580+
submission: submission,
581+
answer: build(:programming_answer),
582+
relative_score: score / 1
583+
})
584+
end
585+
586+
expected_leaderboard =
587+
for answer <- contest_answers do
588+
%{
589+
"answer" => %{"code" => answer.answer.code},
590+
"final_score" => answer.relative_score,
591+
"student_name" => answer.submission.student.user.name,
592+
"submission_id" => answer.submission.id
593+
}
594+
end
595+
|> Enum.sort_by(& &1["final_score"], &>=/2)
596+
597+
for role <- [:admin, :staff] do
598+
course_reg = Map.get(role_crs, role)
599+
600+
resp_leaderboard =
601+
conn
602+
|> sign_in(course_reg.user)
603+
|> get(build_url(course1.id, voting_question.assessment.id))
604+
|> json_response(200)
605+
|> Map.get("questions", [])
606+
|> Enum.find(&(&1["id"] == voting_question.id))
607+
|> Map.get("contestLeaderboard")
608+
609+
assert resp_leaderboard == expected_leaderboard
610+
end
611+
end
612+
613+
test "does not render close leaderboard for students", %{
614+
conn: conn,
615+
course_regs: course_regs,
616+
courses: %{course1: course1},
617+
role_crs: %{student: course_reg},
618+
assessments: assessments
619+
} do
620+
voting_assessment = assessments["practical"].assessment
621+
622+
voting_assessment
623+
|> Assessment.changeset(%{
624+
open_at: Timex.shift(Timex.now(), days: -30),
625+
close_at: Timex.shift(Timex.now(), days: -20)
626+
})
627+
|> Repo.update()
628+
629+
voting_question = assessments["practical"].voting_questions |> List.first()
630+
contest_assessment_number = voting_question.question.contest_number
631+
632+
contest_assessment = Repo.get_by(Assessment, number: contest_assessment_number)
633+
634+
# insert contest question
635+
contest_question =
636+
insert(:programming_question, %{
637+
display_order: 1,
638+
assessment: contest_assessment,
639+
max_xp: 1000
640+
})
641+
642+
# insert contest submissions and answers
643+
contest_submissions =
644+
for student <- Enum.take(course_regs.students, 5) do
645+
insert(:submission, %{assessment: contest_assessment, student: student})
646+
end
647+
648+
_contest_answers =
649+
for {submission, score} <- Enum.with_index(contest_submissions, 1) do
650+
insert(:answer, %{
651+
xp: 1000,
652+
question: contest_question,
653+
submission: submission,
654+
answer: build(:programming_answer),
655+
relative_score: score / 1
656+
})
657+
end
658+
659+
expected_leaderboard = []
660+
661+
resp_leaderboard =
662+
conn
663+
|> sign_in(course_reg.user)
664+
|> get(build_url(course1.id, voting_question.assessment.id))
665+
|> json_response(200)
666+
|> Map.get("questions", [])
667+
|> Enum.find(&(&1["id"] == voting_question.id))
668+
|> Map.get("contestLeaderboard")
669+
670+
assert resp_leaderboard == expected_leaderboard
671+
end
672+
541673
test "it renders assessment question libraries", %{
542674
conn: conn,
543675
courses: %{course1: course1},

test/factories/assessments/question_factory.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ defmodule Cadet.Assessments.QuestionFactory do
9292
content: Faker.Pokemon.name(),
9393
prepend: Faker.Pokemon.location(),
9494
template: Faker.Lorem.Shakespeare.as_you_like_it(),
95-
contest_number: contest_assessment.number
95+
contest_number: contest_assessment.number,
96+
reveal_hours: 48
9697
}
9798
}
9899
end
@@ -104,7 +105,8 @@ defmodule Cadet.Assessments.QuestionFactory do
104105
content: Faker.Pokemon.name(),
105106
prepend: Faker.Pokemon.location(),
106107
template: Faker.Lorem.Shakespeare.as_you_like_it(),
107-
contest_number: contest_assessment.number
108+
contest_number: contest_assessment.number,
109+
reveal_hours: 48
108110
}
109111
end
110112
end

test/support/xml_generator.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ defmodule Cadet.Test.XMLGenerator do
154154

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

157-
voting_field = voting(%{assessment_number: question.question.contest_number})
157+
voting_field =
158+
voting(%{
159+
reveal_hours: question.question.reveal_hours,
160+
assessment_number: question.question.contest_number
161+
})
158162

159163
[
160164
snippet(prepend_field ++ template_field)
@@ -163,7 +167,7 @@ defmodule Cadet.Test.XMLGenerator do
163167
end
164168

165169
defp voting(raw_attr) do
166-
{"VOTING", map_permit_keys(raw_attr, ~w(assessment_number)a)}
170+
{"VOTING", map_permit_keys(raw_attr, ~w(assessment_number reveal_hours)a)}
167171
end
168172

169173
defp deployment(raw_attrs, children) do

0 commit comments

Comments
 (0)