convert links in New Quizzes QTI/XML during CC exports

closes QUIZ-13331
flag = none

test plan:
- create a course in Canvas
- add a wiki page
- add an assignment
- create a New Quiz with at least
one question
- on rich content fields, insert
"internal links" to the wiki page
and the assignment
(feel free t add more internal links
if you have more resources that could
be linked)
- on a rich content field, attach an
image
- export the course and download the .imscc
file
- go to an empty course and import the .imscc
package without migrating to New Quizzes
- observe that all the links on rich content fields
work
- go to an empty course and import the .imscc
file, migrating CQ to NQ
- observe that links appear on the rich content
fields

- Important note: there is a known issue with
Common Cartridge imports where some internal links
don't work in New Quizzes generated by converting
CQ into NQ. For example, wiki page links, and links
to classic quizzes that were migrated into NQ during
the import.

Change-Id: I109a63daf5996dec2dfcecd030dc43852c3d9766
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/344713
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Griffin Zody <griffin.zody@instructure.com>
QA-Review: Griffin Zody <griffin.zody@instructure.com>
Product-Review: Marissa Pio Roda <marissa.pioroda@instructure.com>
This commit is contained in:
Jorge Arteaga 2024-04-08 18:44:32 -03:00
parent 9cb5ef8fac
commit fe6dffd959
3 changed files with 238 additions and 1 deletions

View File

@ -0,0 +1,49 @@
# 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/>.
#
require "nokogiri"
module CC
class NewQuizzesLinksReplacer
def initialize(manifest)
@course = manifest.exporter.course
@user = manifest.exporter.user
@manifest = manifest
end
def replace_links(xml)
doc = Nokogiri::XML(xml || "")
doc.search("*").each do |node|
next unless node.node_name == "mattext" && node["texttype"] == "text/html"
node.content = html_exporter.html_content(node.content)
end
doc.to_xml
end
def html_exporter
@html_exporter ||= CCHelper::HtmlContentExporter.new(@course,
@user,
for_course_copy: false,
key_generator: @manifest)
end
end
end

View File

@ -43,7 +43,13 @@ module CC
dest_dir = File.join(export_dir, file_dir) dest_dir = File.join(export_dir, file_dir)
FileUtils.mkdir_p(dest_dir) FileUtils.mkdir_p(dest_dir)
File.binwrite(File.join(dest_dir, file_name), File.read(f)) file_content = File.read(f)
if file_name.end_with?(".xml", ".qti")
file_content = links_replacer.replace_links(file_content)
end
File.binwrite(File.join(dest_dir, file_name), file_content)
file_path file_path
end end
end end
@ -98,6 +104,10 @@ module CC
private private
def links_replacer
@links_replacer ||= CC::NewQuizzesLinksReplacer.new(@manifest)
end
def uploaded_media_resources(file_paths) def uploaded_media_resources(file_paths)
file_paths.each do |file_path| file_paths.each do |file_path|
file_uuid = file_path.split("/").last file_uuid = file_path.split("/").last

View File

@ -0,0 +1,178 @@
# 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/>.
require "nokogiri"
describe CC::NewQuizzesLinksReplacer do
describe "#replace_links" do
subject { described_class.new(@manifest) }
before do
@course = course_model
@content_export = @course.content_exports.build(global_identifiers: true,
export_type: ContentExport::COMMON_CARTRIDGE,
user: @user)
@exporter = CC::CCExporter.new(@content_export, course: @course, user: @user)
@manifest = CC::Manifest.new(@exporter)
end
context "when the xml contains file links" do
before do
folder = folder_model(name: "Uploaded Media", context: @course)
@attachment = attachment_model(display_name: "aws_opensearch-2.png",
context: @course,
folder:,
uploaded_data: stub_file_data("aws_opensearch-2.png", "...", "image/png"))
end
let(:xml) do
<<~XML
<?xml version="1.0" encoding="UTF-8"?>
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
<assessment ident="g1ac74172132891415c3a61888ca1c1bb" title="my quiz">
<section ident="root_section">
<item ident="g31feb7778351b898105e2ab5150f162d" title="Question">
<presentation>
<material>
<mattext texttype="text/html">&lt;div&gt;&lt;p&gt;insert question here&lt;/p&gt;
&lt;p&gt;&lt;img src="/courses/#{@course.id}/files/#{@attachment.id}/preview" alt="aws_opensearch-2.png"&gt;&lt;/p&gt;&lt;/div&gt;</mattext>
</material>
</presentation>
</item>
</section>
</assessment>
</questestinterop>
XML
end
it "replaces course file links" do
expected_xml = <<~XML
<?xml version="1.0" encoding="UTF-8"?>
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
<assessment ident="g1ac74172132891415c3a61888ca1c1bb" title="my quiz">
<section ident="root_section">
<item ident="g31feb7778351b898105e2ab5150f162d" title="Question">
<presentation>
<material>
<mattext texttype="text/html">&lt;div&gt;&lt;p&gt;insert question here&lt;/p&gt;
&lt;p&gt;&lt;img src="$IMS-CC-FILEBASE$/Uploaded%20Media/aws_opensearch-2.png" alt="aws_opensearch-2.png"&gt;&lt;/p&gt;&lt;/div&gt;</mattext>
</material>
</presentation>
</item>
</section>
</assessment>
</questestinterop>
XML
expect(subject.replace_links(xml)).to eq(expected_xml)
end
end
context "when the xml contains wiki page links" do
before do
@page = @course.wiki_pages.create(title: "My wiki page")
end
let(:xml) do
<<~XML
<?xml version="1.0" encoding="UTF-8"?>
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
<assessment ident="g2267932abda3f486f8304c1beac5a7bf" title="CQ with wiki page link">
<section ident="root_section">
<item ident="g23bd2508145295f459a42456716f8993" title="Question">
<presentation>
<material>
<mattext texttype="text/html">&lt;div&gt;&lt;p&gt;&lt;a title="My wiki page" href="/courses/#{@course.id}/pages/my-wiki-page" data-course-type="wikiPages" data-published="false"&gt;My wiki page&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;</mattext>
</material>
</presentation>
</item>
</section>
</assessment>
</questestinterop>
XML
end
it "replaces wiki page links" do
expected_xml = <<~XML
<?xml version="1.0" encoding="UTF-8"?>
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
<assessment ident="g2267932abda3f486f8304c1beac5a7bf" title="CQ with wiki page link">
<section ident="root_section">
<item ident="g23bd2508145295f459a42456716f8993" title="Question">
<presentation>
<material>
<mattext texttype="text/html">&lt;div&gt;&lt;p&gt;&lt;a title="My wiki page" href="$WIKI_REFERENCE$/pages/#{CC::CCHelper.create_key(@page, global: true)}" data-course-type="wikiPages" data-published="false"&gt;My wiki page&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;</mattext>
</material>
</presentation>
</item>
</section>
</assessment>
</questestinterop>
XML
expect(subject.replace_links(xml)).to eq(expected_xml)
end
context "when the xml contains internal links" do
before do
@assignment = @course.assignments.create!(name: "my quiz")
end
let(:xml) do
<<~XML
<?xml version="1.0" encoding="UTF-8"?>
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
<assessment ident="gb6738794b6587d8959a0de075def4957" title="CQ with internal links">
<section ident="root_section">
<item ident="gb2c34ff9aaf42001322d3ce85dd8a433" title="Question">
<presentation>
<material>
<mattext texttype="text/html">&lt;div&gt;&lt;p&gt;&lt;a title="my quiz" href="/courses/#{@course.id}/assignments/#{@assignment.id}" data-course-type="assignments" data-published="false"&gt;my quiz&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;</mattext>
</material>
</presentation>
</item>
</section>
</assessment>
</questestinterop>
XML
end
it "replaces internal links" do
expected_xml = <<~XML
<?xml version="1.0" encoding="UTF-8"?>
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
<assessment ident="gb6738794b6587d8959a0de075def4957" title="CQ with internal links">
<section ident="root_section">
<item ident="gb2c34ff9aaf42001322d3ce85dd8a433" title="Question">
<presentation>
<material>
<mattext texttype="text/html">&lt;div&gt;&lt;p&gt;&lt;a title="my quiz" href="$CANVAS_OBJECT_REFERENCE$/assignments/#{CC::CCHelper.create_key(@assignment, global: true)}" data-course-type="assignments" data-published="false"&gt;my quiz&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;</mattext>
</material>
</presentation>
</item>
</section>
</assessment>
</questestinterop>
XML
expect(subject.replace_links(xml)).to eq(expected_xml)
end
end
end
end
end