forked from instructure/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Collaborations: support deep linking response
Closes INTEROP-6430 flag=none Support for the deep linking request was added for the collaborations placement in 930f6ed. This change is the second half and allows LTI 1.3 tools to return content items at that placement. Test Plan: 1. Install an LTI 1.3 tool that handles deep linking requests 2. Install an LTI 1.1 tool that supports content item requests (the LTI 1.1 example tool on GitHub works here). 3. Navigate to the collaborations page of a course. 4. Create a new collaboration using the LTI 1.3 tool. Verify the following during creation: - Specifying a "Message" in the deep linking response shows that message in a flash message - Specifying an "Error Message" in the deep linking response shows that error message in a flash message - Upon collaboration creation, Canvas launches the tool to the URL given in the deep linking response in a new tab. 5. Refresh the collaborations page and click on the new collaboration. Verify the tool launches to the correct URL in a new tab. 6. Create a new collaboration using the LTI 1.x tool. Verify the same items listed in step 4. 7. Verify collaborations can be deleted via the "delete" icon. Change-Id: I756b94410b1d8c527e698debd84138008feb8937 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/256594 Tested-by: Service Cloud Jenkins <[email protected]> Reviewed-by: Wagner Goncalves <[email protected]> QA-Review: Wagner Goncalves <[email protected]> Product-Review: Karl Lloyd <[email protected]>
- Loading branch information
Showing
6 changed files
with
394 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Copyright (C) 2021 - 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/>. | ||
*/ | ||
|
||
const deepLinkingResponseMessageType = 'LtiDeepLinkingResponse' | ||
|
||
// Checks to see if a postMessage event is valid for | ||
// deep linking content item processing | ||
export function isValidDeepLinkingEvent(event, env) { | ||
return !!( | ||
event.origin === env.DEEP_LINKING_POST_MESSAGE_ORIGIN && | ||
event.data && | ||
event.data.messageType === deepLinkingResponseMessageType | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright (C) 2021 - 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/>. | ||
*/ | ||
|
||
import {isValidDeepLinkingEvent} from '../DeepLinking' | ||
|
||
describe('isValidDeepLinkingEvent', () => { | ||
let data, event, env, parameters | ||
|
||
beforeEach(() => { | ||
event = {data: {messageType: 'LtiDeepLinkingResponse'}, origin: 'canvas.instructure.com'} | ||
env = {DEEP_LINKING_POST_MESSAGE_ORIGIN: 'canvas.instructure.com'} | ||
parameters = [event, env] | ||
}) | ||
|
||
const subject = () => isValidDeepLinkingEvent(...parameters) | ||
|
||
it('return true', () => { | ||
expect(subject()).toEqual(true) | ||
}) | ||
|
||
describe('when the message origin is incorrect', () => { | ||
beforeEach(() => { | ||
event = {data, origin: 'wrong.origin.com'} | ||
parameters = [event, env] | ||
}) | ||
|
||
it('return false', () => { | ||
expect(subject()).toEqual(false) | ||
}) | ||
}) | ||
|
||
describe('when the event data is not present', () => { | ||
beforeEach(() => { | ||
event = {origin: 'canvas.instructure.com'} | ||
parameters = [event, env] | ||
}) | ||
|
||
it('return false', () => { | ||
expect(subject()).toEqual(false) | ||
}) | ||
}) | ||
|
||
describe('when the messageType is incorrect', () => { | ||
beforeEach(() => { | ||
event = {data: {messageType: 'WrongMessageType'}, origin: 'canvas.instructure.com'} | ||
parameters = [event, env] | ||
}) | ||
|
||
it('return false', () => { | ||
expect(subject()).toEqual(false) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
/* | ||
* Copyright (C) 2021 - 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/>. | ||
*/ | ||
|
||
import $ from 'jquery' | ||
import { | ||
addDeepLinkingListener, | ||
handleDeepLinking, | ||
collaborationUrl, | ||
onExternalContentReady | ||
} from '../collaborations' | ||
|
||
describe('addDeepLinkingListener', () => { | ||
let addEventListener | ||
const subject = () => { | ||
addDeepLinkingListener() | ||
} | ||
|
||
beforeAll(() => { | ||
addEventListener = global.addEventListener | ||
global.addEventListener = jest.fn() | ||
}) | ||
|
||
afterAll(() => { | ||
global.addEventListener = addEventListener | ||
}) | ||
|
||
beforeEach(() => { | ||
global.addEventListener.mockClear() | ||
}) | ||
|
||
it('adds the message handler to the window', () => { | ||
subject() | ||
expect(global.addEventListener).toHaveBeenCalledWith('message', handleDeepLinking) | ||
}) | ||
}) | ||
|
||
describe('handleDeepLinking', () => { | ||
const content_items = [ | ||
{ | ||
type: 'link', | ||
title: 'title', | ||
url: 'http://www.tool.com' | ||
} | ||
] | ||
|
||
const event = overrides => ({ | ||
origin: 'http://www.test.com', | ||
data: {messageType: 'LtiDeepLinkingResponse', content_items}, | ||
...overrides | ||
}) | ||
|
||
let ajaxJSON, flashError, env | ||
|
||
beforeAll(() => { | ||
env = window.ENV | ||
flashError = $.flashError | ||
ajaxJSON = $.ajaxJSON | ||
|
||
window.ENV = { | ||
DEEP_LINKING_POST_MESSAGE_ORIGIN: 'http://www.test.com' | ||
} | ||
|
||
$.flashError = jest.fn() | ||
$.ajaxJSON = jest.fn().mockImplementation(() => ({})) | ||
}) | ||
|
||
afterAll(() => { | ||
window.ENV = env | ||
$.flashError = flashError | ||
$.ajaxJSON = ajaxJSON | ||
}) | ||
|
||
beforeEach(() => { | ||
$.ajaxJSON.mockClear() | ||
$.flashError.mockClear() | ||
}) | ||
|
||
it('creates the collaboration', async () => { | ||
await handleDeepLinking(event()) | ||
expect($.ajaxJSON).toHaveBeenCalledWith( | ||
undefined, | ||
'POST', | ||
{contentItems: JSON.stringify(content_items)}, | ||
expect.anything(), | ||
expect.anything() | ||
) | ||
}) | ||
|
||
describe('when the event is invalid', () => { | ||
const overrides = { | ||
origin: 'http://bad.origin.com' | ||
} | ||
|
||
it('does not attempt to create a collaboration', async () => { | ||
await handleDeepLinking(event(overrides)) | ||
expect($.ajaxJSON).not.toHaveBeenCalled() | ||
}) | ||
}) | ||
|
||
describe('when there is a unhandled error parsing the content item', () => { | ||
const overrides = { | ||
data: {messageType: 'LtiDeepLinkingResponse', content_items: 1} | ||
} | ||
|
||
it('does not attempt to create a collaboration', async () => { | ||
await handleDeepLinking(event(overrides)) | ||
expect($.ajaxJSON).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('shows an error message to the user', async () => { | ||
await handleDeepLinking(event(overrides)) | ||
expect($.flashError).toHaveBeenCalled() | ||
}) | ||
}) | ||
}) | ||
|
||
describe('collaborationUrl', () => { | ||
it('returns a collaboration url', () => { | ||
expect(collaborationUrl(1)).toEqual(`${window.location.toString()}/1`) | ||
}) | ||
}) | ||
|
||
describe('onExternalContentReady', () => { | ||
const params = overrides => [ | ||
{}, | ||
{ | ||
contentItems: {}, | ||
...overrides | ||
} | ||
] | ||
let querySelector, ajaxJSON | ||
|
||
beforeAll(() => { | ||
querySelector = global.document.querySelector | ||
ajaxJSON = $.ajaxJSON | ||
|
||
global.document.querySelector = jest.fn().mockImplementation(() => ({ | ||
href: 'http://www.test.com/update', | ||
getAttribute: () => 'http://www.test.com/create' | ||
})) | ||
$.ajaxJSON = jest.fn().mockImplementation(() => ({})) | ||
}) | ||
|
||
afterAll(() => { | ||
global.document.querySelector = querySelector | ||
$.ajaxJSON = ajaxJSON | ||
}) | ||
|
||
beforeEach(() => { | ||
global.document.querySelector.mockClear() | ||
$.ajaxJSON.mockClear() | ||
}) | ||
|
||
it('creates a new collaboration', () => { | ||
onExternalContentReady(...params()) | ||
expect($.ajaxJSON).toHaveBeenCalledWith( | ||
'http://www.test.com/create', | ||
'POST', | ||
expect.anything(), | ||
expect.anything(), | ||
expect.anything() | ||
) | ||
}) | ||
|
||
describe('with a service id', () => { | ||
it('updates the existing collaboration', () => { | ||
onExternalContentReady(...params({service_id: 1})) | ||
expect($.ajaxJSON).toHaveBeenCalledWith( | ||
'http://www.test.com/update', | ||
'PUT', | ||
expect.anything(), | ||
expect.anything(), | ||
expect.anything() | ||
) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* Copyright (C) 2021 - 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/>. | ||
*/ | ||
|
||
import $ from 'jquery' | ||
import {isValidDeepLinkingEvent} from './DeepLinking' | ||
import processSingleContentItem from './processors/processSingleContentItem' | ||
import I18n from 'i18n!collaborations' | ||
|
||
export const addDeepLinkingListener = () => { | ||
removeDeepLinkingListener() | ||
window.addEventListener('message', handleDeepLinking) | ||
} | ||
|
||
/* | ||
* Creates or updates a Collaboration in Canvas. | ||
* | ||
* A processing function called by both the | ||
* LTI Advantage handleDeepLinking handler and the | ||
* LTI 1.1 content item handler. | ||
*/ | ||
export function onExternalContentReady(e, data) { | ||
const contentItem = {contentItems: JSON.stringify(data.contentItems)} | ||
if (data.service_id) { | ||
updateCollaboration(contentItem, data.service_id) | ||
} else { | ||
createCollaboration(contentItem) | ||
} | ||
} | ||
|
||
/* | ||
* Handles deep linking response events in | ||
* the collaborations UI. Only a single LtiResourceLink | ||
* content item is supported | ||
*/ | ||
export const handleDeepLinking = async event => { | ||
// Don't attempt to process invalid messages | ||
if (!isValidDeepLinkingEvent(event, ENV)) { | ||
return | ||
} | ||
|
||
try { | ||
const item = await processSingleContentItem(event) | ||
onExternalContentReady(event, { | ||
service_id: item.service_id, | ||
contentItems: [item] | ||
}) | ||
} catch (e) { | ||
$.flashError(I18n.t('Error retrieving content from tool')) | ||
} | ||
} | ||
|
||
export function collaborationUrl(id) { | ||
return window.location.toString() + '/' + id | ||
} | ||
|
||
const removeDeepLinkingListener = () => { | ||
window.removeEventListener('message', handleDeepLinking) | ||
} | ||
|
||
function updateCollaboration(contentItem, collab_id) { | ||
const url = document.querySelector('.collaboration_' + collab_id + ' a.title')?.href | ||
$.ajaxJSON(url, 'PUT', contentItem, collaborationSuccess, msg => { | ||
$.screenReaderFlashMessage(I18n.t('Collaboration update failed')) | ||
}) | ||
} | ||
|
||
function createCollaboration(contentItem) { | ||
const url = document.querySelector('#new_collaboration')?.getAttribute('action') | ||
$.ajaxJSON(url, 'POST', contentItem, collaborationSuccess, msg => { | ||
$.screenReaderFlashMessage(I18n.t('Collaboration creation failed')) | ||
}) | ||
} | ||
|
||
function collaborationSuccess(msg) { | ||
openCollaboration(msg.collaboration.id) | ||
window.location.reload() | ||
} | ||
|
||
function openCollaboration(id) { | ||
window.open(collaborationUrl(id)) | ||
} |
Oops, something went wrong.