Skip to content

Commit 2913e69

Browse files
authored
Export leaderboard result (#1102)
* added new route/endpoint to get contest leaderboards * Added new way to display leaderboard * fixed format * fixed credo * changed the building of leaderboard entries to be public * used previously implemented functions to build the leaderboard results * Change the reference of rendering for clarity * Added new path to swaggers * fixed bug which caused wrong result being displayed * Added relevant test cases * fixed minor typo and renaming
1 parent 556f7ad commit 2913e69

File tree

8 files changed

+297
-4
lines changed

8 files changed

+297
-4
lines changed

lib/cadet/assessments/assessments.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -1239,7 +1239,7 @@ defmodule Cadet.Assessments do
12391239
end
12401240

12411241
# Finds the contest_question_id associated with the given voting_question id
1242-
defp fetch_associated_contest_question_id(course_id, voting_question) do
1242+
def fetch_associated_contest_question_id(course_id, voting_question) do
12431243
contest_number = voting_question.question["contest_number"]
12441244

12451245
if is_nil(contest_number) do

lib/cadet_web/admin_controllers/admin_assessments_controller.ex

+72-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ defmodule CadetWeb.AdminAssessmentsController do
66
import Ecto.Query, only: [where: 2]
77
import Cadet.Updater.XMLParser, only: [parse_xml: 4]
88

9+
alias CadetWeb.AssessmentsHelpers
10+
alias Cadet.Assessments.{Question, Assessment}
911
alias Cadet.{Assessments, Repo}
10-
alias Cadet.Assessments.Assessment
1112
alias Cadet.Accounts.CourseRegistration
1213

1314
def index(conn, %{"course_reg_id" => course_reg_id}) do
@@ -134,6 +135,44 @@ defmodule CadetWeb.AdminAssessmentsController do
134135
end
135136
end
136137

138+
def get_score_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
139+
voting_questions =
140+
Question
141+
|> where(type: :voting)
142+
|> where(assessment_id: ^assessment_id)
143+
|> Repo.one()
144+
145+
contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)
146+
147+
result =
148+
contest_id
149+
|> Assessments.fetch_top_relative_score_answers(10)
150+
|> Enum.map(fn entry ->
151+
AssessmentsHelpers.build_contest_leaderboard_entry(entry)
152+
end)
153+
154+
render(conn, "leaderboard.json", leaderboard: result)
155+
end
156+
157+
def get_popular_leaderboard(conn, %{"assessmentid" => assessment_id, "course_id" => course_id}) do
158+
voting_questions =
159+
Question
160+
|> where(type: :voting)
161+
|> where(assessment_id: ^assessment_id)
162+
|> Repo.one()
163+
164+
contest_id = Assessments.fetch_associated_contest_question_id(course_id, voting_questions)
165+
166+
result =
167+
contest_id
168+
|> Assessments.fetch_top_popular_score_answers(10)
169+
|> Enum.map(fn entry ->
170+
AssessmentsHelpers.build_popular_leaderboard_entry(entry)
171+
end)
172+
173+
render(conn, "leaderboard.json", leaderboard: result)
174+
end
175+
137176
defp check_dates(open_at, close_at, assessment) do
138177
if is_nil(open_at) and is_nil(close_at) do
139178
{:ok, assessment}
@@ -230,6 +269,38 @@ defmodule CadetWeb.AdminAssessmentsController do
230269
response(403, "Forbidden")
231270
end
232271

272+
swagger_path :get_popular_leaderboard do
273+
get("/courses/{course_id}/admin/assessments/:assessmentid/popularVoteLeaderboard")
274+
275+
summary("get the top 10 contest entries based on popularity")
276+
277+
security([%{JWT: []}])
278+
279+
parameters do
280+
assessmentId(:path, :integer, "Assessment ID", required: true)
281+
end
282+
283+
response(200, "OK", Schema.array(:Leaderboard))
284+
response(401, "Unauthorised")
285+
response(403, "Forbidden")
286+
end
287+
288+
swagger_path :get_score_leaderboard do
289+
get("/courses/{course_id}/admin/assessments/:assessmentid/scoreLeaderboard")
290+
291+
summary("get the top 10 contest entries based on score")
292+
293+
security([%{JWT: []}])
294+
295+
parameters do
296+
assessmentId(:path, :integer, "Assessment ID", required: true)
297+
end
298+
299+
response(200, "OK", Schema.array(:Leaderboard))
300+
response(401, "Unauthorised")
301+
response(403, "Forbidden")
302+
end
303+
233304
def swagger_definitions do
234305
%{
235306
# Schemas for payloads to modify data

lib/cadet_web/admin_views/admin_assessments_view.ex

+15
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ defmodule CadetWeb.AdminAssessmentsView do
6666
)
6767
end
6868

69+
def render("leaderboard.json", %{leaderboard: leaderboard}) do
70+
render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", as: :contestEntry)
71+
end
72+
73+
def render("contestEntry.json", %{contestEntry: contestEntry}) do
74+
transform_map_for_view(
75+
contestEntry,
76+
%{
77+
student_name: :student_name,
78+
answer: & &1.answer["code"],
79+
final_score: "final_score"
80+
}
81+
)
82+
end
83+
6984
defp password_protected?(nil), do: false
7085

7186
defp password_protected?(_), do: true

lib/cadet_web/controllers/assessments_controller.ex

+14
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,20 @@ defmodule CadetWeb.AssessmentsController do
393393
type(:string)
394394
enum([:none, :processing, :success, :failed])
395395
end,
396+
Leaderboard:
397+
swagger_schema do
398+
description("A list of top entries for leaderboard")
399+
type(:array)
400+
items(Schema.ref(:ContestEntries))
401+
end,
402+
ContestEntries:
403+
swagger_schema do
404+
properties do
405+
student_name(:string, "Name of the student", required: true)
406+
answer(:string, "The code that the student submitted", required: true)
407+
final_score(:float, "The score that the student obtained", required: true)
408+
end
409+
end,
396410

397411
# Schemas for payloads to modify data
398412
UnlockAssessmentPayload:

lib/cadet_web/helpers/assessments_helpers.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ defmodule CadetWeb.AssessmentsHelpers do
102102
})
103103
end
104104

105-
defp build_contest_leaderboard_entry(leaderboard_ans) do
105+
def build_contest_leaderboard_entry(leaderboard_ans) do
106106
Map.put(
107107
transform_map_for_view(leaderboard_ans, %{
108108
submission_id: :submission_id,
@@ -114,7 +114,7 @@ defmodule CadetWeb.AssessmentsHelpers do
114114
)
115115
end
116116

117-
defp build_popular_leaderboard_entry(leaderboard_ans) do
117+
def build_popular_leaderboard_entry(leaderboard_ans) do
118118
Map.put(
119119
transform_map_for_view(leaderboard_ans, %{
120120
submission_id: :submission_id,

lib/cadet_web/router.ex

+12
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ defmodule CadetWeb.Router do
118118
post("/assessments/:assessmentid", AdminAssessmentsController, :update)
119119
delete("/assessments/:assessmentid", AdminAssessmentsController, :delete)
120120

121+
get(
122+
"/assessments/:assessmentid/popularVoteLeaderboard",
123+
AdminAssessmentsController,
124+
:get_popular_leaderboard
125+
)
126+
127+
get(
128+
"/assessments/:assessmentid/scoreLeaderboard",
129+
AdminAssessmentsController,
130+
:get_score_leaderboard
131+
)
132+
121133
get("/grading", AdminGradingController, :index)
122134
get("/grading/summary", AdminGradingController, :grading_summary)
123135
get("/grading/:submissionid", AdminGradingController, :show)

lib/cadet_web/views/assessments_view.ex

+15
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ defmodule CadetWeb.AssessmentsView do
6767
)
6868
end
6969

70+
def render("leaderboard.json", %{leaderboard: leaderboard}) do
71+
render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", as: :contestEntry)
72+
end
73+
74+
def render("contestEntry.json", %{contestEntry: contestEntry}) do
75+
transform_map_for_view(
76+
contestEntry,
77+
%{
78+
student_name: :student_name,
79+
answer: & &1.answer["code"],
80+
final_score: "final_score"
81+
}
82+
)
83+
end
84+
7085
defp password_protected?(nil), do: false
7186

7287
defp password_protected?(_), do: true

test/cadet_web/admin_controllers/admin_assessments_controller_test.exs

+166
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,166 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do
159159
end
160160
end
161161

162+
describe "GET /:assessment_id/popularVoteLeaderboard, unauthenticated" do
163+
test "unauthorized", %{conn: conn, courses: %{course1: course1}} do
164+
config = insert(:assessment_config, %{course: course1})
165+
assessment = insert(:assessment, %{course: course1, config: config})
166+
167+
conn
168+
|> get(build_popular_leaderboard_url(course1.id, assessment.id))
169+
|> response(401)
170+
end
171+
end
172+
173+
describe "GET /:assessment_id/popularVoteLeaderboard, student only" do
174+
@tag authenticate: :student
175+
test "Forbidden", %{conn: conn} do
176+
test_cr = conn.assigns.test_cr
177+
course = test_cr.course
178+
config = insert(:assessment_config, %{course: course})
179+
assessment = insert(:assessment, %{course: course, config: config})
180+
181+
conn
182+
|> get(build_popular_leaderboard_url(course.id, assessment.id))
183+
|> response(403)
184+
end
185+
end
186+
187+
describe "GET /:assessment_id/popularVoteLeaderboard" do
188+
@tag authenticate: :staff
189+
test "successful", %{conn: conn} do
190+
test_cr = conn.assigns.test_cr
191+
course = test_cr.course
192+
193+
config = insert(:assessment_config, %{course: course})
194+
contest_assessment = insert(:assessment, %{course: course, config: config})
195+
contest_students = insert_list(5, :course_registration, %{course: course, role: :student})
196+
contest_question = insert(:programming_question, %{assessment: contest_assessment})
197+
198+
contest_submissions =
199+
contest_students
200+
|> Enum.map(&insert(:submission, %{assessment: contest_assessment, student: &1}))
201+
202+
contest_answer =
203+
contest_submissions
204+
|> Enum.map(
205+
&insert(:answer, %{
206+
question: contest_question,
207+
submission: &1,
208+
popular_score: 10.0,
209+
answer: build(:programming_answer)
210+
})
211+
)
212+
213+
voting_assessment = insert(:assessment, %{course: course, config: config})
214+
215+
insert(
216+
:voting_question,
217+
%{
218+
question: build(:voting_question_content, contest_number: contest_assessment.number),
219+
assessment: voting_assessment
220+
}
221+
)
222+
223+
expected =
224+
contest_answer
225+
|> Enum.map(
226+
&%{
227+
"answer" => &1.answer.code,
228+
"student_name" => &1.submission.student.user.name,
229+
"final_score" => &1.popular_score
230+
}
231+
)
232+
233+
resp =
234+
conn
235+
|> get(build_popular_leaderboard_url(course.id, voting_assessment.id))
236+
|> json_response(200)
237+
238+
assert expected == resp
239+
end
240+
end
241+
242+
describe "GET /:assessment_id/scoreLeaderboard, unauthenticated" do
243+
test "unauthorized", %{conn: conn, courses: %{course1: course1}} do
244+
config = insert(:assessment_config, %{course: course1})
245+
assessment = insert(:assessment, %{course: course1, config: config})
246+
247+
conn
248+
|> get(build_popular_leaderboard_url(course1.id, assessment.id))
249+
|> response(401)
250+
end
251+
end
252+
253+
describe "GET /:assessment_id/scoreLeaderboard, student only" do
254+
@tag authenticate: :student
255+
test "Forbidden", %{conn: conn} do
256+
test_cr = conn.assigns.test_cr
257+
course = test_cr.course
258+
config = insert(:assessment_config, %{course: course})
259+
assessment = insert(:assessment, %{course: course, config: config})
260+
261+
conn
262+
|> get(build_popular_leaderboard_url(course.id, assessment.id))
263+
|> response(403)
264+
end
265+
end
266+
267+
describe "GET /:assessment_id/scoreLeaderboard" do
268+
@tag authenticate: :staff
269+
test "successful", %{conn: conn} do
270+
test_cr = conn.assigns.test_cr
271+
course = test_cr.course
272+
273+
config = insert(:assessment_config, %{course: course})
274+
contest_assessment = insert(:assessment, %{course: course, config: config})
275+
contest_students = insert_list(5, :course_registration, %{course: course, role: :student})
276+
contest_question = insert(:programming_question, %{assessment: contest_assessment})
277+
278+
contest_submissions =
279+
contest_students
280+
|> Enum.map(&insert(:submission, %{assessment: contest_assessment, student: &1}))
281+
282+
contest_answer =
283+
contest_submissions
284+
|> Enum.map(
285+
&insert(:answer, %{
286+
question: contest_question,
287+
submission: &1,
288+
relative_score: 10.0,
289+
answer: build(:programming_answer)
290+
})
291+
)
292+
293+
voting_assessment = insert(:assessment, %{course: course, config: config})
294+
295+
insert(
296+
:voting_question,
297+
%{
298+
question: build(:voting_question_content, contest_number: contest_assessment.number),
299+
assessment: voting_assessment
300+
}
301+
)
302+
303+
expected =
304+
contest_answer
305+
|> Enum.map(
306+
&%{
307+
"answer" => &1.answer.code,
308+
"student_name" => &1.submission.student.user.name,
309+
"final_score" => &1.relative_score
310+
}
311+
)
312+
313+
resp =
314+
conn
315+
|> get(build_score_leaderboard_url(course.id, voting_assessment.id))
316+
|> json_response(200)
317+
318+
assert expected == resp
319+
end
320+
end
321+
162322
describe "POST /, unauthenticated" do
163323
test "unauthorized", %{
164324
conn: conn,
@@ -757,6 +917,12 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do
757917
defp build_user_assessments_url(course_id, course_reg_id),
758918
do: "/v2/courses/#{course_id}/admin/users/#{course_reg_id}/assessments"
759919

920+
defp build_popular_leaderboard_url(course_id, assessment_id),
921+
do: "#{build_url(course_id, assessment_id)}/popularVoteLeaderboard"
922+
923+
defp build_score_leaderboard_url(course_id, assessment_id),
924+
do: "#{build_url(course_id, assessment_id)}/scoreLeaderboard"
925+
760926
defp open_at_asc_comparator(x, y), do: Timex.before?(x.open_at, y.open_at)
761927

762928
defp get_assessment_status(course_reg = %CourseRegistration{}, assessment = %Assessment{}) do

0 commit comments

Comments
 (0)