/
quiz_submissions_api_controller.rb
459 lines (430 loc) · 15 KB
/
quiz_submissions_api_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# frozen_string_literal: true
#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# @API Quiz Submissions
#
# API for accessing quiz submissions
#
# @model QuizSubmission
# {
# "id": "QuizSubmission",
# "required": ["id", "quiz_id"],
# "properties": {
# "id": {
# "description": "The ID of the quiz submission.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "quiz_id": {
# "description": "The ID of the Quiz the quiz submission belongs to.",
# "example": 2,
# "type": "integer",
# "format": "int64"
# },
# "user_id": {
# "description": "The ID of the Student that made the quiz submission.",
# "example": 3,
# "type": "integer",
# "format": "int64"
# },
# "submission_id": {
# "description": "The ID of the Submission the quiz submission represents.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "started_at": {
# "description": "The time at which the student started the quiz submission.",
# "example": "2013-11-07T13:16:18Z",
# "type": "string",
# "format": "date-time"
# },
# "finished_at": {
# "description": "The time at which the student submitted the quiz submission.",
# "example": "2013-11-07T13:16:18Z",
# "type": "string",
# "format": "date-time"
# },
# "end_at": {
# "description": "The time at which the quiz submission will be overdue, and be flagged as a late submission.",
# "example": "2013-11-07T13:16:18Z",
# "type": "string",
# "format": "date-time"
# },
# "attempt": {
# "description": "For quizzes that allow multiple attempts, this field specifies the quiz submission attempt number.",
# "example": 3,
# "type": "integer",
# "format": "int64"
# },
# "extra_attempts": {
# "description": "Number of times the student was allowed to re-take the quiz over the multiple-attempt limit.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "extra_time": {
# "description": "Amount of extra time allowed for the quiz submission, in minutes.",
# "example": 60,
# "type": "integer",
# "format": "int64"
# },
# "manually_unlocked": {
# "description": "The student can take the quiz even if it's locked for everyone else",
# "example": true,
# "type": "boolean"
# },
# "time_spent": {
# "description": "Amount of time spent, in seconds.",
# "example": 300,
# "type": "integer",
# "format": "int64"
# },
# "score": {
# "description": "The score of the quiz submission, if graded.",
# "example": 3,
# "type": "integer",
# "format": "int64"
# },
# "score_before_regrade": {
# "description": "The original score of the quiz submission prior to any re-grading.",
# "example": 2,
# "type": "integer",
# "format": "int64"
# },
# "kept_score": {
# "description": "For quizzes that allow multiple attempts, this is the score that will be used, which might be the score of the latest, or the highest, quiz submission.",
# "example": 5,
# "type": "integer",
# "format": "int64"
# },
# "fudge_points": {
# "description": "Number of points the quiz submission's score was fudged by.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "has_seen_results": {
# "description": "Whether the student has viewed their results to the quiz.",
# "example": true,
# "type": "boolean"
# },
# "workflow_state": {
# "description": "The current state of the quiz submission. Possible values: ['untaken'|'pending_review'|'complete'|'settings_only'|'preview'].",
# "example": "untaken",
# "type": "string"
# },
# "overdue_and_needs_submission": {
# "description": "Indicates whether the quiz submission is overdue and needs submission",
# "example": "false",
# "type": "boolean"
# }
# }
# }
#
class Quizzes::QuizSubmissionsApiController < ApplicationController
include Api::V1::QuizSubmission
include ::Filters::Quizzes
include ::Filters::QuizSubmissions
before_action :require_user, :require_context, :require_quiz
before_action :require_overridden_quiz, :except => [ :index ]
before_action :require_quiz_submission, :except => [ :index, :submission, :create ]
before_action :prepare_service, :only => [ :create, :update, :complete ]
before_action :validate_ldb_status!, :only => [ :create, :complete ]
# @API Get all quiz submissions.
#
# Get a list of all submissions for this quiz. Users who can view or manage
# grades for a course will have submissions from multiple users returned. A
# user who can only submit will have only their own submissions returned. When
# a user has an in-progress submission, only that submission is returned. When
# there isn't an in-progress quiz_submission, all completed submissions,
# including previous attempts, are returned.
#
# @argument include[] [String, "submission"|"quiz"|"user"]
# Associations to include with the quiz submission.
#
# <b>200 OK</b> response code is returned if the request was successful.
#
# @example_response
# {
# "quiz_submissions": [QuizSubmission]
# }
def index
quiz_submissions = if @context.grants_any_right?(@current_user, session, :manage_grades, :view_all_grades)
# teachers have access to all student submissions
visible_student_ids = @context.apply_enrollment_visibility(@context.student_enrollments, @current_user).pluck(:user_id)
Api.paginate @quiz.quiz_submissions.where(:user_id => visible_student_ids),
self,
api_v1_course_quiz_submissions_url(@context, @quiz)
elsif @quiz.grants_right?(@current_user, session, :submit)
# students have access only to their own submissions, both in progress, or completed`
submission = @quiz.quiz_submissions.where(:user_id => @current_user).first
if submission
if submission.workflow_state == "untaken"
[submission]
else
submission.submitted_attempts
end
else
[]
end
end
if quiz_submissions
# trigger delayed grading job for all submission id's which needs grading
quiz_submissions_ids = quiz_submissions.map(&:id).uniq
Quizzes::OutstandingQuizSubmissionManager.new(@quiz).delay_if_production.grade_by_ids(quiz_submissions_ids)
serialize_and_render quiz_submissions
else
render_unauthorized_action
end
end
# @API Get the quiz submission.
#
# Get the submission for this quiz for the current user.
#
# @argument include[] [String, "submission"|"quiz"|"user"]
# Associations to include with the quiz submission.
#
# <b>200 OK</b> response code is returned if the request was successful.
#
# @example_response
# {
# "quiz_submissions": [QuizSubmission]
# }
def submission
unless @quiz.grants_right?(@current_user, session, :submit)
return render_unauthorized_action
end
quiz_submission = @quiz.quiz_submissions.where(user_id: @current_user).first(1)
serialize_and_render(quiz_submission)
end
# @API Get a single quiz submission.
#
# Get a single quiz submission.
#
# @argument include[] [String, "submission"|"quiz"|"user"]
# Associations to include with the quiz submission.
#
# <b>200 OK</b> response code is returned if the request was successful.
#
# @example_response
# {
# "quiz_submissions": [QuizSubmission]
# }
def show
if authorized_action(@quiz_submission, @current_user, :read)
if params.has_key?(:attempt)
retrieve_quiz_submission_attempt!(params[:attempt])
end
serialize_and_render @quiz_submission
end
end
# @API Create the quiz submission (start a quiz-taking session)
#
# Start taking a Quiz by creating a QuizSubmission which you can use to answer
# questions and submit your answers.
#
# @argument access_code [String]
# Access code for the Quiz, if any.
#
# @argument preview [Boolean]
# Whether this should be a preview QuizSubmission and not count towards
# the user's course record. Teachers only.
#
# <b>Responses</b>
#
# * <b>200 OK</b> if the request was successful
# * <b>400 Bad Request</b> if the quiz is locked
# * <b>403 Forbidden</b> if an invalid access code is specified
# * <b>403 Forbidden</b> if the Quiz's IP filter restriction does not pass
# * <b>409 Conflict</b> if a QuizSubmission already exists for this user and quiz
#
# @example_response
# {
# "quiz_submissions": [QuizSubmission]
# }
def create
quiz_submission = if previewing?
@service.create_preview(@quiz, session)
else
if module_locked?
raise RequestError.new("you are not allowed to participate in this quiz", 400)
end
@service.create(@quiz)
end
log_asset_access(@quiz, 'quizzes', 'quizzes', 'participate')
serialize_and_render quiz_submission
end
# @API Update student question scores and comments.
#
# Update the amount of points a student has scored for questions they've
# answered, provide comments for the student about their answer(s), or simply
# fudge the total score by a specific amount of points.
#
# @argument quiz_submissions[][attempt] [Required, Integer]
# The attempt number of the quiz submission that should be updated. This
# attempt MUST be already completed.
#
# @argument quiz_submissions[][fudge_points] [Float]
# Amount of positive or negative points to fudge the total score by.
#
# @argument quiz_submissions[][questions] [Hash]
# A set of scores and comments for each question answered by the student.
# The keys are the question IDs, and the values are hashes of `score` and
# `comment` entries. See {Appendix: Manual Scoring} for more on this
# parameter.
#
# <b>Responses</b>
#
# * <b>200 OK</b> if the request was successful
# * <b>403 Forbidden</b> if you are not a teacher in this course
# * <b>400 Bad Request</b> if the attempt parameter is missing or invalid
# * <b>400 Bad Request</b> if the specified QS attempt is not yet complete
#
# @see Appendix: Manual Scoring
#
# @example_request
# {
# "quiz_submissions": [{
# "attempt": 1,
# "fudge_points": -2.4,
# "questions": {
# "1": {
# "score": 2.5,
# "comment": "This can't be right, but I'll let it pass this one time."
# },
# "2": {
# "score": 0,
# "comment": "Good thinking. Almost!"
# }
# }
# }]
# }
#
# @example_response
# {
# "quiz_submissions": [QuizSubmission]
# }
#
# @!appendix Manual Scoring
#
# {include:file:doc/examples/quiz_submission_manual_scoring.md}
def update
resource_params = params[:quiz_submissions]
unless resource_params.is_a?(Array)
reject! 'missing required key :quiz_submissions'
end
if resource_params = resource_params[0]
@service.update_scores(@quiz_submission,
resource_params[:attempt],
resource_params)
end
serialize_and_render @quiz_submission
end
# @API Complete the quiz submission (turn it in).
#
# Complete the quiz submission by marking it as complete and grading it. When
# the quiz submission has been marked as complete, no further modifications
# will be allowed.
#
# @argument attempt [Required, Integer]
# The attempt number of the quiz submission that should be completed. Note
# that this must be the latest attempt index, as earlier attempts can not
# be modified.
#
# @argument validation_token [Required, String]
# The unique validation token you received when this Quiz Submission was
# created.
#
# @argument access_code [String]
# Access code for the Quiz, if any.
#
# <b>Responses</b>
#
# * <b>200 OK</b> if the request was successful
# * <b>403 Forbidden</b> if an invalid access code is specified
# * <b>403 Forbidden</b> if the Quiz's IP filter restriction does not pass
# * <b>403 Forbidden</b> if an invalid token is specified
# * <b>400 Bad Request</b> if the QS is already complete
# * <b>400 Bad Request</b> if the attempt parameter is missing
# * <b>400 Bad Request</b> if the attempt parameter is not the latest attempt
#
# @example_response
# {
# "quiz_submissions": [QuizSubmission]
# }
def complete
@service.complete @quiz_submission, params[:attempt]
# TODO: should this go in the service instead?
Canvas::LiveEvents.quiz_submitted(@quiz_submission)
serialize_and_render @quiz_submission
end
# @API Get current quiz submission times.
#
# Get the current timing data for the quiz attempt, both the end_at timestamp
# and the time_left parameter.
#
# <b>Responses</b>
#
# * <b>200 OK</b> if the request was successful
#
# @example_response
# {
# "end_at": [DateTime],
# "time_left": [Integer]
# }
def time
if authorized_action(@quiz_submission, @current_user, :record_events)
render :json =>
{
:end_at => @quiz_submission && @quiz_submission.end_at,
:time_left => @quiz_submission && @quiz_submission.time_left
}
end
end
private
def module_locked?
@quiz.locked_for?(@current_user, :check_policies => true, :deep_check_if_needed => true)
end
def previewing?
!!params[:preview]
end
def serialize_and_render(quiz_submissions)
quiz_submissions = [ quiz_submissions ] unless quiz_submissions.is_a? Array
render :json => quiz_submissions_json(
quiz_submissions,
@quiz,
@current_user,
session,
@context,
Array(params[:include]),
params
)
end
def validate_ldb_status!(quiz = @quiz)
if quiz.require_lockdown_browser?
unless ldb_plugin.authorized?(self)
reject! 'this quiz requires the lockdown browser', :forbidden
end
end
end
def ldb_plugin
Canvas::LockdownBrowser.plugin.base
end
end