From 3f94f552564dc9686eeffdd22dab84b62b65fc70 Mon Sep 17 00:00:00 2001 From: Bracken Mosbacker Date: Tue, 9 Apr 2013 14:30:41 -0600 Subject: [PATCH] create course copy and zip migration workers Course copying and importing .zip files will now work the same as other content migrations. Test Plan: * Using the ContentMigration api test course copy and zip imports * Make sure the current course copy and zip import UIs still work * for course copy make sure selective options work closes CNVS-4228 Change-Id: I80a849471dffaf44d683e980cf0b73505b353d83 Reviewed-on: https://gerrit.instructure.com/19740 Tested-by: Jenkins Reviewed-by: James Williams QA-Review: Clare Strong Product-Review: Bracken Mosbacker Reviewed-by: Jeremy Stanley --- .../content_migrations_controller.rb | 23 ++++- app/models/content_migration.rb | 82 +++++------------ app/models/progress.rb | 1 + .../validators/course_copy_validator.rb | 35 +++++++ .../validators/zip_importer_validator.rb | 30 ++++++ .../migration/worker/course_copy_worker.rb | 87 ++++++++++++++++++ .../migration/worker/zip_file_worker.rb | 64 +++++++++++++ lib/canvas/plugins/default_plugins.rb | 33 +++++++ lib/cc/importer/cc_worker.rb | 15 +-- spec/apis/v1/content_migrations_api_spec.rb | 10 ++ spec/fixtures/migration/file.zip | Bin 0 -> 11138 bytes spec/lib/cc/importer/cc_worker_spec.rb | 1 + spec/models/content_migration_spec.rb | 25 +++++ .../lib/academic_benchmark.rb | 2 +- .../lib/academic_benchmark/converter.rb | 10 +- .../spec_canvas/academic_benchmark_spec.rb | 17 ++-- .../qti_exporter/lib/workers/qti_worker.rb | 7 +- 17 files changed, 348 insertions(+), 94 deletions(-) create mode 100644 lib/canvas/migration/validators/course_copy_validator.rb create mode 100644 lib/canvas/migration/validators/zip_importer_validator.rb create mode 100644 lib/canvas/migration/worker/course_copy_worker.rb create mode 100644 lib/canvas/migration/worker/zip_file_worker.rb create mode 100644 spec/fixtures/migration/file.zip diff --git a/app/controllers/content_migrations_controller.rb b/app/controllers/content_migrations_controller.rb index 848bdf2fece..7abeea73da9 100644 --- a/app/controllers/content_migrations_controller.rb +++ b/app/controllers/content_migrations_controller.rb @@ -108,7 +108,7 @@ class ContentMigrationsController < ApplicationController # # You can use the {api:ProgressController#show Progress API} to track the # progress of the migration. The migration's progress is linked to with the - # _progress_url_ value + # _progress_url_ value. # # The two general workflows are: # @@ -124,12 +124,16 @@ class ContentMigrationsController < ApplicationController # 3. {api:ContentMigrationsController#show GET} the ContentMigration # 4. Use the {api:ProgressController#show Progress} specified in _progress_url_ to monitor progress # - # @argument migration_type [string] The type of the migration. Allowed values: canvas_cartridge_importer, common_cartridge_importer, qti_converter, moodle_converter + # @argument migration_type [string] The type of the migration. Allowed values: canvas_cartridge_importer, common_cartridge_importer, course_copy_importer, zip_file_importer, qti_converter, moodle_converter # # @argument pre_attachment[name] [string] Required if uploading a file. This is the first step in uploading a file to the content migration. See the {file:file_uploads.html File Upload Documentation} for details on the file upload workflow. # # @argument pre_attachment[*] (optional) Other file upload properties, See {file:file_uploads.html File Upload Documentation} # + # @argument settings[source_course_id] [string] (optional) The course to copy from for a course copy migration. (required if doing course copy) + # + # @argument settings[folder_id] [string] (optional) The folder to unzip the .zip file into for a zip_file_import. (required if doing .zip file upload) + # # @argument settings[overwrite_quizzes] [boolean] (optional) Whether to overwrite quizzes with the same identifiers between content packages # # @argument settings[question_bank_id] [integer] (optional) The existing question bank ID to import questions into if not specified in the content package @@ -160,15 +164,21 @@ class ContentMigrationsController < ApplicationController # # @returns ContentMigration def create - plugin = Canvas::Plugin.find(params[:migration_type]) - if !plugin + @plugin = Canvas::Plugin.find(params[:migration_type]) + if !@plugin return render(:json => { :message => t('bad_migration_type', "Invalid migration_type") }, :status => :bad_request) end - if plugin.settings && plugin.settings[:requires_file_upload] + settings = @plugin.settings || {} + if settings[:requires_file_upload] if !params[:pre_attachment] || params[:pre_attachment][:name].blank? return render(:json => { :message => t('must_upload_file', "File upload is required") }, :status => :bad_request) end end + if validator = settings[:required_options_validator] + if res = validator.has_error(params[:settings], @current_user, @context) + return render(:json => { :message => res.respond_to?(:call) ? res.call : res }, :status => :bad_request) + end + end @content_migration = @context.content_migrations.build(:user => @current_user, :context => @context, :migration_type => params[:migration_type]) @content_migration.workflow_state = 'created' @@ -186,6 +196,7 @@ class ContentMigrationsController < ApplicationController # @returns ContentMigration def update @content_migration = @context.content_migrations.find(params[:id]) + @plugin = Canvas::Plugin.find(@content_migration.migration_type) update_migration end @@ -200,6 +211,8 @@ class ContentMigrationsController < ApplicationController def update_migration @content_migration.update_migration_settings(params[:settings]) if params[:settings] @content_migration.set_date_shift_options(params[:date_shift_options]) + + params[:selective_import] = false if @plugin.settings && @plugin.settings[:no_selective_import] if Canvas::Plugin.value_to_boolean(params[:selective_import]) #todo selective import options else diff --git a/app/models/content_migration.rb b/app/models/content_migration.rb index 76a2d367f51..e3eb4f3c999 100644 --- a/app/models/content_migration.rb +++ b/app/models/content_migration.rb @@ -267,6 +267,19 @@ class ContentMigration < ActiveRecord::Base add_warning(t('errors.import_error', "Import Error: ") + "#{item_type} - \"#{item_name}\"", warning) end + def fail_with_error!(exception_or_info) + opts={} + if exception_or_info.is_a?(Exception) + opts[:exception] = exception_or_info + else + opts[:error_message] = exception_or_info + end + add_error(t(:unexpected_error, "There was an unexpected error, please contact support."), opts) + self.workflow_state = :failed + job_progress.fail + save + end + # deprecated warning format def old_warnings_format self.migration_issues.map do |mi| @@ -425,7 +438,7 @@ class ContentMigration < ActiveRecord::Base end def for_course_copy? - !!self.source_course + !!self.source_course || (self.migration_type && self.migration_type == 'course_copy_importer') end def set_date_shift_options(opts) @@ -439,63 +452,8 @@ class ContentMigration < ActiveRecord::Base end def copy_course - self.workflow_state = :pre_processing - reset_job_progress - self.migration_settings[:skip_import_notification] = true - self.save - - begin - ce = ContentExport.new - ce.content_migration = self - ce.selected_content = copy_options - ce.course = self.source_course - ce.export_type = ContentExport::COURSE_COPY - ce.user = self.user - ce.save! - self.content_export = ce - - ce.export_course_without_send_later - - if ce.workflow_state == 'exported_for_course_copy' - # use the exported attachment as the import archive - self.attachment = ce.attachment - migration_settings[:migration_ids_to_import] ||= {:copy=>{}} - migration_settings[:migration_ids_to_import][:copy][:everything] = true - # set any attachments referenced in html to be copied - ce.selected_content['attachments'] ||= {} - ce.referenced_files.values.each do |att_mig_id| - ce.selected_content['attachments'][att_mig_id] = true - end - ce.save - - self.save - worker = Canvas::Migration::Worker::CCWorker.new - worker.migration_id = self.id - worker.perform - self.reload - if self.workflow_state == 'exported' - self.workflow_state = :pre_processed - self.update_import_progress(10) - - self.context.copy_attachments_from_course(self.source_course, :content_export => ce, :content_migration => self) - self.update_import_progress(20) - - self.import_content_without_send_later - end - else - self.workflow_state = :failed - migration_settings[:last_error] = "ContentExport failed to export course." - self.save - end - rescue => e - self.workflow_state = :failed - er = ErrorReport.log_exception(:content_migration, e) - migration_settings[:last_error] = "ErrorReport:#{er.id}" - logger.error e - self.save - raise e - ensure - end + worker = Canvas::Migration::Worker::CourseCopyWorker.new + worker.perform(self) end handle_asynchronously :copy_course, :priority => Delayed::LOW_PRIORITY, :max_attempts => 1 @@ -570,7 +528,13 @@ class ContentMigration < ActiveRecord::Base def fast_update_progress(val) reset_job_progress unless job_progress - job_progress.update_completion!(val) + if val == 100 + job_progress.completion = 100 + job_progress.workflow_state = 'completed' + job_progress.save + else + job_progress.update_completion!(val) + end # Until this progress is phased out self.progress = val ContentMigration.where(:id => self).update_all(:progress=>val) diff --git a/app/models/progress.rb b/app/models/progress.rb index 480fe92be8f..b7de8aad672 100644 --- a/app/models/progress.rb +++ b/app/models/progress.rb @@ -29,6 +29,7 @@ class Progress < ActiveRecord::Base workflow do state :queued do event :start, :transitions_to => :running + event :fail, :transitions_to => :failed end state :running do event(:complete, :transitions_to => :completed) { update_completion! 100 } diff --git a/lib/canvas/migration/validators/course_copy_validator.rb b/lib/canvas/migration/validators/course_copy_validator.rb new file mode 100644 index 00000000000..ba29dfab371 --- /dev/null +++ b/lib/canvas/migration/validators/course_copy_validator.rb @@ -0,0 +1,35 @@ +# +# Copyright (C) 2013 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::Migration::Validators::CourseCopyValidator + def self.has_error(options, user, course) + if !options || !options[:source_course_id] + return I18n.t :course_copy_argument_error, 'A course copy requires a source course.' + end + source = Course.find_by_id(options[:source_course_id]) + if source + if !source.grants_rights?(user, nil, :manage) + return I18n.t :course_copy_not_allowed_error, 'You are not allowed to copy the source course.' + end + else + return I18n.t :course_copy_no_course_error, 'The source course was not found.' + end + + false + end +end diff --git a/lib/canvas/migration/validators/zip_importer_validator.rb b/lib/canvas/migration/validators/zip_importer_validator.rb new file mode 100644 index 00000000000..462248efa7f --- /dev/null +++ b/lib/canvas/migration/validators/zip_importer_validator.rb @@ -0,0 +1,30 @@ +# +# Copyright (C) 2013 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::Migration::Validators::ZipImporterValidator + def self.has_error(options, user, course) + if !options || !options[:folder_id] + return I18n.t :zip_argument_error, 'A .zip upload requires a folder to upload to.' + end + if !course.folders.find_by_id(options[:folder_id]) + return I18n.t :zip_no_folder_error, "The specified folder couldn't be found in this course." + end + + false + end +end diff --git a/lib/canvas/migration/worker/course_copy_worker.rb b/lib/canvas/migration/worker/course_copy_worker.rb new file mode 100644 index 00000000000..8904002c35f --- /dev/null +++ b/lib/canvas/migration/worker/course_copy_worker.rb @@ -0,0 +1,87 @@ +# +# 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 . +# +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 + cm.save + cm.job_progress.start + + begin + ce = ContentExport.new + ce.content_migration = cm + ce.selected_content = cm.copy_options + source = cm.source_course || Course.find(cm.migration_settings[:source_course_id]) + ce.course = source + ce.export_type = ContentExport::COURSE_COPY + ce.user = cm.user + ce.save! + cm.content_export = ce + + ce.export_course_without_send_later + + if ce.workflow_state == 'exported_for_course_copy' + # use the exported attachment as the import archive + cm.attachment = ce.attachment + cm.migration_settings[:migration_ids_to_import] ||= {:copy=>{}} + cm.migration_settings[:migration_ids_to_import][:copy][:everything] = true + # set any attachments referenced in html to be copied + ce.selected_content['attachments'] ||= {} + ce.referenced_files.values.each do |att_mig_id| + ce.selected_content['attachments'][att_mig_id] = true + end + ce.save + + cm.save + worker = Canvas::Migration::Worker::CCWorker.new + worker.migration_id = cm.id + worker.perform + cm.reload + if cm.workflow_state == 'exported' + cm.workflow_state = :pre_processed + cm.update_import_progress(10) + + cm.context.copy_attachments_from_course(source, :content_export => ce, :content_migration => cm) + cm.update_import_progress(20) + + cm.import_content_without_send_later + cm.workflow_state = :imported + cm.save + cm.update_import_progress(100) + end + else + cm.workflow_state = :failed + cm.migration_settings[:last_error] = "ContentExport failed to export course." + cm.save + end + rescue => e + cm.fail_with_error!(e) + raise e + end + end + + def self.enqueue(content_migration) + Delayed::Job.enqueue(new(content_migration.id), + :priority => Delayed::LOW_PRIORITY, + :max_attempts => 1, + :strand => content_migration.strand) + end +end diff --git a/lib/canvas/migration/worker/zip_file_worker.rb b/lib/canvas/migration/worker/zip_file_worker.rb new file mode 100644 index 00000000000..3eb0a71f072 --- /dev/null +++ b/lib/canvas/migration/worker/zip_file_worker.rb @@ -0,0 +1,64 @@ +# +# 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 . +# +class Canvas::Migration::Worker::ZipFileWorker < Struct.new(:migration_id) + def perform(cm=nil) + cm ||= ContentMigration.find migration_id + + cm.workflow_state = :importing + cm.migration_settings[:skip_import_notification] = true + cm.job_progress.start + cm.save + + begin + zipfile = cm.attachment.open(:need_local_file => true) + folder = cm.context.folders.find(cm.migration_settings[:folder_id]) + + progress = 0.0 + update_callback = lambda{|pct| + if pct - progress >= 1.0 + cm.update_import_progress(pct) + end + } + + UnzipAttachment.process( + :context => cm.context, + :root_directory => folder, + :filename => zipfile.path, + :callback => update_callback + ) + + zipfile.close + zipfile = nil + + cm.workflow_state = :imported + cm.save + cm.update_import_progress(100) + rescue => e + cm.fail_with_error!(e) + raise e + end + + end + + def self.enqueue(content_migration) + Delayed::Job.enqueue(new(content_migration.id), + :priority => Delayed::LOW_PRIORITY, + :max_attempts => 1, + :strand => content_migration.strand) + end +end diff --git a/lib/canvas/plugins/default_plugins.rb b/lib/canvas/plugins/default_plugins.rb index a68b4608aed..6e82dce8017 100644 --- a/lib/canvas/plugins/default_plugins.rb +++ b/lib/canvas/plugins/default_plugins.rb @@ -138,6 +138,39 @@ Canvas::Plugin.register 'canvas_cartridge_importer', :export_system, { :provides =>{:canvas_cartridge => CC::Importer::Canvas::Converter} }, } +require_dependency 'canvas/migration/worker/course_copy_worker' +Canvas::Plugin.register 'course_copy_importer', :export_system, { + :name => lambda { I18n.t :course_copy_name, 'Copy Canvas Course' }, + :author => 'Instructure', + :author_website => 'http://www.instructure.com', + :description => lambda { I18n.t :course_copy_description, 'Migration plugin for copying canvas courses' }, + :version => '1.0.0', + :select_text => lambda { I18n.t :course_copy_file_description, "Copy a Canvas Course" }, + :hide_from_users => true, # until new UI is done + :settings => { + :worker => 'CourseCopyWorker', + :migration_partial => '', + :requires_file_upload => false, + :required_options_validator => Canvas::Migration::Validators::CourseCopyValidator + }, +} +require_dependency 'canvas/migration/worker/zip_file_worker' +Canvas::Plugin.register 'zip_file_importer', :export_system, { + :name => lambda { I18n.t :zip_file_name, 'Copy Canvas Course' }, + :author => 'Instructure', + :author_website => 'http://www.instructure.com', + :description => lambda { I18n.t :zip_file_description, 'Migration plugin for unpacking plain .zip files into a course' }, + :version => '1.0.0', + :select_text => lambda { I18n.t :zip_file_file_description, "Import plain files from a .zip" }, + :hide_from_users => true, # until new UI is done + :settings => { + :worker => 'ZipFileWorker', + :migration_partial => '', + :requires_file_upload => true, + :no_selective_import => true, + :required_options_validator => Canvas::Migration::Validators::ZipImporterValidator + }, +} Canvas::Plugin.register 'common_cartridge_importer', :export_system, { :name => lambda{ I18n.t :common_cartridge_name, 'Common Cartridge Importer' }, :author => 'Instructure', diff --git a/lib/cc/importer/cc_worker.rb b/lib/cc/importer/cc_worker.rb index f605919a9f3..9f82d01add5 100644 --- a/lib/cc/importer/cc_worker.rb +++ b/lib/cc/importer/cc_worker.rb @@ -18,6 +18,7 @@ class Canvas::Migration::Worker::CCWorker < Struct.new(:migration_id) def perform cm = ContentMigration.find_by_id migration_id + cm.job_progress.start begin cm.update_conversion_progress(1) settings = cm.migration_settings.clone @@ -41,7 +42,6 @@ class Canvas::Migration::Worker::CCWorker < Struct.new(:migration_id) if export_folder_path Canvas::Migration::Worker::upload_exported_data(export_folder_path, cm) Canvas::Migration::Worker::clear_exported_data(export_folder_path) - cm.update_conversion_progress(100) end cm.migration_settings[:worker_class] = converter_class.name @@ -50,6 +50,7 @@ class Canvas::Migration::Worker::CCWorker < Struct.new(:migration_id) end cm.workflow_state = :exported saved = cm.save + cm.update_conversion_progress(100) if cm.import_immediately? cm.import_content_without_send_later @@ -61,13 +62,13 @@ class Canvas::Migration::Worker::CCWorker < Struct.new(:migration_id) end saved + rescue Canvas::Migration::Error + cm.add_error($!.message, :exception => $!) + cm.workflow_state = :failed + cm.job_progress.fail + cm.save rescue => e - report = ErrorReport.log_exception(:content_migration, e) - if cm - cm.workflow_state = :failed - cm.migration_settings[:last_error] = "ErrorReport:#{report.id}" - cm.save - end + cm.fail_with_error!(e) if cm end end diff --git a/spec/apis/v1/content_migrations_api_spec.rb b/spec/apis/v1/content_migrations_api_spec.rb index 6320b3994ae..be5407b5557 100644 --- a/spec/apis/v1/content_migrations_api_spec.rb +++ b/spec/apis/v1/content_migrations_api_spec.rb @@ -136,6 +136,16 @@ describe ContentMigrationsController, :type => :integration do migration.workflow_state.should == "created" migration.job_progress.should be_nil end + + it "should error if expected setting isn't set" do + json = api_call(:post, @migration_url, @params, {:migration_type => 'course_copy_importer'}, {}, :expected_status => 400) + json.should == {"message"=>'A course copy requires a source course.'} + end + + it "should queue if correct settings set" do + # implicitly tests that the response was a 200 + api_call(:post, @migration_url, @params, {:migration_type => 'course_copy_importer', :settings => {:source_course_id => @course.id.to_param}}) + end context "migration file upload" do it "should set attachment pre-flight data" do diff --git a/spec/fixtures/migration/file.zip b/spec/fixtures/migration/file.zip new file mode 100644 index 0000000000000000000000000000000000000000..029440fd2f812f7a06e5930908a450355faeb3f4 GIT binary patch literal 11138 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 U000010096%0000iD*ylh02rgNPyhe` literal 0 HcmV?d00001 diff --git a/spec/lib/cc/importer/cc_worker_spec.rb b/spec/lib/cc/importer/cc_worker_spec.rb index 638b2af5460..9ad7b639415 100644 --- a/spec/lib/cc/importer/cc_worker_spec.rb +++ b/spec/lib/cc/importer/cc_worker_spec.rb @@ -21,6 +21,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper.rb') describe Canvas::Migration::Worker::CCWorker do it "should set the worker_class on the migration" do cm = ContentMigration.create!(:migration_settings => { :no_archive_file => true }) + cm.reset_job_progress Canvas::Migration::Worker.expects(:get_converter).with(anything).returns(CC::Importer::Canvas::Converter) CC::Importer::Canvas::Converter.any_instance.expects(:export).returns({}) worker = Canvas::Migration::Worker::CCWorker.new(cm.id) diff --git a/spec/models/content_migration_spec.rb b/spec/models/content_migration_spec.rb index 67b0cf3a11c..a9aa6f80464 100644 --- a/spec/models/content_migration_spec.rb +++ b/spec/models/content_migration_spec.rb @@ -1753,4 +1753,29 @@ equation: @course, :user => @user,) + cm.migration_type = 'zip_file_importer' + cm.migration_settings[:folder_id] = Folder.root_folders(@course).first.id + cm.save! + + attachment = Attachment.new + attachment.context = cm + attachment.uploaded_data = File.open(zip_path, 'rb') + attachment.filename = 'file.zip' + attachment.save! + + cm.attachment = attachment + cm.save! + + cm.queue_migration + run_jobs + @course.reload + @course.attachments.count.should == 1 + end + end + end diff --git a/vendor/plugins/academic_benchmark/lib/academic_benchmark.rb b/vendor/plugins/academic_benchmark/lib/academic_benchmark.rb index 3d40be28ba1..6c6a399467c 100644 --- a/vendor/plugins/academic_benchmark/lib/academic_benchmark.rb +++ b/vendor/plugins/academic_benchmark/lib/academic_benchmark.rb @@ -32,7 +32,7 @@ module AcademicBenchmark def self.queue_migration_for_guid(guid, user) if !Account.site_admin.grants_right?(user, :manage_global_outcomes) - raise Canvas::Migration::Error.new("User isn't allowed to edit global outcomes") + raise Canvas::Migration::Error.new(I18n.t('academic_benchmark.no_permissions', "User isn't allowed to edit global outcomes")) end cm = ContentMigration.create(:context => Account.site_admin) diff --git a/vendor/plugins/academic_benchmark/lib/academic_benchmark/converter.rb b/vendor/plugins/academic_benchmark/lib/academic_benchmark/converter.rb index 604eea250f9..b6a51b70d3c 100644 --- a/vendor/plugins/academic_benchmark/lib/academic_benchmark/converter.rb +++ b/vendor/plugins/academic_benchmark/lib/academic_benchmark/converter.rb @@ -17,7 +17,7 @@ module AcademicBenchmark def export if content_migration && !Account.site_admin.grants_right?(content_migration.user, :manage_global_outcomes) - raise Canvas::Migration::Error.new("User isn't allowed to edit global outcomes") + raise Canvas::Migration::Error.new(I18n.t('academic_benchmark.no_perms', "User isn't allowed to edit global outcomes")) end if @archive_file @@ -31,14 +31,10 @@ module AcademicBenchmark convert_guids(@settings[:guids]) if @settings[:guids] end else - message = I18n.t('academic_benchmark.no_api_key', "An API key is required to use Academic Benchmarks") - add_warning(message) - raise Canvas::Migration::Error.new("no academic benchmarks api key") + raise Canvas::Migration::Error.new(I18n.t('academic_benchmark.no_api_key', "An API key is required to use Academic Benchmarks")) end else - message = I18n.t('academic_benchmark.no_file', "No outcome file or authority given") - add_warning(message) - raise Canvas::Migration::Error.new("No outcome file or authority given") + raise Canvas::Migration::Error.new(I18n.t('academic_benchmark.no_file', "No outcome file or authority given")) end save_to_file diff --git a/vendor/plugins/academic_benchmark/spec_canvas/academic_benchmark_spec.rb b/vendor/plugins/academic_benchmark/spec_canvas/academic_benchmark_spec.rb index ca38a0795a2..80a3f5eb534 100644 --- a/vendor/plugins/academic_benchmark/spec_canvas/academic_benchmark_spec.rb +++ b/vendor/plugins/academic_benchmark/spec_canvas/academic_benchmark_spec.rb @@ -98,8 +98,7 @@ describe AcademicBenchmark::Converter do @cm.export_content run_jobs @cm.reload - @cm.old_warnings_format.should == [] - @cm.migration_settings[:last_error].should be_nil + @cm.migration_issues.count.should == 0 @cm.workflow_state.should == 'imported' verify_full_import() @@ -112,8 +111,9 @@ describe AcademicBenchmark::Converter do run_jobs @cm.reload + @cm.migration_issues.count.should == 1 + @cm.migration_issues.first.description.should == "User isn't allowed to edit global outcomes" @cm.workflow_state.should == 'failed' - @cm.migration_settings[:last_error].should =~ /ErrorReport:\d+/ end it "should fail if no file or authority set" do @@ -125,8 +125,8 @@ describe AcademicBenchmark::Converter do run_jobs @cm.reload - @cm.old_warnings_format.should == [["No outcome file or authority given", ""]] - @cm.migration_settings[:last_error].should_not be_nil + @cm.migration_issues.count.should == 1 + @cm.migration_issues.first.description.should == "No outcome file or authority given" @cm.workflow_state.should == 'failed' end @@ -144,8 +144,7 @@ describe AcademicBenchmark::Converter do run_jobs @cm.reload - @cm.old_warnings_format.should == [] - @cm.migration_settings[:last_error].should be_nil + @cm.migration_issues.count.should == 0 @cm.workflow_state.should == 'imported' @root_group = LearningOutcomeGroup.global_root_outcome_group @@ -160,8 +159,8 @@ describe AcademicBenchmark::Converter do run_jobs @cm.reload - @cm.old_warnings_format.should == [["An API key is required to use Academic Benchmarks", ""]] - @cm.migration_settings[:last_error].should_not be_nil + @cm.migration_issues.count.should == 1 + @cm.migration_issues.first.description.should == "An API key is required to use Academic Benchmarks" @cm.workflow_state.should == 'failed' end diff --git a/vendor/plugins/qti_exporter/lib/workers/qti_worker.rb b/vendor/plugins/qti_exporter/lib/workers/qti_worker.rb index 67ae7229ef2..d59a9ecf80a 100644 --- a/vendor/plugins/qti_exporter/lib/workers/qti_worker.rb +++ b/vendor/plugins/qti_exporter/lib/workers/qti_worker.rb @@ -44,12 +44,7 @@ module Canvas::Migration cm.save cm.update_import_progress(100) rescue => e - report = ErrorReport.log_exception(:content_migration, e) - if cm - cm.workflow_state = :failed - cm.migration_settings[:last_error] = "ErrorReport:#{report.id}" - cm.save - end + cm.fail_with_error!(e) if cm end end