diff --git a/lib/cadet/incentives/achievements.ex b/lib/cadet/incentives/achievements.ex index cf7b4ae25..5c341f2c1 100644 --- a/lib/cadet/incentives/achievements.ex +++ b/lib/cadet/incentives/achievements.ex @@ -13,7 +13,7 @@ defmodule Cadet.Incentives.Achievements do @doc """ Returns all achievements. - This returns Achievement structs with prerequisites and goal association maps pre-loaded. + Returns Achievement structs with prerequisites and goal association maps pre-loaded. """ @spec get(integer()) :: [Achievement.t()] def get(course_id) when is_ecto_id(course_id) do @@ -115,4 +115,95 @@ defmodule Cadet.Incentives.Achievements do {_, _} -> :ok end end + + @doc """ + Returns a list of all total xp of all students in a course + """ + @spec get_all_students_total_xp(integer()) :: list() + def get_all_students_total_xp(course_id) when is_ecto_id(course_id) do + combined_user_xp_total_query = """ + SELECT + name, + username, + assessment_xp, + achievement_xp + FROM + (SELECT + sum(total_xp) as assessment_xp, + users.name, + users.username, + total_xps.cr_id + FROM + ( + SELECT + sum(sa1."xp") + sum(sa1."xp_adjustment") + max(ss0."xp_bonus") AS "total_xp", + ss0."student_id" as cr_id, + ss0.user_id + FROM + ( + SELECT + submissions.xp_bonus, + submissions.student_id, + submissions.id, + cr_ids.user_id + FROM + submissions + INNER JOIN ( + SELECT + cr.id as id, + cr.user_id + FROM + course_registrations cr + WHERE + cr.course_id = #{course_id} + ) cr_ids on cr_ids.id = submissions.student_id + ) as ss0 + INNER JOIN "answers" sa1 ON ss0."id" = sa1."submission_id" + GROUP BY + ss0."id", + ss0."student_id", + ss0."user_id" + ) total_xps + inner join users on users.id = total_xps.user_id + GROUP BY + username, + cr_id, + name) as total_assessments + LEFT JOIN + (SELECT + sum(s0."xp") as achievement_xp, + s0."course_reg_id" as cr_id + FROM + ( + SELECT + CASE WHEN bool_and(is_variable_xp) THEN SUM(count) ELSE MAX(xp) END AS "xp", + sg3."course_reg_id" AS "course_reg_id" + FROM + "achievements" AS sa0 + INNER JOIN "achievement_to_goal" AS sa1 ON sa1."achievement_uuid" = sa0."uuid" + INNER JOIN "goals" AS sg2 ON sg2."uuid" = sa1."goal_uuid" + RIGHT OUTER JOIN "goal_progress" AS sg3 ON (sg3."goal_uuid" = sg2."uuid") + WHERE + (sa0."course_id" = #{course_id}) + GROUP BY + sa0."uuid", + sg3."course_reg_id" + HAVING + ( + bool_and( + ( + sg3."completed" + AND (sg3."count" >= sg2."target_count") + ) + AND NOT (sg3."course_reg_id" IS NULL) + ) + ) + ) AS s0 + GROUP BY s0."course_reg_id") as total_achievement + ON total_assessments."cr_id" = total_achievement."cr_id" + """ + + all_users_total_xp = Ecto.Adapters.SQL.query!(Repo, combined_user_xp_total_query) + all_users_total_xp.rows + end end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index cc7dcaf5a..91e497084 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -7,6 +7,7 @@ defmodule CadetWeb.AdminUserController do alias Cadet.Repo alias Cadet.{Accounts, Assessments, Courses} alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role} + alias Cadet.Incentives.Achievements # This controller is used to find all users of a course @@ -317,4 +318,29 @@ defmodule CadetWeb.AdminUserController do end } end + + def combined_user_total_xp(conn, _) do + course_id = conn.assigns.course_reg.course_id + all_student_total_xp = Achievements.get_all_students_total_xp(course_id) + + json(conn, %{all_users_xp: all_student_total_xp}) + end + + swagger_path :all_users_combined_total_xp do + get("/courses/{courseId}/admin/users/total_xp") + + summary( + "Get the total xp from achievements and assessments of all users in a specific course" + ) + + security([%{JWT: []}]) + produces("application/json") + + parameters do + courseId(:path, :integer, "Course Id", required: true) + end + + response(200, "OK", Schema.ref(:TotalXPInfo)) + response(401, "Unauthorised") + end end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index d0a0fd275..b7f2f1924 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -123,6 +123,7 @@ defmodule CadetWeb.Router do get("/users", AdminUserController, :index) put("/users", AdminUserController, :upsert_users_and_groups) + get("/users/total_xp", AdminUserController, :combined_user_total_xp) get("/users/:course_reg_id/assessments", AdminAssessmentsController, :index) # The admin route for getting assessment information for a specifc user diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 786210dec..cbacbb30d 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -633,6 +633,149 @@ defmodule CadetWeb.AdminUserControllerTest do end end + describe "GET /v2/courses/{course_id}/admin/users/total_xp" do + @tag authenticate: :admin + test "achievement, one completed goal", %{ + conn: conn + } do + test_cr = conn.assigns.test_cr + course = conn.assigns.test_cr.course + + assessment = insert(:assessment, %{is_published: true, course: course}) + question = insert(:question, %{assessment: assessment}) + + submission = + insert(:submission, %{ + assessment: assessment, + student: test_cr, + status: :submitted, + xp_bonus: 100 + }) + + insert(:answer, %{ + question: question, + submission: submission, + xp: 20, + xp_adjustment: -10 + }) + + goal = + insert( + :goal, + Map.merge( + goal_literal(1), + %{ + course: course, + progress: [ + %{ + count: 1, + completed: true, + course_reg_id: test_cr.id + } + ] + } + ) + ) + + insert(:achievement, %{ + course: course, + title: "Rune Master", + is_task: true, + is_variable_xp: false, + position: 1, + xp: 100, + card_tile_url: + "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/rune-master-tile.png", + goals: [ + %{goal_uuid: goal.uuid} + ] + }) + + resp = + conn + |> get("/v2/courses/#{course.id}/admin/users/total_xp") + |> json_response(200) + + # Getting the first entry of the list + [total_xp_list | _tail] = resp["all_users_xp"] + + # We are checking for correct username, total assessment xp, and + # total achievement xp of the first entry + assert Enum.at(total_xp_list, 1) == test_cr.user.username + assert String.to_integer(Enum.at(total_xp_list, 2)) == 110 + assert String.to_integer(Enum.at(total_xp_list, 3)) == 100 + end + + @tag authenticate: :admin + test "one incomplete acheivement", %{ + conn: conn + } do + test_cr = conn.assigns.test_cr + course = conn.assigns.test_cr.course + + assessment = insert(:assessment, %{is_published: true, course: course}) + question = insert(:question, %{assessment: assessment}) + + submission = + insert(:submission, %{ + assessment: assessment, + student: test_cr, + status: :submitted, + xp_bonus: 100 + }) + + insert(:answer, %{ + question: question, + submission: submission, + xp: 20, + xp_adjustment: -10 + }) + + goal = + insert( + :goal, + Map.merge( + goal_literal(1), + %{ + course: course, + progress: [ + %{ + count: 0, + completed: false, + course_reg_id: test_cr.id + } + ] + } + ) + ) + + insert(:achievement, %{ + course: course, + title: "Rune Master", + is_task: true, + is_variable_xp: false, + position: 1, + xp: 100, + card_tile_url: + "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/rune-master-tile.png", + goals: [ + %{goal_uuid: goal.uuid} + ] + }) + + resp = + conn + |> get("/v2/courses/#{course.id}/admin/users/total_xp") + |> json_response(200) + + [total_xp_list | _tail] = resp["all_users_xp"] + + assert Enum.at(total_xp_list, 1) == test_cr.user.username + assert String.to_integer(Enum.at(total_xp_list, 2)) == 110 + assert Enum.at(total_xp_list, 3) == nil + end + end + defp build_url_users(course_id), do: "/v2/courses/#{course_id}/admin/users" defp build_url_users(course_id, course_reg_id),