Skip to content

Commit 8e5aa2e

Browse files
authored
Refactor leaderboard (#1281)
* Move leaderboard routes into own controller file * Fix casing for calculation of contest score * Add swagger documentation for leaderboard controller * Standardise naming of routes * Refactor contest leaderboard controller routes 1. database methods should be within cadet instead of cadet_web 2. simplify processing to only show leaderboard for voting assessments instead of both submission and voting 3. add error message if assessment is not a contest voting one * Remove imports no longer required * Standardise naming for contest-related assessment endpoints * Fix checking if voting_question is nil * Use case instead of with * Update tests for changes in endpoint
1 parent 471a938 commit 8e5aa2e

File tree

6 files changed

+154
-109
lines changed

6 files changed

+154
-109
lines changed

lib/cadet/assessments/assessments.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,13 @@ defmodule Cadet.Assessments do
10531053
Repo.delete(question)
10541054
end
10551055

1056+
def get_contest_voting_question(assessment_id) do
1057+
Question
1058+
|> where(type: :voting)
1059+
|> where(assessment_id: ^assessment_id)
1060+
|> Repo.one()
1061+
end
1062+
10561063
@doc """
10571064
Public internal api to submit new answers for a question. Possible return values are:
10581065
`{:ok, nil}` -> success

lib/cadet_web/admin_controllers/admin_assessments_controller.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ defmodule CadetWeb.AdminAssessmentsController do
144144

145145
if voting_questions do
146146
Assessments.compute_relative_score(voting_questions.id)
147-
text(conn, "CONTEST SCORE CALCULATED")
147+
text(conn, "Contest scores calculated")
148148
else
149149
text(conn, "No voting questions found for the given assessment")
150150
end

lib/cadet_web/controllers/assessments_controller.ex

Lines changed: 52 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ defmodule CadetWeb.AssessmentsController do
33

44
use PhoenixSwagger
55

6-
import Ecto.Query, only: [where: 2]
7-
86
alias Cadet.{Assessments, Repo}
9-
alias Cadet.Assessments.Question
107
alias CadetWeb.AssessmentsHelpers
118

129
# These roles can save and finalise answers for closed assessments and
@@ -71,96 +68,66 @@ defmodule CadetWeb.AssessmentsController do
7168
end
7269
end
7370

74-
def combined_total_xp_for_all_users(conn, %{"course_id" => course_id}) do
75-
users_with_xp = Assessments.all_user_total_xp(course_id)
76-
json(conn, %{users: users_with_xp.users})
77-
end
78-
79-
def paginated_total_xp_for_leaderboard_display(conn, %{"course_id" => course_id}) do
80-
offset = String.to_integer(conn.params["offset"] || "0")
81-
page_size = String.to_integer(conn.params["page_size"] || "25")
82-
paginated_display = Assessments.all_user_total_xp(course_id, offset, page_size)
83-
json(conn, paginated_display)
84-
end
85-
86-
def get_score_leaderboard(conn, %{
71+
def contest_score_leaderboard(conn, %{
8772
"assessmentid" => assessment_id,
8873
"course_id" => course_id
8974
}) do
90-
visible_entries = String.to_integer(conn.params["visible_entries"] || "10")
91-
voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id)
92-
93-
voting_questions =
94-
Question
95-
|> where(type: :voting)
96-
|> where(assessment_id: ^assessment_id)
97-
|> Repo.one()
98-
|> case do
99-
nil ->
100-
Question
101-
|> where(type: :voting)
102-
|> where(assessment_id: ^voting_id)
103-
|> Repo.one()
104-
105-
question ->
106-
question
107-
end
108-
109-
contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)
110-
111-
result =
112-
contest_id
113-
|> Assessments.fetch_top_relative_score_answers(visible_entries)
114-
|> Enum.map(fn entry ->
115-
updated_entry = %{
116-
entry
117-
| answer: entry.answer["code"]
118-
}
119-
120-
AssessmentsHelpers.build_contest_leaderboard_entry(updated_entry)
121-
end)
122-
123-
json(conn, %{leaderboard: result, voting_id: voting_id})
75+
count = String.to_integer(conn.params["count"] || "10")
76+
77+
case {:voting_question, Assessments.get_contest_voting_question(assessment_id)} do
78+
{:voting_question, voting_question} when not is_nil(voting_question) ->
79+
question_id = Assessments.fetch_associated_contest_question_id(course_id, voting_question)
80+
81+
result =
82+
question_id
83+
|> Assessments.fetch_top_relative_score_answers(count)
84+
|> Enum.map(fn entry ->
85+
updated_entry = %{
86+
entry
87+
| answer: entry.answer["code"]
88+
}
89+
90+
AssessmentsHelpers.build_contest_leaderboard_entry(updated_entry)
91+
end)
92+
93+
json(conn, %{leaderboard: result})
94+
95+
{:voting_question, nil} ->
96+
conn
97+
|> put_status(:not_found)
98+
|> text("Not a contest voting assessment")
99+
end
124100
end
125101

126-
def get_popular_leaderboard(conn, %{
102+
def contest_popular_leaderboard(conn, %{
127103
"assessmentid" => assessment_id,
128104
"course_id" => course_id
129105
}) do
130-
visible_entries = String.to_integer(conn.params["visible_entries"] || "10")
131-
voting_id = Assessments.fetch_contest_voting_assesment_id(assessment_id)
132-
133-
voting_questions =
134-
Question
135-
|> where(type: :voting)
136-
|> where(assessment_id: ^assessment_id)
137-
|> Repo.one()
138-
|> case do
139-
nil ->
140-
Question
141-
|> where(type: :voting)
142-
|> where(assessment_id: ^voting_id)
143-
|> Repo.one()
144-
145-
question ->
146-
question
147-
end
148-
149-
contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)
150-
151-
result =
152-
contest_id
153-
|> Assessments.fetch_top_popular_score_answers(visible_entries)
154-
|> Enum.map(fn entry ->
155-
updated_entry = %{
156-
entry
157-
| answer: entry.answer["code"]
158-
}
159-
160-
AssessmentsHelpers.build_popular_leaderboard_entry(updated_entry)
161-
end)
162-
163-
json(conn, %{leaderboard: result, voting_id: voting_id})
106+
count = String.to_integer(conn.params["count"] || "10")
107+
108+
case {:voting_question, Assessments.get_contest_voting_question(assessment_id)} do
109+
{:voting_question, voting_question} when not is_nil(voting_question) ->
110+
question_id = Assessments.fetch_associated_contest_question_id(course_id, voting_question)
111+
112+
result =
113+
question_id
114+
|> Assessments.fetch_top_popular_score_answers(count)
115+
|> Enum.map(fn entry ->
116+
updated_entry = %{
117+
entry
118+
| answer: entry.answer["code"]
119+
}
120+
121+
AssessmentsHelpers.build_popular_leaderboard_entry(updated_entry)
122+
end)
123+
124+
json(conn, %{leaderboard: result})
125+
126+
{:voting_question, nil} ->
127+
conn
128+
|> put_status(:not_found)
129+
|> text("Not a contest voting assessment")
130+
end
164131
end
165132

166133
def get_all_contests(conn, %{"course_id" => course_id}) do
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
defmodule CadetWeb.LeaderboardController do
2+
use CadetWeb, :controller
3+
4+
use PhoenixSwagger
5+
6+
alias Cadet.Assessments
7+
8+
def xp_all(conn, %{"course_id" => course_id}) do
9+
users_with_xp = Assessments.all_user_total_xp(course_id)
10+
json(conn, %{users: users_with_xp.users})
11+
end
12+
13+
def xp_paginated(conn, %{"course_id" => course_id}) do
14+
offset = String.to_integer(conn.params["offset"] || "0")
15+
page_size = String.to_integer(conn.params["page_size"] || "25")
16+
paginated_display = Assessments.all_user_total_xp(course_id, offset, page_size)
17+
json(conn, paginated_display)
18+
end
19+
20+
swagger_path :xp_all do
21+
get("/courses/{course_id}/leaderboards/xp_all")
22+
23+
summary("Get all users XP in course")
24+
25+
security([%{JWT: []}])
26+
27+
produces("application/json")
28+
29+
response(200, "OK", Schema.ref(:XPLeaderboardUsers))
30+
response(401, "Unauthorised")
31+
end
32+
33+
swagger_path :xp_paginated do
34+
get("/courses/{course_id}/leaderboards/xp")
35+
36+
summary("Get all users XP in course (paginated)")
37+
38+
security([%{JWT: []}])
39+
40+
produces("application/json")
41+
42+
parameters do
43+
offset(:query, :integer, "Pagination offset", required: false, default: 0)
44+
page_size(:query, :integer, "Number of users per page", required: false, default: 25)
45+
end
46+
47+
response(200, "OK", Schema.ref(:XPLeaderboardUsers))
48+
response(401, "Unauthorised")
49+
end
50+
51+
def swagger_definitions do
52+
%{
53+
XPLeaderboardUsers:
54+
swagger_schema do
55+
description("XP Leaderboard Response")
56+
57+
properties do
58+
users(:array, "List of users in the leaderboard",
59+
items: %{
60+
type: :object,
61+
properties: %{
62+
name: %{type: :string, description: "User's full name"},
63+
username: %{type: :string, description: "User's login name"},
64+
rank: %{type: :integer, description: "User's rank"},
65+
user_id: %{type: :integer, description: "User ID"},
66+
total_xp: %{type: :integer, description: "User's total XP"}
67+
}
68+
}
69+
)
70+
71+
total_count(:integer, "Total number of users in the leaderboard (for paginated)")
72+
end
73+
end
74+
}
75+
end
76+
end

lib/cadet_web/router.ex

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,19 @@ defmodule CadetWeb.Router do
120120
put("/user/game_states", UserController, :update_game_states)
121121
put("/user/research_agreement", UserController, :update_research_agreement)
122122

123-
get("/all_users_xp", AssessmentsController, :combined_total_xp_for_all_users)
123+
get("/leaderboards/xp_all", LeaderboardController, :xp_all)
124+
get("/leaderboards/xp", LeaderboardController, :xp_paginated)
124125

125126
get(
126-
"/get_paginated_display",
127+
"/assessments/:assessmentid/contest_popular_leaderboard",
127128
AssessmentsController,
128-
:paginated_total_xp_for_leaderboard_display
129+
:contest_popular_leaderboard
129130
)
130131

131132
get(
132-
"/assessments/:assessmentid/popularVoteLeaderboard",
133+
"/assessments/:assessmentid/contest_score_leaderboard",
133134
AssessmentsController,
134-
:get_popular_leaderboard
135-
)
136-
137-
get(
138-
"/assessments/:assessmentid/scoreLeaderboard",
139-
AssessmentsController,
140-
:get_score_leaderboard
135+
:contest_score_leaderboard
141136
)
142137

143138
get("/all_contests", AssessmentsController, :get_all_contests)
@@ -204,13 +199,13 @@ defmodule CadetWeb.Router do
204199
resources("/sourcecast", AdminSourcecastController, only: [:create, :delete])
205200

206201
post(
207-
"/assessments/:assessmentid/calculateContestScore",
202+
"/assessments/:assessmentid/contest_calculate_score",
208203
AdminAssessmentsController,
209204
:calculate_contest_score
210205
)
211206

212207
post(
213-
"/assessments/:assessmentid/dispatchContestXp",
208+
"/assessments/:assessmentid/contest_dispatch_xp",
214209
AdminAssessmentsController,
215210
:dispatch_contest_xp
216211
)

test/cadet_web/controllers/assessments_controller_test.exs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,13 +1800,13 @@ defmodule CadetWeb.AssessmentsControllerTest do
18001800
end
18011801
end
18021802

1803-
describe "GET /:assessment_id/popularVoteLeaderboard, unauthenticated" do
1803+
describe "GET /:assessment_id/contest_popular_leaderboard, unauthenticated" do
18041804
test "unauthorized", %{conn: conn, courses: %{course1: course1}} do
18051805
config = insert(:assessment_config, %{course: course1})
18061806
assessment = insert(:assessment, %{course: course1, config: config})
18071807

18081808
params = %{
1809-
"visible_entries" => 9
1809+
"count" => 9
18101810
}
18111811

18121812
conn
@@ -1815,13 +1815,13 @@ defmodule CadetWeb.AssessmentsControllerTest do
18151815
end
18161816
end
18171817

1818-
describe "GET /:assessment_id/scoreLeaderboard, unauthenticated" do
1818+
describe "GET /:assessment_id/contest_score_leaderboard, unauthenticated" do
18191819
test "unauthorized", %{conn: conn, courses: %{course1: course1}} do
18201820
config = insert(:assessment_config, %{course: course1})
18211821
assessment = insert(:assessment, %{course: course1, config: config})
18221822

18231823
params = %{
1824-
"visible_entries" => 9
1824+
"count" => 9
18251825
}
18261826

18271827
conn
@@ -1830,7 +1830,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
18301830
end
18311831
end
18321832

1833-
describe "GET /:assessment_id/popularVoteLeaderboard" do
1833+
describe "GET /:assessment_id/contest_popular_leaderboard" do
18341834
@tag authenticate: :student
18351835
test "successful", %{conn: conn, courses: %{course1: course1}} do
18361836
user = conn.assigns[:current_user]
@@ -1882,7 +1882,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
18821882
)
18831883

18841884
params = %{
1885-
"visible_entries" => 1
1885+
"count" => 1
18861886
}
18871887

18881888
resp =
@@ -1894,7 +1894,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
18941894
end
18951895
end
18961896

1897-
describe "GET /:assessment_id/scoreLeaderboard" do
1897+
describe "GET /:assessment_id/contest_score_leaderboard" do
18981898
@tag authenticate: :student
18991899
test "successful", %{conn: conn, courses: %{course1: course1}} do
19001900
user = conn.assigns[:current_user]
@@ -1946,7 +1946,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
19461946
)
19471947

19481948
params = %{
1949-
"visible_entries" => 1
1949+
"count" => 1
19501950
}
19511951

19521952
resp =
@@ -1970,7 +1970,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
19701970
do: "/v2/courses/#{course_id}/assessments/#{assessment_id}/unlock"
19711971

19721972
defp build_popular_leaderboard_url(course_id, assessment_id, params \\ %{}) do
1973-
base_url = "#{build_url(course_id, assessment_id)}/popularVoteLeaderboard"
1973+
base_url = "#{build_url(course_id, assessment_id)}/contest_popular_leaderboard"
19741974

19751975
if params != %{} do
19761976
query_string = URI.encode_query(params)
@@ -1981,7 +1981,7 @@ defmodule CadetWeb.AssessmentsControllerTest do
19811981
end
19821982

19831983
defp build_score_leaderboard_url(course_id, assessment_id, params \\ %{}) do
1984-
base_url = "#{build_url(course_id, assessment_id)}/scoreLeaderboard"
1984+
base_url = "#{build_url(course_id, assessment_id)}/contest_score_leaderboard"
19851985

19861986
if params != %{} do
19871987
query_string = URI.encode_query(params)

0 commit comments

Comments
 (0)