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

source-academy / backend / 47842a6caf5e7005e120b62c15e12b5d781b0514-PR-1347

25 Mar 2026 07:54AM UTC coverage: 88.687% (-0.2%) from 88.926%
47842a6caf5e7005e120b62c15e12b5d781b0514-PR-1347

Pull #1347

github

RichDom2185
fix(ai): Fix tests
Pull Request #1347: Fix IDOR vulnerability

85 of 91 new or added lines in 12 files covered. (93.41%)

12 existing lines in 3 files now uncovered.

3810 of 4296 relevant lines covered (88.69%)

7108.25 hits per line

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

90.91
/lib/cadet_web/admin_controllers/admin_grading_controller.ex
1
defmodule CadetWeb.AdminGradingController do
2
  use CadetWeb, :controller
3
  use PhoenixSwagger
4

5
  alias Cadet.{Assessments, Courses}
6

7
  plug(
8
    CadetWeb.Plug.EnsureResourceScope,
9
    [resource: :submission, param: "submissionid", assign: :scoped_submission]
10
    when action in [
11
           :show,
12
           :update,
13
           :unsubmit,
14
           :unpublish_grades,
15
           :publish_grades,
16
           :autograde_submission,
17
           :autograde_answer
18
         ]
19
  )
20

21
  plug(
22
    CadetWeb.Plug.EnsureResourceScope,
23
    [resource: :question, param: "questionid", assign: :scoped_question]
24
    when action in [:update, :autograde_answer]
25
  )
26

27
  plug(
28
    CadetWeb.Plug.EnsureResourceScope,
29
    [resource: :assessment, param: "assessmentid", assign: :scoped_assessment]
30
    when action in [:publish_all_grades, :unpublish_all_grades]
31
  )
32

33
  @doc """
34
  # Query Parameters
35
  - `pageSize`: Integer. The number of submissions to return. Default 10.
36
  - `offset`: Integer. The number of submissions to skip. Default 0.
37
  - `title`: String. Assessment title.
38
  - `status`: String. Submission status.
39
  - `isFullyGraded`: Boolean. Whether the submission is fully graded.
40
  - `isGradingPublished`: Boolean. Whether the grading is published.
41
  - `group`: Boolean. Only the groups under the grader should be returned.
42
  - `groupName`: String. Group name.
43
  - `name`: String. User name.
44
  - `username`: String. User username.
45
  - `type`: String. Assessment Config type.
46
  - `isManuallyGraded`: Boolean. Whether the assessment is manually graded.
47
  """
48
  def index(conn, %{"group" => group} = params)
49
      when group in ["true", "false"] do
50
    course_reg = conn.assigns[:course_reg]
5✔
51

52
    boolean_params = [:is_fully_graded, :group, :is_manually_graded]
5✔
53
    int_params = [:page_size, :offset]
5✔
54

55
    # Convert string keys to atoms and parse values
56
    params =
5✔
57
      params
58
      |> to_snake_case_atom_keys()
59
      |> Map.put_new(:page_size, "10")
60
      |> Map.put_new(:offset, "0")
61

62
    filtered_boolean_params =
5✔
63
      params
64
      |> Map.take(boolean_params)
65
      |> Map.keys()
66

67
    params =
5✔
68
      params
69
      |> process_map_booleans(filtered_boolean_params)
70
      |> process_map_integers(int_params)
71
      |> Assessments.parse_sort_direction()
72
      |> Assessments.parse_sort_by()
73

74
    case Assessments.submissions_by_grader_for_index(course_reg, params) do
5✔
75
      {:ok, view_model} ->
76
        conn
77
        |> put_status(:ok)
78
        |> put_resp_content_type("application/json")
79
        |> render("gradingsummaries.json", view_model)
5✔
80
    end
81
  end
82

83
  def index(conn, _) do
84
    index(conn, %{"group" => "false"})
2✔
85
  end
86

87
  def index_all_submissions(conn, _) do
88
    index(
×
89
      conn,
90
      %{
91
        "group" => "false",
92
        "pageSize" => "100000000000",
93
        "offset" => "0"
94
      }
95
    )
96
  end
97

98
  def show(conn, %{"submissionid" => submission_id}) when is_ecto_id(submission_id) do
99
    case Assessments.get_answers_in_submission(submission_id) do
2✔
100
      {:ok, {answers, assessment}} ->
101
        case Courses.get_course_config(assessment.course_id) do
2✔
102
          {:ok, course} ->
103
            render(conn, "show.json", course: course, answers: answers, assessment: assessment)
2✔
104

105
          {:error, {status, message}} ->
106
            conn
107
            |> put_status(status)
108
            |> text(message)
×
109
        end
110

111
      {:error, {status, message}} ->
112
        conn
113
        |> put_status(status)
UNCOV
114
        |> text(message)
×
115
    end
116
  end
117

118
  def update(
119
        conn,
120
        %{
121
          "submissionid" => submission_id,
122
          "questionid" => question_id,
123
          "grading" => raw_grading
124
        }
125
      )
126
      when is_ecto_id(submission_id) and is_ecto_id(question_id) do
127
    course_reg = conn.assigns[:course_reg]
4✔
128

129
    grading = raw_grading |> snake_casify_string_keys()
4✔
130

131
    case Assessments.update_grading_info(
4✔
132
           %{submission_id: submission_id, question_id: question_id},
133
           grading,
134
           course_reg
135
         ) do
136
      {:ok, _} ->
137
        text(conn, "OK")
2✔
138

139
      {:error, {status, message}} ->
140
        conn
141
        |> put_status(status)
142
        |> text(message)
2✔
143
    end
144
  end
145

146
  def update(conn, _params) do
147
    conn
148
    |> put_status(:bad_request)
UNCOV
149
    |> text("Missing parameter")
×
150
  end
151

152
  def unsubmit(conn, %{"submissionid" => submission_id}) when is_ecto_id(submission_id) do
153
    course_reg = conn.assigns[:course_reg]
8✔
154

155
    case Assessments.unsubmit_submission(submission_id, course_reg) do
8✔
156
      {:ok, nil} ->
157
        text(conn, "OK")
4✔
158

159
      {:error, {status, message}} ->
160
        conn
161
        |> put_status(status)
162
        |> text(message)
4✔
163
    end
164
  end
165

166
  def unpublish_grades(conn, %{"submissionid" => submission_id}) when is_ecto_id(submission_id) do
167
    course_reg = conn.assigns[:course_reg]
3✔
168

169
    case Assessments.unpublish_grading(submission_id, course_reg) do
3✔
170
      {:ok, nil} ->
171
        text(conn, "OK")
2✔
172

173
      {:error, {status, message}} ->
174
        conn
175
        |> put_status(status)
176
        |> text(message)
1✔
177
    end
178
  end
179

180
  def publish_grades(conn, %{"submissionid" => submission_id}) when is_ecto_id(submission_id) do
181
    course_reg = conn.assigns[:course_reg]
4✔
182

183
    case Assessments.publish_grading(submission_id, course_reg) do
4✔
184
      {:ok, nil} ->
185
        text(conn, "OK")
2✔
186

187
      {:error, {status, message}} ->
188
        conn
189
        |> put_status(status)
190
        |> text(message)
2✔
191
    end
192
  end
193

194
  def publish_all_grades(conn, %{"assessmentid" => assessment_id})
195
      when is_ecto_id(assessment_id) do
196
    course_reg = conn.assigns[:course_reg]
1✔
197

198
    case Assessments.publish_all_graded(course_reg, assessment_id) do
1✔
199
      {:ok, nil} ->
200
        text(conn, "OK")
1✔
201

202
      {:error, {status, message}} ->
203
        conn
204
        |> put_status(status)
205
        |> text(message)
×
206
    end
207
  end
208

209
  def unpublish_all_grades(conn, %{"assessmentid" => assessment_id})
210
      when is_ecto_id(assessment_id) do
211
    course_reg = conn.assigns[:course_reg]
1✔
212

213
    case Assessments.unpublish_all(course_reg, assessment_id) do
1✔
214
      {:ok, nil} ->
215
        text(conn, "OK")
1✔
216

217
      {:error, {status, message}} ->
218
        conn
219
        |> put_status(status)
220
        |> text(message)
×
221
    end
222
  end
223

224
  def autograde_submission(conn, %{"submissionid" => submission_id}) do
225
    course_reg = conn.assigns[:course_reg]
1✔
226

227
    case Assessments.force_regrade_submission(submission_id, course_reg) do
1✔
228
      {:ok, nil} ->
229
        send_resp(conn, :no_content, "")
1✔
230

231
      {:error, {status, message}} ->
232
        conn
233
        |> put_status(status)
UNCOV
234
        |> text(message)
×
235
    end
236
  end
237

238
  def autograde_answer(conn, %{"submissionid" => submission_id, "questionid" => question_id}) do
239
    course_reg = conn.assigns[:course_reg]
1✔
240

241
    case Assessments.force_regrade_answer(submission_id, question_id, course_reg) do
1✔
242
      {:ok, nil} ->
243
        send_resp(conn, :no_content, "")
1✔
244

245
      {:error, {status, message}} ->
246
        conn
247
        |> put_status(status)
UNCOV
248
        |> text(message)
×
249
    end
250
  end
251

252
  def grading_summary(conn, %{"course_id" => course_id}) do
253
    case Assessments.get_group_grading_summary(course_id) do
1✔
254
      {:ok, cols, summary} ->
255
        render(conn, "grading_summary.json", cols: cols, summary: summary)
1✔
256
    end
257
  end
258

259
  swagger_path :index do
1✔
260
    get("/courses/{course_id}/admin/grading")
261

262
    summary("Get a list of all submissions with current user as the grader")
263

264
    security([%{JWT: []}])
265

266
    produces("application/json")
267

268
    parameters do
269
      group(
270
        :query,
271
        :boolean,
272
        "Show only students in the grader's group when true",
273
        required: false
274
      )
275
    end
276

277
    response(200, "OK", Schema.ref(:Submissions))
278
    response(401, "Unauthorised")
279
    response(403, "Forbidden")
280
  end
281

282
  swagger_path :unsubmit do
1✔
283
    post("/courses/{course_id}/admin/grading/{submissionId}/unsubmit")
284
    summary("Unsubmit submission. Can only be done by the Avenger of a student")
285
    security([%{JWT: []}])
286

287
    parameters do
288
      submissionId(:path, :integer, "submission id", required: true)
289
    end
290

291
    response(200, "OK")
292
    response(400, "Invalid parameters")
293
    response(403, "Forbidden")
294
    response(404, "Submission not found")
295
  end
296

297
  swagger_path :autograde_submission do
1✔
298
    post("/courses/{course_id}/admin/grading/{submissionId}/autograde")
299
    summary("Force re-autograding of an entire submission")
300
    security([%{JWT: []}])
301

302
    parameters do
303
      submissionId(:path, :integer, "submission id", required: true)
304
    end
305

306
    response(204, "Successful request")
307
    response(400, "Invalid parameters or submission not submitted")
308
    response(403, "Forbidden")
309
    response(404, "Submission not found")
310
  end
311

312
  swagger_path :autograde_answer do
1✔
313
    post("/courses/{course_id}/admin/grading/{submissionId}/{questionId}/autograde")
314
    summary("Force re-autograding of a question in a submission")
315
    security([%{JWT: []}])
316

317
    parameters do
318
      submissionId(:path, :integer, "submission id", required: true)
319
      questionId(:path, :integer, "question id", required: true)
320
    end
321

322
    response(204, "Successful request")
323
    response(400, "Invalid parameters or submission not submitted")
324
    response(403, "Forbidden")
325
    response(404, "Answer not found")
326
  end
327

328
  swagger_path :show do
1✔
329
    get("/courses/{course_id}/admin/grading/{submissionId}")
330

331
    summary("Get information about a specific submission to be graded")
332

333
    security([%{JWT: []}])
334

335
    produces("application/json")
336

337
    parameters do
338
      submissionId(:path, :integer, "submission id", required: true)
339
    end
340

341
    response(200, "OK", Schema.ref(:GradingInfo))
342
    response(400, "Invalid or missing parameter(s) or submission and/or question not found")
343
    response(401, "Unauthorised")
344
    response(403, "Forbidden")
345
  end
346

347
  swagger_path :update do
1✔
348
    post("/courses/{course_id}/admin/grading/{submissionId}/{questionId}")
349

350
    summary("Update marks given to the answer of a particular question in a submission")
351

352
    security([%{JWT: []}])
353

354
    consumes("application/json")
355
    produces("application/json")
356

357
    parameters do
358
      submissionId(:path, :integer, "submission id", required: true)
359
      questionId(:path, :integer, "question id", required: true)
360
      grading(:body, Schema.ref(:Grading), "adjustments for a question", required: true)
361
    end
362

363
    response(200, "OK")
364
    response(400, "Invalid or missing parameter(s) or submission and/or question not found")
365
    response(401, "Unauthorised")
366
    response(403, "Forbidden")
367
  end
368

369
  swagger_path :grading_summary do
1✔
370
    get("/courses/{course_id}/admin/grading/summary")
371

372
    summary("Receives a summary of grading items done by this grader")
373

374
    security([%{JWT: []}])
375

376
    produces("application/json")
377

378
    response(200, "OK", Schema.array(:GradingSummary))
379
    response(400, "Invalid or missing parameter(s) or submission and/or question not found")
380
    response(401, "Unauthorised")
381
    response(403, "Forbidden")
382
  end
383

384
  def swagger_definitions do
385
    %{
1✔
386
      Submissions:
387
        swagger_schema do
1✔
388
          type(:array)
389
          items(Schema.ref(:Submission))
390
        end,
391
      Submission:
392
        swagger_schema do
1✔
393
          properties do
1✔
394
            id(:integer, "Submission id", required: true)
395
            grade(:integer, "Grade given", required: true)
396
            xp(:integer, "XP earned", required: true)
397
            xpBonus(:integer, "Bonus XP for a given submission", required: true)
398
            xpAdjustment(:integer, "XP adjustment given", required: true)
399
            adjustment(:integer, "Grade adjustment given", required: true)
400

401
            status(
402
              Schema.ref(:AssessmentStatus),
403
              "One of 'not_attempted/attempting/attempted/submitted' indicating whether the assessment has been attempted by the current user",
404
              required: true
405
            )
406

407
            gradedCount(:integer, "Number of questions in this submission that have been graded",
408
              required: true
409
            )
410

411
            assessment(Schema.ref(:AssessmentInfo), "Assessment for which the submission is for",
412
              required: true
413
            )
414

415
            student(Schema.ref(:StudentInfo), "Student who created the submission",
416
              required: true
417
            )
418

419
            unsubmittedBy(Schema.ref(:GraderInfo))
420
            unsubmittedAt(:string, "Last unsubmitted at", format: "date-time", required: false)
421

422
            isGradingPublished(:boolean, "Whether the grading is published", required: true)
1✔
423
          end
424
        end,
425
      AssessmentInfo:
426
        swagger_schema do
1✔
427
          properties do
1✔
428
            id(:integer, "assessment id", required: true)
429

430
            config(Schema.ref(:AssessmentConfig), "Either mission/sidequest/path/contest",
431
              required: true
432
            )
433

434
            title(:string, "Mission title", required: true)
435

436
            maxGrade(
437
              :integer,
438
              "The max grade for this assessment",
439
              required: true
440
            )
441

442
            maxXp(
443
              :integer,
444
              "The max xp for this assessment",
445
              required: true
446
            )
447

448
            questionCount(:integer, "number of questions in this assessment", required: true)
1✔
449
          end
450
        end,
451
      StudentInfo:
452
        swagger_schema do
1✔
453
          properties do
1✔
454
            id(:integer, "student id", required: true)
455
            name(:string, "student name", required: true)
456
            username(:string, "student username", required: true)
457
            groupName(:string, "name of student's group")
458
            groupLeaderId(:integer, "user id of group leader")
1✔
459
          end
460
        end,
461
      GraderInfo:
462
        swagger_schema do
1✔
463
          properties do
1✔
464
            id(:integer, "grader id", required: true)
465
            name(:string, "grader name", required: true)
1✔
466
          end
467
        end,
468
      GradingInfo:
469
        swagger_schema do
1✔
470
          description(
471
            "A list of questions with submitted answers, solution and previous grading info " <>
472
              "if available"
473
          )
474

475
          type(:array)
476

477
          items(
478
            Schema.new do
1✔
479
              properties do
1✔
480
                question(Schema.ref(:Question), "Question", required: true)
481
                grade(Schema.ref(:Grade), "Grading information", required: true)
482
                student(Schema.ref(:StudentInfo), "Student", required: true)
483

484
                solution(
485
                  :string,
486
                  "the marking scheme and model solution to this question. Only available for programming questions",
487
                  required: true
488
                )
489

490
                maxGrade(
491
                  :integer,
492
                  "the max grade that can be given to this question",
493
                  required: true
494
                )
495

496
                maxXp(
1✔
497
                  :integer,
498
                  "the max xp that can be given to this question",
499
                  required: true
500
                )
501
              end
502
            end
503
          )
504
        end,
505
      GradingSummary:
506
        swagger_schema do
1✔
507
          description("Summary of grading items for current user as the grader")
508

509
          properties do
1✔
510
            groupName(:string, "Name of group this grader is in", required: true)
511
            leaderName(:string, "Name of group leader", required: true)
512
            submittedMissions(:integer, "Number of submitted missions", required: true)
513
            submittedSidequests(:integer, "Number of submitted sidequests", required: true)
514
            ungradedMissions(:integer, "Number of ungraded missions", required: true)
515
            ungradedSidequests(:integer, "Number of ungraded sidequests", required: true)
1✔
516
          end
517
        end,
518
      Grade:
519
        swagger_schema do
1✔
520
          properties do
1✔
521
            grade(:integer, "Grade awarded by autograder")
522
            xp(:integer, "XP awarded by autograder")
523
            adjustment(:integer, "Grade adjustment given")
524
            xpAdjustment(:integer, "XP adjustment given", required: true)
525
            grader(Schema.ref(:GraderInfo))
526
            gradedAt(:string, "Last graded at", format: "date-time", required: false)
527
            comments(:string, "Comments given by grader")
1✔
528
          end
529
        end,
530
      Grading:
531
        swagger_schema do
1✔
532
          properties do
1✔
533
            grading(
1✔
534
              Schema.new do
1✔
535
                properties do
1✔
536
                  adjustment(:integer, "Grade adjustment given")
537
                  xpAdjustment(:integer, "XP adjustment given")
538
                  comments(:string, "Comments given by grader")
1✔
539
                end
540
              end
541
            )
542
          end
543
        end
544
    }
545
  end
546
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

© 2026 Coveralls, Inc