imported link refactoring

test plan:
* regression test links within copied content

* should also fix a pre-existing bug where
 links to content within assessment/quiz questions
 were not being copied

closes #CNVS-20890 #CNVS-20740

Change-Id: I1d1b89faba468690eaddaa1e29cd2936c807cc60
Reviewed-on: https://gerrit.instructure.com/55830
Tested-by: Jenkins
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
QA-Review: Michael Hargiss <mhargiss@instructure.com>
Product-Review: James Williams  <jamesw@instructure.com>
This commit is contained in:
James Williams 2015-06-04 07:51:57 -06:00
parent 3c44f4f6e7
commit 91646b65eb
47 changed files with 1012 additions and 656 deletions

View File

@ -229,7 +229,7 @@ class ApplicationController < ActionController::Base
# time or resources that are not well represented by simple time/cpu
# benchmarks, so you can use this method to increase the perceived cost
# of a request by an arbitrary amount. For an anchor, rate limiting
# kicks in when a user has exceeded 600 arbitrary units of cost (it's
# kicks in when a user has exceeded 600 arbitrary units of cost (it's
# a leaky bucket, go see Canvas::RequestThrottle), so using an 'amount'
# param of 600, for example, would max out the bucket immediately
def increment_request_cost(amount)

View File

@ -79,14 +79,55 @@ class AssessmentQuestion < ActiveRecord::Base
end
end
def translate_link_regex
@regex ||= Regexp.new(%{/#{context_type.downcase.pluralize}/#{context_id}/(?:files/(\\d+)/(?:download|preview)|file_contents/(course%20files/[^'"?]*))(?:\\?([^'"]*))?})
end
def file_substitutions
@file_substitutions ||= {}
end
def translate_file_link(link, match_data=nil)
match_data ||= link.match(translate_link_regex)
return link unless match_data
id = match_data[1]
path = match_data[2]
id_or_path = id || path
if !file_substitutions[id_or_path]
if id
file = Attachment.where(context_type: context_type, context_id: context_id, id: id_or_path).first
elsif path
path = URI.unescape(id_or_path)
file = Folder.find_attachment_in_context_with_path(assessment_question_bank.context, path)
end
begin
new_file = file.clone_for(self)
rescue => e
new_file = nil
er_id = Canvas::Errors.capture_exception(:file_clone_during_translate_links, e)[:error_report]
logger.error("Error while cloning attachment during"\
" AssessmentQuestion#translate_links: "\
"id: #{self.id} error_report: #{er_id}")
end
new_file.save if new_file
file_substitutions[id_or_path] = new_file
end
if sub = file_substitutions[id_or_path]
query_rest = match_data[3] ? "&#{match_data[3]}" : ''
"/assessment_questions/#{self.id}/files/#{sub.id}/download?verifier=#{sub.uuid}#{query_rest}"
else
link
end
end
def translate_links
# we can't translate links unless this question has a context (through a bank)
return unless assessment_question_bank && assessment_question_bank.context
# This either matches the id from a url like: /courses/15395/files/11454/download
# or gets the relative path at the end of one like: /courses/15395/file_contents/course%20files/unfiled/test.jpg
regex = Regexp.new(%{/#{context_type.downcase.pluralize}/#{context_id}/(?:files/(\\d+)/(?:download|preview)|file_contents/(course%20files/[^'"?]*))(?:\\?([^'"]*))?})
file_substitutions = {}
deep_translate = lambda do |obj|
if obj.is_a?(Hash)
@ -94,33 +135,8 @@ class AssessmentQuestion < ActiveRecord::Base
elsif obj.is_a?(Array)
obj.map {|v| deep_translate.call(v) }
elsif obj.is_a?(String)
obj.gsub(regex) do |match|
id_or_path = $1 || $2
if !file_substitutions[id_or_path]
if $1
file = Attachment.where(context_type: context_type, context_id: context_id, id: id_or_path).first
elsif $2
path = URI.unescape(id_or_path)
file = Folder.find_attachment_in_context_with_path(assessment_question_bank.context, path)
end
begin
new_file = file.clone_for(self)
rescue => e
new_file = nil
er_id = Canvas::Errors.capture_exception(:file_clone_during_translate_links, e)[:error_report]
logger.error("Error while cloning attachment during"\
" AssessmentQuestion#translate_links: "\
"id: #{self.id} error_report: #{er_id}")
end
new_file.save if new_file
file_substitutions[id_or_path] = new_file
end
if sub = file_substitutions[id_or_path]
query_rest = $3 ? "&#{$3}" : ''
"/assessment_questions/#{self.id}/files/#{sub.id}/download?verifier=#{sub.uuid}#{query_rest}"
else
match
end
obj.gsub(translate_link_regex) do |match|
translate_file_link(match, $~)
end
else
obj

View File

@ -36,7 +36,7 @@ class ContentMigration < ActiveRecord::Base
DATE_FORMAT = "%m/%d/%Y"
attr_accessible :context, :migration_settings, :user, :source_course, :copy_options, :migration_type, :initiated_source
attr_accessor :imported_migration_items, :outcome_to_id_map
attr_accessor :imported_migration_items, :outcome_to_id_map, :attachment_path_id_lookup, :attachment_path_id_lookup_lower
workflow do
state :created
@ -588,27 +588,27 @@ class ContentMigration < ActiveRecord::Base
ContentMigration.where(:id => self).update_all(:progress=>val)
end
def add_missing_content_links(item)
@missing_content_links ||= {}
item[:field] ||= :text
key = "#{item[:class]}_#{item[:id]}_#{item[:field]}"
if item[:missing_links].present?
@missing_content_links[key] = item
else
@missing_content_links.delete(key)
end
def html_converter
@html_converter ||= ImportedHtmlConverter.new(self)
end
def add_warnings_for_missing_content_links
return unless @missing_content_links
@missing_content_links.each_value do |item|
if item[:missing_links].any?
add_warning(t(:missing_content_links_title, "Missing links found in imported content") + " - #{item[:class]} #{item[:field]}",
{:error_message => "#{item[:class]} #{item[:field]} - " + t(:missing_content_links_message,
"The following references could not be resolved:") + " " + item[:missing_links].join(', '),
:fix_issue_html_url => item[:url]})
end
end
def convert_html(*args)
html_converter.convert(*args)
end
def convert_text(*args)
html_converter.convert_text(*args)
end
def resolve_content_links!
html_converter.resolve_content_links!
end
def add_warning_for_missing_content_links(type, field, missing_links, fix_issue_url)
add_warning(t(:missing_content_links_title, "Missing links found in imported content") + " - #{type} #{field}",
{:error_message => "#{type} #{field} - " + t(:missing_content_links_message,
"The following references could not be resolved:") + " " + missing_links.join(', '),
:fix_issue_html_url => fix_issue_url})
end
UPLOAD_TIMEOUT = 1.hour
@ -700,17 +700,31 @@ class ContentMigration < ActiveRecord::Base
def imported_migration_items
@imported_migration_items_hash ||= {}
@imported_migration_items_hash.values.flatten
@imported_migration_items_hash.values.map(&:values).flatten
end
def imported_migration_items_hash(klass)
@imported_migration_items_hash ||= {}
@imported_migration_items_hash[klass.name] ||= {}
end
def imported_migration_items_by_class(klass)
@imported_migration_items_hash ||= {}
@imported_migration_items_hash[klass.name] ||= []
imported_migration_items_hash(klass).values
end
def find_imported_migration_item(klass, migration_id)
imported_migration_items_hash(klass)[migration_id]
end
def add_imported_item(item)
arr = imported_migration_items_by_class(item.class)
arr << item unless arr.include?(item)
imported_migration_items_hash(item.class)[item.migration_id] = item
end
def add_attachment_path(path, migration_id)
self.attachment_path_id_lookup ||= {}
self.attachment_path_id_lookup_lower ||= {}
self.attachment_path_id_lookup[path] = migration_id
self.attachment_path_id_lookup_lower[path.downcase] = migration_id
end
def add_external_tool_translation(migration_id, target_tool, custom_fields)

View File

@ -184,6 +184,7 @@ class Course < ActiveRecord::Base
has_many :context_external_tools, :as => :context, :dependent => :destroy, :order => 'name'
belongs_to :wiki
has_many :quizzes, :class_name => 'Quizzes::Quiz', :as => :context, :dependent => :destroy, :order => 'lock_at, title'
has_many :quiz_questions, :class_name => 'Quizzes::QuizQuestion', :through => :quizzes
has_many :active_quizzes, :class_name => 'Quizzes::Quiz', :as => :context, :include => :assignment, :conditions => ['quizzes.workflow_state != ?', 'deleted'], :order => 'created_at'
has_many :assessment_questions, :through => :assessment_question_banks
has_many :assessment_question_banks, :as => :context, :include => [:assessment_questions, :assessment_question_bank_users]
@ -1967,8 +1968,7 @@ class Course < ActiveRecord::Base
end
attr_accessor :full_migration_hash, :external_url_hash,
:folder_name_lookups, :attachment_path_id_lookup, :attachment_path_id_lookup_lower,
:assignment_group_no_drop_assignments, :migration_results
:folder_name_lookups, :assignment_group_no_drop_assignments, :migration_results
def backup_to_json
@ -2009,7 +2009,6 @@ class Course < ActiveRecord::Base
end
def copy_attachments_from_course(course, options={})
self.attachment_path_id_lookup = {}
root_folder = Folder.root_folders(self).first
root_folder_name = root_folder.name + '/'
ce = options[:content_export]
@ -2025,7 +2024,7 @@ class Course < ActiveRecord::Base
if !ce || ce.export_object?(file)
begin
new_file = file.clone_for(self, nil, :overwrite => true)
self.attachment_path_id_lookup[file.full_display_path.gsub(/\A#{root_folder_name}/, '')] = new_file.migration_id
cm.add_attachment_path(file.full_display_path.gsub(/\A#{root_folder_name}/, ''), new_file.migration_id)
new_folder_id = merge_mapped_id(file.folder)
if file.folder && file.folder.parent_folder_id.nil?

View File

@ -30,4 +30,4 @@ module Importers
end
require_dependency 'importers/account_content_importer'
require_dependency 'importers/course_content_importer'
require_dependency 'importers/course_content_importer'

View File

@ -10,6 +10,8 @@ module Importers
Importers::AssessmentQuestionImporter.process_migration(data, migration)
Importers::LearningOutcomeImporter.process_migration(data, migration)
migration.resolve_content_links!
migration.progress = 100
migration.workflow_state = :imported
migration.save

View File

@ -67,16 +67,6 @@ module Importers
begin
question = self.import_from_migration(question, migration.context, migration, question_bank)
# If the question appears to have links, we need to translate them so that file links point
# to the AssessmentQuestion. Ideally we would just do this before saving the question, but
# the link needs to include the id of the AQ, which we don't have until it's saved. This will
# be a problem as long as we use the question as a context for its attachments. (We're turning this
# hash into a string so we can quickly check if anywhere in the hash might have a URL.)
if question.to_s =~ %r{/files/\d+/(download|preview)}
AssessmentQuestion.find(question[:assessment_question_id]).translate_links
end
question_data[:aq_data][question['migration_id']] = question
rescue
migration.add_import_warning(t('#migration.quiz_question_type', "Quiz Question"), question[:question_name], $!)
@ -90,9 +80,8 @@ module Importers
hash = hash.with_indifferent_access
hash.delete(:question_bank_migration_id) if hash.has_key?(:question_bank_migration_id)
self.prep_for_import(hash, context, migration)
self.prep_for_import(hash, migration, :assessment_question)
missing_links = hash.delete(:missing_links) || {}
import_warnings = hash.delete(:import_warnings) || []
if error = hash.delete(:import_error)
import_warnings << error
@ -115,54 +104,49 @@ module Importers
hash['assessment_question_id'] = id
end
if migration
missing_links.each do |field, links|
migration.add_missing_content_links(:class => self.to_s,
:id => hash['assessment_question_id'], :field => field, :missing_links => links,
:url => "/#{context.class.to_s.underscore.pluralize}/#{context.id}/question_banks/#{bank.id}#question_#{hash['assessment_question_id']}_question_text")
end
if import_warnings
import_warnings.each do |warning|
migration.add_warning(warning, {
:fix_issue_html_url => "/#{context.class.to_s.underscore.pluralize}/#{context.id}/question_banks/#{bank.id}#question_#{hash['assessment_question_id']}_question_text"
})
end
if import_warnings
import_warnings.each do |warning|
migration.add_warning(warning, {
:fix_issue_html_url => "/#{context.class.to_s.underscore.pluralize}/#{context.id}/question_banks/#{bank.id}#question_#{hash['assessment_question_id']}_question_text"
})
end
end
hash
end
def self.prep_for_import(hash, context, migration=nil)
def self.prep_for_import(hash, migration, item_type)
return hash if hash[:prepped_for_import]
hash[:missing_links] = {}
[:question_text, :correct_comments_html, :incorrect_comments_html, :neutral_comments_html, :more_comments_html].each do |field|
hash[:missing_links][field] = []
if hash[field].present?
hash[field] = ImportedHtmlConverter.convert(hash[field], context, migration, {:remove_outer_nodes_if_one_child => true}) do |warn, link|
hash[:missing_links][field] << link if warn == :missing_link
end
hash[field] = migration.convert_html(
hash[field], item_type, hash[:migration_id], field, {:remove_outer_nodes_if_one_child => true}
)
end
end
[:correct_comments, :incorrect_comments, :neutral_comments, :more_comments].each do |field|
html_field = "#{field}_html".to_sym
if hash[field].present? && hash[field] == hash[html_field]
hash.delete(html_field)
end
end
hash[:answers].each_with_index do |answer, i|
[:html, :comments_html, :left_html].each do |field|
key = "answer #{i} #{field}"
hash[:missing_links][key] = []
if answer[field].present?
answer[field] = ImportedHtmlConverter.convert(answer[field], context, migration, {:remove_outer_nodes_if_one_child => true}) do |warn, link|
hash[:missing_links][key] << link if warn == :missing_link
end
answer[field] = migration.convert_html(
answer[field], item_type, hash[:migration_id], key, {:remove_outer_nodes_if_one_child => true}
)
end
end
if answer[:comments].present? && answer[:comments] == answer[:comments_html]
answer.delete(:comments_html)
end
end if hash[:answers]
hash[:prepped_for_import] = true
hash
end

View File

@ -33,14 +33,14 @@ module Importers
end
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:assignment_groups_to_import] && !hash[:assignment_groups_to_import][hash[:migration_id]]
item ||= AssignmentGroup.where(context_id: context, context_type: context.class.to_s, id: hash[:id]).first
item ||= AssignmentGroup.where(context_id: context, context_type: context.class.to_s, migration_id: hash[:migration_id]).first if hash[:migration_id]
item ||= context.assignment_groups.where(name: hash[:title], migration_id: nil).first
item ||= context.assignment_groups.new
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item.migration_id = hash[:migration_id]
item.workflow_state = 'available' if item.deleted?
item.name = hash[:title]

View File

@ -26,7 +26,7 @@ module Importers
end
end
def self.import_from_migration(hash, context, migration=nil, item=nil, quiz=nil)
def self.import_from_migration(hash, context, migration, item=nil, quiz=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:assignments_to_import] && !hash[:assignments_to_import][hash[:migration_id]]
item ||= Assignment.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first
@ -46,20 +46,14 @@ module Importers
self.extend TextHelper
end
missing_links = {:description => [], :instructions => []}
description = ""
if hash[:instructions_in_html] == false
description += ImportedHtmlConverter.convert_text(hash[:description] || "", context)
description += ImportedHtmlConverter.convert_text(hash[:instructions] || "", context)
description += migration.convert_text(hash[:description] || "")
description += migration.convert_text(hash[:instructions] || "")
else
description += ImportedHtmlConverter.convert(hash[:description] || "", context, migration) do |warn, link|
missing_links[:description] << link if warn == :missing_link
end
description += ImportedHtmlConverter.convert(hash[:instructions] || "", context, migration) do |warn, link|
missing_links[:instructions] << link if warn == :missing_link
end
description += migration.convert_html(hash[:description] || "", :assignment, hash[:migration_id], :description)
description += migration.convert_html(hash[:instructions] || "", :assignment, hash[:migration_id], :description)
end
description += Attachment.attachment_list_from_migration(context, hash[:attachment_ids])
item.description = description
@ -107,11 +101,12 @@ module Importers
item.submission_types = 'not_graded'
end
end
if hash[:assignment_group_migration_id]
item.assignment_group = context.assignment_groups.where(migration_id: hash[:assignment_group_migration_id]).first
end
item.assignment_group ||= context.assignment_groups.where(name: t(:imported_assignments_group, "Imported Assignments")).first_or_create
# Associating with a rubric or a quiz might cause item to get saved, no longer indicating
# that it is a new record. We need to know that below, where we add to the list of
# imported items
new_record = item.new_record?
item.save_without_broadcasting!
rubric = nil
rubric = context.rubrics.where(migration_id: hash[:rubric_migration_id]).first if hash[:rubric_migration_id]
@ -156,10 +151,6 @@ module Importers
item.submission_types = 'online_quiz'
item.saved_by = :quiz
end
if hash[:assignment_group_migration_id]
item.assignment_group = context.assignment_groups.where(migration_id: hash[:assignment_group_migration_id]).first
end
item.assignment_group ||= context.assignment_groups.where(name: t(:imported_assignments_group, "Imported Assignments")).first_or_create
hash[:due_at] ||= hash[:due_date]
[:due_at, :lock_at, :unlock_at, :peer_reviews_due_at].each do |key|
@ -178,9 +169,9 @@ module Importers
item.send("#{prop}=", hash[prop]) unless hash[prop].nil?
end
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
if migration && migration.date_shift_options
if migration.date_shift_options
# Unfortunately, we save the assignment here, and then shift dates and
# save the assignment again later in the course migration. Saving here
# would normally schedule the auto peer reviews job with the
@ -193,14 +184,6 @@ module Importers
item.save_without_broadcasting!
item.skip_schedule_peer_reviews = nil
if migration
missing_links.each do |field, missing_links|
migration.add_missing_content_links(:class => item.class.to_s,
:id => item.id, :field => field, :missing_links => missing_links,
:url => "/#{context.class.to_s.underscore.pluralize}/#{context.id}/#{item.class.to_s.demodulize.underscore.pluralize}/#{item.id}")
end
end
if item.submission_types == 'external_tool'
tag = item.create_external_tool_tag(:url => hash[:external_tool_url], :new_tab => hash[:external_tool_new_tab])
if hash[:external_tool_id] && migration && !migration.cross_institution?
@ -212,7 +195,11 @@ module Importers
end
tag.content_type = 'ContextExternalTool'
if !tag.save
migration.add_warning(t('errors.import.external_tool_url', "The url for the external tool assignment \"%{assignment_name}\" wasn't valid.", :assignment_name => item.title)) if migration && tag.errors["url"]
if tag.errors["url"]
migration.add_warning(t('errors.import.external_tool_url',
"The url for the external tool assignment \"%{assignment_name}\" wasn't valid.",
:assignment_name => item.title))
end
item.association(:external_tool_tag).target = nil # otherwise it will trigger destroy on the tag
end
end

View File

@ -57,7 +57,7 @@ module Importers
private
def self.import_from_migration(hash, context, migration=nil, item=nil, created_usage_rights_map={})
def self.import_from_migration(hash, context, migration, item=nil, created_usage_rights_map={})
return nil if hash[:files_to_import] && !hash[:files_to_import][hash[:migration_id]]
item ||= Attachment.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first
item ||= Attachment.where(context_type: context.class.to_s, context_id: context, migration_id: hash[:migration_id]).first # if hash[:migration_id]
@ -71,7 +71,7 @@ module Importers
item.usage_rights_id = find_or_create_usage_rights(context, hash[:usage_rights], created_usage_rights_map) if hash[:usage_rights]
item.set_publish_state_for_usage_rights unless hash[:locked]
item.save_without_broadcasting!
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
end
item
end

View File

@ -34,7 +34,7 @@ module Importers
end
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:events_to_import] && !hash[:events_to_import][hash[:migration_id]]
item ||= CalendarEvent.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first
@ -44,10 +44,8 @@ module Importers
item.migration_id = hash[:migration_id]
item.workflow_state = 'active' if item.deleted?
item.title = hash[:title] || hash[:name]
missing_links = []
item.description = ImportedHtmlConverter.convert(hash[:description] || "", context, migration) do |warn, link|
missing_links << link if warn == :missing_link
end
item.description = migration.convert_html(hash[:description] || "", :calendar_event, hash[:migration_id], :description)
item.description += import_migration_attachment_suffix(hash, context)
item.start_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:start_at] || hash[:start_date])
item.end_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:end_at] || hash[:end_date])
@ -55,12 +53,7 @@ module Importers
item.imported = true
item.save_without_broadcasting!
if migration
migration.add_missing_content_links(:class => item.class.to_s,
:id => item.id, :missing_links => missing_links,
:url => "/#{context.class.to_s.demodulize.underscore.pluralize}/#{context.id}/#{item.class.to_s.demodulize.underscore.pluralize}/#{item.id}")
end
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
if hash[:all_day]
item.all_day = hash[:all_day]
item.save

View File

@ -21,7 +21,7 @@ module Importers
end
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:external_tools_to_import] && !hash[:external_tools_to_import][hash[:migration_id]]

View File

@ -50,13 +50,13 @@ module Importers
migration.context.touch
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:modules_to_import] && !hash[:modules_to_import][hash[:migration_id]]
item ||= ContextModule.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first
item ||= ContextModule.where(context_type: context.class.to_s, context_id: context, migration_id: hash[:migration_id]).first if hash[:migration_id]
item ||= ContextModule.new(:context => context)
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item.name = hash[:title] || hash[:description]
item.migration_id = hash[:migration_id]
if hash[:workflow_state] == 'unpublished'
@ -94,7 +94,7 @@ module Importers
begin
self.add_module_item_from_migration(item, tag_hash, 0, context, item_map, migration)
rescue
migration.add_import_warning(t(:migration_module_item_type, "Module Item"), tag_hash[:title], $!) if migration
migration.add_import_warning(t(:migration_module_item_type, "Module Item"), tag_hash[:title], $!)
end
end
@ -117,7 +117,7 @@ module Importers
end
def self.add_module_item_from_migration(context_module, hash, level, context, item_map, migration=nil)
def self.add_module_item_from_migration(context_module, hash, level, context, item_map, migration)
hash = hash.with_indifferent_access
hash[:migration_id] ||= hash[:item_migration_id]
hash[:migration_id] ||= Digest::MD5.hexdigest(hash[:title]) if hash[:title]
@ -129,7 +129,7 @@ module Importers
else
existing_item.workflow_state = 'active'
end
migration.add_imported_item(existing_item) if migration
migration.add_imported_item(existing_item)
existing_item.migration_id = hash[:migration_id]
hash[:indent] = [hash[:indent] || 0, level].max
resource_class = linked_resource_type_class(hash[:linked_resource_type])
@ -137,7 +137,7 @@ module Importers
wiki = context_module.context.wiki.wiki_pages.where(migration_id: hash[:linked_resource_id]).first if hash[:linked_resource_id]
if wiki
item = context_module.add_item({
:title => hash[:title] || hash[:linked_resource_title],
:title => wiki.title.presence || hash[:title] || hash[:linked_resource_title],
:type => 'wiki_page',
:id => wiki.id,
:indent => hash[:indent].to_i
@ -158,7 +158,7 @@ module Importers
ass = context_module.context.assignments.where(migration_id: hash[:linked_resource_id]).first if hash[:linked_resource_id]
if ass
item = context_module.add_item({
:title => hash[:title] || hash[:linked_resource_title],
:title => ass.title.presence || hash[:title] || hash[:linked_resource_title],
:type => 'assignment',
:id => ass.id,
:indent => hash[:indent].to_i
@ -175,7 +175,7 @@ module Importers
# external url
if url = hash[:url]
if (CanvasHttp.validate_url(hash[:url]) rescue nil)
url = migration.process_domain_substitutions(url) if migration
url = migration.process_domain_substitutions(url)
item = context_module.add_item({
:title => hash[:title] || hash[:linked_resource_title] || hash['description'],
@ -184,7 +184,7 @@ module Importers
:url => url
}, existing_item, :position => context_module.migration_position)
else
migration.add_import_warning(t(:migration_module_item_type, "Module Item"), hash[:title], "#{hash[:url]} is not a valid URL") if migration
migration.add_import_warning(t(:migration_module_item_type, "Module Item"), hash[:title], "#{hash[:url]} is not a valid URL")
end
end
elsif resource_class == ContextExternalTool
@ -194,7 +194,7 @@ module Importers
if hash[:linked_resource_global_id] && (!migration || !migration.cross_institution?)
external_tool_id = hash[:linked_resource_global_id]
elsif migration && arr = migration.find_external_tool_translation(hash[:linked_resource_id])
elsif arr = migration.find_external_tool_translation(hash[:linked_resource_id])
external_tool_id = arr[0]
custom_fields = arr[1]
if custom_fields.present?
@ -206,14 +206,14 @@ module Importers
if external_tool_url
title = hash[:title] || hash[:linked_resource_title] || hash['description']
if migration
external_tool_url = migration.process_domain_substitutions(external_tool_url)
if external_tool_id.nil?
migration.add_warning(t(:foreign_lti_tool,
%q{The account External Tool for module item "%{title}" must be configured before the item can be launched},
:title => title))
end
external_tool_url = migration.process_domain_substitutions(external_tool_url)
if external_tool_id.nil?
migration.add_warning(t(:foreign_lti_tool,
%q{The account External Tool for module item "%{title}" must be configured before the item can be launched},
:title => title))
end
item = context_module.add_item({
:title => title,
:type => 'context_external_tool',
@ -226,7 +226,7 @@ module Importers
quiz = context_module.context.quizzes.where(migration_id: hash[:linked_resource_id]).first if hash[:linked_resource_id]
if quiz
item = context_module.add_item({
:title => hash[:title] || hash[:linked_resource_title],
:title => quiz.title.presence || hash[:title] || hash[:linked_resource_title],
:type => 'quiz',
:indent => hash[:indent].to_i,
:id => quiz.id
@ -236,7 +236,7 @@ module Importers
topic = context_module.context.discussion_topics.where(migration_id: hash[:linked_resource_id]).first if hash[:linked_resource_id]
if topic
item = context_module.add_item({
:title => hash[:title] || hash[:linked_resource_title],
:title => topic.title.presence || hash[:title] || hash[:linked_resource_title],
:type => 'discussion_topic',
:indent => hash[:indent].to_i,
:id => topic.id

View File

@ -9,14 +9,14 @@ module Importers
data['all_files_export']['file_path'] ||= data['all_files_zip']
return unless data['all_files_export']['file_path'] && File.exist?(data['all_files_export']['file_path'])
course.attachment_path_id_lookup ||= {}
course.attachment_path_id_lookup_lower ||= {}
migration.attachment_path_id_lookup ||= {}
migration.attachment_path_id_lookup_lower ||= {}
params = migration.migration_settings[:migration_ids_to_import]
valid_paths = []
(data['file_map'] || {}).each do |id, file|
path = file['path_name'].starts_with?('/') ? file['path_name'][1..-1] : file['path_name']
course.attachment_path_id_lookup[path] = file['migration_id']
course.attachment_path_id_lookup_lower[path.downcase] = file['migration_id']
migration.add_attachment_path(path, file['migration_id'])
if migration.import_object?("attachments", file['migration_id']) || migration.import_object?("files", file['migration_id'])
if file['errored']
migration.add_warning(t(:file_import_warning, "File %{file} could not be found", :file => File.basename(file['path_name'])))
@ -45,7 +45,7 @@ module Importers
:callback => callback,
:logger => logger,
:rename_files => migration.migration_settings[:files_import_allow_rename],
:migration_id_map => course.attachment_path_id_lookup,
:migration_id_map => migration.attachment_path_id_lookup,
}
if root_path = migration.migration_settings[:files_import_root_path]
unzip_opts[:root_directory] = Folder.assert_path(
@ -104,38 +104,29 @@ module Importers
end
end
migration.update_import_progress(31)
question_data = Importers::AssessmentQuestionImporter.process_migration(data, migration); migration.update_import_progress(35)
Importers::GroupImporter.process_migration(data, migration); migration.update_import_progress(36)
Importers::LearningOutcomeImporter.process_migration(data, migration); migration.update_import_progress(37)
Importers::RubricImporter.process_migration(data, migration); migration.update_import_progress(38)
migration.update_import_progress(35)
question_data = Importers::AssessmentQuestionImporter.process_migration(data, migration); migration.update_import_progress(45)
Importers::GroupImporter.process_migration(data, migration); migration.update_import_progress(48)
Importers::LearningOutcomeImporter.process_migration(data, migration); migration.update_import_progress(50)
Importers::RubricImporter.process_migration(data, migration); migration.update_import_progress(52)
course.assignment_group_no_drop_assignments = {}
Importers::AssignmentGroupImporter.process_migration(data, migration); migration.update_import_progress(39)
Importers::ExternalFeedImporter.process_migration(data, migration); migration.update_import_progress(39.5)
Importers::GradingStandardImporter.process_migration(data, migration); migration.update_import_progress(40)
Importers::ContextExternalToolImporter.process_migration(data, migration); migration.update_import_progress(45)
#These need to be ran twice because they can reference each other
Importers::QuizImporter.process_migration(data, migration, question_data); migration.update_import_progress(50)
Importers::DiscussionTopicImporter.process_migration(data, migration);migration.update_import_progress(55)
Importers::WikiPageImporter.process_migration(data, migration);migration.update_import_progress(60)
Importers::AssignmentImporter.process_migration(data, migration);migration.update_import_progress(65)
# and second time...
Importers::ContextModuleImporter.process_migration(data, migration);migration.update_import_progress(70)
Importers::QuizImporter.process_migration(data, migration, question_data); migration.update_import_progress(72)
Importers::DiscussionTopicImporter.process_migration(data, migration);migration.update_import_progress(75)
Importers::WikiPageImporter.process_migration(data, migration);migration.update_import_progress(80)
Importers::AssignmentImporter.process_migration(data, migration);migration.update_import_progress(85)
#These aren't referenced by anything, but reference other things
Importers::CalendarEventImporter.process_migration(data, migration);migration.update_import_progress(90)
Importers::WikiPageImporter.process_migration_course_outline(data, migration);migration.update_import_progress(95)
Importers::AssignmentGroupImporter.process_migration(data, migration); migration.update_import_progress(54)
Importers::ExternalFeedImporter.process_migration(data, migration); migration.update_import_progress(56)
Importers::GradingStandardImporter.process_migration(data, migration); migration.update_import_progress(58)
Importers::ContextExternalToolImporter.process_migration(data, migration); migration.update_import_progress(60)
Importers::QuizImporter.process_migration(data, migration, question_data); migration.update_import_progress(65)
Importers::DiscussionTopicImporter.process_migration(data, migration); migration.update_import_progress(70)
Importers::WikiPageImporter.process_migration(data, migration); migration.update_import_progress(75)
Importers::AssignmentImporter.process_migration(data, migration); migration.update_import_progress(80)
Importers::ContextModuleImporter.process_migration(data, migration); migration.update_import_progress(85)
Importers::WikiPageImporter.process_migration_course_outline(data, migration)
Importers::CalendarEventImporter.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])
self.import_settings_from_migration(course, data, migration); migration.update_import_progress(96)
self.import_settings_from_migration(course, data, migration)
end
migration.update_import_progress(90)
# be very explicit about draft state courses, but be liberal toward legacy courses
if course.wiki.has_no_front_page
@ -155,7 +146,8 @@ module Importers
self.import_syllabus_from_migration(course, syllabus_body, migration) if syllabus_body
end
migration.add_warnings_for_missing_content_links
migration.resolve_content_links!
migration.update_import_progress(95)
begin
#Adjust dates
@ -226,13 +218,7 @@ module Importers
end
def self.import_syllabus_from_migration(course, syllabus_body, migration)
missing_links = []
course.syllabus_body = ImportedHtmlConverter.convert(syllabus_body, course, migration) do |warn, link|
missing_links << link if warn == :missing_link
end
migration.add_missing_content_links(:class => course.class.to_s,
:id => course.id, :field => "syllabus", :missing_links => missing_links,
:url => "/#{course.class.to_s.underscore.pluralize}/#{course.id}/assignments/syllabus")
course.syllabus_body = migration.convert_html(syllabus_body, :syllabus, nil, :syllabus)
end
def self.import_settings_from_migration(course, data, migration)

View File

@ -46,7 +46,7 @@ module Importers
migration.import_object?('announcements', topic['migration_id']))
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
importer = self.new(hash, context, migration, item)
importer.run
end
@ -79,11 +79,10 @@ module Importers
:require_initial_post].each do |attr|
item.send("#{attr}=", options[attr])
end
missing_links = []
type = item.is_a?(Announcement) ? :announcement : :discussion_topic
if options.message
item.message = ImportedHtmlConverter.convert(options.message, context, migration) do |warn, link|
missing_links << link if warn == :missing_link
end
item.message = migration.convert_html(options.message, type, options[:migration_id], :message)
else
item.message = I18n.t('#discussion_topic.empty_message', 'No message')
end
@ -115,7 +114,6 @@ module Importers
item.save_without_broadcasting!
import_migration_item
add_missing_content_links(missing_links)
item.saved_by = nil
item
end
@ -136,15 +134,7 @@ module Importers
end
def import_migration_item
migration.add_imported_item(item) if migration
end
def add_missing_content_links(missing_links)
if migration
migration.add_missing_content_links(class: item.class.to_s,
id: item.id, missing_links: missing_links,
url: "/#{context.class.to_s.underscore.pluralize}/#{context.id}/#{item.class.to_s.demodulize.underscore.pluralize}/#{item.id}")
end
migration.add_imported_item(item)
end
class DiscussionTopicOptions

View File

@ -17,7 +17,7 @@ module Importers
end
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:external_feeds_to_import] && !hash[:external_feeds_to_import][hash[:migration_id]]
item ||= find_or_initialize_from_migration(hash, context)
@ -28,7 +28,7 @@ module Importers
item.header_match = hash[:header_match] unless hash[:header_match].blank?
item.save!
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item
end

View File

@ -23,7 +23,7 @@ module Importers
end
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:grading_standards_to_import] && !hash[:grading_standards_to_import][hash[:migration_id]]
item ||= GradingStandard.where(context_id: context, context_type: context.class.to_s, migration_id: hash[:migration_id]).first if hash[:migration_id]
@ -38,7 +38,7 @@ module Importers
end
item.save!
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item
end
end

View File

@ -16,13 +16,13 @@ module Importers
end
end
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:groups_to_import] && !hash[:groups_to_import][hash[:migration_id]]
item ||= Group.where(context_id: context, context_type: context.class.to_s, id: hash[:id]).first
item ||= Group.where(context_id: context, context_type: context.class.to_s, migration_id: hash[:migration_id]).first if hash[:migration_id]
item ||= context.groups.new
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item.migration_id = hash[:migration_id]
item.name = hash[:title]
item.group_category = hash[:group_category].present? ?
@ -30,7 +30,7 @@ module Importers
GroupCategory.imported_for(context)
item.save!
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item
end
end

View File

@ -36,7 +36,7 @@ module Importers
root_outcome_group.adopt_outcome_group(item)
end
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
if hash[:outcomes]
hash[:outcomes].each do |child|

View File

@ -80,7 +80,7 @@ module Importers
item.save!
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
else
item = outcome
end

View File

@ -0,0 +1,139 @@
module Importers
class LinkParser
module Helpers
def context
@context ||= @migration.context
end
def context_path
@context_path ||= "/#{context.class.to_s.underscore.pluralize}/#{context.id}"
end
def relative_url?(url)
ImportedHtmlConverter.relative_url?(url)
end
end
include Helpers
REFERENCE_KEYWORDS = %w{CANVAS_COURSE_REFERENCE CANVAS_OBJECT_REFERENCE WIKI_REFERENCE IMS_CC_FILEBASE IMS-CC-FILEBASE}
LINK_PLACEHOLDER = "LINK.PLACEHOLDER"
attr_reader :unresolved_link_map
def initialize(migration)
@migration = migration
reset!
end
def reset!
@unresolved_link_map = {}
end
def add_unresolved_link(link, item_type, mig_id, field)
key = {:type => item_type, :migration_id => mig_id}
@unresolved_link_map[key] ||= {}
@unresolved_link_map[key][field] ||= []
@unresolved_link_map[key][field] << link
end
def placeholder(old_value)
"#{LINK_PLACEHOLDER}_#{Digest::MD5.hexdigest(old_value)}"
end
def convert_link(node, attr, item_type, mig_id, field)
return unless node[attr].present?
if attr == 'value'
return unless node['name'] && node['name'] == 'src'
end
url = node[attr].dup
REFERENCE_KEYWORDS.each do |ref|
url.gsub!("%24#{ref}%24", "$#{ref}$")
end
result = parse_url(url, node, attr)
if result[:resolved]
# resolved, just replace and carry on
new_url = result[:new_url] || url
if @migration && !relative_url?(new_url) && processed_url = @migration.process_domain_substitutions(new_url)
new_url = processed_url
end
node[attr] = new_url
else
result.delete(:resolved)
if result[:link_type] == :media_object
# because we may actually change the media comment node itself
# (rather than just replacing a value), we're going to
# replace the entire node with a placeholder
result[:old_value] = node.to_xml
result[:placeholder] = placeholder(result[:old_value])
placeholder_node = Nokogiri::HTML::DocumentFragment.parse(result[:placeholder])
node.replace(placeholder_node)
else
result[:old_value] = node[attr]
result[:placeholder] = placeholder(result[:old_value])
node[attr] = result[:placeholder]
end
add_unresolved_link(result, item_type, mig_id, field)
end
end
def unresolved(type, data={})
{:resolved => false, :link_type => type}.merge(data)
end
def resolved(new_url=nil)
{:resolved => true, :new_url => new_url}
end
# returns a hash with resolution status and data to hold onto if unresolved
def parse_url(url, node, attr)
if url =~ /wiki_page_migration_id=(.*)/
unresolved(:wiki_page, :migration_id => $1)
elsif url =~ /discussion_topic_migration_id=(.*)/
unresolved(:discussion_topic, :migration_id => $1)
elsif url =~ %r{\$CANVAS_COURSE_REFERENCE\$/modules/items/(.*)}
unresolved(:module_item, :migration_id => $1)
elsif url =~ %r{(?:\$CANVAS_OBJECT_REFERENCE\$|\$WIKI_REFERENCE\$)/([^/]*)/(.*)}
unresolved(:object, :type => $1, :migration_id => $2)
elsif url =~ %r{\$CANVAS_COURSE_REFERENCE\$/(.*)}
resolved("#{context_path}/#{$1}")
elsif url =~ %r{\$IMS(?:-|_)CC(?:-|_)FILEBASE\$/(.*)}
rel_path = URI.unescape($1)
if attr == 'href' && node['class'] && node['class'] =~ /instructure_inline_media_comment/
unresolved(:media_object, :rel_path => rel_path)
else
unresolved(:file, :rel_path => rel_path)
end
elsif attr == 'href' && node['class'] && node['class'] =~ /instructure_inline_media_comment/
# Course copy media reference, leave it alone
resolved
elsif attr == 'src' && node['class'] && node['class'] =~ /equation_image/
# Equation image, leave it alone
resolved
elsif url =~ %r{\A/assessment_questions/\d+/files/\d+}
# The file is in the context of an AQ, leave the link alone
resolved
elsif url =~ %r{\A/courses/\d+/files/\d+}
# This points to a specific file already, leave it alone
resolved
elsif @migration && @migration.for_course_copy?
# For course copies don't try to fix relative urls. Any url we can
# correctly alter was changed during the 'export' step
resolved
elsif url.start_with?('#')
# It's just a link to an anchor, leave it alone
resolved
elsif relative_url?(url)
unresolved(:file, :rel_path => URI.unescape(url))
else
resolved
end
end
end
end

View File

@ -0,0 +1,198 @@
module Importers
class LinkReplacer
LINK_TYPE_TO_CLASS = {
:announcement => Announcement,
:assessment_question => AssessmentQuestion,
:assignment => Assignment,
:calendar_event => CalendarEvent,
:discussion_topic => DiscussionTopic,
:quiz => Quizzes::Quiz,
:wiki_page => WikiPage
}
include LinkParser::Helpers
def initialize(migration)
@migration = migration
end
def replace_placeholders!(link_map)
load_questions!(link_map)
link_map.each do |item_key, field_links|
begin
item_key[:item] ||= retrieve_item(item_key)
add_missing_link_warnings!(item_key, field_links)
replace_item_placeholders!(item_key, field_links)
rescue
@migration.add_warning("An error occurred while translating content links", $!)
end
end
end
# these don't get added to the list of imported migration items
def load_questions!(link_map)
aq_item_keys = link_map.keys.select{|item_key| item_key[:type] == :assessment_question}
aq_item_keys.each_slice(100) do |item_keys|
context.assessment_questions.where(:migration_id => item_keys.map{|ikey| ikey[:migration_id]}).each do |aq|
item_keys.detect{|ikey| ikey[:migration_id] == aq.migration_id}[:item] = aq
end
end
qq_item_keys = link_map.keys.select{|item_key| item_key[:type] == :quiz_question}
qq_item_keys.each_slice(100) do |item_keys|
context.quiz_questions.where(:migration_id => item_keys.map{|ikey| ikey[:migration_id]}).each do |qq|
item_keys.detect{|ikey| ikey[:migration_id] == qq.migration_id}[:item] = qq
end
end
end
def retrieve_item(item_key)
klass = LINK_TYPE_TO_CLASS[item_key[:type]]
return unless klass
item = @migration.find_imported_migration_item(klass, item_key[:migration_id])
raise "item not found" unless item
item
end
def add_missing_link_warnings!(item_key, field_links)
fix_issue_url = nil
field_links.each do |field, links|
missing_links = links.select{|link| link[:missing_url] || !link[:new_value]}
if missing_links.any?
fix_issue_url ||= fix_issue_url(item_key)
type = item_key[:type].to_s.humanize.titleize
@migration.add_warning_for_missing_content_links(type, field, missing_links, fix_issue_url)
end
end
end
def fix_issue_url(item_key)
item = item_key[:item]
case item_key[:type]
when :assessment_question
"#{context_path}/question_banks/#{item.assessment_question_bank_id}#question_#{item.id}_question_text"
when :syllabus
"#{context_path}/assignments/syllabus"
when :wiki_page
"#{context_path}/pages/#{item.url}"
else
"#{context_path}/#{item.class.to_s.demodulize.underscore.pluralize}/#{item.id}"
end
end
def replace_item_placeholders!(item_key, field_links, skip_associations=false)
case item_key[:type]
when :syllabus
syllabus = context.syllabus_body
if sub_placeholders!(syllabus, field_links.values.flatten)
context.class.where(:id => context.id).update_all(:syllabus_body => syllabus)
end
when :assessment_question
process_assessment_question!(item_key[:item], field_links.values.flatten)
when :quiz_question
process_quiz_question!(item_key[:item], field_links.values.flatten)
else
item = item_key[:item]
item_updates = {}
field_links.each do |field, links|
html = item.read_attribute(field)
if sub_placeholders!(html, links)
item_updates[field] = html
end
end
if item_updates.present?
item.class.where(:id => item.id).update_all(item_updates)
end
unless skip_associations
process_assignment_types!(item, field_links.values.flatten)
end
end
end
# returns false if no substitutions were made
def sub_placeholders!(html, links)
subbed = false
links.each do |link|
new_value = link[:new_value] || link[:old_value]
if html.sub!(link[:placeholder], new_value)
subbed = true
end
end
subbed
end
def process_assignment_types!(item, links)
case item
when Assignment
if item.discussion_topic
replace_item_placeholders!({:item => item.discussion_topic}, {:message => links}, true)
end
if item.quiz
replace_item_placeholders!({:item => item.quiz}, {:description => links}, true)
end
when DiscussionTopic
if item.assignment
replace_item_placeholders!({:item => item.assignment}, {:description => links}, true)
end
when Quizzes::Quiz
if item.assignment
replace_item_placeholders!({:item => item.assignment}, {:description => links}, true)
end
end
end
def process_assessment_question!(aq, links)
# we have to do a little bit more here because the question_data can get copied all over
quiz_ids = []
Quizzes::QuizQuestion.where(:assessment_question_id => aq.id).find_each do |qq|
qq_yaml = qq['question_data'].to_yaml
if sub_placeholders!(qq_yaml, links)
Quizzes::QuizQuestion.where(:id => qq.id).update_all(:question_data => qq_yaml)
quiz_ids << qq.quiz_id
end
end
if quiz_ids.any?
Quizzes::Quiz.where(:id => quiz_ids.uniq).where("quiz_data IS NOT NULL").find_each do |quiz|
quiz_yaml = quiz['quiz_data'].to_yaml
if sub_placeholders!(quiz_yaml, links)
Quizzes::Quiz.where(:id => quiz.id).update_all(:quiz_data => quiz_yaml)
end
end
end
# we have to do some special link translations for files in assessment questions
# because we stopped doing them in the regular importer
# basically just moving them to the question context
links.each do |link|
next unless link[:new_value]
link[:new_value] = aq.translate_file_link(link[:new_value])
end
aq_yaml = aq['question_data'].to_yaml
if sub_placeholders!(aq_yaml, links)
AssessmentQuestion.where(:id => aq.id).update_all(:question_data => aq_yaml)
end
end
def process_quiz_question!(qq, links)
qq_yaml = qq['question_data'].to_yaml
if sub_placeholders!(qq_yaml, links)
Quizzes::QuizQuestion.where(:id => qq.id).update_all(:question_data => qq_yaml)
end
quiz = Quizzes::Quiz.where(:id => qq.quiz_id).where("quiz_data IS NOT NULL").first
if quiz
quiz_yaml = quiz['quiz_data'].to_yaml
if sub_placeholders!(quiz_yaml, links)
Quizzes::Quiz.where(:id => quiz.id).update_all(:quiz_data => quiz_yaml)
end
end
end
end
end

View File

@ -0,0 +1,165 @@
module Importers
class LinkResolver
include LinkParser::Helpers
def initialize(migration)
@migration = migration
end
def resolve_links!(link_map)
link_map.each do |item_key, field_links|
field_links.each do |_field, links|
links.each do |link|
resolve_link!(link)
end
end
end
end
# finds the :new_value to use to replace the placeholder
def resolve_link!(link)
case link[:link_type]
when :wiki_page
if linked_wiki = context.wiki.wiki_pages.where(migration_id: link[:migration_id]).select('url').first
link[:new_value] = "#{context_path}/pages/#{linked_wiki.url}"
end
when :discussion_topic
if linked_topic = context.discussion_topics.where(migration_id: link[:migration_id]).select('id').first
link[:new_value] = "#{context_path}/discussion_topics/#{linked_topic.id}"
end
when :module_item
if tag = context.context_module_tags.where(:migration_id => link[:migration_id]).select('id').first
link[:new_value] = "#{context_path}/modules/items/#{tag.id}"
end
when :object
type = link[:type]
migration_id = link[:migration_id]
type_for_url = type
type = 'context_modules' if type == 'modules'
type = 'pages' if type == 'wiki'
if type == 'pages'
link[:new_value] = "#{context_path}/pages/#{migration_id}"
elsif type == 'attachments'
if att = context.attachments.where(migration_id: migration_id).first
link[:new_value] = "#{context_path}/files/#{att.id}/preview"
end
elsif context.respond_to?(type) && context.send(type).respond_to?(:where)
if object = context.send(type).where(migration_id: migration_id).first
link[:new_value] = "#{context_path}/#{type_for_url}/#{object.id}"
end
end
when :media_object
# because we actually might change the node itself
# this part is a little trickier
# tl;dr we've replaced the entire node with the placeholder
# see LinkParser for details
rel_path = link[:rel_path]
node = Nokogiri::HTML::DocumentFragment.parse(link[:old_value]).children.first
new_url = resolve_media_comment_data(node, rel_path)
new_url ||= resolve_relative_file_url(rel_path)
unless new_url
new_url ||= missing_relative_file_url(rel_path)
link[:missing_url] = new_url
end
node['href'] = new_url
link[:new_value] = node.to_xml
when :file
rel_path = link[:rel_path]
new_url = resolve_relative_file_url(rel_path)
unless new_url
new_url = missing_relative_file_url(rel_path)
link[:missing_url] = new_url
end
link[:new_value] = new_url
else
raise "unrecognized link_type in unresolved link"
end
end
def missing_relative_file_url(rel_path)
# the rel_path should already be escaped
File.join(URI::escape("#{context_path}/file_contents/#{Folder.root_folders(context).first.name}"), rel_path.gsub(" ", "%20"))
end
def find_file_in_context(rel_path)
mig_id = nil
# This is for backward-compatibility: canvas attachment filenames are escaped
# with '+' for spaces and older exports have files with that instead of %20
alt_rel_path = rel_path.gsub('+', ' ')
if @migration.attachment_path_id_lookup
mig_id ||= @migration.attachment_path_id_lookup[rel_path]
mig_id ||= @migration.attachment_path_id_lookup[alt_rel_path]
end
if !mig_id && @migration.attachment_path_id_lookup_lower
mig_id ||= @migration.attachment_path_id_lookup_lower[rel_path.downcase]
mig_id ||= @migration.attachment_path_id_lookup_lower[alt_rel_path.downcase]
end
mig_id && context.attachments.where(migration_id: mig_id).first
end
def resolve_relative_file_url(rel_path)
new_url = nil
split = rel_path.split('?')
qs = split.pop if split.length > 1
rel_path = split.join('?')
rel_path_parts = Pathname.new(rel_path).each_filename.to_a
# e.g. start with "a/b/c.txt" then try "b/c.txt" then try "c.txt"
while new_url.nil? && rel_path_parts.length > 0
sub_path = File.join(rel_path_parts)
if file = find_file_in_context(sub_path)
new_url = "#{context_path}/files/#{file.id}"
# support other params in the query string, that were exported from the
# original path components and query string. see
# CCHelper::file_query_string
params = Rack::Utils.parse_nested_query(qs.presence || "")
qs = []
new_action = ""
params.each do |k,v|
case k
when /canvas_qs_(.*)/
qs << "#{Rack::Utils.escape($1)}=#{Rack::Utils.escape(v)}"
when /canvas_(.*)/
new_action += "/#{$1}"
end
end
if new_action.present?
new_url += new_action
else
new_url += "/preview"
end
new_url += "?#{qs.join("&")}" if qs.present?
end
rel_path_parts.shift
end
new_url
end
def resolve_media_comment_data(node, rel_path)
if context.respond_to?(:attachment_path_id_lookup) &&
context.attachment_path_id_lookup &&
context.attachment_path_id_lookup[rel_path]
file = context.attachments.where(migration_id: context.attachment_path_id_lookup[rel_path]).first
if file && file.media_object
media_id = file.media_object.media_id
node['id'] = "media_comment_#{media_id}"
return "/media_objects/#{media_id}"
end
end
if node['id'] && node['id'] =~ /\Amedia_comment_(.+)\z/
return "/media_objects/#{$1}"
else
node.delete('class')
node.delete('id')
node.delete('style')
return nil
end
end
end
end

View File

@ -160,7 +160,7 @@ module Importers
# Import a quiz from a hash.
# It assumes that all the referenced questions are already in the database
def self.import_from_migration(hash, context, migration=nil, question_data=nil, item=nil, allow_update = false)
def self.import_from_migration(hash, context, migration, question_data=nil, item=nil, allow_update = false)
hash = hash.with_indifferent_access
# there might not be an import id if it's just a text-only type...
item ||= Quizzes::Quiz.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first if hash[:id]
@ -184,10 +184,7 @@ module Importers
item.scoring_policy = hash[:which_attempt_to_keep] if hash[:which_attempt_to_keep]
missing_links = []
item.description = ImportedHtmlConverter.convert(hash[:description], context, migration) do |warn, link|
missing_links << link if warn == :missing_link
end
item.description = migration.convert_html(hash[:description], :quiz, hash[:migration_id], :description)
%w[
migration_id
@ -220,14 +217,6 @@ module Importers
item.saved_by = :migration
item.save!
if migration
migration.add_missing_content_links(
:class => item.class.to_s,
:id => item.id, :missing_links => missing_links,
:url => "/#{context.class.to_s.demodulize.underscore.pluralize}/#{context.id}/#{item.class.to_s.demodulize.underscore.pluralize}/#{item.id}"
)
end
if question_data
question_data[:qq_ids] ||= {}
hash[:questions] ||= []
@ -295,7 +284,7 @@ module Importers
item.save
item.assignment.save if item.assignment && item.assignment.changed?
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item.saved_by = nil
item
end

View File

@ -3,9 +3,9 @@ module Importers
self.item_class = Quizzes::QuizQuestion
def self.import_from_migration(aq_hash, qq_hash, position, qq_ids, context, migration=nil, quiz=nil, quiz_group=nil)
def self.import_from_migration(aq_hash, qq_hash, position, qq_ids, context, migration, quiz=nil, quiz_group=nil)
unless aq_hash[:prepped_for_import]
Importers::AssessmentQuestionImporter.prep_for_import(aq_hash, context, migration)
Importers::AssessmentQuestionImporter.prep_for_import(aq_hash, migration, :quiz_question)
end
hash = aq_hash.dup
@ -14,16 +14,17 @@ module Importers
hash[:points_possible] = 0 if hash[:points_possible].to_f < 0
mig_id = qq_hash['quiz_question_migration_id'] || qq_hash['migration_id']
if id = qq_ids[mig_id]
Quizzes::QuizQuestion.where(id: id).update_all(quiz_group_id: quiz_group,
assessment_question_id: hash['assessment_question_id'], question_data: hash.to_yaml,
created_at: Time.now.utc, updated_at: Time.now.utc, migration_id: mig_id,
position: position)
else
query = self.item_class.send(:sanitize_sql, [<<-SQL, quiz && quiz.id, quiz_group && quiz_group.id, hash['assessment_question_id'], hash.to_yaml, Time.now.utc, Time.now.utc, hash[:migration_id], position])
INSERT INTO quiz_questions (quiz_id, quiz_group_id, assessment_question_id, question_data, created_at, updated_at, migration_id, position)
VALUES (?,?,?,?,?,?,?,?)
args = [quiz && quiz.id, quiz_group && quiz_group.id, hash['assessment_question_id'],
hash.to_yaml, Time.now.utc, Time.now.utc, mig_id, position]
query = self.item_class.send(:sanitize_sql, [<<-SQL, *args])
INSERT INTO quiz_questions (quiz_id, quiz_group_id, assessment_question_id, question_data, created_at, updated_at, migration_id, position)
VALUES (?,?,?,?,?,?,?,?)
SQL
qq_ids[mig_id] = self.item_class.connection.insert(query, "#{self.item_class.name} Create",
self.item_class.primary_key, nil, self.item_class.sequence_name)

View File

@ -60,7 +60,7 @@ module Importers
end
end
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item.save!
end

View File

@ -39,7 +39,7 @@ module Importers
end
private_class_method :wiki_page_migration?
def self.import_from_migration(hash, context, migration=nil, item=nil)
def self.import_from_migration(hash, context, migration, item=nil)
hash = hash.with_indifferent_access
item ||= WikiPage.where(wiki_id: context.wiki, id: hash[:id]).first
item ||= WikiPage.where(wiki_id: context.wiki, migration_id: hash[:migration_id]).first
@ -71,7 +71,7 @@ module Importers
end
item.set_as_front_page! if !!hash[:front_page] && context.wiki.has_no_front_page
migration.add_imported_item(item) if migration
migration.add_imported_item(item)
item.migration_id = hash[:migration_id]
(hash[:contents] || []).each do |sub_item|
@ -81,7 +81,6 @@ module Importers
}), context, migration)
end
return if hash[:type] && ['folder', 'FOLDER_TYPE'].member?(hash[:type]) && hash[:linked_resource_id]
missing_links = {}
allow_save = true
if hash[:type] == 'linked_resource' || hash[:type] == "URL_TYPE"
allow_save = false
@ -89,21 +88,15 @@ module Importers
item.title = hash[:title] unless hash[:root_folder]
description = ""
if hash[:header]
missing_links[:header] = []
if hash[:header][:is_html]
description += ImportedHtmlConverter.convert(hash[:header][:body] || "", context, migration) do |warn, link|
missing_links[:header] << link if warn == :missing_link
end
description += migration.convert_html(hash[:header][:body], :wiki_page, hash[:migration_id], :body)
else
description += ImportedHtmlConverter.convert_text(hash[:header][:body] || [""], context)
description += migration.convert_text(hash[:header][:body] || [""])
end
end
missing_links[:description] = []
if hash[:description]
description += ImportedHtmlConverter.convert(hash[:description], context, migration) do |warn, link|
missing_links[:description] << link if warn == :missing_link
end
description += migration.convert_html(hash[:description], :wiki_page, hash[:migration_id], :body)
end
contents = ""
@ -120,11 +113,8 @@ module Importers
end
description += "\n<h2>#{sub_item[:title]}</h2>\n" if sub_item[:title]
missing_links[:sub_item] = []
if sub_item[:description]
description += ImportedHtmlConverter.convert(sub_item[:description], context, migration) do |warn, link|
missing_links[:sub_item] << link if warn == :missing_link
end
description += migration.convert_html(sub_item[:description], :wiki_page, hash[:migration_id], :body)
end
elsif sub_item[:type] == 'linked_resource'
@ -159,13 +149,10 @@ module Importers
description += "<ul>\n#{contents}\n</ul>" if contents && contents.length > 0
if hash[:footer]
missing_links[:footer] = []
if hash[:footer][:is_html]
description += ImportedHtmlConverter.convert(hash[:footer][:body] || "", context, migration) do |warn, link|
missing_links[:footer] << link if warn == :missing_link
end
description += migration.convert_html(hash[:footer][:body], :wiki_page, hash[:migration_id], :body)
else
description += ImportedHtmlConverter.convert_text(hash[:footer][:body] || [""], context)
description += migration.convert_text(hash[:footer][:body] || "")
end
end
@ -194,16 +181,12 @@ module Importers
#it's an actual wiki page
item.title = hash[:title].presence || item.url.presence || "unnamed page"
if item.title.length > WikiPage::TITLE_LENGTH
if migration
migration.add_warning(t('warnings.truncated_wiki_title', "The title of the following wiki page was truncated: %{title}", :title => item.title))
end
migration.add_warning(t('warnings.truncated_wiki_title',
"The title of the following wiki page was truncated: %{title}", :title => item.title))
item.title.splice!(0...WikiPage::TITLE_LENGTH) # truncate too-long titles
end
missing_links[:body] = []
item.body = ImportedHtmlConverter.convert(hash[:text] || "", context, migration) do |warn, link|
missing_links[:body] << link if warn == :missing_link
end
item.body = migration.convert_html(hash[:text], :wiki_page, hash[:migration_id], :body)
item.editing_roles = hash[:editing_roles] if hash[:editing_roles].present?
item.notify_of_update = hash[:notify_of_update] if !hash[:notify_of_update].nil?
@ -215,14 +198,7 @@ module Importers
item.user = nil
end
item.save_without_broadcasting!
migration.add_imported_item(item) if migration
if migration
missing_links.each do |field, missing_links|
migration.add_missing_content_links(:class => item.class.to_s,
:id => item.id, :field => field, :missing_links => missing_links,
:url => "/#{context.class.to_s.underscore.pluralize}/#{context.id}/pages/#{item.url}")
end
end
migration.add_imported_item(item)
return item
end
end

View File

@ -81,8 +81,8 @@ describe "Converting Blackboard Vista qti" do
hash[:answers].each { |a| a.delete(:id) }
expect(hash.reject{|k,v| KEYS_TO_IGNORE.include?(k.to_s)}).to eq VistaExpected::TRUE_FALSE2
end
it "should convert image reference" do
it "should convert image reference" do
hash = get_question_hash(vista_question_dir, 'mc', delete_answer_ids=true, opts={})
expect(hash[:question_text]).to match %r{\$CANVAS_OBJECT_REFERENCE\$/attachments/67320753001}
end
@ -98,7 +98,7 @@ describe "Converting Blackboard Vista qti" do
hash = get_question("ID_4609865577341")
expect(hash.reject{|k,v| KEYS_TO_IGNORE.include?(k.to_s)}).to eq VistaExpected::TRUE_FALSE
end
it "should convert multiple choice questions with multiple correct answers (multiple answer)" do
hash = get_question("ID_4609865392341")
expect(hash.reject{|k,v| KEYS_TO_IGNORE.include?(k.to_s)}).to eq VistaExpected::MULTIPLE_ANSWER
@ -147,7 +147,7 @@ describe "Converting Blackboard Vista qti" do
hash = get_question("ID_4609885376341")
expect(hash.reject{|k,v| KEYS_TO_IGNORE.include?(k.to_s)}).to eq VistaExpected::COMBINATION
end
it "should convert fill in multiple blanks questions" do
hash = get_question("ID_4609842630341")
expect(hash.reject{|k,v| KEYS_TO_IGNORE.include?(k.to_s)}).to eq VistaExpected::FILL_IN_MULTIPLE_BLANKS
@ -209,7 +209,7 @@ module VistaExpected
{:text=>"False", :weight=>0, :migration_id=>"false"}],
:question_type=>"true_false_question",
:migration_id=>"ID_4609865577341"}.with_indifferent_access
TRUE_FALSE2 = {:correct_comments=>"",
:points_possible=>1,
:question_name=>"True/False",

View File

@ -18,7 +18,7 @@
class Canvas::Migration::Worker::CourseCopyWorker < Struct.new(:migration_id)
def perform(cm=nil)
cm ||= ContentMigration.find migration_id
cm.workflow_state = :pre_processing
cm.reset_job_progress
cm.migration_settings[:skip_import_notification] = true

View File

@ -29,7 +29,7 @@ module CC::Importer::Standard
MANIFEST_FILE = "imsmanifest.xml"
SUPPORTED_TYPES = /assessment\z|\Aassignment|\Aimswl|\Aimsbasiclti|\Aimsdt|webcontent|learning-application-resource\z/
attr_accessor :resources
# settings will use these keys: :course_name, :base_download_dir
@ -92,7 +92,7 @@ module CC::Importer::Standard
@file_path_migration_id[path] || @file_path_migration_id[path.gsub(%r{\$[^$]*\$|\.\./}, '')] ||
@file_path_migration_id[path.gsub(%r{\$[^$]*\$|\.\./}, '').sub(WEB_RESOURCES_FOLDER + '/', '')]
end
def get_canvas_att_replacement_url(path, resource_dir=nil)
if path.start_with?('../')
if url = get_canvas_att_replacement_url(path.sub('../', ''), resource_dir)
@ -132,7 +132,7 @@ module CC::Importer::Standard
@file_path_migration_id[file[:path_name]] = file[:migration_id]
add_file(file)
end
FILEBASE_REGEX = /\$IMS[-_]CC[-_]FILEBASE\$/
def replace_urls(html, resource_dir=nil)
return "" if html.blank?
@ -182,6 +182,6 @@ module CC::Importer::Standard
tools
end
end
end

View File

@ -23,141 +23,27 @@ class ImportedHtmlConverter
include HtmlTextHelper
CONTAINER_TYPES = ['div', 'p', 'body']
REFERENCE_KEYWORDS = %w{CANVAS_COURSE_REFERENCE CANVAS_OBJECT_REFERENCE WIKI_REFERENCE IMS_CC_FILEBASE IMS-CC-FILEBASE}
# yields warnings
def self.convert(html, context, migration=nil, opts={})
LINK_ATTRS = ['rel', 'href', 'src', 'data', 'value']
attr_reader :link_parser, :link_resolver, :link_replacer
def initialize(migration)
@migration = migration
@link_parser = Importers::LinkParser.new(migration)
@link_resolver = Importers::LinkResolver.new(migration)
@link_replacer = Importers::LinkReplacer.new(migration)
end
def convert(html, item_type, mig_id, field, opts={})
doc = Nokogiri::HTML(html || "")
attrs = ['rel', 'href', 'src', 'data', 'value']
course_path = "/#{context.class.to_s.underscore.pluralize}/#{context.id}"
for_course_copy = false
if migration
for_course_copy = true if migration.for_course_copy?
end
doc.search("*").each do |node|
attrs.each do |attr|
if node[attr].present?
if attr == 'value'
next unless node['name'] && node['name'] == 'src'
end
new_url = nil
missing_relative_url = nil
val = node[attr].dup
REFERENCE_KEYWORDS.each do |ref|
val.gsub!("%24#{ref}%24", "$#{ref}$")
end
if val =~ /wiki_page_migration_id=(.*)/
# This would be from a BB9 migration.
#todo: refactor migration systems to use new $CANVAS...$ flags
#todo: FLAG UNFOUND REFERENCES TO re-attempt in second loop?
if wiki_migration_id = $1
if linked_wiki = context.wiki.wiki_pages.where(migration_id: wiki_migration_id).first
new_url = "#{course_path}/pages/#{linked_wiki.url}"
end
end
elsif val =~ /discussion_topic_migration_id=(.*)/
if topic_migration_id = $1
if linked_topic = context.discussion_topics.where(migration_id: topic_migration_id).first
new_url = "#{course_path}/discussion_topics/#{linked_topic.id}"
end
end
elsif val =~ %r{\$CANVAS_COURSE_REFERENCE\$/modules/items/(.*)}
if tag = context.context_module_tags.where(:migration_id => $1).select('id').first
new_url = "#{course_path}/modules/items/#{tag.id}"
end
elsif val =~ %r{(?:\$CANVAS_OBJECT_REFERENCE\$|\$WIKI_REFERENCE\$)/([^/]*)/(.*)}
type = $1
migration_id = $2
type_for_url = type
type = 'context_modules' if type == 'modules'
type = 'pages' if type == 'wiki'
if type == 'pages'
new_url = "#{course_path}/pages/#{migration_id}"
elsif type == 'attachments'
if att = context.attachments.where(migration_id: migration_id).first
new_url = "#{course_path}/files/#{att.id}/preview"
end
elsif context.respond_to?(type) && context.send(type).respond_to?(:where)
if object = context.send(type).where(migration_id: migration_id).first
new_url = "#{course_path}/#{type_for_url}/#{object.id}"
end
end
elsif val =~ %r{\$CANVAS_COURSE_REFERENCE\$/(.*)}
section = $1
new_url = "#{course_path}/#{section}"
elsif val =~ %r{\$IMS(?:-|_)CC(?:-|_)FILEBASE\$/(.*)}
rel_path = URI.unescape($1)
if attr == 'href' && node['class'] && node['class'] =~ /instructure_inline_media_comment/
new_url = replace_media_comment_data(node, rel_path, context, opts) {|warning, data| yield warning, data if block_given?}
unless new_url
unless new_url = replace_relative_file_url(rel_path, context)
missing_relative_url = rel_path
end
end
else
unless new_url = replace_relative_file_url(rel_path, context)
missing_relative_url = rel_path
end
end
elsif attr == 'href' && node['class'] && node['class'] =~ /instructure_inline_media_comment/
# Course copy media reference, leave it alone
new_url = node[attr]
elsif attr == 'src' && node['class'] && node['class'] =~ /equation_image/
# Equation image, leave it alone
new_url = node[attr]
elsif val =~ %r{\A/assessment_questions/\d+/files/\d+}
# The file is in the context of an AQ, leave the link alone
new_url = node[attr]
elsif val =~ %r{\A/courses/\d+/files/\d+}
# This points to a specific file already, leave it alone
new_url = node[attr]
elsif for_course_copy
# For course copies don't try to fix relative urls. Any url we can
# correctly alter was changed during the 'export' step
new_url = node[attr]
elsif val.start_with?('#')
# It's just a link to an anchor, leave it alone
new_url = node[attr]
else
begin
if relative_url?(node[attr])
unescaped = URI.unescape(val)
unless new_url = replace_relative_file_url(unescaped, context)
missing_relative_url = unescaped
end
else
new_url = node[attr]
end
rescue URI::InvalidURIError
Rails.logger.warn "attempting to translate invalid url: #{node[attr]}"
# leave the url as it was
end
end
if missing_relative_url
node[attr] = replace_missing_relative_url(missing_relative_url, context, course_path)
end
if migration && converted_url = migration.process_domain_substitutions(new_url || val)
if converted_url != (new_url || val)
new_url = converted_url
end
end
if new_url
node[attr] = new_url
else
yield :missing_link, node[attr] if block_given?
end
end
LINK_ATTRS.each do |attr|
@link_parser.convert_link(node, attr, item_type, mig_id, field)
end
end
node = doc.at_css('body')
return "" unless node
if opts[:remove_outer_nodes_if_one_child]
while node.children.size == 1 && node.child.child
break unless CONTAINER_TYPES.member? node.child.name
@ -166,105 +52,29 @@ class ImportedHtmlConverter
end
node.inner_html
rescue
rescue Nokogiri::SyntaxError
""
end
def self.find_file_in_context(rel_path, context)
mig_id = nil
# This is for backward-compatibility: canvas attachment filenames are escaped
# with '+' for spaces and older exports have files with that instead of %20
alt_rel_path = rel_path.gsub('+', ' ')
if context.respond_to?(:attachment_path_id_lookup) && context.attachment_path_id_lookup
mig_id ||= context.attachment_path_id_lookup[rel_path]
mig_id ||= context.attachment_path_id_lookup[alt_rel_path]
end
if !mig_id && context.respond_to?(:attachment_path_id_lookup_lower) && context.attachment_path_id_lookup_lower
mig_id ||= context.attachment_path_id_lookup_lower[rel_path.downcase]
mig_id ||= context.attachment_path_id_lookup_lower[alt_rel_path.downcase]
end
mig_id && context.attachments.where(migration_id: mig_id).first
def convert_text(text)
format_message(text || "")[0]
end
def self.replace_relative_file_url(rel_path, context)
new_url = nil
split = rel_path.split('?')
qs = split.pop if split.length > 1
rel_path = split.join('?')
def resolve_content_links!
link_map = @link_parser.unresolved_link_map
return unless link_map.present?
rel_path_parts = Pathname.new(rel_path).each_filename.to_a
# e.g. start with "a/b/c.txt" then try "b/c.txt" then try "c.txt"
while new_url.nil? && rel_path_parts.length > 0
sub_path = File.join(rel_path_parts)
if file = find_file_in_context(sub_path, context)
new_url = "/courses/#{context.id}/files/#{file.id}"
# support other params in the query string, that were exported from the
# original path components and query string. see
# CCHelper::file_query_string
params = Rack::Utils.parse_nested_query(qs.presence || "")
qs = []
new_action = ""
params.each do |k,v|
case k
when /canvas_qs_(.*)/
qs << "#{Rack::Utils.escape($1)}=#{Rack::Utils.escape(v)}"
when /canvas_(.*)/
new_action += "/#{$1}"
end
end
if new_action.present?
new_url += new_action
else
new_url += "/preview"
end
new_url += "?#{qs.join("&")}" if qs.present?
end
rel_path_parts.shift
end
new_url
@link_resolver.resolve_links!(link_map)
@link_replacer.replace_placeholders!(link_map)
@link_parser.reset!
end
def self.replace_missing_relative_url(rel_path, context, course_path)
# the rel_path should already be escaped
File.join(URI::escape("#{course_path}/file_contents/#{Folder.root_folders(context).first.name}"), rel_path)
end
def self.replace_media_comment_data(node, rel_path, context, opts={})
if context.respond_to?(:attachment_path_id_lookup) &&
context.attachment_path_id_lookup &&
context.attachment_path_id_lookup[rel_path]
file = context.attachments.where(migration_id: context.attachment_path_id_lookup[rel_path]).first
if file && file.media_object
media_id = file.media_object.media_id
node['id'] = "media_comment_#{media_id}"
return "/media_objects/#{media_id}"
end
end
if node['id'] && node['id'] =~ /\Amedia_comment_(.+)\z/
link = "/media_objects/#{$1}"
yield :missing_link, link
return link
else
node.delete('class')
node.delete('id')
node.delete('style')
yield :missing_link, rel_path
return nil
end
end
def self.relative_url?(url)
URI.parse(url).relative? && !url.to_s.start_with?("//")
rescue URI::InvalidURIError
# leave the url as it was
Rails.logger.warn "attempting to translate invalid url: #{url}"
false
end
def self.convert_text(text, context, import_source=:webct)
instance.format_message(text || "")[0]
end
def self.instance
@@instance ||= self.new
end
end
end

View File

@ -50,7 +50,6 @@ def get_import_data(sub_folder, hash_name)
end
def import_example_questions(context)
migration = context.content_migrations.new
questions = []
QUESTIONS.each do |question|
if import_data_exists?(['vista', 'quiz'], question[0])
@ -59,12 +58,12 @@ def import_example_questions(context)
end
end
hash = {'assessment_questions' => {'assessment_questions' => questions}}
Importers::AssessmentQuestionImporter.process_migration(hash, migration)
Importers::AssessmentQuestionImporter.process_migration(hash, @migration)
end
def get_import_context(system=nil)
context = course_model
context.import_source == :webct if system == 'vista'
context
end

View File

@ -96,7 +96,7 @@ describe "Canvas Cartridge importing" do
hash = {:migration_id=>CC::CCHelper.create_key(a),
:title=>a.title,
:assignment_group_migration_id=>CC::CCHelper.create_key(ag2)}
Importers::AssignmentImporter.import_from_migration(hash, @copy_to)
Importers::AssignmentImporter.import_from_migration(hash, @copy_to, @migration)
ag2_2.reload
expect(ag2_2.assignments.count).to eq 1
@ -567,8 +567,9 @@ describe "Canvas Cartridge importing" do
hash = @converter.convert_wiki(doc, 'some-page')
hash = hash.with_indifferent_access
#import into new course
@copy_to.attachment_path_id_lookup = { 'unfiled/ohai there.txt' => attachment_import.migration_id }
Importers::WikiPageImporter.import_from_migration(hash, @copy_to)
@migration.attachment_path_id_lookup = { 'unfiled/ohai there.txt' => attachment_import.migration_id }
Importers::WikiPageImporter.import_from_migration(hash, @copy_to, @migration)
@migration.resolve_content_links!
page_2 = @copy_to.wiki.wiki_pages.where(migration_id: migration_id).first
expect(page_2.title).to eq page.title
@ -595,7 +596,7 @@ describe "Canvas Cartridge importing" do
to_att.migration_id = CC::CCHelper.create_key(from_att)
to_att.save
path = to_att.full_display_path.gsub('course files/', '')
@copy_to.attachment_path_id_lookup = {path => to_att.migration_id}
@migration.attachment_path_id_lookup = {path => to_att.migration_id}
body_with_link = %{<p>Watup? <strong>eh?</strong>
<a href=\"/courses/%s/assignments\">Assignments</a>
@ -625,6 +626,7 @@ describe "Canvas Cartridge importing" do
hash = hash.with_indifferent_access
#import into new course
Importers::WikiPageImporter.process_migration({'wikis' => [hash, nil]}, @migration)
@migration.resolve_content_links!
expect(ErrorReport.last.message).to match /nil wiki/
@ -650,7 +652,7 @@ describe "Canvas Cartridge importing" do
hash = @converter.convert_wiki(doc, 'blti-link')
hash = hash.with_indifferent_access
#import into new course
Importers::WikiPageImporter.import_from_migration(hash, @copy_to)
Importers::WikiPageImporter.import_from_migration(hash, @copy_to, @migration)
page_2 = @copy_to.wiki.wiki_pages.where(migration_id: migration_id).first
expect(page_2.title).to eq page.title
@ -703,7 +705,7 @@ describe "Canvas Cartridge importing" do
hash = @converter.parse_canvas_assignment_data(meta_doc, html_doc)
hash = hash.with_indifferent_access
#import
Importers::AssignmentImporter.import_from_migration(hash, @copy_to)
Importers::AssignmentImporter.import_from_migration(hash, @copy_to, @migration)
asmnt_2 = @copy_to.assignments.where(migration_id: migration_id).first
expect(asmnt_2.title).to eq asmnt.title
@ -742,7 +744,7 @@ describe "Canvas Cartridge importing" do
hash = @converter.parse_canvas_assignment_data(meta_doc, html_doc)
hash = hash.with_indifferent_access
#import
Importers::AssignmentImporter.import_from_migration(hash, @copy_to)
Importers::AssignmentImporter.import_from_migration(hash, @copy_to, @migration)
asmnt_2 = @copy_to.assignments.where(migration_id: migration_id).first
expect(asmnt_2.submission_types).to eq "external_tool"
@ -803,7 +805,7 @@ XML
hash = @converter.convert_topic(cc_doc, meta_doc)
hash = hash.with_indifferent_access
#import
Importers::DiscussionTopicImporter.import_from_migration(hash, @copy_to)
Importers::DiscussionTopicImporter.import_from_migration(hash, @copy_to, @migration)
dt_2 = @copy_to.discussion_topics.where(migration_id: migration_id).first
expect(dt_2.title).to eq dt.title
@ -851,7 +853,7 @@ XML
ag1.migration_id = CC::CCHelper.create_key(assignment.assignment_group)
ag1.save!
#import
Importers::DiscussionTopicImporter.import_from_migration(hash, @copy_to)
Importers::DiscussionTopicImporter.import_from_migration(hash, @copy_to, @migration)
dt_2 = @copy_to.discussion_topics.where(migration_id: migration_id).first
expect(dt_2.title).to eq dt.title
@ -961,7 +963,7 @@ XML
ag.migration_id = "i713e960ab2685259505efeb08cd48a1d"
ag.save!
Importers::QuizImporter.import_from_migration(quiz_hash, @copy_to, nil, {})
Importers::QuizImporter.import_from_migration(quiz_hash, @copy_to, @migration, {})
q = @copy_to.quizzes.where(migration_id: "ie3d8f8adfad423eb225229c539cdc450").first
a = q.assignment
expect(a.assignment_group.id).to eq ag.id

View File

@ -19,43 +19,57 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ImportedHtmlConverter do
# tests link_parser and link_resolver
context ".convert" do
before(:each) do
before :once do
course
@path = "/courses/#{@course.id}/"
@migration = @course.content_migrations.create!
@converter = @migration.html_converter
end
def convert_and_replace(test_string)
html = @migration.convert_html(test_string, 'sometype', 'somemigid', 'somefield')
link_map = @converter.link_parser.unresolved_link_map
@converter.link_resolver.resolve_links!(link_map)
if link_map.present?
@converter.link_replacer.sub_placeholders!(html, link_map.values.map(&:values).flatten)
end
html
end
it "should convert a wiki reference" do
test_string = %{<a href="%24WIKI_REFERENCE%24/wiki/test-wiki-page">Test Wiki Page</a>}
@course.wiki.wiki_pages.create!(:title => "Test Wiki Page", :body => "stuff")
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}pages/test-wiki-page">Test Wiki Page</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}pages/test-wiki-page">Test Wiki Page</a>}
end
it "should convert a wiki reference without $ escaped" do
test_string = %{<a href="$WIKI_REFERENCE$/wiki/test-wiki-page">Test Wiki Page</a>}
@course.wiki.wiki_pages.create!(:title => "Test Wiki Page", :body => "stuff")
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}pages/test-wiki-page">Test Wiki Page</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}pages/test-wiki-page">Test Wiki Page</a>}
end
it "should convert a wiki reference by migration id" do
test_string = %{<a href="wiki_page_migration_id=123456677788">Test Wiki Page</a>}
wiki = @course.wiki.wiki_pages.create(:title => "Test Wiki Page", :body => "stuff")
wiki.migration_id = "123456677788"
wiki.save!
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}pages/test-wiki-page">Test Wiki Page</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}pages/test-wiki-page">Test Wiki Page</a>}
end
it "should convert a discussion reference by migration id" do
test_string = %{<a href="discussion_topic_migration_id=123456677788">Test topic</a>}
topic = @course.discussion_topics.create(:title => "Test discussion")
topic.migration_id = "123456677788"
topic.save!
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}discussion_topics/#{topic.id}">Test topic</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}discussion_topics/#{topic.id}">Test topic</a>}
end
def make_test_att
@ -67,84 +81,84 @@ describe ImportedHtmlConverter do
it "should find an attachment by migration id" do
att = make_test_att()
test_string = %{<p>This is an image: <br /><img src="%24CANVAS_OBJECT_REFERENCE%24/attachments/1768525836051" alt=":(" /></p>}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<p>This is an image: <br><img src="#{@path}files/#{att.id}/preview" alt=":("></p>}
expect(convert_and_replace(test_string)).to eq %{<p>This is an image: <br><img src="#{@path}files/#{att.id}/preview" alt=":("></p>}
end
it "should find an attachment by path" do
att = make_test_att()
test_string = %{<p>This is an image: <br /><img src="%24IMS_CC_FILEBASE%24/test.png" alt=":(" /></p>}
# if there isn't a path->migration id map it'll be a relative course file path
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<p>This is an image: <br><img src="#{@path}file_contents/course%20files/test.png" alt=":("></p>}
@course.attachment_path_id_lookup = {"test.png" => att.migration_id}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<p>This is an image: <br><img src="#{@path}files/#{att.id}/preview" alt=":("></p>}
expect(convert_and_replace(test_string)).to eq %{<p>This is an image: <br><img src="#{@path}file_contents/course%20files/test.png" alt=":("></p>}
@migration.attachment_path_id_lookup = {"test.png" => att.migration_id}
expect(convert_and_replace(test_string)).to eq %{<p>This is an image: <br><img src="#{@path}files/#{att.id}/preview" alt=":("></p>}
end
it "should find an attachment by a path with a space" do
att = make_test_att()
@course.attachment_path_id_lookup = {"subfolder/with a space/test.png" => att.migration_id}
@migration.attachment_path_id_lookup = {"subfolder/with a space/test.png" => att.migration_id}
test_string = %{<img src="subfolder/with%20a%20space/test.png" alt="nope" />}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
expect(convert_and_replace(test_string)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
test_string = %{<img src="subfolder/with+a+space/test.png" alt="nope" />}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
expect(convert_and_replace(test_string)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
end
it "should find an attachment even if the link has an extraneous folder" do
att = make_test_att()
@course.attachment_path_id_lookup = {"subfolder/test.png" => att.migration_id}
@migration.attachment_path_id_lookup = {"subfolder/test.png" => att.migration_id}
test_string = %{<img src="anotherfolder/subfolder/test.png" alt="nope" />}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
expect(convert_and_replace(test_string)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
end
it "should find an attachment by path if capitalization is different" do
att = make_test_att()
@course.attachment_path_id_lookup = {"subfolder/withCapital/test.png" => "wrong!"}
@course.attachment_path_id_lookup_lower = {"subfolder/withcapital/test.png" => att.migration_id}
@migration.attachment_path_id_lookup = {"subfolder/withCapital/test.png" => "wrong!"}
@migration.attachment_path_id_lookup_lower = {"subfolder/withcapital/test.png" => att.migration_id}
test_string = %{<img src="subfolder/WithCapital/TEST.png" alt="nope" />}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
expect(convert_and_replace(test_string)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
end
it "should find an attachment with query params" do
att = make_test_att()
@course.attachment_path_id_lookup = {"test.png" => att.migration_id}
@migration.attachment_path_id_lookup = {"test.png" => att.migration_id}
test_string = %{<img src="%24IMS_CC_FILEBASE%24/test.png?canvas_customaction=1&canvas_qs_customparam=1" alt="nope" />}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<img src="#{@path}files/#{att.id}/customaction?customparam=1" alt="nope">}
expect(convert_and_replace(test_string)).to eq %{<img src="#{@path}files/#{att.id}/customaction?customparam=1" alt="nope">}
test_string = %{<img src="%24IMS_CC_FILEBASE%24/test.png?canvas_qs_customparam2=3" alt="nope" />}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<img src="#{@path}files/#{att.id}/preview?customparam2=3" alt="nope">}
expect(convert_and_replace(test_string)).to eq %{<img src="#{@path}files/#{att.id}/preview?customparam2=3" alt="nope">}
test_string = %{<img src="%24IMS_CC_FILEBASE%24/test.png?notarelevantparam" alt="nope" />}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
expect(convert_and_replace(test_string)).to eq %{<img src="#{@path}files/#{att.id}/preview" alt="nope">}
end
it "should convert course section urls" do
test_string = %{<a href="%24CANVAS_COURSE_REFERENCE%24/discussion_topics">discussions</a>}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}discussion_topics">discussions</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}discussion_topics">discussions</a>}
end
it "should leave invalid and absolute urls alone" do
test_string = %{<a href="stupid &^%$ url">Linkage</a><br><a href="http://www.example.com/poop">Linkage</a>}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="stupid%20&amp;%5E%%24%20url">Linkage</a><br><a href="http://www.example.com/poop">Linkage</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="stupid%20&amp;%5E%%24%20url">Linkage</a><br><a href="http://www.example.com/poop">Linkage</a>}
end
it "should prepend course files for unrecognized relative urls" do
test_string = %{<a href="/relative/path/to/file">Linkage</a>}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}file_contents/course%20files/relative/path/to/file">Linkage</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}file_contents/course%20files/relative/path/to/file">Linkage</a>}
test_string = %{<a href="relative/path/to/file">Linkage</a>}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}file_contents/course%20files/relative/path/to/file">Linkage</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}file_contents/course%20files/relative/path/to/file">Linkage</a>}
test_string = %{<a href="relative/path/to/file%20with%20space.html">Linkage</a>}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<a href="#{@path}file_contents/course%20files/relative/path/to/file%20with%20space.html">Linkage</a>}
expect(convert_and_replace(test_string)).to eq %{<a href="#{@path}file_contents/course%20files/relative/path/to/file%20with%20space.html">Linkage</a>}
end
it "should preserve media comment links" do
test_string = <<-HTML.strip
<p>
@ -153,13 +167,13 @@ describe ImportedHtmlConverter do
</p>
HTML
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq test_string.gsub("/courses/#{course.id}/file_contents/course%20files",'')
expect(convert_and_replace(test_string)).to eq test_string
end
it "should handle and repair half broken media links" do
test_string = %{<p><a href="/courses/#{@course.id}/file_contents/%24IMS_CC_FILEBASE%24/#" class="instructure_inline_media_comment video_comment" id="media_comment_0_l4l5n0wt">this is a media comment</a><br><br></p>}
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq %{<p><a href="/media_objects/0_l4l5n0wt" class="instructure_inline_media_comment video_comment" id="media_comment_0_l4l5n0wt">this is a media comment</a><br><br></p>}
expect(convert_and_replace(test_string)).to eq %{<p><a href="/media_objects/0_l4l5n0wt" class="instructure_inline_media_comment video_comment" id="media_comment_0_l4l5n0wt">this is a media comment</a><br><br></p>}
end
it "should only convert url params" do
@ -175,7 +189,7 @@ describe ImportedHtmlConverter do
</object>
HTML
expect(ImportedHtmlConverter.convert(test_string, @course)).to match_ignoring_whitespace(<<-HTML.strip)
expect(convert_and_replace(test_string)).to match_ignoring_whitespace(<<-HTML.strip)
<object>
<param name="controls" value="CONSOLE">
<param name="controller" value="true">
@ -188,25 +202,25 @@ describe ImportedHtmlConverter do
it "should leave an anchor tag alone" do
test_string = '<p><a href="#anchor_ref">ref</a></p>'
expect(ImportedHtmlConverter.convert(test_string, @course)).to eq test_string
expect(convert_and_replace(test_string)).to eq test_string
end
end
context ".relative_url?" do
it "should recognize an absolute url" do
expect(ImportedHtmlConverter.relative_url?("http://example.com")).to eq false
end
it "should recognize relative urls" do
expect(ImportedHtmlConverter.relative_url?("/relative/eh")).to eq true
expect(ImportedHtmlConverter.relative_url?("also/relative")).to eq true
expect(ImportedHtmlConverter.relative_url?("watup/nothing.html#anchoritbaby")).to eq true
expect(ImportedHtmlConverter.relative_url?("watup/nothing?absolutely=1")).to eq true
end
it "should error on invalid urls" do
expect { ImportedHtmlConverter.relative_url?("stupid &^%$ url") }.to raise_error(URI::InvalidURIError)
it "should not error on invalid urls" do
expect(ImportedHtmlConverter.relative_url?("stupid &^%$ url")).to be_falsey
end
end

View File

@ -838,5 +838,69 @@ equation: <img class="equation_image" title="Log_216" src="/equation_images/Log_
expect(quiz_to.assignment).to eq a_to
expect(a_to).to be_published
end
it "should correctly copy links to quizzes inside assessment questions" do
link_quiz = @copy_from.quizzes.create!(:title => "linked quiz")
html = "<a href=\"/courses/%s/quizzes/%s\">linky</a>"
bank = @copy_from.assessment_question_banks.create!(:title => 'bank')
data = {'question_name' => 'test question', 'question_type' => 'essay_question',
'question_text' => (html % [@copy_from.id, link_quiz.id])}
aq = bank.assessment_questions.create!(:question_data => data)
other_quiz = @copy_from.quizzes.create!(:title => "other quiz")
qq = other_quiz.quiz_questions.create!(:question_data => data)
other_quiz.generate_quiz_data
other_quiz.published_at = Time.now
other_quiz.workflow_state = 'available'
other_quiz.save!
run_course_copy
link_quiz2 = @copy_to.quizzes.where(migration_id: mig_id(link_quiz)).first
expected_html = (html % [@copy_to.id, link_quiz2.id])
other_quiz2 = @copy_to.quizzes.where(migration_id: mig_id(other_quiz)).first
aq2 = @copy_to.assessment_questions.where(migration_id: mig_id(aq)).first
qq2 = other_quiz2.quiz_questions.first
expect(aq2.question_data['question_text']).to eq expected_html
expect(qq2.question_data['question_text']).to eq expected_html
expect(other_quiz2.quiz_data.first['question_text']).to eq expected_html
end
it "should correctly copy links to quizzes inside standalone quiz questions" do
# i.e. quiz questions imported independently from their original assessment question
link_quiz = @copy_from.quizzes.create!(:title => "linked quiz")
html = "<a href=\"/courses/%s/quizzes/%s\">linky</a>"
bank = @copy_from.assessment_question_banks.create!(:title => 'bank')
data = {'question_name' => 'test question', 'question_type' => 'essay_question',
'question_text' => (html % [@copy_from.id, link_quiz.id])}
aq = bank.assessment_questions.create!(:question_data => data)
other_quiz = @copy_from.quizzes.create!(:title => "other quiz")
qq = other_quiz.quiz_questions.create!(:question_data => data)
other_quiz.generate_quiz_data
other_quiz.published_at = Time.now
other_quiz.workflow_state = 'available'
other_quiz.save!
@cm.copy_options = {
:quizzes => {mig_id(link_quiz) => "1", mig_id(other_quiz) => "1"}
}
run_course_copy
link_quiz2 = @copy_to.quizzes.where(migration_id: mig_id(link_quiz)).first
expected_html = (html % [@copy_to.id, link_quiz2.id])
other_quiz2 = @copy_to.quizzes.where(migration_id: mig_id(other_quiz)).first
qq2 = other_quiz2.quiz_questions.first
expect(qq2.question_data['question_text']).to eq expected_html
expect(other_quiz2.quiz_data.first['question_text']).to eq expected_html
end
end
end

View File

@ -19,7 +19,7 @@
require File.expand_path(File.dirname(__FILE__) + '../../../import_helper')
describe "Assessment Question import from hash" do
SYSTEMS.each do |system|
QUESTIONS.each do |q|
if import_data_exists? [system, 'quiz'], q[0]
@ -68,7 +68,7 @@ describe "Assessment Question import from hash" do
data = {'assessment_questions' => {'assessment_questions' => [q]}}
migration = ContentMigration.create!(:context => context)
Importers::AssessmentQuestionImporter.process_migration(data, migration)
bank = AssessmentQuestionBank.where(context_type: context.class.to_s, context_id: context, title: q[:question_bank_name]).first
expect(bank.assessment_questions.count).to eq 1
expect(bank.assessment_questions.first.migration_id).to eq q[:migration_id]
@ -139,7 +139,7 @@ describe "Assessment Question import from hash" do
question_data[:aq_data][question[:migration_id]] = context.assessment_questions.where(migration_id: question[:migration_id]).first
quiz = get_import_data 'cengage', 'quiz'
Importers::QuizImporter.import_from_migration(quiz, context, nil, question_data)
Importers::QuizImporter.import_from_migration(quiz, context, migration, question_data)
quiz = context.quizzes.where(migration_id: quiz[:migration_id]).first
group = quiz.quiz_groups.first
@ -150,7 +150,7 @@ end
def test_question_import(hash_name, system, question_type=nil)
question_type ||= "#{hash_name}_question"
q = get_import_data [system, 'quiz'], hash_name
q = get_import_data [system, 'quiz'], hash_name
# q[:question_type].should == question_type
context = get_import_context(system)
data = {'assessment_questions' => {'assessment_questions' => [q]}}

View File

@ -25,16 +25,17 @@ describe "Importing Assignment Groups" do
it "should import from #{system}" do
data = get_import_data(system, 'assignment_group')
context = get_import_context(system)
migration = context.content_migrations.create!
data[:assignment_groups_to_import] = {}
expect {
expect(Importers::AssignmentGroupImporter.import_from_migration(data, context)).to be_nil
expect(Importers::AssignmentGroupImporter.import_from_migration(data, context, migration)).to be_nil
}.to change(AssignmentGroup, :count).by(0)
data[:assignment_groups_to_import][data[:migration_id]] = true
expect {
Importers::AssignmentGroupImporter.import_from_migration(data, context)
Importers::AssignmentGroupImporter.import_from_migration(data, context)
Importers::AssignmentGroupImporter.import_from_migration(data, context, migration)
Importers::AssignmentGroupImporter.import_from_migration(data, context, migration)
}.to change(AssignmentGroup, :count).by(1)
g = AssignmentGroup.where(migration_id: data[:migration_id]).first
@ -45,20 +46,22 @@ describe "Importing Assignment Groups" do
it "should reuse existing empty assignment groups with the same name" do
course_model
migration = @course.content_migrations.create!
assignment_group = @course.assignment_groups.create! name: 'teh group'
assignment_group_json = { 'title' => 'teh group', 'migration_id' => '123' }
Importers::AssignmentGroupImporter.import_from_migration(assignment_group_json, @course)
Importers::AssignmentGroupImporter.import_from_migration(assignment_group_json, @course, migration)
expect(assignment_group.reload.migration_id).to eq('123')
expect(@course.assignment_groups.count).to eq 1
end
it "should not match assignment groups with migration ids by name" do
course_model
migration = @course.content_migrations.create!
assignment_group = @course.assignment_groups.create name: 'teh group'
assignment_group.migration_id = '456'
assignment_group.save!
assignment_group_json = { 'title' => 'teh group', 'migration_id' => '123' }
Importers::AssignmentGroupImporter.import_from_migration(assignment_group_json, @course)
Importers::AssignmentGroupImporter.import_from_migration(assignment_group_json, @course, migration)
expect(assignment_group.reload.migration_id).to eq('456')
expect(@course.assignment_groups.count).to eq 2
end
@ -66,12 +69,13 @@ describe "Importing Assignment Groups" do
it "should get attached to an assignment" do
data = get_import_data('bb8', 'assignment_group')
context = get_import_context('bb8')
migration = context.content_migrations.create!
expect {
Importers::AssignmentGroupImporter.import_from_migration(data, context)
Importers::AssignmentGroupImporter.import_from_migration(data, context, migration)
}.to change(AssignmentGroup, :count).by(1)
expect {
ass = Importers::AssignmentImporter.import_from_migration(get_import_data('bb8', 'assignment'), context)
ass = Importers::AssignmentImporter.import_from_migration(get_import_data('bb8', 'assignment'), context, migration)
expect(ass.assignment_group.name).to eq data[:title]
}.to change(AssignmentGroup, :count).by(0)
end

View File

@ -25,19 +25,20 @@ describe "Importing assignments" do
it "should import assignments for #{system}" do
data = get_import_data(system, 'assignment')
context = get_import_context(system)
migration = context.content_migrations.create!
data[:assignments_to_import] = {}
expect {
expect(Importers::AssignmentImporter.import_from_migration(data, context)).to be_nil
expect(Importers::AssignmentImporter.import_from_migration(data, context, migration)).to be_nil
}.to change(Assignment, :count).by(0)
data[:assignments_to_import][data[:migration_id]] = true
expect {
Importers::AssignmentImporter.import_from_migration(data, context)
Importers::AssignmentImporter.import_from_migration(data, context)
Importers::AssignmentImporter.import_from_migration(data, context, migration)
Importers::AssignmentImporter.import_from_migration(data, context, migration)
}.to change(Assignment, :count).by(1)
a = Assignment.where(migration_id: data[:migration_id]).first
expect(a.title).to eq data[:title]
expect(a.description).to include(data[:instructions]) if data[:instructions]
expect(a.description).to include(data[:description]) if data[:description]
@ -50,6 +51,7 @@ describe "Importing assignments" do
it "should import grading information when rubric is included" do
file_data = get_import_data('', 'assignment')
context = get_import_context('')
migration = context.content_migrations.create!
assignment_hash = file_data.find{|h| h['migration_id'] == '4469882339231'}.with_indifferent_access
@ -58,13 +60,14 @@ describe "Importing assignments" do
rubric.points_possible = 42
rubric.save!
Importers::AssignmentImporter.import_from_migration(assignment_hash, context)
Importers::AssignmentImporter.import_from_migration(assignment_hash, context, migration)
a = Assignment.where(migration_id: assignment_hash[:migration_id]).first
expect(a.points_possible).to eq rubric.points_possible
end
it "should infer the default name when importing a nameless assignment" do
course_model
migration = @course.content_migrations.create!
nameless_assignment_hash = {
"migration_id" => "ib4834d160d180e2e91572e8b9e3b1bc6",
"assignment_group_migration_id" => "i2bc4b8ea8fac88f1899e5e95d76f3004",
@ -84,13 +87,14 @@ describe "Importing assignments" do
"position" => 6,
"peer_review_count" => 0
}
Importers::AssignmentImporter.import_from_migration(nameless_assignment_hash, @course)
Importers::AssignmentImporter.import_from_migration(nameless_assignment_hash, @course, migration)
assignment = @course.assignments.where(migration_id: 'ib4834d160d180e2e91572e8b9e3b1bc6').first
expect(assignment.title).to eq 'untitled assignment'
end
it "should schedule auto peer reviews if dates are not shifted " do
course_model
migration = @course.content_migrations.create!
assign_hash = {
"migration_id" => "ib4834d160d180e2e91572e8b9e3b1bc6",
"assignment_group_migration_id" => "i2bc4b8ea8fac88f1899e5e95d76f3004",
@ -104,7 +108,7 @@ describe "Importing assignments" do
"peer_reviews_due_at" => 1401947999000
}
expects_job_with_tag('Assignment#do_auto_peer_review') {
Importers::AssignmentImporter.import_from_migration(assign_hash, @course)
Importers::AssignmentImporter.import_from_migration(assign_hash, @course, migration)
}
end
@ -122,14 +126,11 @@ describe "Importing assignments" do
"due_at" => 1401947999000,
"peer_reviews_due_at" => 1401947999000
}
migration = mock()
migration.stubs(:for_course_copy?)
migration.stubs(:add_missing_content_links)
migration.stubs(:add_imported_item)
migration = @course.content_migrations.create!
migration.stubs(:date_shift_options).returns(true)
expects_job_with_tag('Assignment#do_auto_peer_review', 0) {
Importers::AssignmentImporter.import_from_migration(assign_hash, @course, migration)
}
end
end

View File

@ -30,6 +30,7 @@ module Importers
let(:attachment) do
stub(:context= => true,
:migration_id= => true,
:migration_id => migration_id,
:save_without_broadcasting! => true,
:set_publish_state_for_usage_rights => nil)
end

View File

@ -23,6 +23,8 @@ describe Importers::CalendarEventImporter do
let_once(:migration_course) { course(active_all: true) }
let(:migration) { migration_course.content_migrations.create! }
let(:migration_assignment) do
assignment = migration_course.assignments.build(title: 'migration assignment')
assignment.workflow_state = 'active'
@ -80,7 +82,7 @@ describe Importers::CalendarEventImporter do
attachment_type: 'external_url',
attachment_value: 'http://example.com'
}
Importers::CalendarEventImporter.import_from_migration(hash, migration_course, nil, event)
Importers::CalendarEventImporter.import_from_migration(hash, migration_course, migration, event)
expect(event).not_to be_new_record
expect(event.imported).to be_truthy
expect(event.migration_id).to eq '42'
@ -143,14 +145,15 @@ describe Importers::CalendarEventImporter do
it "should import calendar events for #{system}" do
data = get_import_data(system, 'calendar_event')
context = get_import_context(system)
migration = context.content_migrations.create!
data[:events_to_import] = {}
expect(Importers::CalendarEventImporter.import_from_migration(data, context)).to be_nil
expect(Importers::CalendarEventImporter.import_from_migration(data, context, migration)).to be_nil
expect(context.calendar_events.count).to eq 0
data[:events_to_import][data[:migration_id]] = true
Importers::CalendarEventImporter.import_from_migration(data, context)
Importers::CalendarEventImporter.import_from_migration(data, context)
Importers::CalendarEventImporter.import_from_migration(data, context, migration)
Importers::CalendarEventImporter.import_from_migration(data, context, migration)
expect(context.calendar_events.count).to eq 1
event = CalendarEvent.where(migration_id: data[:migration_id]).first

View File

@ -3,14 +3,16 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
describe Importers::ContextExternalToolImporter do
it "should work for course-level tools" do
course_model
tool = Importers::ContextExternalToolImporter.import_from_migration({:title => 'tool', :url => 'http://example.com'}, @course)
migration = @course.content_migrations.create!
tool = Importers::ContextExternalToolImporter.import_from_migration({:title => 'tool', :url => 'http://example.com'}, @course, migration)
expect(tool).not_to be_nil
expect(tool.context).to eq @course
end
it "should work for account-level tools" do
course_model
tool = Importers::ContextExternalToolImporter.import_from_migration({:title => 'tool', :url => 'http://example.com'}, @course.account)
migration = @course.account.content_migrations.create!
tool = Importers::ContextExternalToolImporter.import_from_migration({:title => 'tool', :url => 'http://example.com'}, @course.account, migration)
expect(tool).not_to be_nil
expect(tool.context).to eq @course.account
end

View File

@ -25,13 +25,14 @@ describe "Importing modules" do
it "should import from #{system}" do
data = get_import_data(system, 'module')
context = get_import_context(system)
migration = context.content_migrations.create!
data[:modules_to_import] = {}
expect(Importers::ContextModuleImporter.import_from_migration(data, context)).to be_nil
expect(Importers::ContextModuleImporter.import_from_migration(data, context, migration)).to be_nil
expect(context.context_modules.count).to eq 0
data[:modules_to_import][data[:migration_id]] = true
Importers::ContextModuleImporter.import_from_migration(data, context)
Importers::ContextModuleImporter.import_from_migration(data, context)
Importers::ContextModuleImporter.import_from_migration(data, context, migration)
Importers::ContextModuleImporter.import_from_migration(data, context, migration)
expect(context.context_modules.count).to eq 1
mod = ContextModule.where(migration_id: data[:migration_id]).first
@ -40,30 +41,32 @@ describe "Importing modules" do
end
end
end
it "should link to url objects" do
data = get_import_data('vista', 'module')
context = get_import_context('vista')
migration = context.content_migrations.create!
context.external_url_hash = {}
topic = Importers::ContextModuleImporter.import_from_migration(data, context)
topic = Importers::ContextModuleImporter.import_from_migration(data, context, migration)
expect(topic.content_tags.count).to eq 2
end
it "should link to objects on the second pass" do
data = get_import_data('bb8', 'module')
context = get_import_context('bb8')
migration = context.content_migrations.create!
context.external_url_hash = {}
topic = Importers::ContextModuleImporter.import_from_migration(data, context)
topic = Importers::ContextModuleImporter.import_from_migration(data, context, migration)
expect(topic.content_tags.count).to eq 0
ass = get_import_data('bb8', 'assignment')
Importers::AssignmentImporter.import_from_migration(ass, context)
Importers::AssignmentImporter.import_from_migration(ass, context, migration)
expect(context.assignments.count).to eq 1
topic = Importers::ContextModuleImporter.import_from_migration(data, context)
topic = Importers::ContextModuleImporter.import_from_migration(data, context, migration)
expect(topic.content_tags.count).to eq 1
end

View File

@ -30,14 +30,15 @@ describe Importers::DiscussionTopicImporter do
data = data.with_indifferent_access
context = get_import_context(system)
migration = context.content_migrations.create!
data[:topics_to_import] = {}
expect(Importers::DiscussionTopicImporter.import_from_migration(data, context)).to be_nil
expect(Importers::DiscussionTopicImporter.import_from_migration(data, context, migration)).to be_nil
expect(context.discussion_topics.count).to eq 0
data[:topics_to_import][data[:migration_id]] = true
Importers::DiscussionTopicImporter.import_from_migration(data, context)
Importers::DiscussionTopicImporter.import_from_migration(data, context)
Importers::DiscussionTopicImporter.import_from_migration(data, context, migration)
Importers::DiscussionTopicImporter.import_from_migration(data, context, migration)
expect(context.discussion_topics.count).to eq 1
topic = DiscussionTopic.where(migration_id: data[:migration_id]).first
@ -62,13 +63,14 @@ describe Importers::DiscussionTopicImporter do
it "should import assignments for #{system}" do
data = get_import_data(system, 'announcements')
context = get_import_context(system)
migration = context.content_migrations.create!
data[:topics_to_import] = {}
expect(Importers::DiscussionTopicImporter.import_from_migration(data, context)).to be_nil
expect(Importers::DiscussionTopicImporter.import_from_migration(data, context, migration)).to be_nil
expect(context.discussion_topics.count).to eq 0
data[:topics_to_import][data[:migration_id]] = true
Importers::DiscussionTopicImporter.import_from_migration(data, context)
Importers::DiscussionTopicImporter.import_from_migration(data, context)
Importers::DiscussionTopicImporter.import_from_migration(data, context, migration)
Importers::DiscussionTopicImporter.import_from_migration(data, context, migration)
expect(context.discussion_topics.count).to eq 1
topic = DiscussionTopic.where(migration_id: data[:migration_id]).first
@ -82,12 +84,13 @@ describe Importers::DiscussionTopicImporter do
it "should not attach files when no attachment_migration_id is specified" do
data = get_import_data('bb8', 'discussion_topic').first.with_indifferent_access
context = get_import_context('bb8')
migration = context.content_migrations.create!
data[:attachment_migration_id] = nil
attachment_model(:context => context) # create a file with no migration id
data[:topics_to_import] = {data[:migration_id] => true}
Importers::DiscussionTopicImporter.import_from_migration(data, context)
Importers::DiscussionTopicImporter.import_from_migration(data, context, migration)
topic = DiscussionTopic.where(migration_id: data[:migration_id]).first
expect(topic.attachment).to be_nil

View File

@ -23,13 +23,14 @@ describe Importers::ExternalFeedImporter do
context ".import_from_migration" do
it "creates a feed from the provided hash" do
@course = course
migration = @course.content_migrations.create!
data = {
url: 'http://www.example.com/feed',
title: 'test feed',
verbosity: 'link_only',
header_match: ''
}
feed = Importers::ExternalFeedImporter.import_from_migration(data, @course)
feed = Importers::ExternalFeedImporter.import_from_migration(data, @course, migration)
expect(feed.url).to eq data[:url]
expect(feed.title).to eq data[:title]
expect(feed.verbosity).to eq data[:verbosity]

View File

@ -25,14 +25,15 @@ describe "Importing Groups" do
it "should import from #{system}" do
data = get_import_data(system, 'group')
context = get_import_context(system)
migration = context.content_migrations.create!
data[:groups_to_import] = {}
expect(Importers::GroupImporter.import_from_migration(data, context)).to be_nil
expect(Importers::GroupImporter.import_from_migration(data, context, migration)).to be_nil
expect(context.groups.count).to eq 0
data[:groups_to_import][data[:migration_id]] = true
Importers::GroupImporter.import_from_migration(data, context)
Importers::GroupImporter.import_from_migration(data, context)
Importers::GroupImporter.import_from_migration(data, context, migration)
Importers::GroupImporter.import_from_migration(data, context, migration)
expect(context.groups.count).to eq 1
g = Group.where(migration_id: data[:migration_id]).first
@ -44,8 +45,9 @@ describe "Importing Groups" do
it "should attach to a discussion" do
data = get_import_data('bb8', 'group')
context = get_import_context('bb8')
migration = context.content_migrations.create!
Importers::GroupImporter.import_from_migration(data, context)
Importers::GroupImporter.import_from_migration(data, context, migration)
expect(context.groups.count).to eq 1
category = get_import_data('bb8', 'group_discussion')
@ -54,7 +56,7 @@ describe "Importing Groups" do
topic['group_id'] = category['group_id']
group = Group.where(context_id: context, context_type: context.class.to_s, migration_id: topic['group_id']).first
if group
Importers::DiscussionTopicImporter.import_from_migration(topic, group)
Importers::DiscussionTopicImporter.import_from_migration(topic, group, migration)
end
end
@ -64,15 +66,17 @@ describe "Importing Groups" do
it "should respect group_category from the hash" do
course_with_teacher
migration = @course.content_migrations.create!
group = @course.groups.build
Importers::GroupImporter.import_from_migration({:group_category => "random category"}, @course, nil, group)
Importers::GroupImporter.import_from_migration({:group_category => "random category"}, @course, migration, group)
expect(group.group_category.name).to eq "random category"
end
it "should default group_category to imported if not in the hash" do
course_with_teacher
migration = @course.content_migrations.create!
group = @course.groups.build
Importers::GroupImporter.import_from_migration({}, @course, nil, group)
Importers::GroupImporter.import_from_migration({}, @course, migration, group)
expect(group.group_category).to eq GroupCategory.imported_for(@course)
end
end

View File

@ -21,13 +21,14 @@ require File.expand_path(File.dirname(__FILE__) + '../../../import_helper')
describe "Importers::QuizImporter" do
before(:once) do
course_model
@migration = @course.content_migrations.create!
end
it "should get the quiz properties" do
context = course_model
question_data = import_example_questions context
data = get_import_data ['vista', 'quiz'], 'simple_quiz_data'
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
quiz = Quizzes::Quiz.where(migration_id: data[:migration_id]).first
expect(quiz.title).to eq data[:title]
expect(quiz.scoring_policy).to eq data[:which_attempt_to_keep]
@ -37,23 +38,23 @@ describe "Importers::QuizImporter" do
expect(quiz.shuffle_answers).to eq data[:shuffle_answers]
expect(quiz.show_correct_answers).to eq data[:show_correct_answers]
end
it "should complete a quiz question reference" do
context = course_model
question_data = import_example_questions context
data = get_import_data ['vista', 'quiz'], 'simple_quiz_data'
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
quiz = Quizzes::Quiz.where(migration_id: data[:migration_id]).first
expect(quiz.quiz_questions.active.count).to eq 1
# Check if the expected question name is in there
expect(quiz.quiz_questions.active.first.question_data[:question_name]).to eq "Rocket Bee!"
end
it "should import a text only question" do
context = get_import_context
question_data = import_example_questions context
data = get_import_data ['vista', 'quiz'], 'text_only_quiz_data'
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
quiz = Quizzes::Quiz.where(migration_id: data[:migration_id]).first
expect(quiz.unpublished_question_count).to eq 2
expect(quiz.quiz_questions.active.count).to eq 2
@ -61,12 +62,12 @@ describe "Importers::QuizImporter" do
expect(sorted_questions.first.question_data[:question_text]).to eq data[:questions].first[:question_text]
expect(sorted_questions.first.question_data[:question_type]).to eq 'text_only_question'
end
it "should import a question group" do
context = get_import_context
question_data = import_example_questions context
data = get_import_data ['vista', 'quiz'], 'group_quiz_data'
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
quiz = Quizzes::Quiz.where(migration_id: data[:migration_id]).first
expect(quiz.quiz_groups.count).to eq 1
expect(quiz.quiz_groups.first.quiz_questions.active.count).to eq 3
@ -78,8 +79,8 @@ describe "Importers::QuizImporter" do
context = get_import_context
question_data = import_example_questions context
data = get_import_data ['vista', 'quiz'], 'text_only_quiz_data'
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
expect(Quizzes::Quiz.count).to eq 1
quiz = Quizzes::Quiz.where(migration_id: data[:migration_id]).first
expect(quiz.assignment).to be_nil
@ -91,7 +92,7 @@ describe "Importers::QuizImporter" do
quiz_hash = get_import_data ['vista', 'quiz'], 'simple_quiz_data'
data = {'assessments' => {'assessments' => [quiz_hash]}}
migration = context.content_migrations.create!
Importers::CourseContentImporter.import_content(context, data, nil, migration)
Importers::CourseContentImporter.import_content(context, data, @migration, migration)
expect(Assignment.count).to eq 0
expect(Quizzes::Quiz.count).to eq 1
@ -111,7 +112,7 @@ describe "Importers::QuizImporter" do
data = {'assessments' => {'assessments' => [quiz_hash]}, 'assignments' => [assignment_hash]}
migration = context.content_migrations.create!
Importers::CourseContentImporter.import_content(context, data, nil, migration)
Importers::CourseContentImporter.import_content(context, data, @migration, migration)
expect(Assignment.count).to eq 1
expect(Quizzes::Quiz.count).to eq 1
@ -121,10 +122,12 @@ describe "Importers::QuizImporter" do
expect(quiz.assignment).not_to be_nil
expect(quiz.quiz_type).to eq 'assignment'
end
it "should convert relative file references to course-relative file references" do
context = course_model
context = @course
import_example_questions context
@migration.resolve_content_links!
question = AssessmentQuestion.where(migration_id: '4393906433391').first
expect(question.data[:question_text]).to eq "Why does that bee/rocket ship company suck? <img src=\"/courses/#{context.id}/file_contents/course%20files/rocket.png\">"
question = AssessmentQuestion.where(migration_id: 'URN-X-WEBCT-VISTA_V2-790EA1350E1A681DE0440003BA07D9B4').first
@ -135,13 +138,13 @@ describe "Importers::QuizImporter" do
context = get_import_context
question_data = import_example_questions context
data = get_import_data ['vista', 'quiz'], 'simple_quiz_data'
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
quiz = Quizzes::Quiz.where(migration_id: data[:migration_id]).first
expect(quiz.quiz_questions.active.first.question_data[:question_name]).to eq "Rocket Bee!"
question_data[:aq_data][data['questions'].first[:migration_id]]['question_name'] = "Not Rocket Bee?"
Importers::QuizImporter.import_from_migration(data, context, nil, question_data)
Importers::QuizImporter.import_from_migration(data, context, @migration, question_data)
expect(quiz.quiz_questions.active.first.question_data[:question_name]).to eq "Not Rocket Bee?"
end

View File

@ -25,9 +25,10 @@ describe "Importing wikis" do
it "should import for #{system}" do
data = get_import_data(system, 'wiki')
context = get_import_context(system)
migration = context.content_migrations.create!
Importers::WikiPageImporter.import_from_migration(data, context)
Importers::WikiPageImporter.import_from_migration(data, context)
Importers::WikiPageImporter.import_from_migration(data, context, migration)
Importers::WikiPageImporter.import_from_migration(data, context, migration)
expect(context.wiki.wiki_pages.count).to eq 1
wiki = WikiPage.where(migration_id: data[:migration_id]).first
@ -39,12 +40,14 @@ describe "Importing wikis" do
it "should update BB9 wiki page links to the correct url" do
data = get_import_data('bb9', 'wikis')
context = get_import_context('bb9')
migration = context.content_migrations.create!
2.times do
data.each do |wiki|
Importers::WikiPageImporter.import_from_migration(wiki, context)
Importers::WikiPageImporter.import_from_migration(wiki, context, migration)
end
end
migration.resolve_content_links!
# The wiki references should resolve to course urls
expect(context.wiki.wiki_pages.count).to eq 18
wiki = WikiPage.where(migration_id: 'res00146').first