provide lti resource links importer into course copy

refs INTEROP-6502
flag=none

* Have an LTI 1.3 installed;
* Have a Course recorded;
* Have an Assignment with lti resource links recorded in the `Course`
  context, use the RCE editor placement;
* Have an Assignment with lti resource link recorded in the `Assignment`
  context, use External Tool Submission;
* You sould be able to create a new course or use someone available;
* Into course settings, you sould be able to `Import Course Content` by
  choosing an option from the `Content type` dropdown menu;
* When the job is completed, you should be able to check if the resource
  links were created properly using rails console:
    $ Course.find(ID).assignments.map(&:lti_resource_links)
    $ Course.find(ID).lti_resource_links
  and you should check if the new assignments were launching the tool and
  expanding the custom params as expected;

Change-Id: I9ffc71a832409e6cfd488822f1e39d5c129bfff8
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/258967
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Xander Moffatt <xmoffatt@instructure.com>
QA-Review: Xander Moffatt <xmoffatt@instructure.com>
Product-Review: Wagner Goncalves <wagner.goncalves@instructure.com>
This commit is contained in:
Wagner Gonçalves 2021-02-19 13:24:35 -03:00 committed by Wagner Goncalves
parent b241135806
commit 3504465caa
14 changed files with 438 additions and 3 deletions

View File

@ -69,6 +69,7 @@ class Assignment < ActiveRecord::Base
restrict_columns :state, [:workflow_state]
attribute :lti_resource_link_custom_params, :string, default: nil
attribute :lti_resource_link_lookup_uuid, :string, default: nil
has_many :submissions, -> { active.preload(:grading_period) }, inverse_of: :assignment
has_many :all_submissions, class_name: 'Submission', dependent: :delete_all
@ -1111,9 +1112,17 @@ class Assignment < ActiveRecord::Base
end
if lti_1_3_external_tool_tag? && !lti_resource_links.empty?
return if primary_resource_link.custom == lti_resource_link_custom_params_as_hash
options = {}
primary_resource_link.update!(custom: lti_resource_link_custom_params_as_hash)
unless primary_resource_link.custom == lti_resource_link_custom_params_as_hash
options[:custom] = lti_resource_link_custom_params_as_hash
end
options[:lookup_uuid] = lti_resource_link_lookup_uuid unless lti_resource_link_lookup_uuid.nil?
return if options.empty?
primary_resource_link.update!(options)
end
end
end

View File

@ -368,6 +368,7 @@ module Importers
item.needs_update_cached_due_dates = true if item.new_record? || item.update_cached_due_dates?
item.save_without_broadcasting!
item.skip_schedule_peer_reviews = nil
item.lti_resource_link_lookup_uuid = hash['resource_link_lookup_uuid']
create_lti_13_models(hash, context, migration, item)

View File

@ -159,6 +159,7 @@ module Importers
migration.update_import_progress(85)
Importers::WikiPageImporter.process_migration_course_outline(data, migration)
Importers::CalendarEventImporter.process_migration(data, migration)
Importers::LtiResourceLinkImporter.process_migration(data, migration)
everything_selected = !migration.copy_options || migration.is_set?(migration.copy_options[:everything])
if everything_selected || migration.is_set?(migration.copy_options[:all_course_settings])

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
#
# 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/>.
require_dependency 'importers'
module Importers
class LtiResourceLinkImporter < Importer
self.item_class = Lti::ResourceLink
def self.process_migration(hash, migration)
lti_resource_links = hash.with_indifferent_access['lti_resource_links']
return false unless lti_resource_links
# Recover all resource links recorded by Importers::AssignmentImporter to
resource_links_from_assignments = imported_resource_links_from_assignments(migration)
# When a resource link was created into Assignment context we have to
# update the custom params.
# Otherwise, we have to create a resource link for Course context.
lti_resource_links.each do |lti_resource_link|
updated = update_custom_for_resource_link_from_assignment_context(resource_links_from_assignments, lti_resource_link)
next if updated
create_or_update_resource_link_for_a_course_context(lti_resource_link, migration)
end
true
end
def self.create_or_update_resource_link_for_a_course_context(lti_resource_link, migration)
custom_params = Lti::DeepLinkingUtil.validate_custom_params(lti_resource_link['custom'])
destination_course = migration.context
# In case a user import a course, and then delete the assignments, in the
# next importation, should we need to consider just the RL's active?
resource_link_for_course = Lti::ResourceLink.find_by(
context: destination_course,
lookup_uuid: lti_resource_link['lookup_uuid']
)
unless resource_link_for_course
tool = ContextExternalTool.find_external_tool(lti_resource_link['launch_url'], destination_course)
resource_link_for_course = Lti::ResourceLink.new(
context_external_tool: tool,
context: destination_course,
custom: custom_params,
lookup_uuid: lti_resource_link['lookup_uuid']
)
end
resource_link_for_course.custom = custom_params
resource_link_for_course.save
end
def self.find_resource_link_from_assignment_context(resource_links_from_assignments, lookup_uuid)
resource_links_from_assignments.find { |item| item.lookup_uuid == lookup_uuid }
end
def self.imported_resource_links_from_assignments(migration)
migration.context.assignments.joins(:lti_resource_links).map(&:lti_resource_links).flatten
end
def self.update_custom_for_resource_link_from_assignment_context(resource_links_from_assignments, lti_resource_link)
resource_link = find_resource_link_from_assignment_context(
resource_links_from_assignments,
lti_resource_link['lookup_uuid']
)
return false unless resource_link
resource_link.update(
custom: Lti::DeepLinkingUtil.validate_custom_params(lti_resource_link['custom'])
)
true
end
end
end

View File

@ -28,6 +28,7 @@ module CC::Importer::Canvas
include WebcontentConverter
include QuizConverter
include MediaTrackConverter
include LtiResourceLinkConverter
MANIFEST_FILE = "imsmanifest.xml"
@ -69,6 +70,8 @@ module CC::Importer::Canvas
set_progress(70)
@course[:media_tracks] = convert_media_tracks(settings_doc(MEDIA_TRACKS))
set_progress(71)
@course[:lti_resource_links] = convert_lti_resource_links
set_progress(72)
convert_quizzes if Qti.qti_enabled?
set_progress(80)

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
#
# 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/>.
#
module CC::Importer::Canvas
module LtiResourceLinkConverter
include CC::Importer
FLOAT_REGEX = /^[-+]?\d+[.]\d+$/.freeze
INTEGER_REGEX = /^[-+]?\d+$/.freeze
def convert_lti_resource_links
resource_links = []
@manifest.css('resource[type$=imsbasiclti_xmlv1p3]').each do |resource|
identifier = resource.attributes['identifier'].value
resource_link_element = resource.at_css("file[href$='lti_resource_links/#{identifier}.xml']")
next unless resource_link_element
path = @package_root.item_path(resource_link_element['href'])
document = open_file_xml(path)
next unless document
custom = {}
lookup_uuid = nil
document.xpath("//blti:custom//lticm:property").each do |el|
key = el.attributes['name'].value
value = el.content
next if key.empty?
# As `el.content` returns a String, we're trying to convert the
# custom parameter value to the orignal data type
value = if FLOAT_REGEX.match? value
value.to_f
elsif INTEGER_REGEX.match? value
value.to_i
elsif value == 'true'
true
elsif value == 'false'
false
else
value
end
custom[key.to_sym] = value
end
document.xpath("//blti:extensions//lticm:property").each do |el|
lookup_uuid = el.content if el.attributes['name'].value == 'lookup_uuid'
end
launch_url = document.xpath("//blti:launch_url").first.content
resource_links << {
custom: custom,
launch_url: launch_url,
lookup_uuid: lookup_uuid
}
end
resource_links
end
end
end

View File

@ -99,6 +99,7 @@ module CC::Importer::Standard
assignment["external_tool_migration_id"] = get_node_val(meta_doc, "external_tool_identifierref") if meta_doc.at_css("external_tool_identifierref")
assignment["external_tool_id"] = get_node_val(meta_doc, "external_tool_external_identifier") if meta_doc.at_css("external_tool_external_identifier")
assignment["tool_setting"] = get_tool_setting(meta_doc) if meta_doc.at_css('tool_setting').present?
assignment["resource_link_lookup_uuid"] = get_node_val(meta_doc, "resource_link_lookup_uuid") if meta_doc.at_css("resource_link_lookup_uuid")
if meta_doc.at_css("saved_rubric_comments comment")
assignment[:saved_rubric_comments] = {}

View File

@ -43,5 +43,8 @@
<resource identifier="i964fd8107ac2c2e75e9a142971693976" type="tool_profile">
<file href="i964fd8107ac2c2e75e9a142971693976.json"/>
</resource>
<resource identifier="g534facc599d2202cf67dce042ee3105b" type="imsbasiclti_xmlv1p3">
<file href="lti_resource_links/g534facc599d2202cf67dce042ee3105b.xml"/>
</resource>
</resources>
</manifest>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p3" xmlns:blti="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imslticc_v1p3.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 imslticp_v1p0.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 imsbasiclti_v1p0p1.xsd">
<blti:title>LTI 1.3 a30691c6</blti:title>
<blti:description>1.3 Test Tool</blti:description>
<blti:launch_url>http://lti13testtool.docker/launch</blti:launch_url>
<blti:custom>
<lticm:property name="param1">some string</lticm:property>
<lticm:property name="param2">1</lticm:property>
<lticm:property name="param3">2.56</lticm:property>
<lticm:property name="param4">true</lticm:property>
<lticm:property name="param5">false</lticm:property>
<lticm:property name="param6">a12.5</lticm:property>
<lticm:property name="param7">5d781f15-c6b0-4901-a1f7-2a77e7bf4982</lticm:property>
<lticm:property name="param8">+1(855)552-2338</lticm:property>
</blti:custom>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="lookup_uuid">1b302c1e-c0a2-42dc-88b6-c029699a7c7a</lticm:property>
</blti:extensions>
</cartridge_basiclti_link>

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
#
# 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/>.
require File.expand_path(File.dirname(__FILE__) + '/../../../../import_helper')
describe CC::Importer::Canvas::LtiResourceLinkConverter do
subject do
Class.new do
include CC::Importer::Canvas::LtiResourceLinkConverter
def initialize(manifest, path)
@manifest = manifest
@package_root = PackageRoot.new(path)
end
end.new(manifest, path)
end
let(:manifest) { ImportHelper.get_import_data_xml('unzipped', 'imsmanifest') }
let(:path) { File.expand_path(File.dirname(__FILE__) + '/../../../../fixtures/importer/unzipped') }
describe '#convert_lti_resource_links' do
it 'extract custom params and lookup_uuid' do
lti_resource_links = subject.convert_lti_resource_links
expect(lti_resource_links).to include(
a_hash_including(
custom: {
param1: 'some string',
param2: 1,
param3: 2.56,
param4: true,
param5: false,
param6: 'a12.5',
param7: '5d781f15-c6b0-4901-a1f7-2a77e7bf4982',
param8: '+1(855)552-2338',
},
lookup_uuid: '1b302c1e-c0a2-42dc-88b6-c029699a7c7a',
launch_url: 'http://lti13testtool.docker/launch'
)
)
end
end
end

View File

@ -1175,6 +1175,30 @@ XML
expect(@copy_to.attachments.where(migration_id: 'ghi').first).to be_deleted
end
context 'importing lti resource links' do
let(:data) do
{
'lti_resource_links' => [
{
'custom' => {
'param1' => 'value1'
},
'lookup_uuid' => '1b302c1e-c0a2-42dc-88b6-c029699a7c7a',
'context_id' => @copy_from.id,
'context_type' => 'Course'
}
]
}
end
let(:migration) { ContentMigration.create(context: @copy_to) }
it 'process migration from LtiResourceLinkImporter' do
expect(Importers::LtiResourceLinkImporter).to receive(:process_migration).once.with(data, migration)
Importers::CourseContentImporter.import_content(@copy_to, data, nil, migration)
end
end
context "warnings for missing links in imported html" do
it "should add warnings for assessment questions" do
data = {

View File

@ -0,0 +1,113 @@
# frozen_string_literal: true
#
# 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/>.
#
require 'spec_helper'
describe Importers::LtiResourceLinkImporter do
subject { described_class.process_migration(hash, migration) }
let!(:source_course) { course_model }
let!(:destination_course) { course_model }
let!(:migration) { ContentMigration.create(context: destination_course, source_course: source_course) }
let!(:tool) { external_tool_model(context: destination_course) }
context 'when `lti_resource_links` is not given' do
let(:hash) { { lti_resource_links: nil } }
it 'does not import lti resource links' do
expect(subject).to eq false
end
end
context 'when `lti_resource_links` is given' do
let(:custom_params) do
{ 'param1' => 'value1 ' }
end
let(:lookup_uuid) { '1b302c1e-c0a2-42dc-88b6-c029699a7c7a' }
let(:hash) do
{
'lti_resource_links' => [
{
'custom' => custom_params,
'lookup_uuid' => lookup_uuid,
'launch_url' => tool.url
}
]
}
end
context 'when the Lti::ResourceLink.context_type is an Assignment' do
let!(:assignment) do
destination_course.assignments.create!(
submission_types: 'external_tool',
external_tool_tag_attributes: { content: tool }
)
end
let!(:resource_link) do
Lti::ResourceLink.create!(
context_external_tool: tool,
context: assignment,
lookup_uuid: lookup_uuid,
custom: nil
)
end
it 'update the custom params' do
expect(resource_link.custom).to be_nil
expect(subject).to eq true
resource_link.reload
expect(resource_link.custom).to eq custom_params
end
end
context 'when the Lti::ResourceLink.context_type is a Course' do
context 'and the resource link was not recorded' do
it 'create the new resource link' do
expect(subject).to eq true
expect(destination_course.lti_resource_links.size).to eq 1
expect(destination_course.lti_resource_links.first.lookup_uuid).to eq lookup_uuid
expect(destination_course.lti_resource_links.first.custom).to eq custom_params
end
end
context 'and the resource link was recorded' do
before do
destination_course.lti_resource_links.create!(
context_external_tool: tool,
custom: nil,
lookup_uuid: lookup_uuid
)
end
it 'update the custom params' do
expect(subject).to eq true
expect(destination_course.lti_resource_links.size).to eq 1
expect(destination_course.lti_resource_links.first.lookup_uuid).to eq lookup_uuid
expect(destination_course.lti_resource_links.first.custom).to eq custom_params
end
end
end
end
end

View File

@ -9045,7 +9045,7 @@ describe Assignment do
it_behaves_like 'line item and resource link existence check'
it_behaves_like 'assignment to line item attribute sync check'
it 'change the `custom` attribute at resource link when it is informed' do
it 'change the `custom` attribute at resource link when it is given' do
assignment.lti_resource_link_custom_params = nil
assignment.save!
assignment.reload
@ -9077,6 +9077,18 @@ describe Assignment do
expect(resource_link.custom).to eq new_custom_params.with_indifferent_access
end
it 'change the `lookup_uuid` attribute at resource link when it is given' do
lookup_uuid = '3d719897-4274-44ab-aff2-2fbd3c9d2977'
assignment.lti_resource_link_lookup_uuid = lookup_uuid
assignment.save!
assignment.reload
resource_link = assignment.line_items.first.resource_link
expect(resource_link.lookup_uuid).to eq lookup_uuid
end
context 'and no resource link or line item exist' do
let(:resource_link) { subject.line_items.first.resource_link }
let(:line_item) { subject.line_items.first }

View File

@ -462,6 +462,12 @@ describe ContentMigration do
@copy_from.homeroom_course = true
@copy_from.save!
@copy_from.lti_resource_links.create!(
context_external_tool: external_tool_model(context: @copy_from),
custom: nil,
lookup_uuid: '1b302c1e-c0a2-42dc-88b6-c029699a7c7a'
)
run_course_copy
#compare settings
@ -485,6 +491,9 @@ describe ContentMigration do
expect(@copy_to.send(att)).to eq(@copy_from.send(att)), "@copy_to.#{att}: expected #{@copy_from.send(att)}, got #{@copy_to.send(att)}"
end
expect(@copy_to.tab_configuration).to eq @copy_from.tab_configuration
expect(@copy_to.lti_resource_links.size).to eq 1
expect(@copy_to.lti_resource_links.first.lookup_uuid).to eq '1b302c1e-c0a2-42dc-88b6-c029699a7c7a'
end
it "should copy the overridable course visibility setting" do