Skip to content

Add GET/LIST API for Quiz Extensions and NQ Accommodations#2640

Closed
jasonfill wants to merge 0 commit into
instructure:masterfrom
SmarterServices:master
Closed

Add GET/LIST API for Quiz Extensions and NQ Accommodations#2640
jasonfill wants to merge 0 commit into
instructure:masterfrom
SmarterServices:master

Conversation

@jasonfill

@jasonfill jasonfill commented Jun 6, 2026

Copy link
Copy Markdown

The Quiz Extensions and New Quizzes Accommodations APIs currently only support creating extensions (POST), but provide no way to read them back. This makes it impossible for integrations to query what extensions have been granted to students, and there is no event-driven notification when extensions are set through the UI.

This change adds:

  1. GET /api/v1/courses/:course_id/quizzes/:quiz_id/extensions

    • Lists quiz extensions from quiz_submissions where extra_attempts, extra_time, or manually_unlocked are set
    • Supports ?user_id= filter and pagination
    • Requires manage_grades or manage_assignments permission
  2. GET /api/v1/courses/:course_id/new_quizzes/:assignment_id/accommodations
    GET /api/v1/courses/:course_id/new_quizzes/:assignment_id/accommodations/:user_id

    • Proxies to the New Quizzes service at /api/assignments/:id/participants
    • Authenticates via ServicesJwt, uses Canvas.timeout_protection
    • Returns extra_time, extra_attempts, reduce_choices_enabled
    • Returns 502 for upstream failures (timeout, invalid JSON, non-200)
  3. Live Events

    • quiz_extension_created: emitted from QuizExtensionsController and CourseQuizExtensionsController after extensions are saved for Classic Quizzes
    • Uses global IDs (Switchman) for cross-shard uniqueness

Note

This does NOT create live events for NQ as that is a closed system and I could not see a way to do that properly.

flag=none

Test Plan:

  • As an admin, create a classic quiz with students enrolled
  • POST to create quiz extensions for a student (extra_attempts: 2)
  • GET /api/v1/courses/:id/quizzes/:id/extensions
    • Verify response contains the extension with integer user_id/quiz_id
    • Verify students without extensions are excluded
    • Test ?user_id= filter returns only that student
  • As a student, GET the extensions endpoint
    • Verify 403 Forbidden is returned
  • Create a New Quizzes (quiz_lti) assignment
  • POST to set accommodations (extra_time, extra_attempts)
  • GET /api/v1/courses/:id/new_quizzes/:assignment_id/accommodations
    • Verify accommodations are returned from the NQ service
  • GET /api/v1/courses/:id/new_quizzes/:assignment_id/accommodations/:user_id
    • Verify single user accommodation is returned
    • Verify 404 for a user with no accommodation
  • Verify live events are emitted in the live events stream when extensions are created via the API
  • Run RSpec:
    bin/rspec spec/apis/v1/quizzes/quiz_extensions_index_api_spec.rb
    spec/apis/v1/quizzes/new_quiz_accommodations_api_spec.rb
    spec/lib/canvas/live_events_quiz_extensions_spec.rb
    spec/apis/v1/quizzes/quiz_extensions_api_spec.rb

@CLAassistant

CLAassistant commented Jun 6, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ jasonfill
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

# }
#
def index
return render_unauthorized_action unless authorized_to_read?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking, shouldn't the authorized_to_read? belong to before_action? It looks like currently an unauthorized user can probe assignment_type and service configuration. It's not very dangerous, but since we had an incident, perhaps it would be better safe than sorry?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth moved to before_action in NewQuizAccommodationsController — authorize_read now runs before require_assignment and require_new_quizzes_service, preventing unauthorized users from probing assignment type or service configuration.


private

def serialize_jsonapi(quiz_extensions)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We changed the serialize_jsonapi method. The problem is, it's used by both new index and the pre-existing create actions. Now the method returns raw integers instead of strings. Wouldn't it cause issues for the existing consumers?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored original serializer for create in QuizExtensionsController. Now create uses the original serialize_jsonapi (serializer-based, stringified IDs) and index uses a new serialize_extensions method (raw hashes, integer IDs).

extra_attempts = participant["extra_attempts"]
reduce_choices_enabled = participant["reduce_choices_enabled"]

next unless extra_time.to_i > 0 || extra_attempts.to_i > 0 || reduce_choices_enabled == true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nitpick: we use == true here. That means the service response of "reduce_choices_enabled" => "true" will be considered truthy. Normally we wouldn't care, but, as I can see, in the other places alternate key names are handled (canvas_user_id, time_extension). So we currently have inconsistency here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce_choices_enabled boolean coercion - replaced == true with Canvas::Plugin.value_to_boolean(...) to consistently handle string "true" from the NQ service, matching the defensive approach used for alternate key names (canvas_user_id, time_extension)

@jasonfill

Copy link
Copy Markdown
Author

@dbuimistrau I am closing this PR and have opened this one: #2644. The changes requested are in the new PR. We needed to do this to clean up our repos master branch so we could get a few more PRs worked up and submitted off a clean master.

@jasonfill jasonfill closed this Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants