Skip to content
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

Export leaderboard result #1102

Merged
merged 41 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2da5101
rename hasTokenCounter to hasVotingFeature under assessmentConfig
DesSnowy Mar 20, 2024
fe7854c
Added hasVotingFeatures to assessment config
DesSnowy Mar 21, 2024
d2bfd92
Added hasVotingFeature to assessmentConfig
DesSnowy Mar 21, 2024
cb18e73
Added hasTokenCounter and hasVotingFeatures to assessments
DesSnowy Mar 21, 2024
6012b11
hasTokenCounter and hasVotingFeatures to be updated to ones in assess…
DesSnowy Mar 21, 2024
ade5541
Added hasTokenCounter and hasVotingFeatures to be shown when assessme…
DesSnowy Mar 21, 2024
284da06
Added hasTokenCounter into swaggers
DesSnowy Mar 21, 2024
96d1dc7
Changed test cases to include hasVotingFeatures and hasTokenCounter
DesSnowy Mar 21, 2024
9687930
Added a way to change hasTokenCounter and hasVotingFeatures from the …
DesSnowy Mar 22, 2024
0bc2486
fixed format
DesSnowy Mar 22, 2024
6c5f9e6
Merge branch 'source-academy:master' into RenameHasTokenCounterToHasV…
DesSnowy Mar 22, 2024
394cb52
fixed format
DesSnowy Mar 22, 2024
49ac07d
fixed alias format
DesSnowy Mar 22, 2024
4c059eb
Merge branch 'RenameHasTokenCounterToHasVotingFeatures' of github.com…
DesSnowy Mar 22, 2024
74dfaed
added new function to delete voteSubmissions and call InsertVoting
DesSnowy Mar 24, 2024
41c61a3
Added ability to reassign voting
DesSnowy Mar 24, 2024
88f3653
rename update_voting to reassign_voting for clarity
DesSnowy Mar 24, 2024
48205a8
added test case for reassign_voting
DesSnowy Mar 24, 2024
f93efca
rename reassignEntriesForVoting to assignEntriesForVoting
DesSnowy Mar 26, 2024
6ee9e46
added isVotingPublished to AssessmentOverview and changed testCases t…
DesSnowy Mar 26, 2024
f625fff
fixed format
DesSnowy Mar 26, 2024
dcde401
Merge branch 'master' into ControlOfInsertVoting
DesSnowy Mar 28, 2024
7b03f68
Moved is_voting_published to be a function in assessments
DesSnowy Mar 28, 2024
2c8a332
Prevent deletions of submissions if voting has not been published
DesSnowy Mar 28, 2024
2339b36
fix format
DesSnowy Mar 28, 2024
293af4b
fix bug where submission is deleted regardless of reassigning_voting …
DesSnowy Mar 28, 2024
faec738
changed label for better clarity
DesSnowy Mar 29, 2024
b989c5f
added new route/endpoint to get contest leaderboards
DesSnowy Apr 5, 2024
2fdc109
Added new way to display leaderboard
DesSnowy Apr 5, 2024
b2bc496
fixed format
DesSnowy Apr 5, 2024
e1673f6
fixed credo
DesSnowy Apr 5, 2024
6cbc842
changed the building of leaderboard entries to be public
DesSnowy Apr 6, 2024
8d84973
used previously implemented functions to build the leaderboard results
DesSnowy Apr 6, 2024
0cebe51
Change the reference of rendering for clarity
DesSnowy Apr 6, 2024
5e3ad27
Added new path to swaggers
DesSnowy Apr 6, 2024
7e7a740
Merge branch 'master' into ExportLeaderboardResult
DesSnowy Apr 6, 2024
21e91dd
fixed bug which caused wrong result being displayed
DesSnowy Apr 6, 2024
e986394
Added relevant test cases
DesSnowy Apr 6, 2024
ba8f9e3
Merge branch 'ExportLeaderboardResult' of github.com:DesSnowy/source-…
DesSnowy Apr 6, 2024
85a6078
Merge branch 'master' of https://github.com/source-academy/backend in…
RichDom2185 Apr 6, 2024
ec1c11a
fixed minor typo and renaming
DesSnowy Apr 6, 2024
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
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
Loading