Skip to content

Commit

Permalink
Export leaderboard result (#1102)
Browse files Browse the repository at this point in the history
* added new route/endpoint to get contest leaderboards

* Added new way to display leaderboard

* fixed format

* fixed credo

* changed the building of leaderboard entries to be public

* used previously implemented functions to build the leaderboard results

* Change the reference of rendering for clarity

* Added new path to swaggers

* fixed bug which caused wrong result being  displayed

* Added relevant test cases

* fixed minor typo and renaming
  • Loading branch information
DesSnowy authored Apr 6, 2024
1 parent 556f7ad commit 2913e69
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 4 deletions.
2 changes: 1 addition & 1 deletion lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1239,7 +1239,7 @@ defmodule Cadet.Assessments do
end

# Finds the contest_question_id associated with the given voting_question id
defp fetch_associated_contest_question_id(course_id, voting_question) do
def fetch_associated_contest_question_id(course_id, voting_question) do
contest_number = voting_question.question["contest_number"]

if is_nil(contest_number) do
Expand Down
73 changes: 72 additions & 1 deletion lib/cadet_web/admin_controllers/admin_assessments_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ defmodule CadetWeb.AdminAssessmentsController do
import Ecto.Query, only: [where: 2]
import Cadet.Updater.XMLParser, only: [parse_xml: 4]

alias CadetWeb.AssessmentsHelpers
alias Cadet.Assessments.{Question, Assessment}
alias Cadet.{Assessments, Repo}
alias Cadet.Assessments.Assessment
alias Cadet.Accounts.CourseRegistration

def index(conn, %{"course_reg_id" => course_reg_id}) do
Expand Down Expand Up @@ -134,6 +135,44 @@ defmodule CadetWeb.AdminAssessmentsController do
end
end

def get_score_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
voting_questions =
Question
|> where(type: :voting)
|> where(assessment_id: ^assessment_id)
|> Repo.one()

contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)

result =
contest_id
|> Assessments.fetch_top_relative_score_answers(10)
|> Enum.map(fn entry ->
AssessmentsHelpers.build_contest_leaderboard_entry(entry)
end)

render(conn, "leaderboard.json", leaderboard: result)
end

def get_popular_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
voting_questions =
Question
|> where(type: :voting)
|> where(assessment_id: ^assessment_id)
|> Repo.one()

contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)

result =
contest_id
|> Assessments.fetch_top_popular_score_answers(10)
|> Enum.map(fn entry ->
AssessmentsHelpers.build_popular_leaderboard_entry(entry)
end)

render(conn, "leaderboard.json", leaderboard: result)
end

defp check_dates(open_at, close_at, assessment) do
if is_nil(open_at) and is_nil(close_at) do
{:ok, assessment}
Expand Down Expand Up @@ -230,6 +269,38 @@ defmodule CadetWeb.AdminAssessmentsController do
response(403, "Forbidden")
end

swagger_path :get_popular_leaderboard do
get("/courses/{course_id}/admin/assessments/:assessmentid/popularVoteLeaderboard")

summary("get the top 10 contest entries based on popularity")

security([%{JWT: []}])

parameters do
assessmentId(:path, :integer, "Assessment ID", required: true)
end

response(200, "OK", Schema.array(:Leaderboard))
response(401, "Unauthorised")
response(403, "Forbidden")
end

swagger_path :get_score_leaderboard do
get("/courses/{course_id}/admin/assessments/:assessmentid/scoreLeaderboard")

summary("get the top 10 contest entries based on score")

security([%{JWT: []}])

parameters do
assessmentId(:path, :integer, "Assessment ID", required: true)
end

response(200, "OK", Schema.array(:Leaderboard))
response(401, "Unauthorised")
response(403, "Forbidden")
end

def swagger_definitions do
%{
# Schemas for payloads to modify data
Expand Down
15 changes: 15 additions & 0 deletions lib/cadet_web/admin_views/admin_assessments_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ defmodule CadetWeb.AdminAssessmentsView do
)
end

def render("leaderboard.json", %{leaderboard: leaderboard}) do
render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", as: :contestEntry)
end

def render("contestEntry.json", %{contestEntry: contestEntry}) do
transform_map_for_view(
contestEntry,
%{
student_name: :student_name,
answer: & &1.answer["code"],
final_score: "final_score"
}
)
end

defp password_protected?(nil), do: false

defp password_protected?(_), do: true
Expand Down
14 changes: 14 additions & 0 deletions lib/cadet_web/controllers/assessments_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,20 @@ defmodule CadetWeb.AssessmentsController do
type(:string)
enum([:none, :processing, :success, :failed])
end,
Leaderboard:
swagger_schema do
description("A list of top entries for leaderboard")
type(:array)
items(Schema.ref(:ContestEntries))
end,
ContestEntries:
swagger_schema do
properties do
student_name(:string, "Name of the student", required: true)
answer(:string, "The code that the student submitted", required: true)
final_score(:float, "The score that the student obtained", required: true)
end
end,

# Schemas for payloads to modify data
UnlockAssessmentPayload:
Expand Down
4 changes: 2 additions & 2 deletions lib/cadet_web/helpers/assessments_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ defmodule CadetWeb.AssessmentsHelpers do
})
end

defp build_contest_leaderboard_entry(leaderboard_ans) do
def build_contest_leaderboard_entry(leaderboard_ans) do
Map.put(
transform_map_for_view(leaderboard_ans, %{
submission_id: :submission_id,
Expand All @@ -114,7 +114,7 @@ defmodule CadetWeb.AssessmentsHelpers do
)
end

defp build_popular_leaderboard_entry(leaderboard_ans) do
def build_popular_leaderboard_entry(leaderboard_ans) do
Map.put(
transform_map_for_view(leaderboard_ans, %{
submission_id: :submission_id,
Expand Down
12 changes: 12 additions & 0 deletions lib/cadet_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ defmodule CadetWeb.Router do
post("/assessments/:assessmentid", AdminAssessmentsController, :update)
delete("/assessments/:assessmentid", AdminAssessmentsController, :delete)

get(
"/assessments/:assessmentid/popularVoteLeaderboard",
AdminAssessmentsController,
:get_popular_leaderboard
)

get(
"/assessments/:assessmentid/scoreLeaderboard",
AdminAssessmentsController,
:get_score_leaderboard
)

get("/grading", AdminGradingController, :index)
get("/grading/summary", AdminGradingController, :grading_summary)
get("/grading/:submissionid", AdminGradingController, :show)
Expand Down
15 changes: 15 additions & 0 deletions lib/cadet_web/views/assessments_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ defmodule CadetWeb.AssessmentsView do
)
end

def render("leaderboard.json", %{leaderboard: leaderboard}) do
render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", as: :contestEntry)
end

def render("contestEntry.json", %{contestEntry: contestEntry}) do
transform_map_for_view(
contestEntry,
%{
student_name: :student_name,
answer: & &1.answer["code"],
final_score: "final_score"
}
)
end

defp password_protected?(nil), do: false

defp password_protected?(_), do: true
Expand Down
166 changes: 166 additions & 0 deletions test/cadet_web/admin_controllers/admin_assessments_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,166 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do
end
end

describe "GET /:assessment_id/popularVoteLeaderboard, unauthenticated" do
test "unauthorized", %{conn: conn, courses: %{course1: course1}} do
config = insert(:assessment_config, %{course: course1})
assessment = insert(:assessment, %{course: course1, config: config})

conn
|> get(build_popular_leaderboard_url(course1.id, assessment.id))
|> response(401)
end
end

describe "GET /:assessment_id/popularVoteLeaderboard, student only" do
@tag authenticate: :student
test "Forbidden", %{conn: conn} do
test_cr = conn.assigns.test_cr
course = test_cr.course
config = insert(:assessment_config, %{course: course})
assessment = insert(:assessment, %{course: course, config: config})

conn
|> get(build_popular_leaderboard_url(course.id, assessment.id))
|> response(403)
end
end

describe "GET /:assessment_id/popularVoteLeaderboard" do
@tag authenticate: :staff
test "successful", %{conn: conn} do
test_cr = conn.assigns.test_cr
course = test_cr.course

config = insert(:assessment_config, %{course: course})
contest_assessment = insert(:assessment, %{course: course, config: config})
contest_students = insert_list(5, :course_registration, %{course: course, role: :student})
contest_question = insert(:programming_question, %{assessment: contest_assessment})

contest_submissions =
contest_students
|> Enum.map(&insert(:submission, %{assessment: contest_assessment, student: &1}))

contest_answer =
contest_submissions
|> Enum.map(
&insert(:answer, %{
question: contest_question,
submission: &1,
popular_score: 10.0,
answer: build(:programming_answer)
})
)

voting_assessment = insert(:assessment, %{course: course, config: config})

insert(
:voting_question,
%{
question: build(:voting_question_content, contest_number: contest_assessment.number),
assessment: voting_assessment
}
)

expected =
contest_answer
|> Enum.map(
&%{
"answer" => &1.answer.code,
"student_name" => &1.submission.student.user.name,
"final_score" => &1.popular_score
}
)

resp =
conn
|> get(build_popular_leaderboard_url(course.id, voting_assessment.id))
|> json_response(200)

assert expected == resp
end
end

describe "GET /:assessment_id/scoreLeaderboard, unauthenticated" do
test "unauthorized", %{conn: conn, courses: %{course1: course1}} do
config = insert(:assessment_config, %{course: course1})
assessment = insert(:assessment, %{course: course1, config: config})

conn
|> get(build_popular_leaderboard_url(course1.id, assessment.id))
|> response(401)
end
end

describe "GET /:assessment_id/scoreLeaderboard, student only" do
@tag authenticate: :student
test "Forbidden", %{conn: conn} do
test_cr = conn.assigns.test_cr
course = test_cr.course
config = insert(:assessment_config, %{course: course})
assessment = insert(:assessment, %{course: course, config: config})

conn
|> get(build_popular_leaderboard_url(course.id, assessment.id))
|> response(403)
end
end

describe "GET /:assessment_id/scoreLeaderboard" do
@tag authenticate: :staff
test "successful", %{conn: conn} do
test_cr = conn.assigns.test_cr
course = test_cr.course

config = insert(:assessment_config, %{course: course})
contest_assessment = insert(:assessment, %{course: course, config: config})
contest_students = insert_list(5, :course_registration, %{course: course, role: :student})
contest_question = insert(:programming_question, %{assessment: contest_assessment})

contest_submissions =
contest_students
|> Enum.map(&insert(:submission, %{assessment: contest_assessment, student: &1}))

contest_answer =
contest_submissions
|> Enum.map(
&insert(:answer, %{
question: contest_question,
submission: &1,
relative_score: 10.0,
answer: build(:programming_answer)
})
)

voting_assessment = insert(:assessment, %{course: course, config: config})

insert(
:voting_question,
%{
question: build(:voting_question_content, contest_number: contest_assessment.number),
assessment: voting_assessment
}
)

expected =
contest_answer
|> Enum.map(
&%{
"answer" => &1.answer.code,
"student_name" => &1.submission.student.user.name,
"final_score" => &1.relative_score
}
)

resp =
conn
|> get(build_score_leaderboard_url(course.id, voting_assessment.id))
|> json_response(200)

assert expected == resp
end
end

describe "POST /, unauthenticated" do
test "unauthorized", %{
conn: conn,
Expand Down Expand Up @@ -757,6 +917,12 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do
defp build_user_assessments_url(course_id, course_reg_id),
do: "/v2/courses/#{course_id}/admin/users/#{course_reg_id}/assessments"

defp build_popular_leaderboard_url(course_id, assessment_id),
do: "#{build_url(course_id, assessment_id)}/popularVoteLeaderboard"

defp build_score_leaderboard_url(course_id, assessment_id),
do: "#{build_url(course_id, assessment_id)}/scoreLeaderboard"

defp open_at_asc_comparator(x, y), do: Timex.before?(x.open_at, y.open_at)

defp get_assessment_status(course_reg = %CourseRegistration{}, assessment = %Assessment{}) do
Expand Down

0 comments on commit 2913e69

Please sign in to comment.