diff --git a/app/models/assignment.rb b/app/models/assignment.rb index eecd41fb1db..66ae3d0cbcd 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -1373,9 +1373,17 @@ class Assignment < ActiveRecord::Base end timestamp = hash[:due_date].to_i rescue 0 item.due_at = Time.at(timestamp / 1000) if timestamp > 0 + item.assignment_group ||= context.assignment_groups.find_by_migration_id(hash[:assignment_group_migration_id]) if hash[:assignment_group_migration_id] item.assignment_group ||= context.assignment_groups.find_or_create_by_name("Imported Assignments") context.imported_migration_items << item if context.imported_migration_items && item.new_record? item.save_without_broadcasting! + + if context.respond_to?(:assignment_group_no_drop_assignments) && context.assignment_group_no_drop_assignments + if group = context.assignment_group_no_drop_assignments[item.migration_id] + AssignmentGroup.add_never_drop_assignment(group, item) + end + end + item end diff --git a/app/models/assignment_group.rb b/app/models/assignment_group.rb index c9e67bbdbec..b4923d54a48 100644 --- a/app/models/assignment_group.rb +++ b/app/models/assignment_group.rb @@ -194,10 +194,35 @@ class AssignmentGroup < ActiveRecord::Base context.imported_migration_items << item if context.imported_migration_items && item.new_record? item.migration_id = hash[:migration_id] item.name = hash[:title] + item.position = hash[:position].to_i if hash[:position] && hash[:position].to_i > 0 + item.group_weight = hash[:group_weight] if hash[:group_weight] + + if hash[:rules] && hash[:rules].length > 0 + rules = "" + hash[:rules].each do |rule| + if rule[:drop_type] == "drop_lowest" || rule[:drop_type] == "drop_highest" + rules += "#{rule[:drop_type]}:#{rule[:drop_count]}\n" + elsif rule[:drop_type] == "never_drop" + if context.respond_to?(:assignment_group_no_drop_assignments) + context.assignment_group_no_drop_assignments[rule[:assignment_migration_id]] = item + end + end + end + item.rules = rules unless rules == '' + end item.save! - context.imported_migration_items << item item end + + def self.add_never_drop_assignment(group, assignment) + rule = "never_drop:#{assignment.id}\n" + if group.rules + group.rules += rule + else + group.rules = rule + end + group.save + end end diff --git a/app/models/content_export.rb b/app/models/content_export.rb index efe4a36a1ee..d933b30aa5a 100644 --- a/app/models/content_export.rb +++ b/app/models/content_export.rb @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . # -require 'cc/cc' +require 'cc' class ContentExport < ActiveRecord::Base include Workflow belongs_to :course diff --git a/app/models/context_external_tool.rb b/app/models/context_external_tool.rb index 1804c0f27a5..c3c24bb45ae 100644 --- a/app/models/context_external_tool.rb +++ b/app/models/context_external_tool.rb @@ -168,4 +168,34 @@ class ContextExternalTool < ActiveRecord::Base named_scope :active, :conditions => ['context_external_tools.workflow_state != ?', 'deleted'] def self.serialization_excludes; [:shared_secret,:settings]; end + + def self.process_migration(data, migration) + tools = data['external_tools'] ? data['external_tools']: [] + to_import = migration.to_import 'external_tools' + tools.each do |tool| + if tool['migration_id'] && (!to_import || to_import[tool['migration_id']]) + import_from_migration(tool, migration.context) + end + end + end + + def self.import_from_migration(hash, context, 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]] + item ||= find_by_context_id_and_context_type_and_migration_id(context.id, context.class.to_s, hash[:migration_id]) if hash[:migration_id] + item ||= context.context_external_tools.new + item.migration_id = hash[:migration_id] + item.name = hash[:title] + item.description = hash[:description] + item.url = hash[:url] unless hash[:url].blank? + item.domain = hash[:domain] unless hash[:domain].blank? + item.privacy_level = hash[:privacy_level] + item.consumer_key = 'fake' + item.shared_secret = 'fake' + + item.save! + context.imported_migration_items << item if context.imported_migration_items && item.new_record? + item + end + end diff --git a/app/models/course.rb b/app/models/course.rb index a1b9c426b31..7e688e27a31 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -1176,6 +1176,8 @@ class Course < ActiveRecord::Base @imported_migration_items = [] # These only need to be processed once + import_settings_from_migration(data) + migration.fast_update_progress(0.5) process_migration_files(data, migration) migration.fast_update_progress(20) Attachment.process_migration(data, migration) @@ -1185,10 +1187,13 @@ class Course < ActiveRecord::Base Group.process_migration(data, migration) LearningOutcome.process_migration(data, migration) Rubric.process_migration(data, migration) + @assignment_group_no_drop_assignments = {} AssignmentGroup.process_migration(data, migration) migration.fast_update_progress(40) Quiz.process_migration(data, migration, question_data) migration.fast_update_progress(50) + #todo - Import external tools when there are post-migration messages to tell the user to add shared secret/password + #ContextExternalTool.process_migration(data, migration) 2.times do |i| DiscussionTopic.process_migration(data, migration) @@ -1236,6 +1241,25 @@ class Course < ActiveRecord::Base attr_accessor :full_migration_hash attr_accessor :external_url_hash attr_accessor :folder_name_lookups + attr_accessor :assignment_group_no_drop_assignments + + def import_settings_from_migration(data) + return unless data[:course_settings] + settings = data[:course_settings] + self.name = settings['title'] + dates = ['conclude_at', 'start_at'] + settings.slice(*Course.clonable_attributes.map(&:to_s)).each do |key, val| + if dates.member? key + timestamp = val.to_i/ 1000 rescue 0 + if timestamp > 0 + t = Time.at(timestamp) + self.send("#{key}=", Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec)) + end + else + self.send("#{key}=", val) + end + end + end def backup_to_json backup.to_json diff --git a/app/views/content_imports/_cc_config.html.erb b/app/views/content_imports/_cc_config.html.erb new file mode 100644 index 00000000000..48e5a882fde --- /dev/null +++ b/app/views/content_imports/_cc_config.html.erb @@ -0,0 +1,11 @@ +<% js_block do %> + +<% end %> \ No newline at end of file diff --git a/config/initializers/cc_importer.rb b/config/initializers/cc_importer.rb new file mode 100644 index 00000000000..64b30963522 --- /dev/null +++ b/config/initializers/cc_importer.rb @@ -0,0 +1,3 @@ + +# Register the common cartridge importer as a plugin +require 'cc/importer/init.rb' \ No newline at end of file diff --git a/db/migrate/20110411214502_add_migration_ids_for_cc_importing.rb b/db/migrate/20110411214502_add_migration_ids_for_cc_importing.rb new file mode 100644 index 00000000000..b6e218af6c9 --- /dev/null +++ b/db/migrate/20110411214502_add_migration_ids_for_cc_importing.rb @@ -0,0 +1,15 @@ +class AddMigrationIdsForCcImporting < ActiveRecord::Migration + def self.up + add_column :context_external_tools, :migration_id, :string + add_column :external_feeds, :migration_id, :string + add_column :grading_standards, :migration_id, :string + add_column :learning_outcome_groups, :migration_id, :string + end + + def self.down + remove_column :context_external_tools, :migration_id + remove_column :external_feeds, :migration_id + remove_column :grading_standards, :migration_id + remove_column :learning_outcome_groups, :migration_id + end +end diff --git a/lib/canvas_migration/migrator.rb b/lib/canvas_migration/migrator.rb index f52f47b80bb..8205640e3a0 100644 --- a/lib/canvas_migration/migrator.rb +++ b/lib/canvas_migration/migrator.rb @@ -26,18 +26,21 @@ class Canvas::Migrator def initialize(settings, migration_type) @settings = settings @settings[:migration_type] = migration_type - download_archive + @manifest = nil + @error_count = 0 + @errors = [] @course = {:file_map=>{}, :wikis=>[]} + @course[:name] = @settings[:course_name] + + return if settings[:testing] + + download_archive @archive_file_path = @settings[:export_archive_path] raise "The #{migration_type} archive zip wasn't at the specified location: #{@archive_file_path}." if @archive_file_path.nil? or !File.exists?(@archive_file_path) @unzipped_file_path = File.join(File.dirname(@archive_file_path), "#{migration_type}_#{File.basename(@archive_file_path, '.zip')}") @base_export_dir = @settings[:base_download_dir] || find_export_dir @course[:export_folder_path] = File.expand_path(@base_export_dir) - @course[:name] = @settings[:course_name] make_export_dir - @manifest = nil - @error_count = 0 - @errors = [] end def export diff --git a/lib/canvas_migration/xml_helper.rb b/lib/canvas_migration/xml_helper.rb index 1e3a71e83b4..33f0e8b321c 100644 --- a/lib/canvas_migration/xml_helper.rb +++ b/lib/canvas_migration/xml_helper.rb @@ -22,6 +22,13 @@ module Canvas::XMLHelper Time.parse(string).to_i * 1000 rescue nil end + def get_node_att(node, selector, attribute, default=nil) + if node = node.at_css(selector) + return node[attribute] + end + default + end + def get_node_val(node, selector, default=nil) node.at_css(selector) ? node.at_css(selector).text : default end @@ -68,10 +75,10 @@ module Canvas::XMLHelper end def open_file(path) - ::Nokogiri::HTML(open(path)) + File.exists?(path) ? ::Nokogiri::HTML(open(path)) : nil end def open_file_xml(path) - ::Nokogiri::XML(open(path)) + File.exists?(path) ? ::Nokogiri::XML(open(path)) : nil end end \ No newline at end of file diff --git a/lib/cc/cc.rb b/lib/cc.rb similarity index 98% rename from lib/cc/cc.rb rename to lib/cc.rb index 1b0b4e79a29..54b2de3d5f6 100644 --- a/lib/cc/cc.rb +++ b/lib/cc.rb @@ -38,3 +38,4 @@ require "cc/web_links" require 'cc/resource' require 'cc/organization' require 'cc/qti/qti' +require 'cc/importer' diff --git a/lib/cc/assignment_groups.rb b/lib/cc/assignment_groups.rb index f06dacdac59..80aead1a3d0 100644 --- a/lib/cc/assignment_groups.rb +++ b/lib/cc/assignment_groups.rb @@ -17,12 +17,18 @@ # module CC module AssignmentGroups - def create_assignment_groups + def create_assignment_groups(document=nil) return nil unless @course.assignment_groups.active.count > 0 - group_file = File.new(File.join(@canvas_resource_dir, CCHelper::ASSIGNMENT_GROUPS), 'w') - rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::ASSIGNMENT_GROUPS) - document = Builder::XmlMarkup.new(:target=>group_file, :indent=>2) + if document + group_file = nil + rel_path = nil + else + group_file = File.new(File.join(@canvas_resource_dir, CCHelper::ASSIGNMENT_GROUPS), 'w') + rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::ASSIGNMENT_GROUPS) + document = Builder::XmlMarkup.new(:target=>group_file, :indent=>2) + end + document.instruct! document.assignmentGroups( "xmlns" => CCHelper::CANVAS_NAMESPACE, @@ -60,7 +66,7 @@ module CC end end - group_file.close + group_file.close if group_file rel_path end end diff --git a/lib/cc/basic_lti_links.rb b/lib/cc/basic_lti_links.rb index ddc1df39a45..32489234533 100644 --- a/lib/cc/basic_lti_links.rb +++ b/lib/cc/basic_lti_links.rb @@ -63,12 +63,18 @@ module CC end - def create_external_tools + def create_external_tools(document=nil) return nil unless @course.context_external_tools.count > 0 - lti_file = File.new(File.join(@canvas_resource_dir, CCHelper::EXTERNAL_TOOLS), 'w') - rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::EXTERNAL_TOOLS) - document = Builder::XmlMarkup.new(:target=>lti_file, :indent=>2) + if document + lti_file = nil + rel_path = nil + else + lti_file = File.new(File.join(@canvas_resource_dir, CCHelper::EXTERNAL_TOOLS), 'w') + rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::EXTERNAL_TOOLS) + document = Builder::XmlMarkup.new(:target=>lti_file, :indent=>2) + end + document.instruct! document.externalTools( "xmlns" => CCHelper::CANVAS_NAMESPACE, @@ -88,7 +94,7 @@ module CC end end - lti_file.close + lti_file.close if lti_file rel_path end end diff --git a/lib/cc/canvas_resource.rb b/lib/cc/canvas_resource.rb index d3f933be009..cd75dc85ea3 100644 --- a/lib/cc/canvas_resource.rb +++ b/lib/cc/canvas_resource.rb @@ -59,10 +59,15 @@ module CC end end - def create_course_settings(migration_id) - course_file = File.new(File.join(@canvas_resource_dir, CCHelper::COURSE_SETTINGS), 'w') - rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::COURSE_SETTINGS) - document = Builder::XmlMarkup.new(:target=>course_file, :indent=>2) + def create_course_settings(migration_id, document=nil) + if document + course_file = nil + rel_path = nil + else + course_file = File.new(File.join(@canvas_resource_dir, CCHelper::COURSE_SETTINGS), 'w') + rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::COURSE_SETTINGS) + document = Builder::XmlMarkup.new(:target=>course_file, :indent=>2) + end document.instruct! document.course("identifier" => migration_id, "xmlns" => CCHelper::CANVAS_NAMESPACE, @@ -75,10 +80,10 @@ module CC atts = Course.clonable_attributes atts -= [:name, :start_at, :conclude_at, :grading_standard_id, :hidden_tabs, :tab_configuration, :syllabus_body] atts.each do |att| - c.tag!(att, @course.send(att)) unless @course.send(att).blank? + c.tag!(att, @course.send(att)) unless @course.send(att).nil? || @course.send(att) == '' end end - course_file.close + course_file.close if course_file rel_path end end diff --git a/lib/cc/cc_helper.rb b/lib/cc/cc_helper.rb index e9ebbe74c8b..4c8100968d9 100644 --- a/lib/cc/cc_helper.rb +++ b/lib/cc/cc_helper.rb @@ -80,12 +80,12 @@ module CCHelper def self.ims_date(date=nil) date ||= Time.now - date.strftime(IMS_DATE) + date.utc.strftime(IMS_DATE) end def self.ims_datetime(date=nil) date ||= Time.now - date.strftime(IMS_DATETIME) + date.utc.strftime(IMS_DATETIME) end def self.html_page(html, title, course, user) diff --git a/lib/cc/importer.rb b/lib/cc/importer.rb new file mode 100644 index 00000000000..297e847aeba --- /dev/null +++ b/lib/cc/importer.rb @@ -0,0 +1,27 @@ +# +# Copyright (C) 2011 Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# + +module CC + module Importer + include CC::CCHelper + include Canvas::XMLHelper + end +end + +require 'cc/importer/course_settings' +require 'cc/importer/cc_converter' diff --git a/lib/cc/importer/cc_converter.rb b/lib/cc/importer/cc_converter.rb new file mode 100644 index 00000000000..433c7ef7c72 --- /dev/null +++ b/lib/cc/importer/cc_converter.rb @@ -0,0 +1,46 @@ +# +# Copyright (C) 2011 Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# +module CC::Importer + class CCConverter < Canvas::Migrator + include CC::Importer + include CourseSettings + + MANIFEST_FILE = "imsmanifest.xml" + + # settings will use these keys: :course_name, :base_download_dir + def initialize(settings) + super(settings, "cc") + end + + # exports the package into the intermediary json + def export(to_export = SCRAPE_ALL_HASH) + to_export = SCRAPE_ALL_HASH.merge to_export if to_export + unzip_archive + + @manifest = open_file(File.join(@unzipped_file_path, MANIFEST_FILE)) + convert_non_dependant_course_settings + + + #close up shop + save_to_file + delete_unzipped_archive + @course + end + + end +end diff --git a/lib/cc/importer/cc_worker.rb b/lib/cc/importer/cc_worker.rb new file mode 100644 index 00000000000..5a3f421e4ea --- /dev/null +++ b/lib/cc/importer/cc_worker.rb @@ -0,0 +1,54 @@ +# +# Copyright (C) 2011 Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# +module Canvas + module MigrationWorker + class CCWorker < Struct.new(:migration_id) + + def perform + cm = ContentMigration.find migration_id + settings = cm.migration_settings.clone + settings[:content_migration_id] = migration_id + settings[:user_id] = cm.user_id + settings[:attachment_id] = cm.attachment.id rescue nil + + converter = CC::Importer.CCConverter.new(settings) + course = converter.export + export_folder_path = course[:export_folder_path] + overview_file_path = course[:overview_file_path] + + if overview_file_path + file = File.new(overview_file_path) + Canvas::MigrationWorker::upload_overview_file(file, cm) + end + if export_folder_path + Canvas::MigrationWorker::upload_exported_data(export_folder_path, cm) + Canvas::MigrationWorker::clear_exported_data(export_folder_path) + end + + cm.migration_settings[:migration_ids_to_import] = {:copy=>{:assessment_questions=>true}} + cm.workflow_state = :exported + cm.progress = 0 + cm.save + end + + def self.enqueue(content_migration) + Delayed::Job.enqueue(new(content_migration.id), :priority => Delayed::LOW_PRIORITY) + end + end + end +end diff --git a/lib/cc/importer/course_settings.rb b/lib/cc/importer/course_settings.rb new file mode 100644 index 00000000000..f7d25fca03f --- /dev/null +++ b/lib/cc/importer/course_settings.rb @@ -0,0 +1,104 @@ +# +# Copyright (C) 2011 Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# +module CC::Importer + module CourseSettings + include CC::Importer + + def course_settings_doc(file) + open_file_xml File.join(@unzipped_file_path, COURSE_SETTINGS_DIR, file) + end + + def convert_non_dependant_course_settings + @course[:course] = convert_course_settings(course_settings_doc(COURSE_SETTINGS)) + @course[:assignment_groups] = convert_assignment_groups(course_settings_doc(ASSIGNMENT_GROUPS)) + @course[:external_tools] = convert_external_tools(course_settings_doc(EXTERNAL_TOOLS)) + end + + def convert_course_settings(doc) + course = {} + return course unless doc + course[:migration_id] = get_node_att(doc, 'course', 'identifier') + + ['title', 'course_code', 'hashtag', 'default_wiki_editing_roles', + 'turnitin_comments', 'default_view', 'license', + 'group_weighting_scheme'].each do |string_type| + val = get_node_val(doc, string_type) + course[string_type] = val unless val.nil? + end + ['is_public', 'indexed', 'publish_grades_immediately', 'allow_student_wiki_edits', + 'allow_student_assignment_edits', 'show_public_context_messages', + 'allow_student_forum_attachments', 'allow_student_organized_groups', + 'show_all_discussion_entries', 'open_enrollment', 'allow_wiki_comments', + 'self_enrollment'].each do |bool_val| + val = get_bool_val(doc, bool_val) + course[bool_val] = val unless val.nil? + end + ['start_at', 'conclude_at'].each do |date_type| + val = get_time_val(doc, date_type) + course[date_type] = val unless val.nil? + end + + course['storage_quota'] = get_int_val(doc, 'storage_quota') + + course + end + + def convert_assignment_groups(doc = nil) + groups = [] + return groups unless doc + doc.css('assignmentGroup').each do |node| + group = {} + group['migration_id'] = node['identifier'] + group['title'] = get_node_val(node, 'title') + group['position'] = get_int_val(node, 'position') + group['group_weight'] = get_float_val(node, 'group_weight') + group['rules'] = [] + node.css('rules rule').each do |r_node| + rule = {} + rule['drop_type'] = get_node_val(r_node, 'drop_type') + rule['drop_count'] = get_int_val(r_node, 'drop_count') + rule['assignment_migration_id'] = get_node_val(r_node, 'identifierref') + group['rules'] << rule + end + + groups << group + end + + groups + end + + def convert_external_tools(doc) + tools = [] + return tools unless doc + doc.css('externalTool').each do |node| + tool = {} + tool['migration_id'] = node['identifier'] + tool['title'] = get_node_val(node, 'title') + tool['description'] = get_node_val(node, 'description') + tool['domain'] = get_node_val(node, 'domain') + tool['url'] = get_node_val(node, 'url') + tool['privacy_level'] = get_node_val(node, 'privacy_level') + + tools << tool + end + + tools + end + + end +end \ No newline at end of file diff --git a/lib/cc/importer/init.rb b/lib/cc/importer/init.rb new file mode 100644 index 00000000000..ea895325243 --- /dev/null +++ b/lib/cc/importer/init.rb @@ -0,0 +1,33 @@ +# +# Copyright (C) 2011 Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# + +require 'canvas/plugin' +require 'cc/importer/cc_worker' +Rails.configuration.to_prepare do + Canvas::Plugin.register :common_cartridge_importer, :export_system, { + :name => 'Common Cartridge Importer', + :author => 'Instructurecon', + :description => 'This enables converting a canvas CC export to the intermediary json format to be imported', + :version => '1.0.0', + :settings => { + :worker=>'CCWorker', + :migration_partial => 'cc_config', + :select_text => "Canvas Course Export (.imscc)" + } + } +end \ No newline at end of file diff --git a/lib/imported_html_converter.rb b/lib/imported_html_converter.rb index dd7ba019592..ef2af3bd235 100644 --- a/lib/imported_html_converter.rb +++ b/lib/imported_html_converter.rb @@ -20,27 +20,49 @@ class ImportedHtmlConverter include TextHelper def self.convert(html, context) doc = Nokogiri::HTML(html || "") - root_folder_name = nil attrs = ['rel', 'href', 'src', 'data', 'value'] + course_path = "/#{context.class.to_s.underscore.pluralize}/#{context.id}" + root_folder_name = Folder.root_folders(context).first.name doc.search("*").each do |node| attrs.each do |attr| if node[attr] if node[attr] =~ /wiki_page_migration_id=([^'"]*)/ - # This would be from a BB9 migration. + # 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.find_by_migration_id(wiki_migration_id) - node[attr] = URI::escape("/#{context.class.to_s.underscore.pluralize}/#{context.id}/wiki/#{linked_wiki.url}") + node[attr] = URI::escape("#{course_path}/wiki/#{linked_wiki.url}") end end elsif node[attr] =~ /discussion_topic_migration_id=([^'"]*)/ if topic_migration_id = $1 if linked_topic = context.discussion_topics.find_by_migration_id(topic_migration_id) - node[attr] = URI::escape("/#{context.class.to_s.underscore.pluralize}/#{context.id}/discussion_topics/#{linked_topic.id}") + node[attr] = URI::escape("#{course_path}/discussion_topics/#{linked_topic.id}") end end + elsif node[attr] =~ %r{(?:\$CANVAS_OBJECT_REFERENCE\$|\$WIKI_REFERENCE\$)/([^/]*)/([^'"]*)} + type = $1 + migration_id = $2 + if type == 'wiki' + if page = context.wiki.wiki_pages.find_by_url(migration_id) + node[attr] = URI::escape("#{course_path}/wiki/#{page.url}") + end + elsif context.respond_to?(type) && context.send(type).respond_to?(:find_by_migration_id) + if object = context.send(type).find_by_migration_id(migration_id) + node[attr] = URI::escape("#{course_path}/#{type}/#{object.id}") + end + end + elsif node[attr] =~ %r{\$CANVAS_COURSE_REFERENCE\$/([^'"]*)} + section = $1 + node[attr] = URI::escape("#{course_path}/#{section}") + elsif node[attr] =~ %r{\$IMS_CC_FILEBASE\$/([^'"]*)} + rel_path = $1 + # the rel_path should already be escaped + node[attr] = URI::escape("#{course_path}/file_contents/#{root_folder_name}/") + rel_path elsif relative_url?(node[attr]) - root_folder_name ||= Folder.root_folders(context).first.name - node[attr] = URI::escape("/#{context.class.to_s.underscore.pluralize}/#{context.id}/file_contents/#{root_folder_name}/") + node[attr] + # the rel_path should already be escaped + node[attr] = URI::escape("#{course_path}/file_contents/#{root_folder_name}/") + node[attr] end end end diff --git a/spec/lib/cc/cc_spec_helper.rb b/spec/lib/cc/cc_spec_helper.rb new file mode 100644 index 00000000000..8d60228cf2a --- /dev/null +++ b/spec/lib/cc/cc_spec_helper.rb @@ -0,0 +1,29 @@ +# +# Copyright (C) 2011 Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# + +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') + +CC_XML_EXPORT_DIR = File.dirname(__FILE__) + '/../../fixtures/cc/cc_export' + +def get_cc_converter + CC::Importer::CCConverter.new({:testing=>true}) +end + +def get_cc_export_file(rel_path) + File.join(CC_XML_EXPORT_DIR, rel_path) +end diff --git a/spec/lib/cc/importer/cc_converter_spec.rb b/spec/lib/cc/importer/cc_converter_spec.rb new file mode 100644 index 00000000000..e7bce0cfc90 --- /dev/null +++ b/spec/lib/cc/importer/cc_converter_spec.rb @@ -0,0 +1,167 @@ +require File.dirname(__FILE__) + '/../cc_spec_helper' + +describe "Common Cartridge importing" do + before(:each) do + @converter = get_cc_converter + @copy_from = course_model + @from_teacher = @user + @copy_to = course_model + + exporter = CC::CCExporter.new(nil, :course=>@copy_from, :user=>@from_teacher) + manifest = CC::Manifest.new(exporter) + @resource = CC::Resource.new(manifest, nil) + @migration = Object.new + @migration.stub!(:to_import).and_return(nil) + @migration.stub!(:context).and_return(@copy_to) + end + + it "should import course settings" do + #set all the possible values to non-default values + @copy_from.start_at = 5.minutes.ago + @copy_from.conclude_at = 1.month.from_now + @copy_from.is_public = false + @copy_from.name = "haha copy from test &" + @copy_from.course_code = 'something funny' + @copy_from.publish_grades_immediately = false + @copy_from.allow_student_wiki_edits = true + @copy_from.allow_student_assignment_edits = true + @copy_from.hashtag = 'oi' + @copy_from.show_public_context_messages = false + @copy_from.allow_student_forum_attachments = false + @copy_from.default_wiki_editing_roles = 'teachers' + @copy_from.allow_student_organized_groups = false + @copy_from.default_view = 'modules' + @copy_from.show_all_discussion_entries = false + @copy_from.open_enrollment = true + @copy_from.storage_quota = 444 + @copy_from.allow_wiki_comments = true + @copy_from.turnitin_comments = "Don't plagiarize" + @copy_from.self_enrollment = true + @copy_from.license = "cc_by_nc_nd" + + @copy_from.save! + + #export to xml + builder = Builder::XmlMarkup.new(:indent=>2) + @resource.create_course_settings("1", builder) + #convert to json + doc = Nokogiri::XML(builder.target!) + hash = @converter.convert_course_settings(doc) + #import json into new course + hash = hash.with_indifferent_access + @copy_to.import_settings_from_migration({:course_settings=>hash}) + @copy_to.save! + + #compare settings + @copy_to.conclude_at.to_i.should == @copy_from.conclude_at.to_i + @copy_to.start_at.to_i.should == @copy_from.start_at.to_i + atts = Course.clonable_attributes + atts -= [:start_at, :conclude_at, :grading_standard_id, :hidden_tabs, :tab_configuration, :syllabus_body] + atts.each do |att| + @copy_to.send(att).should == @copy_from.send(att) + end + end + + it "should convert assignment groups" do + ag1 = @copy_from.assignment_groups.new + ag1.name = "Boring assignments" + ag1.position = 1 + ag1.group_weight = 77.7 + ag1.save! + ag2 = @copy_from.assignment_groups.new + ag2.name = "Super not boring assignments" + ag2.position = 2 + ag2.group_weight = 20 + ag2.save! + a = ag2.assignments.new + a.title = "Can't drop me" + a.context = @copy_from + a.save! + ag2.rules = "drop_lowest:2\ndrop_highest:5\nnever_drop:%s\n" % a.id + ag2.save! + + #export to xml + builder = Builder::XmlMarkup.new(:indent=>2) + @resource.create_assignment_groups(builder) + #convert to json + doc = Nokogiri::XML(builder.target!) + hash = @converter.convert_assignment_groups(doc) + #import json into new course + @copy_to.assignment_group_no_drop_assignments = {} + AssignmentGroup.process_migration({'assignment_groups'=>hash}, @migration) + @copy_to.save! + + #compare settings + ag1_2 = @copy_to.assignment_groups.find_by_migration_id(CC::CCHelper.create_key(ag1)) + ag1_2.name.should == ag1.name + ag1_2.position.should == ag1.position + ag1_2.group_weight.should == ag1.group_weight + ag1_2.rules.should == ag1.rules + + ag2_2 = @copy_to.assignment_groups.find_by_migration_id(CC::CCHelper.create_key(ag2)) + ag2_2.name.should == ag2.name + ag2_2.position.should == ag2.position + ag2_2.group_weight.should == ag2.group_weight + ag2_2.rules.should == "drop_lowest:2\ndrop_highest:5\n" + + #import assignment + hash = {:migration_id=>CC::CCHelper.create_key(a), + :title=>a.title, + :assignment_group_migration_id=>CC::CCHelper.create_key(ag2)} + Assignment.import_from_migration(hash, @copy_to) + + ag2_2.reload + ag2_2.assignments.count.should == 1 + a_2 = ag2_2.assignments.first + ag2_2.rules.should == "drop_lowest:2\ndrop_highest:5\nnever_drop:%s\n" % a_2.id + end + + it "should convert external tools" do + tool1 = @copy_from.context_external_tools.new + tool1.url = 'http://instructure.com' + tool1.name = 'instructure' + tool1.description = "description of boring" + tool1.privacy_level = 'name_only' + tool1.consumer_key = 'haha' + tool1.shared_secret = "don't share me" + tool1.save! + tool2 = @copy_from.context_external_tools.new + tool2.domain = 'example.com' + tool2.name = 'example' + tool2.description = "example.com? That's the best you could come up with?" + tool2.privacy_level = 'anonymous' + tool2.consumer_key = 'haha' + tool2.shared_secret = "don't share me" + tool2.save! + + #export to xml + builder = Builder::XmlMarkup.new(:indent=>2) + @resource.create_external_tools(builder) + #convert to json + doc = Nokogiri::XML(builder.target!) + hash = @converter.convert_external_tools(doc) + #import json into new course + ContextExternalTool.process_migration({'external_tools'=>hash}, @migration) + @copy_to.save! + + #compare settings + t1 = @copy_to.context_external_tools.find_by_migration_id(CC::CCHelper.create_key(tool1)) + t1.url.should == tool1.url + t1.name.should == tool1.name + t1.description.should == tool1.description + t1.workflow_state.should == tool1.workflow_state + t1.domain.should == nil + t1.consumer_key.should == 'fake' + t1.shared_secret.should == 'fake' + + t2 = @copy_to.context_external_tools.find_by_migration_id(CC::CCHelper.create_key(tool2)) + t2.domain.should == tool2.domain + t2.url.should == nil + t2.name.should == tool2.name + t2.description.should == tool2.description + t2.workflow_state.should == tool2.workflow_state + t2.consumer_key.should == 'fake' + t2.shared_secret.should == 'fake' + end + +end