diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 414fde364..28602b298 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -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 diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 29e8baee7..d9a992267 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -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 @@ -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} @@ -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 diff --git a/lib/cadet_web/admin_views/admin_assessments_view.ex b/lib/cadet_web/admin_views/admin_assessments_view.ex index d08b758ae..159c5b848 100644 --- a/lib/cadet_web/admin_views/admin_assessments_view.ex +++ b/lib/cadet_web/admin_views/admin_assessments_view.ex @@ -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 diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index ce42aae49..ec64f68c9 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -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: diff --git a/lib/cadet_web/helpers/assessments_helpers.ex b/lib/cadet_web/helpers/assessments_helpers.ex index 6a03dbff2..967df1131 100644 --- a/lib/cadet_web/helpers/assessments_helpers.ex +++ b/lib/cadet_web/helpers/assessments_helpers.ex @@ -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, @@ -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, diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 32e94eca2..896410583 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -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) diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index 67442faa5..d7da3ad5d 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -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 diff --git a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs index f1a3ff0c3..0b0dc1483 100644 --- a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs @@ -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, @@ -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