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:
John Corrigan 2015-09-09 10:49:24 -05:00
parent 6148c935a4
commit c825ffc544
16 changed files with 206 additions and 29 deletions

5
lib/cc/exporter/epub.rb Normal file
View File

@ -0,0 +1,5 @@
module CC::Exporter
module Epub
FILE_PATH = 'media'
end
end

View File

@ -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)).

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -14,4 +14,4 @@
<hr/>
<% if content_type_sorting %>
<a href="assignments.xhtml">Back to Assignment Index</a>
<% end %>
<% end %>

View File

@ -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>

View File

@ -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>

View File

@ -17,4 +17,4 @@
<hr/>
<% if content_type_sorting %>
<a href="topics.xhtml">Back to Discussion Topic Index</a>
<% end %>
<% end %>

View File

@ -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