From 49a3e15cc3124a06e9b60414fa9ebf7f1bd86b96 Mon Sep 17 00:00:00 2001 From: ShenyiCui Date: Thu, 13 Oct 2022 17:33:30 +0800 Subject: [PATCH 1/8] :sparkles: feat: add all_users_total_xp route - add all_users_total_xp function and route --- .../admin_user_controller.ex | 90 +++++++++++++++++++ lib/cadet_web/router.ex | 1 + 2 files changed, 91 insertions(+) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index b57e42a74..35699a157 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -317,4 +317,94 @@ defmodule CadetWeb.AdminUserController do end } end + + def combined_user_total_xp(conn, _) 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 = 41 + ) 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" = 41) + 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) + IO.inspect all_users_total_xp.rows + + json(conn, %{all_users_xp: all_users_total_xp.rows}) + end end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 9ba04adee..85022206d 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -129,6 +129,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 From 6ff2655179bd27452dfe17583263a76aa077b695 Mon Sep 17 00:00:00 2001 From: ShenyiCui Date: Thu, 13 Oct 2022 17:58:23 +0800 Subject: [PATCH 2/8] :rotating_light: fix: linter style warnings --- lib/cadet_web/admin_controllers/admin_user_controller.ex | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 35699a157..fbede34f2 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -319,8 +319,7 @@ defmodule CadetWeb.AdminUserController do end def combined_user_total_xp(conn, _) do - combined_user_xp_total_query = - """ + combined_user_xp_total_query = """ SELECT name, username, @@ -403,8 +402,6 @@ defmodule CadetWeb.AdminUserController do """ all_users_total_xp = Ecto.Adapters.SQL.query!(Repo, combined_user_xp_total_query) - IO.inspect all_users_total_xp.rows - json(conn, %{all_users_xp: all_users_total_xp.rows}) end end From 81c790bdb9fbb6fccf375354dc164b7ccbec268e Mon Sep 17 00:00:00 2001 From: ShenyiCui Date: Tue, 1 Nov 2022 01:36:01 +0800 Subject: [PATCH 3/8] feat: add swagger path for new route --- .../admin_controllers/admin_user_controller.ex | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index fbede34f2..4ccefadb9 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -404,4 +404,20 @@ defmodule CadetWeb.AdminUserController do all_users_total_xp = Ecto.Adapters.SQL.query!(Repo, combined_user_xp_total_query) json(conn, %{all_users_xp: all_users_total_xp.rows}) 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 From 5f12f0bd810c93ff49b56ac3b301b622f1358b11 Mon Sep 17 00:00:00 2001 From: ShenyiCui Date: Mon, 7 Nov 2022 14:00:18 +0800 Subject: [PATCH 4/8] :bug: fix: achievement xp calculation error --- lib/cadet_web/admin_controllers/admin_user_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 4ccefadb9..aa73c044d 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -391,7 +391,7 @@ defmodule CadetWeb.AdminUserController do bool_and( ( sg3."completed" - AND (sg3."count" = sg2."target_count") + AND (sg3."count" >= sg2."target_count") ) AND NOT (sg3."course_reg_id" IS NULL) ) From dfb4a37f9ca6fe28a1fdd1a4d9f34b8a6bdc0abf Mon Sep 17 00:00:00 2001 From: ShenyiCui Date: Thu, 10 Nov 2022 11:55:43 +0800 Subject: [PATCH 5/8] :bug: fix: add dynamic query to user_total_xp --- lib/cadet_web/admin_controllers/admin_user_controller.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index aa73c044d..c2f685fb7 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -319,6 +319,8 @@ defmodule CadetWeb.AdminUserController do end def combined_user_total_xp(conn, _) do + course_id = conn.assigns.course_reg.course_id + combined_user_xp_total_query = """ SELECT name, @@ -353,7 +355,7 @@ defmodule CadetWeb.AdminUserController do FROM course_registrations cr WHERE - cr.course_id = 41 + 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" @@ -382,7 +384,7 @@ defmodule CadetWeb.AdminUserController do 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" = 41) + (sa0."course_id" = #{course_id}) GROUP BY sa0."uuid", sg3."course_reg_id" From 0574cc83b4a63f3ee71f7f359d9960cbc65dee68 Mon Sep 17 00:00:00 2001 From: Richard Qi <55354921+riccqi@users.noreply.github.com> Date: Fri, 13 Jan 2023 10:30:21 +0800 Subject: [PATCH 6/8] refactor query under achievement context --- lib/cadet/incentives/achievements.ex | 93 ++++++++++++++++++- .../admin_user_controller.ex | 91 ++---------------- 2 files changed, 98 insertions(+), 86 deletions(-) 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 c2f685fb7..24f6afcf0 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 @@ -320,97 +321,17 @@ defmodule CadetWeb.AdminUserController do 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) - 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) - json(conn, %{all_users_xp: all_users_total_xp.rows}) + 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") + summary( + "Get the total xp from achievements and assessments of all users in a specific course" + ) security([%{JWT: []}]) produces("application/json") From 86231b3ec7b92f3761340a6a8b8c9d2df3b11c2d Mon Sep 17 00:00:00 2001 From: Richard Qi <55354921+riccqi@users.noreply.github.com> Date: Fri, 13 Jan 2023 11:40:57 +0800 Subject: [PATCH 7/8] add tests for admin total xp route --- .../admin_user_controller_test.exs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) 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 1ff900295..b226315a9 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,148 @@ 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, 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), From f2a7de44902db6b9e3cb29e0c7d9fcd084b2ee76 Mon Sep 17 00:00:00 2001 From: Richard Qi <55354921+riccqi@users.noreply.github.com> Date: Fri, 13 Jan 2023 11:43:45 +0800 Subject: [PATCH 8/8] fix mix format --- .../cadet_web/admin_controllers/admin_user_controller_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 b226315a9..d481cfd3d 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -699,7 +699,8 @@ defmodule CadetWeb.AdminUserControllerTest do # Getting the first entry of the list [total_xp_list | _tail] = resp["all_users_xp"] - # We are checking for correct username, total assessment xp, total achievement xp of the first entry + # 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