render images, audio and video in epubs
fixes CNVS-21798 Summary of changes: - Add files to epub as it's being built. - Update links to image files to be path specific to epub. - Update links to audio files to be epub-compatible `<audio>` tags. - Update links to video files to be epub-compatible `<video>` tags. test plan (requires console access): - Prereq: have a course with uploaded images, audio and video files, and reference that media in assignment bodies. - Make sure you have at least one completed content export in the db (can be triggered via console or via the content export UI). - open rails console - run the following commands: - - `ContentExport.include(CC::Exporter::Epub::Exportable)` - - `ContentExport.last.convert_to_epub` - This will output a path to the generated epub, something like: "/var/folders/7g/w8y0n7_j18v65h93xhmm9z4hxzj3r2/T/2052fbd0-860d-4114-9969-0adc7f5ecb79.Name of Course.epub" - Open up the folder of the file (assuming you're on Mac OSX) like so: `open -a finder /var/folders/7g/w8y0n7_j18v65h93xhmm9z4hxzj3r2/T`. - Find the file in that directory (in this case the file name is 2052fbd0-860d-4114-9969-0adc7f5ecb79.Name of Course.epub - Observe that images displayed inline work. - Observe that links to images work. - Observe that links to audio have been converted to audio players, and that the controls work. - Observe that links to video have been converted to video players, and that the controls work. - Repeat steps for discussion topics, wiki pages & quiz descriptions. Change-Id: I5a48c3300ccffc00230d94137a93731394ef0ebb Reviewed-on: https://gerrit.instructure.com/62842 Tested-by: Jenkins Reviewed-by: Matt Berns <mberns@instructure.com> QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com> Product-Review: Cosme Salazar <cosme@instructure.com>
This commit is contained in:
parent
6148c935a4
commit
c825ffc544
|
@ -0,0 +1,5 @@
|
|||
module CC::Exporter
|
||||
module Epub
|
||||
FILE_PATH = 'media'
|
||||
end
|
||||
end
|
|
@ -1,12 +1,24 @@
|
|||
module CC::Exporter::Epub
|
||||
class Book
|
||||
include CC::Exporter::Epub::Converters
|
||||
|
||||
def initialize(content)
|
||||
@title = content.delete(:title)
|
||||
@files = content.delete(:files)
|
||||
@content = content
|
||||
end
|
||||
attr_reader :content, :title
|
||||
attr_reader :content, :files, :title
|
||||
|
||||
def add_files
|
||||
files.each do |file_data|
|
||||
File.open(file_data[:full_path]) do |file|
|
||||
epub.add_item(file_data[:local_path], file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build
|
||||
add_files
|
||||
content.each do |key, template|
|
||||
epub.add_ordered_item("#{key}.xhtml").
|
||||
add_content(StringIO.new(template.parse)).
|
||||
|
|
|
@ -12,7 +12,7 @@ module CC::Exporter::Epub::Converters
|
|||
html_path = File.join @unzipped_file_path, res.at_css('file[href$="html"]')['href']
|
||||
|
||||
meta_node = open_file_xml(meta_path)
|
||||
html_node = open_file(html_path)
|
||||
html_node = convert_media_from_node!(open_file(html_path))
|
||||
|
||||
next unless html_node
|
||||
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
module CC::Exporter::Epub::Converters
|
||||
class CartridgeConverter < Canvas::Migration::Migrator
|
||||
include CC::CCHelper
|
||||
include Canvas::Migration::XMLHelper
|
||||
include WikiEpubConverter
|
||||
include AssignmentEpubConverter
|
||||
include TopicEpubConverter
|
||||
include QuizEpubConverter
|
||||
include ModuleEpubConverter
|
||||
include FilesConverter
|
||||
include MediaConverter
|
||||
|
||||
MANIFEST_FILE = "imsmanifest.xml"
|
||||
|
||||
|
@ -17,10 +20,6 @@ module CC::Exporter::Epub::Converters
|
|||
@resource_nodes_for_flat_manifest = {}
|
||||
end
|
||||
|
||||
def export_directory
|
||||
File.dirname(@archive.file)
|
||||
end
|
||||
|
||||
# exports the package into the intermediary json
|
||||
def export
|
||||
unzip_archive
|
||||
|
@ -30,6 +29,8 @@ module CC::Exporter::Epub::Converters
|
|||
get_all_resources(@manifest)
|
||||
|
||||
@course[:title] = get_node_val(@manifest, "string")
|
||||
@course[:files] = convert_files
|
||||
|
||||
set_progress(10)
|
||||
@course[:wikis] = convert_wikis
|
||||
set_progress(20)
|
||||
|
@ -41,6 +42,7 @@ module CC::Exporter::Epub::Converters
|
|||
set_progress(50)
|
||||
@course[:modules] = convert_modules
|
||||
|
||||
# close up shop
|
||||
save_to_file
|
||||
set_progress(90)
|
||||
delete_unzipped_archive
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
module CC::Exporter::Epub::Converters
|
||||
module FilesConverter
|
||||
include CC::CCHelper
|
||||
include CC::Exporter
|
||||
|
||||
def convert_files
|
||||
files = []
|
||||
@manifest.css("resource[type=#{WEBCONTENT}][href^=#{WEB_RESOURCES_FOLDER}]").each do |res|
|
||||
full_path = File.expand_path(get_full_path(res['href']))
|
||||
local_path = res['href'].sub(WEB_RESOURCES_FOLDER, CC::Exporter::Epub::FILE_PATH)
|
||||
files << {
|
||||
migration_id: res['identifier'],
|
||||
local_path: local_path,
|
||||
file_name: File.basename(local_path),
|
||||
full_path: full_path
|
||||
}
|
||||
end
|
||||
files
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
module CC::Exporter::Epub::Converters
|
||||
module MediaConverter
|
||||
include CC::CCHelper
|
||||
|
||||
def convert_media_from_node!(html_node)
|
||||
html_node.tap do |node|
|
||||
convert_media_paths!(node)
|
||||
convert_audio_tags!(node)
|
||||
convert_video_tags!(node)
|
||||
end
|
||||
end
|
||||
|
||||
def convert_media_from_string!(html_string)
|
||||
html_node = Nokogiri::HTML::DocumentFragment.parse(html_string)
|
||||
convert_media_from_node!(html_node).to_s
|
||||
end
|
||||
|
||||
def convert_media_paths!(html_node)
|
||||
{ a: 'href', img: 'src' }.each do |tag, attr|
|
||||
html_node.search(tag).each do |match|
|
||||
match[attr] = CGI.unescape(match[attr]).gsub(WEB_CONTENT_TOKEN, CC::Exporter::Epub::FILE_PATH)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_audio_tags!(html_node)
|
||||
html_node.search('a.instructure_audio_link').each do |audio_link|
|
||||
audio_link.replace(<<-AUDIO_TAG)
|
||||
<audio src="#{audio_link['href']}" controls="controls" />
|
||||
AUDIO_TAG
|
||||
end
|
||||
end
|
||||
|
||||
def convert_video_tags!(html_node)
|
||||
html_node.search('a.instructure_video_link').each do |video_link|
|
||||
video_link.replace(<<-VIDEO_TAG)
|
||||
<video src="#{video_link['href']}" controls="controls" />
|
||||
VIDEO_TAG
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,7 +23,7 @@ module CC::Exporter::Epub::Converters
|
|||
quiz_meta_data = open_file_xml(quiz_meta_link)
|
||||
|
||||
quiz[:title] = get_node_val(quiz_meta_data, "title")
|
||||
quiz[:description] = get_node_val(quiz_meta_data, "description")
|
||||
quiz[:description] = convert_media_from_string!(get_node_val(quiz_meta_data, "description"))
|
||||
quiz[:due_at] = get_node_val(quiz_meta_data, "due_at")
|
||||
quiz[:lock_at] = get_node_val(quiz_meta_data, "lock_at")
|
||||
quiz[:unlock_at] = get_node_val(quiz_meta_data, "unlock_at")
|
||||
|
|
|
@ -9,7 +9,7 @@ module CC::Exporter::Epub::Converters
|
|||
cc_path = File.join @unzipped_file_path, res.at_css('file')['href']
|
||||
|
||||
canvas_id = get_node_att(res, 'dependency', 'identifierref')
|
||||
if canvas_id && meta_res = @manifest.at_css(%{resource[identifier="#{canvas_id}"]})
|
||||
if canvas_id && (meta_res = @manifest.at_css(%{resource[identifier="#{canvas_id}"]}))
|
||||
canvas_path = File.join @unzipped_file_path, meta_res.at_css('file')['href']
|
||||
meta_node = open_file_xml(canvas_path)
|
||||
else
|
||||
|
@ -25,7 +25,7 @@ module CC::Exporter::Epub::Converters
|
|||
|
||||
def convert_topic(cc_doc, meta_doc)
|
||||
topic = {"resource_type" => :topics}
|
||||
topic['description'] = get_node_val(cc_doc, 'text')
|
||||
topic['description'] = convert_media_from_string!(get_node_val(cc_doc, 'text'))
|
||||
topic['title'] = get_node_val(cc_doc, 'title')
|
||||
if meta_doc
|
||||
topic['title'] = get_node_val(meta_doc, 'title')
|
||||
|
@ -45,4 +45,4 @@ module CC::Exporter::Epub::Converters
|
|||
topic
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,10 +21,10 @@ module CC::Exporter::Epub::Converters
|
|||
title, body, meta = get_html_title_and_body_and_meta_fields(doc)
|
||||
wiki[:title] = title
|
||||
wiki[:front_page] = meta['front_page'] == 'true'
|
||||
wiki[:text] = body
|
||||
wiki[:text] = convert_media_from_string!(body)
|
||||
wiki[:identifier] = wiki_name
|
||||
wiki
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,7 +30,10 @@ module CC::Exporter::Epub
|
|||
end
|
||||
|
||||
def templates
|
||||
@_templates ||= {title: cartridge_json[:title]}.tap do |hash|
|
||||
@_templates ||= {
|
||||
title: cartridge_json[:title],
|
||||
files: cartridge_json[:files]
|
||||
}.tap do |hash|
|
||||
resources = sort_by_content ? [:assignments, :topics, :quizzes, :wikis] : [:modules]
|
||||
resources.each do |type|
|
||||
hash.merge!(type => create_template(type))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
module CC::Exporter::Epub
|
||||
class Template
|
||||
include TextHelper
|
||||
|
||||
def initialize(content, base_template)
|
||||
@content = content[:resources] || []
|
||||
@base_template = base_template
|
||||
|
@ -18,9 +19,7 @@ module CC::Exporter::Epub
|
|||
end
|
||||
|
||||
def parse
|
||||
Nokogiri::XML(build, &:noblanks).to_xml({
|
||||
save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
|
||||
}).strip
|
||||
Nokogiri::HTML(build, &:noblanks).to_xhtml.strip
|
||||
end
|
||||
|
||||
def module_item(item)
|
||||
|
@ -57,4 +56,4 @@ module CC::Exporter::Epub
|
|||
resources.find{|resource| resource[:identifier] == identifier}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,4 +14,4 @@
|
|||
<hr/>
|
||||
<% if content_type_sorting %>
|
||||
<a href="assignments.xhtml">Back to Assignment Index</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.1//EN' 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8"/>
|
||||
<title><%= title %></title>
|
||||
<!-- embedded styling will be inserted here -->
|
||||
</head>
|
||||
|
@ -19,4 +17,4 @@
|
|||
<div style="page-break-before:always;"></div>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.1//EN' 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8"/>
|
||||
<title>Course Modules</title>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -33,4 +31,4 @@
|
|||
<% end %>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
<hr/>
|
||||
<% if content_type_sorting %>
|
||||
<a href="topics.xhtml">Back to Discussion Topic Index</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../../cc_spec_helper')
|
||||
|
||||
describe "MediaConverter" do
|
||||
class MediaConverterTest
|
||||
include CC::Exporter::Epub::Converters::MediaConverter
|
||||
end
|
||||
|
||||
describe "#convert_media_paths!" do
|
||||
let(:doc) do
|
||||
Nokogiri::HTML::DocumentFragment.parse(<<-HTML)
|
||||
<div>
|
||||
<a href="#{CGI.escape(MediaConverterTest::WEB_CONTENT_TOKEN)}/path/to/img.jpg">
|
||||
Image Link
|
||||
</a>
|
||||
<img src="#{CGI.escape(MediaConverterTest::WEB_CONTENT_TOKEN)}/path/to/img.jpg" />
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
subject(:test_instance) { MediaConverterTest.new }
|
||||
|
||||
it "should update link hrefs containing WEB_CONTENT_TOKEN" do
|
||||
expect(doc.search('a').all? do |element|
|
||||
element['href'].match(CGI.escape(MediaConverterTest::WEB_CONTENT_TOKEN))
|
||||
end).to be_truthy, 'precondition'
|
||||
|
||||
test_instance.convert_media_paths!(doc)
|
||||
|
||||
expect(doc.search('a').all? do |element|
|
||||
element['href'].match(CC::Exporter::Epub::FILE_PATH)
|
||||
end).to be_truthy
|
||||
|
||||
expect(doc.search('a').all? do |element|
|
||||
element['href'].match(CGI.escape(MediaConverterTest::WEB_CONTENT_TOKEN))
|
||||
end).to be_falsey
|
||||
end
|
||||
|
||||
it "should update img srcs containing WEB_CONTENT_TOKEN" do
|
||||
expect(doc.search('img').all? do |element|
|
||||
element['src'].match(CGI.escape(MediaConverterTest::WEB_CONTENT_TOKEN))
|
||||
end).to be_truthy, 'precondition'
|
||||
|
||||
test_instance.convert_media_paths!(doc)
|
||||
|
||||
expect(doc.search('img').all? do |element|
|
||||
element['src'].match(CC::Exporter::Epub::FILE_PATH)
|
||||
end).to be_truthy
|
||||
|
||||
expect(doc.search('img').all? do |element|
|
||||
element['src'].match(CGI.escape(MediaConverterTest::WEB_CONTENT_TOKEN))
|
||||
end).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "#convert_audio_tags!" do
|
||||
let(:doc) do
|
||||
Nokogiri::HTML::DocumentFragment.parse(<<-HTML)
|
||||
<a href="#{CC::Exporter::Epub::FILE_PATH}/path/to/audio.mp3"
|
||||
class="instructure_audio_link">
|
||||
Audio Link
|
||||
</a>
|
||||
HTML
|
||||
end
|
||||
subject(:test_instance) { MediaConverterTest.new }
|
||||
|
||||
it "should change a tags to audio tags" do
|
||||
expect(doc.search('a').any?).to be_truthy, 'precondition'
|
||||
expect(doc.search('audio').empty?).to be_truthy, 'precondition'
|
||||
|
||||
test_instance.convert_audio_tags!(doc)
|
||||
|
||||
expect(doc.search('a').empty?).to be_truthy
|
||||
expect(doc.search('audio').any?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#convert_video_tags!" do
|
||||
let(:doc) do
|
||||
Nokogiri::HTML::DocumentFragment.parse(<<-HTML)
|
||||
<a href="#{CC::Exporter::Epub::FILE_PATH}/path/to/audio.mp3"
|
||||
class="instructure_video_link">
|
||||
Video Link
|
||||
</a>
|
||||
HTML
|
||||
end
|
||||
subject(:test_instance) { MediaConverterTest.new }
|
||||
|
||||
it "should change a tags to audio tags" do
|
||||
expect(doc.search('a').any?).to be_truthy, 'precondition'
|
||||
expect(doc.search('video').empty?).to be_truthy, 'precondition'
|
||||
|
||||
test_instance.convert_video_tags!(doc)
|
||||
|
||||
expect(doc.search('a').empty?).to be_truthy
|
||||
expect(doc.search('video').any?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue