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

source-academy / backend / 9c2823cf899c9314fd4ccec9dd6c3b589d83e839

04 Dec 2025 05:47PM UTC coverage: 88.716% (-0.9%) from 89.621%
9c2823cf899c9314fd4ccec9dd6c3b589d83e839

push

github

web-flow
AI-powered marking (#1248)

* feat: v1 of AI-generated comments

* feat: added logging of inputs and outputs

* Update generate_ai_comments.ex

* feat: function to save outputs to database

* Format answers json before sending to LLM

* Add LLM Prompt to question params when submitting assessment xml file

* Add LLM Prompt to api response when grading view is open

* feat: added llm_prompt from qn to raw_prompt

* feat: enabling/disabling of LLM feature by course level

* feat: added llm_grading boolean field to course creation API

* feat: added api key storage in courses & edit api key/enable llm grading

* feat: encryption for llm_api_key

* feat: added final comment editing route

* feat: added logging of chosen comments

* fix: bugs when certain fields were missing

* feat: updated tests

* formatting

* fix: error handling when calling openai API

* fix: credo issues

* formatting

* Address some comments

* Fix formatting

* rm IO.inspect

* a

* Use case instead of if

* Streamlines generate_ai_comments to only send the selected question and its relevant info + use the correct llm_prompt

* Remove unncessary field

* default: false for llm_grading

* Add proper linking between ai_comments table and submissions. Return it to submission retrieval as well

* Resolve some migration comments

* Add llm_model and llm_api_url to the DB + schema

* Moves api key, api url, llm model and course prompt to course level

* Add encryption_key to env

* Do not hardcode formatting instructions

* Add Assessment level prompts to the XML

* Return some additional info for composing of prompts

* Remove un-used 'save comments'

* Fix existing assessment tests

* Fix generate_ai_comments test cases

* Fix bug preventing avengers from generating ai comments

* Fix up tests + error msgs

* Formatting

* some mix credo suggestions

* format

* Fix credo issue

* bug fix + credo fixes

* Fix tests

* format

* Modify test.exs

* Update lib/cadet_web/controllers/gener... (continued)

118 of 174 new or added lines in 9 files covered. (67.82%)

1 existing line in 1 file now uncovered.

3758 of 4236 relevant lines covered (88.72%)

7103.93 hits per line

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

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

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

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

15
  pipeline :ensure_auth do
856✔
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
775✔
24
    plug(:assign_course)
25
  end
26

27
  pipeline :ensure_staff do
119✔
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

123
    get("/leaderboards/xp_all", LeaderboardController, :xp_all)
×
124
    get("/leaderboards/xp", LeaderboardController, :xp_paginated)
×
125

126
    get(
2✔
127
      "/assessments/:assessmentid/contest_popular_leaderboard",
128
      AssessmentsController,
129
      :contest_popular_leaderboard
130
    )
131

132
    get(
2✔
133
      "/assessments/:assessmentid/contest_score_leaderboard",
134
      AssessmentsController,
135
      :contest_score_leaderboard
136
    )
137

138
    get("/all_contests", AssessmentsController, :get_all_contests)
×
139

140
    get("/config", CoursesController, :index)
3✔
141

142
    get("/team/:assessmentid", TeamController, :index)
2✔
143
  end
144

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

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

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

158
    get("/assets/:foldername", AdminAssetsController, :index)
6✔
159
    post("/assets/:foldername/*filename", AdminAssetsController, :upload)
9✔
160
    delete("/assets/:foldername/*filename", AdminAssetsController, :delete)
8✔
161

162
    post("/assessments", AdminAssessmentsController, :create)
5✔
163
    post("/assessments/:assessmentid", AdminAssessmentsController, :update)
15✔
164
    delete("/assessments/:assessmentid", AdminAssessmentsController, :delete)
5✔
165

166
    get("/grading/all_submissions", AdminGradingController, :index_all_submissions)
×
167

168
    post(
2✔
169
      "/grading/:assessmentid/publish_all_grades",
170
      AdminGradingController,
171
      :publish_all_grades
172
    )
173

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

180
    put("/users/:course_reg_id/role", AdminUserController, :update_role)
7✔
181
    delete("/users/:course_reg_id", AdminUserController, :delete_user)
7✔
182

183
    put("/config", AdminCoursesController, :update_course_config)
8✔
184
    # TODO: Missing corresponding Swagger path entry
185
    get("/config/assessment_configs", AdminCoursesController, :get_assessment_configs)
3✔
186
    put("/config/assessment_configs", AdminCoursesController, :update_assessment_configs)
8✔
187
    # TODO: Missing corresponding Swagger path entry
188
    delete(
5✔
189
      "/config/assessment_config/:assessment_config_id",
190
      AdminCoursesController,
191
      :delete_assessment_config
192
    )
193
  end
194

195
  # Admin pages (Access: All staff)
196
  scope "/v2/courses/:course_id/admin", CadetWeb do
197
    pipe_through([:api, :auth, :ensure_auth, :course, :ensure_staff])
198

199
    resources("/sourcecast", AdminSourcecastController, only: [:create, :delete])
200

201
    post(
×
202
      "/assessments/:assessmentid/contest_calculate_score",
203
      AdminAssessmentsController,
204
      :calculate_contest_score
205
    )
206

207
    post(
×
208
      "/assessments/:assessmentid/contest_dispatch_xp",
209
      AdminAssessmentsController,
210
      :dispatch_contest_xp
211
    )
212

213
    get("/grading", AdminGradingController, :index)
8✔
214
    get("/grading/summary", AdminGradingController, :grading_summary)
2✔
215

216
    get("/grading/:submissionid", AdminGradingController, :show)
6✔
217
    post("/grading/:submissionid/unsubmit", AdminGradingController, :unsubmit)
10✔
218
    post("/grading/:submissionid/unpublish_grades", AdminGradingController, :unpublish_grades)
3✔
219
    post("/grading/:submissionid/publish_grades", AdminGradingController, :publish_grades)
4✔
220
    post("/grading/:submissionid/autograde", AdminGradingController, :autograde_submission)
3✔
221
    post("/grading/:submissionid/:questionid", AdminGradingController, :update)
9✔
222

223
    post(
4✔
224
      "/generate-comments/:answer_id",
225
      AICodeAnalysisController,
226
      :generate_ai_comments
227
    )
228

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

NEW
235
    post(
×
236
      "/save-final-comment/:answer_id",
237
      AICodeAnalysisController,
238
      :save_final_comment
239
    )
240

NEW
241
    post(
×
242
      "/save-chosen-comments/:submissionid/:questionid",
243
      AICodeAnalysisController,
244
      :save_chosen_comments
245
    )
246

247
    get("/users", AdminUserController, :index)
5✔
248
    get("/users/teamformation", AdminUserController, :get_students)
×
249
    put("/users", AdminUserController, :upsert_users_and_groups)
10✔
250
    get("/users/:course_reg_id/assessments", AdminAssessmentsController, :index)
4✔
251

252
    # The admin route for getting assessment information for a specifc user
253
    # TODO: Missing Swagger path
254
    get(
×
255
      "/users/:course_reg_id/assessments/:assessmentid",
256
      AdminAssessmentsController,
257
      :get_assessment
258
    )
259

260
    # The admin route for getting total xp of a specific user
261
    get("/users/:course_reg_id/total_xp", AdminUserController, :combined_total_xp)
1✔
262
    get("/users/:course_reg_id/goals", AdminGoalsController, :index_goals_with_progress)
1✔
263
    post("/users/:course_reg_id/goals/:uuid/progress", AdminGoalsController, :update_progress)
3✔
264

265
    put("/achievements", AdminAchievementsController, :bulk_update)
4✔
266
    put("/achievements/:uuid", AdminAchievementsController, :update)
3✔
267
    delete("/achievements/:uuid", AdminAchievementsController, :delete)
3✔
268

269
    get("/goals", AdminGoalsController, :index)
5✔
270
    put("/goals", AdminGoalsController, :bulk_update)
3✔
271
    put("/goals/:uuid", AdminGoalsController, :update)
3✔
272
    delete("/goals/:uuid", AdminGoalsController, :delete)
3✔
273

274
    post("/stories", AdminStoriesController, :create)
4✔
275
    delete("/stories/:storyid", AdminStoriesController, :delete)
4✔
276
    post("/stories/:storyid", AdminStoriesController, :update)
4✔
277

278
    get("/teams", AdminTeamsController, :index)
6✔
279
    post("/teams", AdminTeamsController, :create)
7✔
280
    delete("/teams/:teamid", AdminTeamsController, :delete)
5✔
281
    put("/teams/:teamid", AdminTeamsController, :update)
4✔
282
    post("/teams/upload", AdminTeamsController, :bulk_upload)
×
283
  end
284

285
  # Other scopes may use custom stacks.
286
  # scope "/api", CadetWeb do
287
  #   pipe_through :api
288
  # end
289

290
  def swagger_info do
291
    %{
1✔
292
      info: %{
293
        version: "2.0",
294
        title: "cadet"
295
      },
296
      basePath: "/v2",
297
      securityDefinitions: %{
298
        JWT: %{
299
          type: "apiKey",
300
          in: "header",
301
          name: "Authorization"
302
        }
303
      }
304
    }
305
  end
306

307
  scope "/swagger" do
308
    forward("/", PhoenixSwagger.Plug.SwaggerUI, otp_app: :cadet, swagger_file: "swagger.json")
1✔
309
  end
310

311
  scope "/", CadetWeb do
312
    get("/", DefaultController, :index)
1✔
313
  end
314

315
  if Mix.env() == :dev do
316
    forward("/sent_emails", Bamboo.SentEmailViewerPlug)
317
  end
318

319
  defp assign_course(conn, _opts) do
320
    course_id = conn.path_params["course_id"]
775✔
321

322
    course_reg =
775✔
323
      Cadet.Accounts.CourseRegistrations.get_user_record(conn.assigns.current_user.id, course_id)
775✔
324

325
    case course_reg do
775✔
326
      nil -> conn |> send_resp(403, "Forbidden") |> halt()
10✔
327
      cr -> assign(conn, :course_reg, cr)
765✔
328
    end
329
  end
330

331
  defp ensure_role(conn, opts) do
332
    if not is_nil(conn.assigns.current_user) and conn.assigns.course_reg.role in opts do
199✔
333
      conn
148✔
334
    else
335
      conn
336
      |> send_resp(403, "Forbidden")
337
      |> halt()
51✔
338
    end
339
  end
340
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