reuse quizzes launch session in speedgrader

We used to support reusing the same launch session when the teacher
switches students in SpeedGrader, but this functionality was
accidentally removed. This PS re-enables this functionality.

closes QUIZ-10207
closes EVAL-2673
flag=none

Test plan:
1. Create a Quizzes.Next quiz assigned to at least three students
2. As two of the students, take the quiz. As the third student, don't
   take the quiz.
3. As the teacher, go to SpeedGrader for the quiz. Notice that
   Quizzes.Next launches when you navigate to the first student that has
   taken the quiz. Then navigate to the other student that has taken
   the quiz and verify that we're not launching a new Quizzes.Next
   session (you can verify this via the Network tab) — instead, the
   content in the existing session is updated to show the new student's
   information.
4. When navigating from a student that has taken the quiz to a student
   that has not taken the quiz, verify the "No submission" text is shown
   in SpeedGrader.
5. If a Quizzes.Next session has already been launched (from previously
   viewing a student that has taken the quiz), when navigating from a
   student that has not taken the quiz to a student that has taken the
   quiz, verify we're not launching a new Quizzes.Next session.

Change-Id: If0346e205c14d65815fe1fc872534f185a3b73d7
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/303326
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Spencer Olson <solson@instructure.com>
Product-Review: Marissa Pio Roda <marissa.pioroda@instructure.com>
Reviewed-by: Ricardo Oliveira <ricardo.oliveira@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
This commit is contained in:
Dustin Cowles 2022-10-14 14:28:05 -06:00 committed by Spencer Olson
parent 77338f8fdc
commit 9db7a4dcab
5 changed files with 117 additions and 8 deletions

View File

@ -895,6 +895,7 @@ class GradebooksController < ApplicationController
@outer_frame = true
log_asset_access(["speed_grader", @context], "grades", "other")
env = {
SINGLE_NQ_SESSION_ENABLED: Account.site_admin.feature_enabled?(:single_new_quiz_session_in_speedgrader),
EMOJIS_ENABLED: @context.feature_enabled?(:submission_comment_emojis),
EMOJI_DENY_LIST: @context.root_account.settings[:emoji_deny_list],
MANAGE_GRADES: @context.grants_right?(@current_user, session, :manage_grades),

View File

@ -132,3 +132,15 @@ originality_reports_for_a2:
state: on # enable for local development
test:
state: on # enable for the deployed 'test' environment
single_new_quiz_session_in_speedgrader:
state: hidden
applies_to: SiteAdmin
display_name: Single Quizzes.Next Session in SpeedGrader
description: If set, Quizzes.Next will operate using a single launch session in SpeedGrader
environments:
ci:
state: allowed_on # enable for automated testing builds and local testing
development:
state: allowed_on # enable for local development
test:
state: allowed_on # enable for the deployed 'test' environment

View File

@ -93,10 +93,12 @@ QUnit.module('SpeedGrader', rootHooks => {
sandbox.stub(SpeedGraderHelpers, 'reloadPage')
setupFixtures()
fakeENV.setup({SINGLE_NQ_SESSION_ENABLED: true})
})
rootHooks.afterEach(() => {
teardownFixtures()
fakeENV.teardown()
})
QUnit.module('SpeedGrader#showDiscussion', {
@ -3272,6 +3274,7 @@ QUnit.module('SpeedGrader', rootHooks => {
help_url: 'example.com/foo',
settings_url: 'example.com/settings',
show_help_menu_item: false,
SINGLE_NQ_SESSION_ENABLED: true,
})
sinon.stub($, 'getJSON')
sinon.stub($, 'ajaxJSON')
@ -6417,10 +6420,21 @@ QUnit.module('SpeedGrader', rootHooks => {
})
})
QUnit.module('#loadSubmissionPreview', hooks => {
QUnit.module('#loadSubmissionPreview', contextHooks => {
const EG = SpeedGrader.EG
async function postMessage(message, targetOrigin) {
await new Promise(resolve => {
const listen = () => {
window.removeEventListener('message', listen, false)
resolve()
}
hooks.beforeEach(() => {
window.addEventListener('message', listen, false)
window.postMessage(message, targetOrigin)
})
}
contextHooks.beforeEach(() => {
setupFixtures(`
<div id='this_student_does_not_have_a_submission'></div>
<div id='iframe_holder'>
@ -6433,11 +6447,53 @@ QUnit.module('SpeedGrader', rootHooks => {
}
})
hooks.afterEach(() => {
contextHooks.afterEach(() => {
SpeedGrader.teardown()
})
QUnit.module('when a submission is unsubmitted', () => {
QUnit.module('when the student has submitted for a quizzesNext quiz', hooks => {
let submission
hooks.beforeEach(() => {
EG.currentStudent.submission = {
submission_type: 'basic_lti_launch',
workflow_state: 'pending_review',
}
submission = {submission_type: 'basic_lti_launch'}
})
test('launches a quizzesNext session if it has not yet been launched', async () => {
await postMessage({subject: 'quizzesNext.register'}, '*')
const ltiLaunchStub = sandbox.stub(EG, 'renderLtiLaunch')
EG.loadSubmissionPreview(null, submission)
ok(ltiLaunchStub.calledOnce)
})
QUnit.module('when a quizzesNext session has already been launched', () => {
test('does not launch a new quizzesNext session when in "single session" mode', async () => {
await postMessage({subject: 'quizzesNext.register'}, '*')
EG.loadSubmissionPreview(null, submission)
const ltiLaunchStub = sandbox.stub(EG, 'renderLtiLaunch')
EG.loadSubmissionPreview(null, submission)
ok(ltiLaunchStub.notCalled)
})
test('launches a new quizzesNext session when in "session per student" mode', async () => {
await postMessage(
{
subject: 'quizzesNext.register',
payload: {singleLtiLaunch: false},
},
'*'
)
EG.loadSubmissionPreview(null, submission)
const ltiLaunchStub = sandbox.stub(EG, 'renderLtiLaunch')
EG.loadSubmissionPreview(null, submission)
ok(ltiLaunchStub.calledOnce)
})
})
})
QUnit.module('when the student has not submitted', () => {
test('shows the "this student does not have a submission" div', () => {
const $noSubmission = $('#this_student_does_not_have_a_submission')
$noSubmission.hide()
@ -6451,6 +6507,32 @@ QUnit.module('SpeedGrader', rootHooks => {
strictEqual($('#iframe_holder').html(), '')
})
QUnit.module('when quizzesNext is loaded', () => {
test('does not empty the iframe when operating in "single session" mode', async () => {
await postMessage(
{subject: 'quizzesNext.register', payload: {singleLtiLaunch: true}},
'*'
)
EG.loadSubmissionPreview()
ok($('#iframe_holder').text().includes('I am an iframe holder!'))
})
test('operates in "single session" mode by default for quizzesNext', async () => {
await postMessage({subject: 'quizzesNext.register'}, '*')
EG.loadSubmissionPreview()
ok($('#iframe_holder').text().includes('I am an iframe holder!'))
})
test('empties the iframe when operating in "session per student" mode', async () => {
await postMessage(
{subject: 'quizzesNext.register', payload: {singleLtiLaunch: false}},
'*'
)
EG.loadSubmissionPreview()
notOk($('#iframe_holder').text().includes('I am an iframe holder!'))
})
})
})
})

View File

@ -227,6 +227,8 @@ let gradeeLabel
let sessionTimer
let isAdmin
let showSubmissionOverride
let externalToolLaunchOptions = {singleLtiLaunch: false}
let externalToolLoaded = false
let provisionalGraderDisplayNames
let EG
const customProvisionalGraderLabel = I18n.t('Custom')
@ -257,6 +259,8 @@ function setupBeforeLeavingSpeedgrader() {
}
function teardownBeforeLeavingSpeedgrader() {
externalToolLaunchOptions = {singleLtiLaunch: false}
externalToolLoaded = false
window.removeEventListener('beforeunload', EG.beforeLeavingSpeedgrader)
}
@ -2726,7 +2730,9 @@ EG = {
this.currentStudent.submission.workflow_state === 'unsubmitted'
) {
$this_student_does_not_have_a_submission.show()
this.emptyIframeHolder()
if (!ENV.SINGLE_NQ_SESSION_ENABLED || !externalToolLaunchOptions.singleLtiLaunch) {
this.emptyIframeHolder()
}
} else if (
this.currentStudent.submission &&
this.currentStudent.submission.submitted_at &&
@ -2737,7 +2743,12 @@ EG = {
} else if (attachment) {
this.renderAttachment(attachment)
} else if (submission && submission.submission_type === 'basic_lti_launch') {
this.renderLtiLaunch($iframe_holder, ENV.lti_retrieve_url, submission)
if (!ENV.SINGLE_NQ_SESSION_ENABLED || !externalToolLoaded || !externalToolLaunchOptions.singleLtiLaunch) {
this.renderLtiLaunch($iframe_holder, ENV.lti_retrieve_url, submission)
externalToolLoaded = true
} else {
$iframe_holder.show()
}
} else {
this.renderSubmissionPreview()
}
@ -4257,8 +4268,11 @@ export default {
EG.setUpAssessmentAuditTray()
}
function registerQuizzesNext(overriddenShowSubmission) {
function registerQuizzesNext(overriddenShowSubmission, launchOptions) {
showSubmissionOverride = overriddenShowSubmission
if (launchOptions) {
externalToolLaunchOptions = launchOptions
}
}
quizzesNextSpeedGrading(EG, $iframe_holder, registerQuizzesNext, refreshGrades, window)

View File

@ -90,7 +90,7 @@ function quizzesNextSpeedGrading(
switch (message.subject) {
case 'quizzesNext.register':
EG.setGradeReadOnly(true)
return registerCb(postChangeSubmissionMessage)
return registerCb(postChangeSubmissionMessage, message.payload || {singleLtiLaunch: true})
case 'quizzesNext.submissionUpdate':
return refreshGradesCb(quizzesNextChange, retryRefreshGrades, 1000)
}