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

✨ Feat: Add all_users_total_xp function and route #914

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 92 additions & 1 deletion lib/cadet/incentives/achievements.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
"""
Comment on lines +124 to +204
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a rationale why we have to use such a long raw SQL query? Why don't we just use our ORM methods?


all_users_total_xp = Ecto.Adapters.SQL.query!(Repo, combined_user_xp_total_query)
all_users_total_xp.rows
end
end
26 changes: 26 additions & 0 deletions lib/cadet_web/admin_controllers/admin_user_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/cadet_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
143 changes: 143 additions & 0 deletions test/cadet_web/admin_controllers/admin_user_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading