• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / backend / 415c28e8ee8b88e6acc822aca1500269b5f844ad

06 Aug 2025 03:59PM UTC coverage: 91.52% (-0.9%) from 92.467%
415c28e8ee8b88e6acc822aca1500269b5f844ad

push

github

web-flow
Leaderboard (#1238)

* added 'enable_leaderboard' columns in courses table

* Leaderboard create course config, leaderboard page routing, leaderboard sql query for all users in course

* added 'top_leaderboard_display' columns in courses table

* added 'all_user_total_xp' function for leaderboard display

* add top leaderboard display options to course settings (select how many to display in leaderboard)

* added contest scores fetching and contest score calculation

* Refactor query execution in assessments module for improved readability

* added functions to fetch contest scoring and voting

* changes to default values

* updated tests

* Fixed xp fetching for all users

* Add top contest leaderboard display configuration and update related tests.
Updated leaderboard fetching and exporting for assessment workspace leaderboard.
Added Leaderboard Dropdown contests fetching.

* Added automatic XP assignment for winning contest entries

* Implement XP assignment for winning contest entries based on contest voting XML and added dispatch endpoint for XP customisation

* Add default value for XP values and improve XP assignment logic for contest entries

* No tiebreak for contest scoring

* Refactor contest scoring endpoints for authentication errors

* Enhance leaderboard update logic and improve error handling for voting questions

* Refactor XP assignment logic for voting questions and set default XP values.
Refactor Score calculation logic to reset to 0 before calculating.

* Temporary Assessment Workspace leaderboard fix for testing

* Fixed tests for assessments (default XP to award for contests)

* Refactor contest fetching logic to filter by voting question contest numbers

* Refactor leaderboard query logic to use RANK() and improve code readability.
Uncommented leaderboard portions after finalising testing

* temporary fix for STePS

* Add ranking to assessment workspace leaderboard queries and update view helpers to include rank

* Post-STePS f... (continued)

117 of 156 new or added lines in 5 files covered. (75.0%)

7 existing lines in 2 files now uncovered.

3216 of 3514 relevant lines covered (91.52%)

7703.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.47
/lib/cadet_web/router.ex
1
defmodule CadetWeb.Router do
2
  use CadetWeb, :router
3

4
  pipeline :api do
871✔
5
    plug(:accepts, ["json"])
6
    plug(:fetch_session)
7
    plug(:put_secure_browser_headers)
8
  end
9

10
  pipeline :auth do
871✔
11
    plug(Cadet.Auth.Pipeline)
12
    plug(CadetWeb.Plug.AssignCurrentUser)
13
  end
14

15
  pipeline :ensure_auth do
852✔
16
    plug(Guardian.Plug.EnsureAuthenticated)
17
  end
18

19
  pipeline :rate_limit do
6✔
20
    plug(CadetWeb.Plugs.RateLimiter)
21
  end
22

23
  pipeline :course do
771✔
24
    plug(:assign_course)
25
  end
26

27
  pipeline :ensure_staff do
115✔
28
    plug(:ensure_role, [:staff, :admin])
29
  end
30

31
  pipeline :ensure_admin do
80✔
32
    plug(:ensure_role, [:admin])
33
  end
34

35
  scope "/", CadetWeb do
36
    get("/.well-known/jwks.json", JWKSController, :index)
4✔
37
  end
38

39
  scope "/sso" do
40
    forward("/", Samly.Router)
×
41
  end
42

43
  # V2 API
44

45
  # Public Pages
46
  scope "/v2", CadetWeb do
47
    pipe_through([:api, :auth])
48

49
    # get("/sourcecast", SourcecastController, :index)
50
    post("/auth/refresh", AuthController, :refresh)
5✔
51
    post("/auth/login", AuthController, :create)
6✔
52
    post("/auth/logout", AuthController, :logout)
3✔
53
    get("/auth/saml_redirect", AuthController, :saml_redirect)
5✔
54
    get("/auth/saml_redirect_vscode", AuthController, :saml_redirect_vscode)
×
55
    get("/auth/exchange", AuthController, :exchange)
×
56
  end
57

58
  scope "/v2", CadetWeb do
59
    # no sessions or anything here
60

61
    get("/devices/:secret/cert", DevicesController, :get_cert)
3✔
62
    get("/devices/:secret/key", DevicesController, :get_key)
3✔
63
    get("/devices/:secret/client_id", DevicesController, :get_client_id)
2✔
64
    get("/devices/:secret/mqtt_endpoint", DevicesController, :get_mqtt_endpoint)
1✔
65
  end
66

67
  # Authenticated Pages without course
68
  scope "/v2", CadetWeb do
69
    pipe_through([:api, :auth, :ensure_auth])
70

71
    get("/user", UserController, :index)
3✔
72
    get("/user/latest_viewed_course", UserController, :get_latest_viewed)
3✔
73
    put("/user/latest_viewed_course", UserController, :update_latest_viewed)
2✔
74

75
    post("/config/create", CoursesController, :create)
5✔
76

77
    get("/devices", DevicesController, :index)
2✔
78
    post("/devices", DevicesController, :register)
4✔
79
    post("/devices/:id", DevicesController, :edit)
4✔
80
    delete("/devices/:id", DevicesController, :deregister)
3✔
81
    get("/devices/:id/ws_endpoint", DevicesController, :get_ws_endpoint)
4✔
82
  end
83

84
  # LLM-related endpoints
85
  scope "/v2/chats", CadetWeb do
86
    pipe_through([:api, :auth, :ensure_auth, :rate_limit])
87

88
    post("", ChatController, :init_chat)
2✔
89
    post("/:conversationId/message", ChatController, :chat)
5✔
90
  end
91

92
  # Authenticated Pages with course
93
  scope "/v2/courses/:course_id", CadetWeb do
94
    pipe_through([:api, :auth, :ensure_auth, :course])
95

96
    get("/sourcecast", SourcecastController, :index)
2✔
97

98
    get("/assessments", AssessmentsController, :index)
17✔
99
    get("/assessments/:assessmentid", AssessmentsController, :show)
93✔
100
    post("/assessments/:assessmentid/unlock", AssessmentsController, :unlock)
9✔
101
    post("/assessments/:assessmentid/submit", AssessmentsController, :submit)
365✔
102
    post("/assessments/question/:questionid/answer", AnswerController, :submit)
60✔
103

104
    post(
4✔
105
      "/assessments/question/:questionid/answerLastModified",
106
      AnswerController,
107
      :check_last_modified
108
    )
109

110
    get("/achievements", IncentivesController, :index_achievements)
2✔
111
    get("/self/goals", IncentivesController, :index_goals)
3✔
112
    post("/self/goals/:uuid/progress", IncentivesController, :update_progress)
2✔
113

114
    get("/stories", StoriesController, :index)
4✔
115

116
    get("/notifications", NotificationsController, :index)
3✔
117
    post("/notifications/acknowledge", NotificationsController, :acknowledge)
3✔
118

119
    get("/user/total_xp", UserController, :combined_total_xp)
1✔
120
    put("/user/game_states", UserController, :update_game_states)
2✔
121
    put("/user/research_agreement", UserController, :update_research_agreement)
2✔
122

NEW
123
    get("/all_users_xp", AssessmentsController, :combined_total_xp_for_all_users)
×
124

NEW
125
    get(
×
126
      "/get_paginated_display",
127
      AssessmentsController,
128
      :paginated_total_xp_for_leaderboard_display
129
    )
130

131
    get(
2✔
132
      "/assessments/:assessmentid/popularVoteLeaderboard",
133
      AssessmentsController,
134
      :get_popular_leaderboard
135
    )
136

137
    get(
2✔
138
      "/assessments/:assessmentid/scoreLeaderboard",
139
      AssessmentsController,
140
      :get_score_leaderboard
141
    )
142

NEW
143
    get("/all_contests", AssessmentsController, :get_all_contests)
×
144

145
    get("/config", CoursesController, :index)
3✔
146

147
    get("/team/:assessmentid", TeamController, :index)
2✔
148
  end
149

150
  # Admin pages (Access: Course administrators only - these routes can cause substantial damage)
151
  @doc """
152
    NOTE: This scope must come before the routes for all staff below.
153

154
    This is due to the all-staff route "/grading/:submissionid/:questionid", which would pattern match
155
    and overshadow "/grading/:assessmentid/publish_all_grades".
156

157
    If an admin route will overshadow an all-staff route as well, a suggested better solution would be a
158
    per-route permission level check.
159
  """
160
  scope "/v2/courses/:course_id/admin", CadetWeb do
161
    pipe_through([:api, :auth, :ensure_auth, :course, :ensure_admin])
162

163
    get("/assets/:foldername", AdminAssetsController, :index)
6✔
164
    post("/assets/:foldername/*filename", AdminAssetsController, :upload)
9✔
165
    delete("/assets/:foldername/*filename", AdminAssetsController, :delete)
8✔
166

167
    post("/assessments", AdminAssessmentsController, :create)
5✔
168
    post("/assessments/:assessmentid", AdminAssessmentsController, :update)
15✔
169
    delete("/assessments/:assessmentid", AdminAssessmentsController, :delete)
5✔
170

171
    get("/grading/all_submissions", AdminGradingController, :index_all_submissions)
×
172

173
    post(
2✔
174
      "/grading/:assessmentid/publish_all_grades",
175
      AdminGradingController,
176
      :publish_all_grades
177
    )
178

179
    post(
2✔
180
      "/grading/:assessmentid/unpublish_all_grades",
181
      AdminGradingController,
182
      :unpublish_all_grades
183
    )
184

185
    put("/users/:course_reg_id/role", AdminUserController, :update_role)
7✔
186
    delete("/users/:course_reg_id", AdminUserController, :delete_user)
7✔
187

188
    put("/config", AdminCoursesController, :update_course_config)
8✔
189
    # TODO: Missing corresponding Swagger path entry
190
    get("/config/assessment_configs", AdminCoursesController, :get_assessment_configs)
3✔
191
    put("/config/assessment_configs", AdminCoursesController, :update_assessment_configs)
8✔
192
    # TODO: Missing corresponding Swagger path entry
193
    delete(
5✔
194
      "/config/assessment_config/:assessment_config_id",
195
      AdminCoursesController,
196
      :delete_assessment_config
197
    )
198
  end
199

200
  # Admin pages (Access: All staff)
201
  scope "/v2/courses/:course_id/admin", CadetWeb do
202
    pipe_through([:api, :auth, :ensure_auth, :course, :ensure_staff])
203

204
    resources("/sourcecast", AdminSourcecastController, only: [:create, :delete])
205

NEW
206
    post(
×
207
      "/assessments/:assessmentid/calculateContestScore",
208
      AdminAssessmentsController,
209
      :calculate_contest_score
210
    )
211

NEW
212
    post(
×
213
      "/assessments/:assessmentid/dispatchContestXp",
214
      AdminAssessmentsController,
215
      :dispatch_contest_xp
216
    )
217

218
    get("/grading", AdminGradingController, :index)
8✔
219
    get("/grading/summary", AdminGradingController, :grading_summary)
2✔
220

221
    get("/grading/:submissionid", AdminGradingController, :show)
6✔
222
    post("/grading/:submissionid/unsubmit", AdminGradingController, :unsubmit)
10✔
223
    post("/grading/:submissionid/unpublish_grades", AdminGradingController, :unpublish_grades)
3✔
224
    post("/grading/:submissionid/publish_grades", AdminGradingController, :publish_grades)
4✔
225
    post("/grading/:submissionid/autograde", AdminGradingController, :autograde_submission)
3✔
226
    post("/grading/:submissionid/:questionid", AdminGradingController, :update)
9✔
227

228
    post(
3✔
229
      "/grading/:submissionid/:questionid/autograde",
230
      AdminGradingController,
231
      :autograde_answer
232
    )
233

234
    get("/users", AdminUserController, :index)
5✔
235
    get("/users/teamformation", AdminUserController, :get_students)
×
236
    put("/users", AdminUserController, :upsert_users_and_groups)
10✔
237
    get("/users/:course_reg_id/assessments", AdminAssessmentsController, :index)
4✔
238

239
    # The admin route for getting assessment information for a specifc user
240
    # TODO: Missing Swagger path
241
    get(
×
242
      "/users/:course_reg_id/assessments/:assessmentid",
243
      AdminAssessmentsController,
244
      :get_assessment
245
    )
246

247
    # The admin route for getting total xp of a specific user
248
    get("/users/:course_reg_id/total_xp", AdminUserController, :combined_total_xp)
1✔
249
    get("/users/:course_reg_id/goals", AdminGoalsController, :index_goals_with_progress)
1✔
250
    post("/users/:course_reg_id/goals/:uuid/progress", AdminGoalsController, :update_progress)
3✔
251

252
    put("/achievements", AdminAchievementsController, :bulk_update)
4✔
253
    put("/achievements/:uuid", AdminAchievementsController, :update)
3✔
254
    delete("/achievements/:uuid", AdminAchievementsController, :delete)
3✔
255

256
    get("/goals", AdminGoalsController, :index)
5✔
257
    put("/goals", AdminGoalsController, :bulk_update)
3✔
258
    put("/goals/:uuid", AdminGoalsController, :update)
3✔
259
    delete("/goals/:uuid", AdminGoalsController, :delete)
3✔
260

261
    post("/stories", AdminStoriesController, :create)
4✔
262
    delete("/stories/:storyid", AdminStoriesController, :delete)
4✔
263
    post("/stories/:storyid", AdminStoriesController, :update)
4✔
264

265
    get("/teams", AdminTeamsController, :index)
6✔
266
    post("/teams", AdminTeamsController, :create)
7✔
267
    delete("/teams/:teamid", AdminTeamsController, :delete)
5✔
268
    put("/teams/:teamid", AdminTeamsController, :update)
4✔
269
    post("/teams/upload", AdminTeamsController, :bulk_upload)
×
270
  end
271

272
  # Other scopes may use custom stacks.
273
  # scope "/api", CadetWeb do
274
  #   pipe_through :api
275
  # end
276

277
  def swagger_info do
278
    %{
1✔
279
      info: %{
280
        version: "2.0",
281
        title: "cadet"
282
      },
283
      basePath: "/v2",
284
      securityDefinitions: %{
285
        JWT: %{
286
          type: "apiKey",
287
          in: "header",
288
          name: "Authorization"
289
        }
290
      }
291
    }
292
  end
293

294
  scope "/swagger" do
295
    forward("/", PhoenixSwagger.Plug.SwaggerUI, otp_app: :cadet, swagger_file: "swagger.json")
1✔
296
  end
297

298
  scope "/", CadetWeb do
299
    get("/", DefaultController, :index)
1✔
300
  end
301

302
  if Mix.env() == :dev do
303
    forward("/sent_emails", Bamboo.SentEmailViewerPlug)
304
  end
305

306
  defp assign_course(conn, _opts) do
307
    course_id = conn.path_params["course_id"]
771✔
308

309
    course_reg =
771✔
310
      Cadet.Accounts.CourseRegistrations.get_user_record(conn.assigns.current_user.id, course_id)
771✔
311

312
    case course_reg do
771✔
313
      nil -> conn |> send_resp(403, "Forbidden") |> halt()
10✔
314
      cr -> assign(conn, :course_reg, cr)
761✔
315
    end
316
  end
317

318
  defp ensure_role(conn, opts) do
319
    if not is_nil(conn.assigns.current_user) and conn.assigns.course_reg.role in opts do
195✔
320
      conn
144✔
321
    else
322
      conn
323
      |> send_resp(403, "Forbidden")
324
      |> halt()
51✔
325
    end
326
  end
327
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc