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

source-academy / backend / 5203125b39ec8db67efb51989a9fa8eae24b1a54

16 Nov 2024 11:39AM UTC coverage: 93.661% (-0.1%) from 93.78%
5203125b39ec8db67efb51989a9fa8eae24b1a54

push

github

web-flow
Transfer groundControl (and admin panel) from staff to admin route (#1180)

* Create a new staff scope

* Move Admin Panel requests into admin scope

* Change appropriate routes into admin scope

* Find-replace galore

* Fix linting

* Linting does not work :(

* Revert "Find-replace galore"

This reverts commit e77aa0505.

* Revert "Change appropriate routes into admin scope"

This reverts commit 18dc689a4.

* Revert "Create a new staff scope"

This reverts commit 6b7e54e98.

* Move dangerous routes into a new scope

* Fix linting

* Linting works in mysterious ways

* One more formatting change

* Swap order of all-staff and admin-only routes

This swap prevents the all-staff route,
"/grading/:submissionid/:questionid", from pattern
matching and overshadowing the admin-only route
"/grading/:assessmentid/publish_all_grades". Thankfully, no admin routes
overshadow staff routes, so a quick fix can be done here.

* Update error message for grading routes

* Update error messages for users

* Add test cases for assets for staff

Create test cases to indicate that non-admin staff can only read assets,
but not create, modify, or delete them.

* Update test auth to admin for assets

* Update and add tests for course config routes

Updates positive test auth from staff to admin, adds negative tests to
ensure that non-admin staff are unable to read, update, create, or
delete course configs.

* Update and add tests for assessment-level routes

Update the modification / deletion test auth from staff to admin, and
create tests to ensure that non-admin staff are not able to delete /
unpublish them

* Fix sourcecast error

* Revert "Fix sourcecast error"

This reverts commit 831ca601d.

* Transfer asset routes to admin

* Revert accidental formatting changes

9 of 9 new or added lines in 1 file covered. (100.0%)

4 existing lines in 2 files now uncovered.

3103 of 3313 relevant lines covered (93.66%)

1065.55 hits per line

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

97.65
/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
6

7
  @doc """
8
  # Query Parameters
9
  - `pageSize`: Integer. The number of submissions to return. Default 10.
10
  - `offset`: Integer. The number of submissions to skip. Default 0.
11
  - `title`: String. Assessment title.
12
  - `status`: String. Submission status.
13
  - `isFullyGraded`: Boolean. Whether the submission is fully graded.
14
  - `isGradingPublished`: Boolean. Whether the grading is published.
15
  - `group`: Boolean. Only the groups under the grader should be returned.
16
  - `groupName`: String. Group name.
17
  - `name`: String. User name.
18
  - `username`: String. User username.
19
  - `type`: String. Assessment Config type.
20
  - `isManuallyGraded`: Boolean. Whether the assessment is manually graded.
21
  """
22
  def index(conn, %{"group" => group} = params)
23
      when group in ["true", "false"] do
24
    course_reg = conn.assigns[:course_reg]
5✔
25

26
    boolean_params = [:is_fully_graded, :group, :is_manually_graded]
5✔
27
    int_params = [:page_size, :offset]
5✔
28

29
    # Convert string keys to atoms and parse values
30
    params =
5✔
31
      params
32
      |> to_snake_case_atom_keys()
33
      |> Map.put_new(:page_size, "10")
34
      |> Map.put_new(:offset, "0")
35

36
    filtered_boolean_params =
5✔
37
      params
38
      |> Map.take(boolean_params)
39
      |> Map.keys()
40

41
    params =
5✔
42
      params
43
      |> process_map_booleans(filtered_boolean_params)
44
      |> process_map_integers(int_params)
45
      |> Assessments.parse_sort_direction()
46
      |> Assessments.parse_sort_by()
47

48
    case Assessments.submissions_by_grader_for_index(course_reg, params) do
5✔
49
      {:ok, view_model} ->
50
        conn
51
        |> put_status(:ok)
52
        |> put_resp_content_type("application/json")
53
        |> render("gradingsummaries.json", view_model)
5✔
54
    end
55
  end
56

57
  def index(conn, _) do
58
    index(conn, %{"group" => "false"})
2✔
59
  end
60

61
  def show(conn, %{"submissionid" => submission_id}) when is_ecto_id(submission_id) do
62
    case Assessments.get_answers_in_submission(submission_id) do
4✔
63
      {:ok, {answers, assessment}} ->
64
        render(conn, "show.json", answers: answers, assessment: assessment)
2✔
65

66
      {:error, {status, message}} ->
67
        conn
68
        |> put_status(status)
69
        |> text(message)
2✔
70
    end
71
  end
72

73
  def update(
74
        conn,
75
        %{
76
          "submissionid" => submission_id,
77
          "questionid" => question_id,
78
          "grading" => raw_grading
79
        }
80
      )
81
      when is_ecto_id(submission_id) and is_ecto_id(question_id) do
82
    course_reg = conn.assigns[:course_reg]
4✔
83

84
    grading = raw_grading |> snake_casify_string_keys()
4✔
85

86
    case Assessments.update_grading_info(
4✔
87
           %{submission_id: submission_id, question_id: question_id},
88
           grading,
89
           course_reg
90
         ) do
91
      {:ok, _} ->
92
        text(conn, "OK")
2✔
93

94
      {:error, {status, message}} ->
95
        conn
96
        |> put_status(status)
97
        |> text(message)
2✔
98
    end
99
  end
100

101
  def update(conn, _params) do
102
    conn
103
    |> put_status(:bad_request)
104
    |> text("Missing parameter")
2✔
105
  end
106

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

110
    case Assessments.unsubmit_submission(submission_id, course_reg) do
8✔
111
      {:ok, nil} ->
112
        text(conn, "OK")
4✔
113

114
      {:error, {status, message}} ->
115
        conn
116
        |> put_status(status)
117
        |> text(message)
4✔
118
    end
119
  end
120

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

124
    case Assessments.unpublish_grading(submission_id, course_reg) do
3✔
125
      {:ok, nil} ->
126
        text(conn, "OK")
2✔
127

128
      {:error, {status, message}} ->
129
        conn
130
        |> put_status(status)
131
        |> text(message)
1✔
132
    end
133
  end
134

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

138
    case Assessments.publish_grading(submission_id, course_reg) do
4✔
139
      {:ok, nil} ->
140
        text(conn, "OK")
2✔
141

142
      {:error, {status, message}} ->
143
        conn
144
        |> put_status(status)
145
        |> text(message)
2✔
146
    end
147
  end
148

149
  def publish_all_grades(conn, %{"assessmentid" => assessment_id})
150
      when is_ecto_id(assessment_id) do
151
    course_reg = conn.assigns[:course_reg]
1✔
152

153
    case Assessments.publish_all_graded(course_reg, assessment_id) do
1✔
154
      {:ok, nil} ->
155
        text(conn, "OK")
1✔
156

157
      {:error, {status, message}} ->
158
        conn
159
        |> put_status(status)
UNCOV
160
        |> text(message)
×
161
    end
162
  end
163

164
  def unpublish_all_grades(conn, %{"assessmentid" => assessment_id})
165
      when is_ecto_id(assessment_id) do
166
    course_reg = conn.assigns[:course_reg]
1✔
167

168
    case Assessments.unpublish_all(course_reg, assessment_id) do
1✔
169
      {:ok, nil} ->
170
        text(conn, "OK")
1✔
171

172
      {:error, {status, message}} ->
173
        conn
174
        |> put_status(status)
UNCOV
175
        |> text(message)
×
176
    end
177
  end
178

179
  def autograde_submission(conn, %{"submissionid" => submission_id}) do
180
    course_reg = conn.assigns[:course_reg]
2✔
181

182
    case Assessments.force_regrade_submission(submission_id, course_reg) do
2✔
183
      {:ok, nil} ->
184
        send_resp(conn, :no_content, "")
1✔
185

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

193
  def autograde_answer(conn, %{"submissionid" => submission_id, "questionid" => question_id}) do
194
    course_reg = conn.assigns[:course_reg]
2✔
195

196
    case Assessments.force_regrade_answer(submission_id, question_id, course_reg) do
2✔
197
      {:ok, nil} ->
198
        send_resp(conn, :no_content, "")
1✔
199

200
      {:error, {status, message}} ->
201
        conn
202
        |> put_status(status)
203
        |> text(message)
1✔
204
    end
205
  end
206

207
  def grading_summary(conn, %{"course_id" => course_id}) do
208
    case Assessments.get_group_grading_summary(course_id) do
1✔
209
      {:ok, cols, summary} ->
210
        render(conn, "grading_summary.json", cols: cols, summary: summary)
1✔
211
    end
212
  end
213

214
  swagger_path :index do
1✔
215
    get("/courses/{course_id}/admin/grading")
216

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

219
    security([%{JWT: []}])
220

221
    produces("application/json")
222

223
    parameters do
224
      group(
225
        :query,
226
        :boolean,
227
        "Show only students in the grader's group when true",
228
        required: false
229
      )
230
    end
231

232
    response(200, "OK", Schema.ref(:Submissions))
233
    response(401, "Unauthorised")
234
    response(403, "Forbidden")
235
  end
236

237
  swagger_path :unsubmit do
1✔
238
    post("/courses/{course_id}/admin/grading/{submissionId}/unsubmit")
239
    summary("Unsubmit submission. Can only be done by the Avenger of a student")
240
    security([%{JWT: []}])
241

242
    parameters do
243
      submissionId(:path, :integer, "submission id", required: true)
244
    end
245

246
    response(200, "OK")
247
    response(400, "Invalid parameters")
248
    response(403, "Forbidden")
249
    response(404, "Submission not found")
250
  end
251

252
  swagger_path :autograde_submission do
1✔
253
    post("/courses/{course_id}/admin/grading/{submissionId}/autograde")
254
    summary("Force re-autograding of an entire submission")
255
    security([%{JWT: []}])
256

257
    parameters do
258
      submissionId(:path, :integer, "submission id", required: true)
259
    end
260

261
    response(204, "Successful request")
262
    response(400, "Invalid parameters or submission not submitted")
263
    response(403, "Forbidden")
264
    response(404, "Submission not found")
265
  end
266

267
  swagger_path :autograde_answer do
1✔
268
    post("/courses/{course_id}/admin/grading/{submissionId}/{questionId}/autograde")
269
    summary("Force re-autograding of a question in a submission")
270
    security([%{JWT: []}])
271

272
    parameters do
273
      submissionId(:path, :integer, "submission id", required: true)
274
      questionId(:path, :integer, "question id", required: true)
275
    end
276

277
    response(204, "Successful request")
278
    response(400, "Invalid parameters or submission not submitted")
279
    response(403, "Forbidden")
280
    response(404, "Answer not found")
281
  end
282

283
  swagger_path :show do
1✔
284
    get("/courses/{course_id}/admin/grading/{submissionId}")
285

286
    summary("Get information about a specific submission to be graded")
287

288
    security([%{JWT: []}])
289

290
    produces("application/json")
291

292
    parameters do
293
      submissionId(:path, :integer, "submission id", required: true)
294
    end
295

296
    response(200, "OK", Schema.ref(:GradingInfo))
297
    response(400, "Invalid or missing parameter(s) or submission and/or question not found")
298
    response(401, "Unauthorised")
299
    response(403, "Forbidden")
300
  end
301

302
  swagger_path :update do
1✔
303
    post("/courses/{course_id}/admin/grading/{submissionId}/{questionId}")
304

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

307
    security([%{JWT: []}])
308

309
    consumes("application/json")
310
    produces("application/json")
311

312
    parameters do
313
      submissionId(:path, :integer, "submission id", required: true)
314
      questionId(:path, :integer, "question id", required: true)
315
      grading(:body, Schema.ref(:Grading), "adjustments for a question", required: true)
316
    end
317

318
    response(200, "OK")
319
    response(400, "Invalid or missing parameter(s) or submission and/or question not found")
320
    response(401, "Unauthorised")
321
    response(403, "Forbidden")
322
  end
323

324
  swagger_path :grading_summary do
1✔
325
    get("/courses/{course_id}/admin/grading/summary")
326

327
    summary("Receives a summary of grading items done by this grader")
328

329
    security([%{JWT: []}])
330

331
    produces("application/json")
332

333
    response(200, "OK", Schema.array(:GradingSummary))
334
    response(400, "Invalid or missing parameter(s) or submission and/or question not found")
335
    response(401, "Unauthorised")
336
    response(403, "Forbidden")
337
  end
338

339
  def swagger_definitions do
340
    %{
1✔
341
      Submissions:
342
        swagger_schema do
1✔
343
          type(:array)
344
          items(Schema.ref(:Submission))
345
        end,
346
      Submission:
347
        swagger_schema do
1✔
348
          properties do
1✔
349
            id(:integer, "Submission id", required: true)
350
            grade(:integer, "Grade given", required: true)
351
            xp(:integer, "XP earned", required: true)
352
            xpBonus(:integer, "Bonus XP for a given submission", required: true)
353
            xpAdjustment(:integer, "XP adjustment given", required: true)
354
            adjustment(:integer, "Grade adjustment given", required: true)
355

356
            status(
357
              Schema.ref(:AssessmentStatus),
358
              "One of 'not_attempted/attempting/attempted/submitted' indicating whether the assessment has been attempted by the current user",
359
              required: true
360
            )
361

362
            gradedCount(:integer, "Number of questions in this submission that have been graded",
363
              required: true
364
            )
365

366
            assessment(Schema.ref(:AssessmentInfo), "Assessment for which the submission is for",
367
              required: true
368
            )
369

370
            student(Schema.ref(:StudentInfo), "Student who created the submission", required: true)
371

372
            unsubmittedBy(Schema.ref(:GraderInfo))
373
            unsubmittedAt(:string, "Last unsubmitted at", format: "date-time", required: false)
374

375
            isGradingPublished(:boolean, "Whether the grading is published", required: true)
1✔
376
          end
377
        end,
378
      AssessmentInfo:
379
        swagger_schema do
1✔
380
          properties do
1✔
381
            id(:integer, "assessment id", required: true)
382

383
            config(Schema.ref(:AssessmentConfig), "Either mission/sidequest/path/contest",
384
              required: true
385
            )
386

387
            title(:string, "Mission title", required: true)
388

389
            maxGrade(
390
              :integer,
391
              "The max grade for this assessment",
392
              required: true
393
            )
394

395
            maxXp(
396
              :integer,
397
              "The max xp for this assessment",
398
              required: true
399
            )
400

401
            questionCount(:integer, "number of questions in this assessment", required: true)
1✔
402
          end
403
        end,
404
      StudentInfo:
405
        swagger_schema do
1✔
406
          properties do
1✔
407
            id(:integer, "student id", required: true)
408
            name(:string, "student name", required: true)
409
            username(:string, "student username", required: true)
410
            groupName(:string, "name of student's group")
411
            groupLeaderId(:integer, "user id of group leader")
1✔
412
          end
413
        end,
414
      GraderInfo:
415
        swagger_schema do
1✔
416
          properties do
1✔
417
            id(:integer, "grader id", required: true)
418
            name(:string, "grader name", required: true)
1✔
419
          end
420
        end,
421
      GradingInfo:
422
        swagger_schema do
1✔
423
          description(
424
            "A list of questions with submitted answers, solution and previous grading info " <>
425
              "if available"
426
          )
427

428
          type(:array)
429

430
          items(
431
            Schema.new do
1✔
432
              properties do
1✔
433
                question(Schema.ref(:Question), "Question", required: true)
434
                grade(Schema.ref(:Grade), "Grading information", required: true)
435
                student(Schema.ref(:StudentInfo), "Student", required: true)
436

437
                solution(
438
                  :string,
439
                  "the marking scheme and model solution to this question. Only available for programming questions",
440
                  required: true
441
                )
442

443
                maxGrade(
444
                  :integer,
445
                  "the max grade that can be given to this question",
446
                  required: true
447
                )
448

449
                maxXp(
1✔
450
                  :integer,
451
                  "the max xp that can be given to this question",
452
                  required: true
453
                )
454
              end
455
            end
456
          )
457
        end,
458
      GradingSummary:
459
        swagger_schema do
1✔
460
          description("Summary of grading items for current user as the grader")
461

462
          properties do
1✔
463
            groupName(:string, "Name of group this grader is in", required: true)
464
            leaderName(:string, "Name of group leader", required: true)
465
            submittedMissions(:integer, "Number of submitted missions", required: true)
466
            submittedSidequests(:integer, "Number of submitted sidequests", required: true)
467
            ungradedMissions(:integer, "Number of ungraded missions", required: true)
468
            ungradedSidequests(:integer, "Number of ungraded sidequests", required: true)
1✔
469
          end
470
        end,
471
      Grade:
472
        swagger_schema do
1✔
473
          properties do
1✔
474
            grade(:integer, "Grade awarded by autograder")
475
            xp(:integer, "XP awarded by autograder")
476
            adjustment(:integer, "Grade adjustment given")
477
            xpAdjustment(:integer, "XP adjustment given", required: true)
478
            grader(Schema.ref(:GraderInfo))
479
            gradedAt(:string, "Last graded at", format: "date-time", required: false)
480
            comments(:string, "Comments given by grader")
1✔
481
          end
482
        end,
483
      Grading:
484
        swagger_schema do
1✔
485
          properties do
1✔
486
            grading(
1✔
487
              Schema.new do
1✔
488
                properties do
1✔
489
                  adjustment(:integer, "Grade adjustment given")
490
                  xpAdjustment(:integer, "XP adjustment given")
491
                  comments(:string, "Comments given by grader")
1✔
492
                end
493
              end
494
            )
495
          end
496
        end
497
    }
498
  end
499
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