content migration creation api
allows creating content migration through the api Test Plan: * Create a content migration through the api along with a file upload * You should be able to follow along with the progress and the migration should be complete when the progress is done * Try to update the migration type, you should get an error * Upload a wrong file type the first time, then go through the file upload process again on the update endpoint. refs CNVS-4228 Change-Id: Iab1082ca68b61d1b0493c191b48169a164444d06 Reviewed-on: https://gerrit.instructure.com/19108 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: James Williams <jamesw@instructure.com> QA-Review: Clare Strong <clare@instructure.com> Product-Review: Bracken Mosbacker <bracken@instructure.com> Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
This commit is contained in:
parent
690dd65081
commit
93643f6d6c
|
@ -123,7 +123,7 @@ class ContentImportsController < ApplicationController
|
|||
def migrate_content_choose
|
||||
if authorized_action(@context, @current_user, :manage_content)
|
||||
@content_migration = ContentMigration.for_context(@context).find(params[:id]) #)_all_by_context_id_and_context_type(@context.id, @context.class.to_s).last
|
||||
if @content_migration.progress && @content_migration.progress >= 100
|
||||
if @content_migration.workflow_state == "imported"
|
||||
flash[:notice] = t 'notices.already_imported', "That extraction has already been imported into the course"
|
||||
redirect_to named_context_url(@context, :context_url)
|
||||
return
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
|
||||
# @API Content Migrations
|
||||
# @beta
|
||||
#
|
||||
# API for accessing content migrations and migration issues
|
||||
# @object ContentMigration
|
||||
|
@ -24,28 +25,44 @@
|
|||
# // the unique identifier for the migration
|
||||
# id: 370663,
|
||||
#
|
||||
# // API url to the content migration's issues
|
||||
# migration_issues_url: "https://example.com/api/v1/courses/1/content_migrations/1/migration_issues"
|
||||
# // the type of content migration
|
||||
# migration_type: common_cartridge_importer,
|
||||
#
|
||||
# // url to download the file uploaded for this migration
|
||||
# // the name of the content migration type
|
||||
# migration_type_title: "Canvas Cartridge Importer",
|
||||
#
|
||||
# // API url to the content migration's issues
|
||||
# migration_issues_url: "https://example.com/api/v1/courses/1/content_migrations/1/migration_issues",
|
||||
#
|
||||
# // attachment api object for the uploaded file
|
||||
# // may not be present for all migrations
|
||||
# content_archive_download_url: "https://example.com/api/v1/courses/1/content_migrations/1/download_archive"
|
||||
# attachment: {url:"https://example.com/api/v1/courses/1/content_migrations/1/download_archive"},
|
||||
#
|
||||
# // The api endpoint for polling the current progress
|
||||
# progress_url: "https://example.com/api/v1/progress/4",
|
||||
#
|
||||
# // The user who started the migration
|
||||
# user_id: 4
|
||||
# user_id: 4,
|
||||
#
|
||||
# // Current state of the issue: created pre_processing pre_processed pre_process_error converting converted importing imported failed
|
||||
# workflow_state: "exporting"
|
||||
# // Current state of the content migration: pre_processing pre_processed running waiting_for_select completed failed
|
||||
# workflow_state: "running",
|
||||
#
|
||||
# // timestamps
|
||||
# started_at: "2012-06-01T00:00:00-06:00",
|
||||
# finished_at: "2012-06-01T00:00:00-06:00",
|
||||
#
|
||||
# // file uploading data, see {file:file_uploads.html File Upload Documentation} for file upload workflow
|
||||
# // This works a little differently in that all the file data is in the pre_attachment hash
|
||||
# // if there is no upload_url then there was an attachment pre-processing error, the error message will be in the message key
|
||||
# // This data will only be here after a create or update call
|
||||
# pre_attachment:{upload_url: "", message: "file exceeded quota", upload_params: {...}}
|
||||
#
|
||||
# }
|
||||
class ContentMigrationsController < ApplicationController
|
||||
include Api::V1::ContentMigration
|
||||
|
||||
before_filter :require_context
|
||||
before_filter :require_auth
|
||||
|
||||
# @API List content migrations
|
||||
#
|
||||
|
@ -75,30 +92,139 @@ class ContentMigrationsController < ApplicationController
|
|||
#
|
||||
# @returns ContentMigration
|
||||
def show
|
||||
return unless require_content_migration
|
||||
@content_migration = @context.content_migrations.find(params[:id])
|
||||
render :json => content_migration_json(@content_migration, @current_user, session)
|
||||
end
|
||||
|
||||
def download_archive
|
||||
return unless require_content_migration
|
||||
|
||||
if @content_migration.attachment
|
||||
if Attachment.s3_storage?
|
||||
redirect_to @content_migration.attachment.cacheable_s3_download_url
|
||||
else
|
||||
cancel_cache_buster
|
||||
send_file(@content_migration.attachment.full_filename, :type => @content_migration.attachment.content_type, :disposition => 'attachment')
|
||||
end
|
||||
else
|
||||
render :status => 404, :json => {:message => t('no_archive', "There is no archive for this content migration")}
|
||||
# @API Create a content migration
|
||||
#
|
||||
# Create a content migration. If the migration requires a file to be uploaded
|
||||
# the actual processing of the file will start once the file upload process is completed.
|
||||
# File uploading works as described in the {file:file_uploads.html File Upload Documentation}
|
||||
# except that the values are set on a *pre_attachment* sub-hash.
|
||||
#
|
||||
# For migrations that don't require a file to be uploaded, like course copy, the
|
||||
# processing will begin as soon as the migration is created.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# The two general workflows are:
|
||||
#
|
||||
# If no file upload is needed:
|
||||
#
|
||||
# 1. POST to create
|
||||
# 2. Use the {api:ProgressController#show Progress} specified in _progress_url_ to monitor progress
|
||||
#
|
||||
# For file uploading:
|
||||
#
|
||||
# 1. POST to create with file info in *pre_attachment*
|
||||
# 2. Do {file:file_uploads.html file upload processing} using the data in the *pre_attachment* data
|
||||
# 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 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[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
|
||||
# @argument settings[question_bank_name] [string] (optional) The question bank to import questions into if not specified in the content package, if both bank id and name are set, id will take precedence.
|
||||
#
|
||||
# @argument date_shift_options[shift_dates] [boolean] (optional) Whether to shift dates
|
||||
# @argument date_shift_options[old_start_date] [yyyy-mm-dd] (optional) The original start date of the source content/course
|
||||
# @argument date_shift_options[old_end_date] [yyyy-mm-dd] (optional) The original end date of the source content/course
|
||||
# @argument date_shift_options[new_start_date] [yyyy-mm-dd] (optional) The new start date for the content/course
|
||||
# @argument date_shift_options[new_end_date] [yyyy-mm-dd] (optional) The new end date for the source content/course
|
||||
# @argument date_shift_options[day_substitutions][x] [integer] (optional) Move anything scheduled for day 'x' to the specified day. (0-Sunday, 1-Monday, 2-Tuesday, 3-Wednesday, 4-Thursday, 5-Friday, 6-Saturday)
|
||||
#
|
||||
# @example_request
|
||||
#
|
||||
# curl 'https://<canvas>/api/v1/courses/<course_id>/content_migrations' \
|
||||
# -F 'migration_type=common_cartridge_importer' \
|
||||
# -F 'settings[question_bank_name]=importquestions' \
|
||||
# -F 'date_shift_options[old_start_date]=1999-01-01' \
|
||||
# -F 'date_shift_options[new_start_date]=2013-09-01' \
|
||||
# -F 'date_shift_options[old_end_date]=1999-04-15' \
|
||||
# -F 'date_shift_options[new_end_date]=2013-12-15' \
|
||||
# -F 'date_shift_options[day_substitutions][1]=2' \
|
||||
# -F 'date_shift_options[day_substitutions][2]=3' \
|
||||
# -F 'date_shift_options[shift_dates]=true' \
|
||||
# -F 'pre_attachment[name]=mycourse.imscc' \
|
||||
# -F 'pre_attachment[size]=12345' \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
# @returns ContentMigration
|
||||
def create
|
||||
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]
|
||||
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
|
||||
|
||||
@content_migration = @context.content_migrations.build(:user => @current_user, :context => @context, :migration_type => params[:migration_type])
|
||||
@content_migration.workflow_state = 'created'
|
||||
|
||||
update_migration
|
||||
end
|
||||
|
||||
# @API Update a content migration
|
||||
#
|
||||
# Update a content migration. Takes same arguments as create except that you can't change the migration type.
|
||||
# However, changing most settings after the migration process has started will not do anything.
|
||||
# Generally updating the content migration will be used when there is a file upload problem.
|
||||
# If the first upload has a problem you can supply new _pre_attachment_ values to start the process again.
|
||||
#
|
||||
# @returns ContentMigration
|
||||
def update
|
||||
@content_migration = @context.content_migrations.find(params[:id])
|
||||
|
||||
update_migration
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
def require_content_migration
|
||||
@content_migration = @context.content_migrations.find(params[:id])
|
||||
return authorized_action(@context, @current_user, :manage_content)
|
||||
def require_auth
|
||||
authorized_action(@context, @current_user, :manage_content)
|
||||
end
|
||||
|
||||
def update_migration
|
||||
@content_migration.update_migration_settings(params[:settings]) if params[:settings]
|
||||
@content_migration.set_date_shift_options(params[:date_shift_options])
|
||||
if Canvas::Plugin.value_to_boolean(params[:selective_import])
|
||||
#todo selective import options
|
||||
else
|
||||
@content_migration.migration_settings[:import_immediately] = true
|
||||
@content_migration.copy_options = {:everything => true}
|
||||
@content_migration.migration_settings[:migration_ids_to_import] = {:copy => {:everything => true}}
|
||||
end
|
||||
|
||||
if @content_migration.save
|
||||
preflight_json = nil
|
||||
if params[:pre_attachment]
|
||||
@content_migration.workflow_state = 'pre_processing'
|
||||
preflight_json = api_attachment_preflight(@content_migration, request, :params => params[:pre_attachment], :check_quota => true, :do_submit_to_scribd => false, :return_json => true)
|
||||
if preflight_json[:error]
|
||||
@content_migration.workflow_state = 'pre_process_error'
|
||||
end
|
||||
@content_migration.save!
|
||||
elsif !params.has_key?(:do_not_run) || !Canvas::Plugin.value_to_boolean(params[:do_not_run])
|
||||
@content_migration.queue_migration
|
||||
end
|
||||
|
||||
render :json => content_migration_json(@content_migration, @current_user, session, preflight_json)
|
||||
else
|
||||
render :json => @content_migration.errors, :status => :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -564,7 +564,7 @@ class FilesController < ApplicationController
|
|||
def api_create_success
|
||||
@attachment = Attachment.find_by_id_and_uuid(params[:id], params[:uuid])
|
||||
return render(:nothing => true, :status => :bad_request) unless @attachment.try(:file_state) == 'deleted'
|
||||
duplicate_handling = check_duplicate_handling_option(request)
|
||||
duplicate_handling = check_duplicate_handling_option(request.params)
|
||||
return unless duplicate_handling
|
||||
return unless check_quota_after_attachment(request)
|
||||
if Attachment.s3_storage?
|
||||
|
@ -576,6 +576,11 @@ class FilesController < ApplicationController
|
|||
@attachment.save!
|
||||
end
|
||||
@attachment.handle_duplicates(duplicate_handling)
|
||||
|
||||
if @attachment.context.respond_to?(:file_upload_success_callback)
|
||||
@attachment.context.file_upload_success_callback(@attachment)
|
||||
end
|
||||
|
||||
json = attachment_json(@attachment,@current_user)
|
||||
# render as_text for IE, otherwise it'll prompt
|
||||
# to download the JSON response
|
||||
|
|
|
@ -458,33 +458,42 @@ class AssessmentQuestion < ActiveRecord::Base
|
|||
|
||||
logger.debug "adding #{total} assessment questions"
|
||||
|
||||
default_bank = migration.question_bank_id ? migration.context.assessment_question_banks.find_by_id(migration.question_bank_id) : nil
|
||||
banks = {}
|
||||
questions.each do |question|
|
||||
question_bank = nil
|
||||
question[:question_bank_name] = nil if question[:question_bank_name] == ''
|
||||
question[:question_bank_name], question[:question_bank_migration_id] = bank_map[question[:migration_id]] if question[:question_bank_name].blank?
|
||||
question[:question_bank_name] ||= migration.question_bank_name
|
||||
question[:question_bank_name] ||= AssessmentQuestionBank.default_imported_title
|
||||
if default_bank
|
||||
question_bank = default_bank
|
||||
else
|
||||
question[:question_bank_name] ||= migration.question_bank_name
|
||||
question[:question_bank_name] ||= AssessmentQuestionBank.default_imported_title
|
||||
end
|
||||
if question[:assessment_question_migration_id]
|
||||
question_data[:qq_data][question['migration_id']] = question
|
||||
next
|
||||
end
|
||||
hash_id = "#{question[:question_bank_id]}_#{question[:question_bank_name]}"
|
||||
if !banks[hash_id]
|
||||
unless bank = migration.context.assessment_question_banks.find_by_title_and_migration_id(question[:question_bank_name], question[:question_bank_id])
|
||||
bank = migration.context.assessment_question_banks.new
|
||||
bank.title = question[:question_bank_name]
|
||||
bank.migration_id = question[:question_bank_id]
|
||||
bank.save!
|
||||
if !question_bank
|
||||
hash_id = "#{question[:question_bank_id]}_#{question[:question_bank_name]}"
|
||||
if !banks[hash_id]
|
||||
unless bank = migration.context.assessment_question_banks.find_by_title_and_migration_id(question[:question_bank_name], question[:question_bank_id])
|
||||
bank = migration.context.assessment_question_banks.new
|
||||
bank.title = question[:question_bank_name]
|
||||
bank.migration_id = question[:question_bank_id]
|
||||
bank.save!
|
||||
end
|
||||
if bank.workflow_state == 'deleted'
|
||||
bank.workflow_state = 'active'
|
||||
bank.save!
|
||||
end
|
||||
banks[hash_id] = bank
|
||||
end
|
||||
if bank.workflow_state == 'deleted'
|
||||
bank.workflow_state = 'active'
|
||||
bank.save!
|
||||
end
|
||||
banks[hash_id] = bank
|
||||
question_bank = banks[hash_id]
|
||||
end
|
||||
|
||||
begin
|
||||
question = AssessmentQuestion.import_from_migration(question, migration.context, banks[hash_id])
|
||||
question = AssessmentQuestion.import_from_migration(question, migration.context, 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
|
||||
|
|
|
@ -668,6 +668,7 @@ class Attachment < ActiveRecord::Base
|
|||
def self.get_quota(context)
|
||||
quota = 0
|
||||
quota_used = 0
|
||||
context = context.quota_context if context.respond_to?(:quota_context) && context.quota_context
|
||||
if context
|
||||
Shackles.activate(:slave) do
|
||||
quota = Setting.get_cached('context_default_quota', 50.megabytes.to_s).to_i
|
||||
|
|
|
@ -178,6 +178,7 @@ class ContentExport < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def fast_update_progress(val)
|
||||
content_migration.update_conversion_progress(val) if content_migration
|
||||
self.progress = val
|
||||
ContentExport.where(:id => self).update_all(:progress=>val)
|
||||
end
|
||||
|
|
|
@ -27,6 +27,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
belongs_to :source_course, :class_name => 'Course'
|
||||
has_one :content_export
|
||||
has_many :migration_issues
|
||||
has_one :job_progress, :class_name => 'Progress', :as => :context
|
||||
has_a_broadcast_policy
|
||||
serialize :migration_settings
|
||||
before_save :infer_defaults
|
||||
|
@ -53,7 +54,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
'web_links' => false,
|
||||
'wikis' => false
|
||||
}
|
||||
attr_accessible :context, :migration_settings, :user, :source_course, :copy_options
|
||||
attr_accessible :context, :migration_settings, :user, :source_course, :copy_options, :migration_type
|
||||
attr_accessor :outcome_to_id_map
|
||||
|
||||
workflow do
|
||||
|
@ -94,6 +95,11 @@ class ContentMigration < ActiveRecord::Base
|
|||
exclude_hidden ? plugins.select{|p|!p.meta[:hide_from_users]} : plugins
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |user, session| self.context.grants_right?(user, session, :manage_files) }
|
||||
can :manage_files and can :read
|
||||
end
|
||||
|
||||
# the stream item context is decided by calling asset.context(user), i guess
|
||||
# to differentiate from the normal asset.context() call that may not give us
|
||||
# the context we want. in this case, they're one and the same.
|
||||
|
@ -102,6 +108,10 @@ class ContentMigration < ActiveRecord::Base
|
|||
self.original_context
|
||||
end
|
||||
|
||||
def quota_context
|
||||
self.context
|
||||
end
|
||||
|
||||
def migration_settings
|
||||
read_attribute(:migration_settings) || write_attribute(:migration_settings,{}.with_indifferent_access)
|
||||
end
|
||||
|
@ -138,6 +148,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
|
||||
def migration_ids_to_import=(val)
|
||||
migration_settings[:migration_ids_to_import] = val
|
||||
set_date_shift_options val[:copy]
|
||||
end
|
||||
|
||||
def infer_defaults
|
||||
|
@ -174,6 +185,14 @@ class ContentMigration < ActiveRecord::Base
|
|||
migration_settings[:question_bank_name]
|
||||
end
|
||||
|
||||
def question_bank_id=(bank_id)
|
||||
migration_settings[:question_bank_id] = bank_id
|
||||
end
|
||||
|
||||
def question_bank_id
|
||||
migration_settings[:question_bank_id]
|
||||
end
|
||||
|
||||
def course_archive_download_url=(url)
|
||||
migration_settings[:course_archive_download_url] = url
|
||||
end
|
||||
|
@ -182,8 +201,12 @@ class ContentMigration < ActiveRecord::Base
|
|||
self.context.root_account rescue nil
|
||||
end
|
||||
|
||||
def migration_type
|
||||
read_attribute(:migration_type) || migration_settings['migration_type']
|
||||
end
|
||||
|
||||
def plugin_type
|
||||
if plugin = Canvas::Plugin.find(migration_settings['migration_type'])
|
||||
if plugin = Canvas::Plugin.find(migration_type)
|
||||
plugin.metadata(:select_text) || plugin.name
|
||||
else
|
||||
t(:unknown, 'Unknown')
|
||||
|
@ -255,10 +278,39 @@ class ContentMigration < ActiveRecord::Base
|
|||
def warnings
|
||||
old_warnings_format.map(&:first)
|
||||
end
|
||||
|
||||
|
||||
# This will be called by the files api after the attachment finishes uploading
|
||||
def file_upload_success_callback(att)
|
||||
if att.file_state == "available"
|
||||
self.attachment = att
|
||||
self.workflow_state = :pre_processed
|
||||
self.save
|
||||
self.queue_migration
|
||||
else
|
||||
self.workflow_state = :pre_process_error
|
||||
self.add_warning(t('bad_attachment', "The file was not successfully uploaded."))
|
||||
end
|
||||
end
|
||||
|
||||
def reset_job_progress
|
||||
self.progress = 0
|
||||
if self.job_progress
|
||||
p = self.job_progress
|
||||
else
|
||||
p = Progress.new(:context => self, :tag => "content_migration")
|
||||
self.job_progress = p
|
||||
end
|
||||
p.workflow_state = :queued
|
||||
p.completion = 0
|
||||
p.user = self.user
|
||||
p.save!
|
||||
p
|
||||
end
|
||||
|
||||
def export_content
|
||||
reset_job_progress
|
||||
check_quiz_id_prepender
|
||||
plugin = Canvas::Plugin.find(migration_settings['migration_type'])
|
||||
plugin = Canvas::Plugin.find(migration_type)
|
||||
if plugin
|
||||
begin
|
||||
if Canvas::Migration::Worker.const_defined?(plugin.settings['worker'])
|
||||
|
@ -271,7 +323,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
end
|
||||
rescue NameError
|
||||
self.workflow_state = 'failed'
|
||||
message = "The migration plugin #{migration_settings['migration_type']} doesn't have a worker."
|
||||
message = "The migration plugin #{migration_type} doesn't have a worker."
|
||||
migration_settings[:last_error] = message
|
||||
ErrorReport.log_exception(:content_migration, $!)
|
||||
logger.error message
|
||||
|
@ -279,15 +331,16 @@ class ContentMigration < ActiveRecord::Base
|
|||
end
|
||||
else
|
||||
self.workflow_state = 'failed'
|
||||
message = "No migration plugin of type #{migration_settings['migration_type']} found."
|
||||
message = "No migration plugin of type #{migration_type} found."
|
||||
migration_settings[:last_error] = message
|
||||
logger.error message
|
||||
self.save
|
||||
end
|
||||
end
|
||||
alias_method :queue_migration, :export_content
|
||||
|
||||
def check_quiz_id_prepender
|
||||
if !migration_settings[:id_prepender] && !migration_settings[:overwrite_questions]
|
||||
if !migration_settings[:id_prepender] && (!migration_settings[:overwrite_questions] || !migration_settings[:overwrite_quizzes])
|
||||
# only prepend an id if the course already has some migrated questions/quizzes
|
||||
if self.context.assessment_questions.where('assessment_questions.migration_id IS NOT NULL').exists? ||
|
||||
(self.context.respond_to?(:quizzes) && self.context.quizzes.where('quizzes.migration_id IS NOT NULL').exists?)
|
||||
|
@ -304,6 +357,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
return false unless mig_id
|
||||
return true unless migration_settings[:migration_ids_to_import] && migration_settings[:migration_ids_to_import][:copy] && migration_settings[:migration_ids_to_import][:copy].length > 0
|
||||
return true if is_set?(to_import(:everything))
|
||||
return true if copy_options && copy_options[:everything]
|
||||
|
||||
return true if is_set?(to_import("all_#{asset_type}"))
|
||||
|
||||
|
@ -317,6 +371,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def import_content
|
||||
reset_job_progress unless import_immediately?
|
||||
self.workflow_state = :importing
|
||||
self.save
|
||||
|
||||
|
@ -366,15 +421,26 @@ class ContentMigration < ActiveRecord::Base
|
|||
|
||||
def copy_options=(options)
|
||||
self.migration_settings[:copy_options] = options
|
||||
set_date_shift_options options
|
||||
end
|
||||
|
||||
def for_course_copy?
|
||||
!!self.source_course
|
||||
end
|
||||
|
||||
def set_date_shift_options(opts)
|
||||
if opts && Canvas::Plugin.value_to_boolean(opts[:shift_dates])
|
||||
self.migration_settings[:date_shift_options] = opts.slice(:shift_dates, :old_start_date, :old_end_date, :new_start_date, :new_end_date, :day_substitutions, :time_zone)
|
||||
end
|
||||
end
|
||||
|
||||
def date_shift_options
|
||||
self.migration_settings[:date_shift_options]
|
||||
end
|
||||
|
||||
def copy_course
|
||||
self.workflow_state = :pre_processing
|
||||
self.progress = 0
|
||||
reset_job_progress
|
||||
self.migration_settings[:skip_import_notification] = true
|
||||
self.save
|
||||
|
||||
|
@ -395,14 +461,6 @@ class ContentMigration < ActiveRecord::Base
|
|||
self.attachment = ce.attachment
|
||||
migration_settings[:migration_ids_to_import] ||= {:copy=>{}}
|
||||
migration_settings[:migration_ids_to_import][:copy][:everything] = true
|
||||
if copy_options[:shift_dates]
|
||||
migration_settings[:migration_ids_to_import][:copy][:shift_dates] = copy_options[:shift_dates]
|
||||
migration_settings[:migration_ids_to_import][:copy][:old_start_date] = copy_options[:old_start_date]
|
||||
migration_settings[:migration_ids_to_import][:copy][:old_end_date] = copy_options[:old_end_date]
|
||||
migration_settings[:migration_ids_to_import][:copy][:new_start_date] = copy_options[:new_start_date]
|
||||
migration_settings[:migration_ids_to_import][:copy][:new_end_date] = copy_options[:new_end_date]
|
||||
migration_settings[:migration_ids_to_import][:copy][:day_substitutions] = copy_options[:day_substitutions]
|
||||
end
|
||||
# set any attachments referenced in html to be copied
|
||||
ce.selected_content['attachments'] ||= {}
|
||||
ce.referenced_files.values.each do |att_mig_id|
|
||||
|
@ -417,10 +475,10 @@ class ContentMigration < ActiveRecord::Base
|
|||
self.reload
|
||||
if self.workflow_state == 'exported'
|
||||
self.workflow_state = :pre_processed
|
||||
self.progress = 10
|
||||
self.update_import_progress(10)
|
||||
|
||||
self.context.copy_attachments_from_course(self.source_course, :content_export => ce, :content_migration => self)
|
||||
self.progress = 20
|
||||
self.update_import_progress(20)
|
||||
|
||||
self.import_content_without_send_later
|
||||
end
|
||||
|
@ -469,6 +527,28 @@ class ContentMigration < ActiveRecord::Base
|
|||
@zip_file.close if @zip_file
|
||||
@zip_file = nil
|
||||
end
|
||||
|
||||
def finished_converting
|
||||
#todo finish progress if selective
|
||||
end
|
||||
|
||||
# expects values between 0 and 100 for the conversion process
|
||||
def update_conversion_progress(prog)
|
||||
if import_immediately?
|
||||
fast_update_progress(prog * 0.5)
|
||||
else
|
||||
fast_update_progress(prog)
|
||||
end
|
||||
end
|
||||
|
||||
# expects values between 0 and 100 for the import process
|
||||
def update_import_progress(prog)
|
||||
if import_immediately?
|
||||
fast_update_progress(50 + (prog * 0.5))
|
||||
else
|
||||
fast_update_progress(prog)
|
||||
end
|
||||
end
|
||||
|
||||
def progress
|
||||
return nil if self.workflow_state == 'created'
|
||||
|
@ -489,6 +569,9 @@ class ContentMigration < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def fast_update_progress(val)
|
||||
reset_job_progress unless job_progress
|
||||
job_progress.update_completion!(val)
|
||||
# Until this progress is phased out
|
||||
self.progress = val
|
||||
ContentMigration.where(:id => self).update_all(:progress=>val)
|
||||
end
|
||||
|
|
|
@ -1849,7 +1849,7 @@ class Course < ActiveRecord::Base
|
|||
current += 1
|
||||
if (current - last) > 10
|
||||
last = current
|
||||
migration.fast_update_progress((current.to_f/total) * 18.0)
|
||||
migration.update_import_progress((current.to_f/total) * 18.0)
|
||||
end
|
||||
end
|
||||
unzip_opts = {
|
||||
|
@ -1866,7 +1866,7 @@ class Course < ActiveRecord::Base
|
|||
root_path, migration.context)
|
||||
end
|
||||
unzipper = UnzipAttachment.new(unzip_opts)
|
||||
migration.fast_update_progress(1.0)
|
||||
migration.update_import_progress(1.0)
|
||||
unzipper.process
|
||||
end
|
||||
end
|
||||
|
@ -1895,42 +1895,42 @@ class Course < ActiveRecord::Base
|
|||
if !migration.for_course_copy?
|
||||
# These only need to be processed once
|
||||
Attachment.skip_media_object_creation do
|
||||
process_migration_files(data, migration); migration.fast_update_progress(18)
|
||||
Attachment.process_migration(data, migration); migration.fast_update_progress(20)
|
||||
process_migration_files(data, migration); migration.update_import_progress(18)
|
||||
Attachment.process_migration(data, migration); migration.update_import_progress(20)
|
||||
mo_attachments = self.imported_migration_items.find_all { |i| i.is_a?(Attachment) && i.media_entry_id.present? }
|
||||
import_media_objects(mo_attachments, migration)
|
||||
end
|
||||
end
|
||||
|
||||
migration.fast_update_progress(31)
|
||||
question_data = AssessmentQuestion.process_migration(data, migration); migration.fast_update_progress(35)
|
||||
Group.process_migration(data, migration); migration.fast_update_progress(36)
|
||||
LearningOutcome.process_migration(data, migration); migration.fast_update_progress(37)
|
||||
Rubric.process_migration(data, migration); migration.fast_update_progress(38)
|
||||
migration.update_import_progress(31)
|
||||
question_data = AssessmentQuestion.process_migration(data, migration); migration.update_import_progress(35)
|
||||
Group.process_migration(data, migration); migration.update_import_progress(36)
|
||||
LearningOutcome.process_migration(data, migration); migration.update_import_progress(37)
|
||||
Rubric.process_migration(data, migration); migration.update_import_progress(38)
|
||||
@assignment_group_no_drop_assignments = {}
|
||||
AssignmentGroup.process_migration(data, migration); migration.fast_update_progress(39)
|
||||
ExternalFeed.process_migration(data, migration); migration.fast_update_progress(39.5)
|
||||
GradingStandard.process_migration(data, migration); migration.fast_update_progress(40)
|
||||
Quiz.process_migration(data, migration, question_data); migration.fast_update_progress(50)
|
||||
ContextExternalTool.process_migration(data, migration); migration.fast_update_progress(54)
|
||||
AssignmentGroup.process_migration(data, migration); migration.update_import_progress(39)
|
||||
ExternalFeed.process_migration(data, migration); migration.update_import_progress(39.5)
|
||||
GradingStandard.process_migration(data, migration); migration.update_import_progress(40)
|
||||
Quiz.process_migration(data, migration, question_data); migration.update_import_progress(50)
|
||||
ContextExternalTool.process_migration(data, migration); migration.update_import_progress(54)
|
||||
|
||||
#These need to be ran twice because they can reference each other
|
||||
DiscussionTopic.process_migration(data, migration);migration.fast_update_progress(55)
|
||||
WikiPage.process_migration(data, migration);migration.fast_update_progress(60)
|
||||
Assignment.process_migration(data, migration);migration.fast_update_progress(65)
|
||||
ContextModule.process_migration(data, migration);migration.fast_update_progress(70)
|
||||
DiscussionTopic.process_migration(data, migration);migration.update_import_progress(55)
|
||||
WikiPage.process_migration(data, migration);migration.update_import_progress(60)
|
||||
Assignment.process_migration(data, migration);migration.update_import_progress(65)
|
||||
ContextModule.process_migration(data, migration);migration.update_import_progress(70)
|
||||
# and second time...
|
||||
DiscussionTopic.process_migration(data, migration);migration.fast_update_progress(75)
|
||||
WikiPage.process_migration(data, migration);migration.fast_update_progress(80)
|
||||
Assignment.process_migration(data, migration);migration.fast_update_progress(85)
|
||||
DiscussionTopic.process_migration(data, migration);migration.update_import_progress(75)
|
||||
WikiPage.process_migration(data, migration);migration.update_import_progress(80)
|
||||
Assignment.process_migration(data, migration);migration.update_import_progress(85)
|
||||
|
||||
#These aren't referenced by anything, but reference other things
|
||||
CalendarEvent.process_migration(data, migration);migration.fast_update_progress(90)
|
||||
WikiPage.process_migration_course_outline(data, migration);migration.fast_update_progress(95)
|
||||
CalendarEvent.process_migration(data, migration);migration.update_import_progress(90)
|
||||
WikiPage.process_migration_course_outline(data, migration);migration.update_import_progress(95)
|
||||
|
||||
everything_selected = !migration.copy_options || migration.is_set?(migration.copy_options[:everything])
|
||||
if everything_selected || migration.is_set?(migration.copy_options[:all_course_settings])
|
||||
import_settings_from_migration(data, migration); migration.fast_update_progress(96)
|
||||
import_settings_from_migration(data, migration); migration.update_import_progress(96)
|
||||
end
|
||||
|
||||
syllabus_should_be_added = everything_selected || migration.copy_options[:syllabus_body]
|
||||
|
@ -1943,8 +1943,7 @@ class Course < ActiveRecord::Base
|
|||
|
||||
begin
|
||||
#Adjust dates
|
||||
if bool_res(params[:copy][:shift_dates])
|
||||
shift_options = (bool_res(params[:copy][:shift_dates]) rescue false) ? params[:copy] : {}
|
||||
if shift_options = migration.date_shift_options
|
||||
shift_options = shift_date_options(self, shift_options)
|
||||
@imported_migration_items.each do |event|
|
||||
if event.is_a?(Assignment)
|
||||
|
@ -2089,7 +2088,7 @@ class Course < ActiveRecord::Base
|
|||
total = attachments.count + 1
|
||||
|
||||
attachments.each_with_index do |file, i|
|
||||
cm.fast_update_progress((i.to_f/total) * 18.0) if cm && (i % 10 == 0)
|
||||
cm.update_import_progress((i.to_f/total) * 18.0) if cm && (i % 10 == 0)
|
||||
|
||||
if !ce || ce.export_object?(file)
|
||||
begin
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class Progress < ActiveRecord::Base
|
||||
belongs_to :context, :polymorphic => true
|
||||
belongs_to :user
|
||||
attr_accessible :context, :tag, :completion, :message
|
||||
|
||||
validates_presence_of :context_id
|
||||
|
|
|
@ -762,7 +762,8 @@ ActionController::Routing::Routes.draw do |map|
|
|||
api.with_options(:controller => :content_migrations) do |cm|
|
||||
cm.get 'courses/:course_id/content_migrations/:id', :action => :show, :path_name => 'course_content_migration'
|
||||
cm.get 'courses/:course_id/content_migrations', :action => :index, :path_name => 'course_content_migration_list'
|
||||
cm.get 'courses/:course_id/content_migrations/:id/download_archive', :action => 'download_archive', :conditions => {:method => :get}, :path_name => 'course_content_migration_download'
|
||||
cm.post 'courses/:course_id/content_migrations', :action => :create, :path_name => 'course_content_migration_create'
|
||||
cm.put 'courses/:course_id/content_migrations/:id', :action => :update, :path_name => 'course_content_migration_update'
|
||||
end
|
||||
|
||||
api.with_options(:controller => :migration_issues) do |mi|
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class AddTypeToContentMigration < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def self.up
|
||||
add_column :content_migrations, :migration_type, :string
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :content_migrations, :migration_type
|
||||
end
|
||||
end
|
|
@ -57,49 +57,62 @@ module Api::V1::Attachment
|
|||
# render the attachment ajax_upload_params on success, or a relevant
|
||||
# error on failure.
|
||||
def api_attachment_preflight(context, request, opts = {})
|
||||
params = opts[:params] || request.params
|
||||
@attachment = Attachment.new
|
||||
@attachment.context = context
|
||||
@attachment.filename = request.params[:name]
|
||||
@attachment.filename = params[:name]
|
||||
atts = process_attachment_params(params)
|
||||
atts.delete(:display_name)
|
||||
@attachment.attributes = atts
|
||||
@attachment.submission_attachment = true if opts[:submission_attachment]
|
||||
@attachment.file_state = 'deleted'
|
||||
@attachment.workflow_state = 'unattached'
|
||||
@attachment.content_type = request.params[:content_type].presence || Attachment.mimetype(@attachment.filename)
|
||||
@attachment.content_type = params[:content_type].presence || Attachment.mimetype(@attachment.filename)
|
||||
# Handle deprecated folder path
|
||||
request.params[:parent_folder_path] ||= request.params[:folder]
|
||||
params[:parent_folder_path] ||= params[:folder]
|
||||
if opts.key?(:folder)
|
||||
@attachment.folder = folder
|
||||
elsif request.params[:parent_folder_path] && request.params[:parent_folder_id]
|
||||
elsif params[:parent_folder_path] && params[:parent_folder_id]
|
||||
render :json => {:message => I18n.t('lib.api.attachments.only_one_folder', "Can't set folder path and folder id")}, :status => 400
|
||||
return
|
||||
elsif request.params[:parent_folder_id]
|
||||
@attachment.folder = context.folders.find(request.params.delete(:parent_folder_id))
|
||||
elsif context.respond_to?(:folders) && request.params[:parent_folder_path].is_a?(String)
|
||||
@attachment.folder = Folder.assert_path(request.params[:parent_folder_path], context)
|
||||
elsif params[:parent_folder_id]
|
||||
@attachment.folder = context.folders.find(params.delete(:parent_folder_id))
|
||||
elsif context.respond_to?(:folders) && params[:parent_folder_path].is_a?(String)
|
||||
@attachment.folder = Folder.assert_path(params[:parent_folder_path], context)
|
||||
end
|
||||
duplicate_handling = check_duplicate_handling_option(request)
|
||||
duplicate_handling = check_duplicate_handling_option(params)
|
||||
if opts[:check_quota]
|
||||
get_quota
|
||||
if request.params[:size] && @quota < @quota_used + request.params[:size].to_i
|
||||
render(:json => { :message => 'file size exceeds quota' }, :status => :bad_request)
|
||||
return
|
||||
if params[:size] && @quota < @quota_used + params[:size].to_i
|
||||
message = { :message => I18n.t('lib.api.over_quota', 'file size exceeds quota') }
|
||||
if opts[:return_json]
|
||||
message[:error] = true
|
||||
return message
|
||||
else
|
||||
render(:json => message, :status => :bad_request)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
@attachment.save!
|
||||
if request.params[:url]
|
||||
@attachment.send_later_enqueue_args(:clone_url, { :priority => Delayed::LOW_PRIORITY, :max_attempts => 1, :n_strand => 'file_download' }, request.params[:url], duplicate_handling, opts[:check_quota])
|
||||
render :json => { :id => @attachment.id, :upload_status => 'pending', :status_url => api_v1_file_status_url(@attachment, @attachment.uuid) }
|
||||
if params[:url]
|
||||
@attachment.send_later_enqueue_args(:clone_url, { :priority => Delayed::LOW_PRIORITY, :max_attempts => 1, :n_strand => 'file_download' }, params[:url], duplicate_handling, opts[:check_quota])
|
||||
json = { :id => @attachment.id, :upload_status => 'pending', :status_url => api_v1_file_status_url(@attachment, @attachment.uuid) }
|
||||
else
|
||||
duplicate_handling = nil if duplicate_handling == 'overwrite'
|
||||
quota_exemption = opts[:check_quota] ? nil : @attachment.quota_exemption_key
|
||||
render :json => @attachment.ajax_upload_params(@current_pseudonym,
|
||||
json = @attachment.ajax_upload_params(@current_pseudonym,
|
||||
api_v1_files_create_url(:on_duplicate => duplicate_handling, :quota_exemption => quota_exemption),
|
||||
api_v1_files_create_success_url(@attachment, :uuid => @attachment.uuid, :on_duplicate => duplicate_handling, :quota_exemption => quota_exemption),
|
||||
:ssl => request.ssl?, :file_param => opts[:file_param]).
|
||||
slice(:upload_url,:upload_params,:file_param,:remote_url)
|
||||
end
|
||||
|
||||
if opts[:return_json]
|
||||
json
|
||||
else
|
||||
render :json => json
|
||||
end
|
||||
end
|
||||
|
||||
def check_quota_after_attachment(request)
|
||||
|
@ -111,8 +124,8 @@ module Api::V1::Attachment
|
|||
return true
|
||||
end
|
||||
|
||||
def check_duplicate_handling_option(request)
|
||||
duplicate_handling = request.params[:on_duplicate].presence || 'overwrite'
|
||||
def check_duplicate_handling_option(params)
|
||||
duplicate_handling = params[:on_duplicate].presence || 'overwrite'
|
||||
unless %w(rename overwrite).include?(duplicate_handling)
|
||||
render(:json => { :message => 'invalid on_duplicate option' }, :status => :bad_request)
|
||||
return nil
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
module Api::V1::ContentMigration
|
||||
include Api::V1::Json
|
||||
include Api::V1::Attachment
|
||||
|
||||
def content_migrations_json(migrations, current_user, session)
|
||||
migrations.map do |migration|
|
||||
|
@ -25,14 +26,33 @@ module Api::V1::ContentMigration
|
|||
end
|
||||
end
|
||||
|
||||
def content_migration_json(migration, current_user, session)
|
||||
json = api_json(migration, current_user, session, :only => %w(id user_id workflow_state started_at finished_at))
|
||||
json[:workflow_state] = 'converting' if json[:workflow_state] == 'exporting'
|
||||
json[:workflow_state] = 'converted' if json[:workflow_state] == 'exported'
|
||||
def content_migration_json(migration, current_user, session, attachment_preflight=nil)
|
||||
json = api_json(migration, current_user, session, :only => %w(id user_id workflow_state started_at finished_at migration_type))
|
||||
if json[:workflow_state] == 'created'
|
||||
json[:workflow_state] = 'pre_processing'
|
||||
elsif json[:workflow_state] == 'pre_process_error'
|
||||
json[:workflow_state] = 'failed'
|
||||
elsif json[:workflow_state] == 'exported' && !migration.import_immediately?
|
||||
json[:workflow_state] = 'waiting_for_select'
|
||||
elsif ['exporting', 'importing', 'exported'].member?(json[:workflow_state])
|
||||
json[:workflow_state] = 'running'
|
||||
elsif json[:workflow_state] == 'imported'
|
||||
json[:workflow_state] = 'completed'
|
||||
end
|
||||
json[:migration_issues_url] = api_v1_course_content_migration_migration_issue_list_url(migration.context_id, migration.id)
|
||||
json[:migration_issues_count] = migration.migration_issues.count
|
||||
if migration.attachment
|
||||
json[:content_archive_download_url] = api_v1_course_content_migration_download_url(migration.context_id, migration.id)
|
||||
if attachment_preflight
|
||||
json[:pre_attachment] = attachment_preflight
|
||||
elsif migration.attachment
|
||||
json[:attachment] = attachment_json(migration.attachment, current_user, {}, {:can_manage_files => true})
|
||||
end
|
||||
if migration.job_progress
|
||||
json['progress_url'] = polymorphic_url([:api_v1, migration.job_progress])
|
||||
end
|
||||
if plugin = Canvas::Plugin.find(migration.migration_type)
|
||||
if plugin.meta[:name] && plugin.meta[:name].respond_to?(:call)
|
||||
json['migration_type_title'] = plugin.meta[:name].call
|
||||
end
|
||||
end
|
||||
json
|
||||
end
|
||||
|
|
|
@ -93,8 +93,8 @@ module MigratorHelper
|
|||
end
|
||||
|
||||
def set_progress(progress)
|
||||
if content_migration && content_migration.respond_to?(:fast_update_progress)
|
||||
content_migration.fast_update_progress(progress)
|
||||
if content_migration && content_migration.respond_to?(:update_conversion_progress)
|
||||
content_migration.update_conversion_progress(progress)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -125,28 +125,30 @@ Canvas::Plugin.register('tinychat', nil, {
|
|||
})
|
||||
require_dependency 'cc/importer/cc_worker'
|
||||
Canvas::Plugin.register 'canvas_cartridge_importer', :export_system, {
|
||||
:name => lambda{ t :name, 'Canvas Cartridge Importer' },
|
||||
:name => lambda{ I18n.t 'canvas_cartridge_name', 'Canvas Cartridge Importer' },
|
||||
:author => 'Instructure',
|
||||
:author_website => 'http://www.instructure.com',
|
||||
:description => lambda{ t :description, 'This enables converting a canvas export to the intermediary json format to be imported' },
|
||||
:description => lambda{ I18n.t :canvas_cartridge_description, 'This enables converting a canvas export to the intermediary json format to be imported' },
|
||||
:version => '1.0.0',
|
||||
:select_text => lambda{ t :file_description, "Canvas Course Export Package" },
|
||||
:select_text => lambda{ I18n.t :canvas_cartridge_file_description, "Canvas Course Export Package" },
|
||||
:settings => {
|
||||
:worker => 'CCWorker',
|
||||
:migration_partial => 'canvas_config',
|
||||
:requires_file_upload => true,
|
||||
:provides =>{:canvas_cartridge => CC::Importer::Canvas::Converter}
|
||||
},
|
||||
}
|
||||
Canvas::Plugin.register 'common_cartridge_importer', :export_system, {
|
||||
:name => lambda{ t :name, 'Common Cartridge Importer' },
|
||||
:name => lambda{ I18n.t :common_cartridge_name, 'Common Cartridge Importer' },
|
||||
:author => 'Instructure',
|
||||
:author_website => 'http://www.instructure.com',
|
||||
:description => lambda{ t :description, 'This enables converting a Common Cartridge packages in the intermediary json format to be imported' },
|
||||
:description => lambda{ I18n.t :common_cartridge_description, 'This enables converting a Common Cartridge packages in the intermediary json format to be imported' },
|
||||
:version => '1.0.0',
|
||||
:select_text => lambda{ t :file_description, "Common Cartridge 1.0/1.1/1.2 Package" },
|
||||
:select_text => lambda{ I18n.t :common_cartridge_file_description, "Common Cartridge 1.0/1.1/1.2 Package" },
|
||||
:settings => {
|
||||
:worker => 'CCWorker',
|
||||
:migration_partial => 'cc_config',
|
||||
:requires_file_upload => true,
|
||||
:provides =>{:common_cartridge=>CC::Importer::Standard::Converter,
|
||||
:common_cartridge_1_0=>CC::Importer::Standard::Converter,
|
||||
:common_cartridge_1_1=>CC::Importer::Standard::Converter,
|
||||
|
|
|
@ -19,7 +19,7 @@ class Canvas::Migration::Worker::CCWorker < Struct.new(:migration_id)
|
|||
def perform
|
||||
cm = ContentMigration.find_by_id migration_id
|
||||
begin
|
||||
cm.fast_update_progress(1)
|
||||
cm.update_conversion_progress(1)
|
||||
settings = cm.migration_settings.clone
|
||||
settings[:content_migration_id] = migration_id
|
||||
settings[:user_id] = cm.user_id
|
||||
|
@ -36,12 +36,12 @@ class Canvas::Migration::Worker::CCWorker < Struct.new(:migration_id)
|
|||
if overview_file_path
|
||||
file = File.new(overview_file_path)
|
||||
Canvas::Migration::Worker::upload_overview_file(file, cm)
|
||||
cm.fast_update_progress(95)
|
||||
cm.update_conversion_progress(95)
|
||||
end
|
||||
if export_folder_path
|
||||
Canvas::Migration::Worker::upload_exported_data(export_folder_path, cm)
|
||||
Canvas::Migration::Worker::clear_exported_data(export_folder_path)
|
||||
cm.fast_update_progress(100)
|
||||
cm.update_conversion_progress(100)
|
||||
end
|
||||
|
||||
cm.migration_settings[:worker_class] = converter_class.name
|
||||
|
@ -49,12 +49,11 @@ class Canvas::Migration::Worker::CCWorker < Struct.new(:migration_id)
|
|||
cm.migration_settings[:migration_ids_to_import] = {:copy=>{:everything => true}}
|
||||
end
|
||||
cm.workflow_state = :exported
|
||||
cm.progress = 0
|
||||
saved = cm.save
|
||||
|
||||
if cm.import_immediately?
|
||||
cm.import_content_without_send_later
|
||||
cm.progress = 100
|
||||
cm.update_import_progress(100)
|
||||
saved = cm.save
|
||||
if converter.respond_to?(:post_process)
|
||||
converter.post_process
|
||||
|
|
|
@ -285,9 +285,12 @@ define([
|
|||
var progress = 0;
|
||||
if (course_import) {
|
||||
progress = Math.max($(".copy_progress").progressbar('option', 'value') || 0, course_import.progress);
|
||||
if( course_import.workflow_state == "exported") {
|
||||
progress = 0;
|
||||
}
|
||||
$(".copy_progress").progressbar('option', 'value', progress);
|
||||
}
|
||||
if (course_import && course_import.progress >= 100) {
|
||||
if (course_import && course_import.workflow_state == 'imported') {
|
||||
$.flashMessage(I18n.t('messages.import_complete', "Import Complete! Returning to the Course Page..."));
|
||||
location.href = $(".course_url").attr('href');
|
||||
} else if (course_import && course_import.workflow_state == 'failed') {
|
||||
|
|
|
@ -25,6 +25,8 @@ describe ContentMigrationsController, :type => :integration do
|
|||
@params = { :controller => 'content_migrations', :format => 'json', :course_id => @course.id.to_param}
|
||||
|
||||
@migration = @course.content_migrations.create
|
||||
@migration.migration_type = 'common_cartridge_importer'
|
||||
@migration.context = @course
|
||||
@migration.user = @user
|
||||
@migration.started_at = 1.week.ago
|
||||
@migration.finished_at = 1.day.ago
|
||||
|
@ -67,16 +69,28 @@ describe ContentMigrationsController, :type => :integration do
|
|||
it "should return migration" do
|
||||
@migration.attachment = Attachment.create!(:context => @migration, :filename => "test.txt", :uploaded_data => StringIO.new("test file"))
|
||||
@migration.save!
|
||||
progress = Progress.create!(:tag => "content_migration", :context => @migration)
|
||||
json = api_call(:get, @migration_url, @params)
|
||||
|
||||
json['id'].should == @migration.id
|
||||
json['migration_type'].should == @migration.migration_type
|
||||
json['finished_at'].should_not be_nil
|
||||
json['started_at'].should_not be_nil
|
||||
json['user_id'].should == @user.id
|
||||
json["workflow_state"].should == "created"
|
||||
json["workflow_state"].should == "pre_processing"
|
||||
json["migration_issues_url"].should == "http://www.example.com/api/v1/courses/#{@course.id}/content_migrations/#{@migration.id}/migration_issues"
|
||||
json["migration_issues_count"].should == 0
|
||||
json["content_archive_download_url"].should == "http://www.example.com/api/v1/courses/#{@course.id}/content_migrations/#{@migration.id}/download_archive"
|
||||
json["attachment"]["url"].should =~ %r{/files/#{@migration.attachment.id}/download}
|
||||
json['progress_url'].should == "http://www.example.com/api/v1/progress/#{progress.id}"
|
||||
json['migration_type_title'].should == 'Common Cartridge Importer'
|
||||
end
|
||||
|
||||
it "should return waiting_for_select when it's supposed to" do
|
||||
@migration.workflow_state = 'exported'
|
||||
@migration.migration_settings[:import_immediately] = false
|
||||
@migration.save!
|
||||
json = api_call(:get, @migration_url, @params)
|
||||
json['workflow_state'].should == 'waiting_for_select'
|
||||
end
|
||||
|
||||
it "should 404" do
|
||||
|
@ -89,22 +103,138 @@ describe ContentMigrationsController, :type => :integration do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'download_archive' do
|
||||
describe 'create' do
|
||||
|
||||
before do
|
||||
@migration_url = @migration_url + "/#{@migration.id}/download_archive"
|
||||
@params = @params.merge({:action => 'download_archive', :id => @migration.id.to_param})
|
||||
@params = {:controller => 'content_migrations', :format => 'json', :course_id => @course.id.to_param, :action => 'create'}
|
||||
@post_params = {:migration_type => 'common_cartridge_importer', :pre_attachment => {:name => "test.zip"}}
|
||||
end
|
||||
|
||||
it "should send file" do
|
||||
@migration.attachment = Attachment.create!(:context => @migration, :filename => "test.txt", :uploaded_data => StringIO.new("test file"))
|
||||
@migration.save!
|
||||
raw_api_call :get, @migration_url, @params
|
||||
response.header['Content-Disposition'].should == 'attachment; filename="test.txt"'
|
||||
it "should error for unknown type" do
|
||||
json = api_call(:post, @migration_url, @params, {:migration_type => 'jerk'}, {}, :expected_status => 400)
|
||||
json.should == {"message"=>"Invalid migration_type"}
|
||||
end
|
||||
|
||||
it "should 404" do
|
||||
api_call(:get, @migration_url, @params, {}, {}, :expected_status => 404)
|
||||
it "should queue a migration" do
|
||||
@post_params.delete :pre_attachment
|
||||
p = Canvas::Plugin.new("hi")
|
||||
p.stubs(:settings).returns('worker' => 'CCWorker')
|
||||
Canvas::Plugin.stubs(:find).returns(p)
|
||||
json = api_call(:post, @migration_url, @params, @post_params)
|
||||
json["workflow_state"].should == 'running'
|
||||
migration = ContentMigration.find json['id']
|
||||
migration.workflow_state.should == "exporting"
|
||||
migration.job_progress.workflow_state.should == 'queued'
|
||||
end
|
||||
|
||||
it "should not queue a migration if do_not_run flag is set" do
|
||||
@post_params.delete :pre_attachment
|
||||
Canvas::Plugin.stubs(:find).returns(Canvas::Plugin.new("oi"))
|
||||
json = api_call(:post, @migration_url, @params, @post_params.merge(:do_not_run => true))
|
||||
json["workflow_state"].should == 'pre_processing'
|
||||
migration = ContentMigration.find json['id']
|
||||
migration.workflow_state.should == "created"
|
||||
migration.job_progress.should be_nil
|
||||
end
|
||||
|
||||
context "migration file upload" do
|
||||
it "should set attachment pre-flight data" do
|
||||
json = api_call(:post, @migration_url, @params, @post_params)
|
||||
json['pre_attachment'].should_not be_nil
|
||||
json['pre_attachment']["upload_params"]["key"].end_with?("test.zip").should == true
|
||||
end
|
||||
|
||||
it "should not queue migration with pre_attachent on create" do
|
||||
json = api_call(:post, @migration_url, @params, @post_params)
|
||||
json["workflow_state"].should == 'pre_processing'
|
||||
migration = ContentMigration.find json['id']
|
||||
migration.workflow_state.should == "pre_processing"
|
||||
end
|
||||
|
||||
it "should error if upload file required but not provided" do
|
||||
@post_params.delete :pre_attachment
|
||||
json = api_call(:post, @migration_url, @params, @post_params, {}, :expected_status => 400)
|
||||
json.should == {"message"=>"File upload is required"}
|
||||
end
|
||||
|
||||
it "should queue the migration when file finishes uploading" do
|
||||
local_storage!
|
||||
@attachment = Attachment.create!(:context => @migration, :filename => "test.zip", :uploaded_data => StringIO.new("test file"))
|
||||
@attachment.file_state = "deleted"
|
||||
@attachment.workflow_state = "unattached"
|
||||
@attachment.save
|
||||
@migration.attachment = @attachment
|
||||
@migration.save!
|
||||
@attachment.workflow_state = nil
|
||||
@content = Tempfile.new(["test", ".zip"])
|
||||
def @content.content_type
|
||||
"application/zip"
|
||||
end
|
||||
@content.write("test file")
|
||||
@content.rewind
|
||||
@attachment.uploaded_data = @content
|
||||
@attachment.save!
|
||||
api_call(:post, "/api/v1/files/#{@attachment.id}/create_success?uuid=#{@attachment.uuid}",
|
||||
{:controller => "files", :action => "api_create_success", :format => "json", :id => @attachment.to_param, :uuid => @attachment.uuid})
|
||||
|
||||
@migration.reload
|
||||
@migration.attachment.should_not be_nil
|
||||
@migration.workflow_state.should == "exporting"
|
||||
@migration.job_progress.workflow_state.should == 'queued'
|
||||
end
|
||||
|
||||
it "should error if course quota exceeded" do
|
||||
@post_params.merge!(:pre_attachment => {:name => "test.zip", :size => 1.gigabyte})
|
||||
json = api_call(:post, @migration_url, @params, @post_params)
|
||||
json['pre_attachment'].should == {"message"=>"file size exceeds quota", "error" => true}
|
||||
json["workflow_state"].should == 'failed'
|
||||
migration = ContentMigration.find json['id']
|
||||
migration.workflow_state = 'pre_process_error'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'update' do
|
||||
before do
|
||||
@migration_url = "/api/v1/courses/#{@course.id}/content_migrations/#{@migration.id}"
|
||||
@params = {:controller => 'content_migrations', :format => 'json', :course_id => @course.id.to_param, :action => 'update', :id => @migration.id.to_param}
|
||||
@post_params = {}
|
||||
end
|
||||
|
||||
it "should queue a migration" do
|
||||
json = api_call(:put, @migration_url, @params, @post_params)
|
||||
json["workflow_state"].should == 'running'
|
||||
@migration.reload
|
||||
@migration.workflow_state.should == "exporting"
|
||||
@migration.job_progress.workflow_state.should == 'queued'
|
||||
end
|
||||
|
||||
it "should not queue a migration if do_not_run flag is set" do
|
||||
json = api_call(:put, @migration_url, @params, @post_params.merge(:do_not_run => true))
|
||||
json["workflow_state"].should == 'pre_processing'
|
||||
migration = ContentMigration.find json['id']
|
||||
migration.workflow_state.should == "created"
|
||||
migration.job_progress.should be_nil
|
||||
end
|
||||
|
||||
it "should not change migration_type" do
|
||||
json = api_call(:put, @migration_url, @params, @post_params.merge(:migration_type => "oioioi"))
|
||||
json['migration_type'].should == 'common_cartridge_importer'
|
||||
end
|
||||
|
||||
it "should reset progress after queue" do
|
||||
p = @migration.reset_job_progress
|
||||
p.completion = 100
|
||||
p.workflow_state = 'completed'
|
||||
p.save!
|
||||
api_call(:put, @migration_url, @params, @post_params)
|
||||
p.reload
|
||||
p.completion.should == 0
|
||||
p.workflow_state.should == 'queued'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
|
|
@ -46,11 +46,15 @@ describe "Files API", :type => :integration do
|
|||
@attachment.save!
|
||||
end
|
||||
|
||||
def call_create_success
|
||||
api_call(:post, "/api/v1/files/#{@attachment.id}/create_success?uuid=#{@attachment.uuid}",
|
||||
{:controller => "files", :action => "api_create_success", :format => "json", :id => @attachment.to_param, :uuid => @attachment.uuid})
|
||||
end
|
||||
|
||||
it "should set the attachment to available (local storage)" do
|
||||
local_storage!
|
||||
upload_data
|
||||
json = api_call(:post, "/api/v1/files/#{@attachment.id}/create_success?uuid=#{@attachment.uuid}",
|
||||
{ :controller => "files", :action => "api_create_success", :format => "json", :id => @attachment.to_param, :uuid => @attachment.uuid })
|
||||
json = call_create_success
|
||||
@attachment.reload
|
||||
json.should == {
|
||||
'id' => @attachment.id,
|
||||
|
@ -80,8 +84,7 @@ describe "Files API", :type => :integration do
|
|||
:content_length => 1234,
|
||||
})
|
||||
|
||||
json = api_call(:post, "/api/v1/files/#{@attachment.id}/create_success?uuid=#{@attachment.uuid}",
|
||||
{ :controller => "files", :action => "api_create_success", :format => "json", :id => @attachment.to_param, :uuid => @attachment.uuid })
|
||||
json = call_create_success
|
||||
@attachment.reload
|
||||
json.should == {
|
||||
'id' => @attachment.id,
|
||||
|
@ -117,6 +120,29 @@ describe "Files API", :type => :integration do
|
|||
{ :controller => "files", :action => "api_create_success", :format => "json", :id => @attachment.to_param, :uuid => @attachment.uuid })
|
||||
response.status.to_i.should == 400
|
||||
end
|
||||
|
||||
context "upload success context callback" do
|
||||
before do
|
||||
Course.any_instance.stubs(:file_upload_success_callback)
|
||||
Course.any_instance.expects(:file_upload_success_callback).with(@attachment)
|
||||
end
|
||||
|
||||
it "should call back for s3" do
|
||||
s3_storage!
|
||||
AWS::S3::S3Object.any_instance.expects(:head).returns({
|
||||
:content_type => 'text/plain',
|
||||
:content_length => 1234,
|
||||
})
|
||||
json = call_create_success
|
||||
end
|
||||
|
||||
it "should call back for local storage" do
|
||||
local_storage!
|
||||
upload_data
|
||||
json = call_create_success
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#index" do
|
||||
|
|
|
@ -1076,12 +1076,10 @@ describe ContentMigration do
|
|||
asmnt.save!
|
||||
@copy_from.reload
|
||||
|
||||
@cm.migration_settings[:migration_ids_to_import] = {
|
||||
:copy => {
|
||||
:shift_dates => true,
|
||||
:day_substitutions => {today.wday.to_s => (today.wday + 1).to_s}
|
||||
}
|
||||
}
|
||||
@cm.copy_options = @cm.copy_options.merge(
|
||||
:shift_dates => true,
|
||||
:day_substitutions => {today.wday.to_s => (today.wday + 1).to_s}
|
||||
)
|
||||
@cm.save!
|
||||
|
||||
run_course_copy
|
||||
|
@ -1127,9 +1125,7 @@ describe ContentMigration do
|
|||
cm.end_at = old_start + 3.days
|
||||
cm.save!
|
||||
|
||||
@cm.migration_settings[:migration_ids_to_import] = {
|
||||
:copy => options
|
||||
}
|
||||
@cm.copy_options = options
|
||||
@cm.save!
|
||||
|
||||
run_course_copy
|
||||
|
@ -1208,18 +1204,16 @@ describe ContentMigration do
|
|||
assignment = @copy_from.assignments.create! :title => 'Assignment', :due_at => old_date
|
||||
assignment.save!
|
||||
|
||||
migration_settings = {
|
||||
:copy => {
|
||||
:everything => true,
|
||||
:shift_dates => true,
|
||||
:old_start_date => old_start_date,
|
||||
:old_end_date => old_end_date,
|
||||
:new_start_date => new_start_date,
|
||||
:new_end_date => new_end_date
|
||||
}
|
||||
opts = {
|
||||
:everything => true,
|
||||
:shift_dates => true,
|
||||
:old_start_date => old_start_date,
|
||||
:old_end_date => old_end_date,
|
||||
:new_start_date => new_start_date,
|
||||
:new_end_date => new_end_date
|
||||
}
|
||||
migration_settings[:copy][:time_zone] = options[:time_zone].name if options.include?(:time_zone)
|
||||
@cm.migration_settings[:migration_ids_to_import] = migration_settings
|
||||
opts[:time_zone] = options[:time_zone].name if options.include?(:time_zone)
|
||||
@cm.copy_options = @cm.copy_options.merge(opts)
|
||||
@cm.save!
|
||||
|
||||
run_course_copy
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
Rails.configuration.to_prepare do
|
||||
|
||||
Canvas::Plugin.register :moodle_converter, :export_system, {
|
||||
:name => proc { t(:name, 'Moodle Importer') },
|
||||
:name => proc { I18n.t(:m2c_name, 'Moodle Importer') },
|
||||
:author => 'Divergent Logic',
|
||||
:description => 'This enables importing Moodle 1.9 .zip files to Canvas.',
|
||||
:version => '1.0.0',
|
||||
:select_text => proc { t(:file_description, 'Moodle 1.9 .zip file') },
|
||||
:select_text => proc { I18n.t(:m2c_file_description, 'Moodle 1.9 .zip file') },
|
||||
:settings => {
|
||||
:migration_partial => 'moodle_config',
|
||||
:worker=> 'CCWorker',
|
||||
|
|
|
@ -2,12 +2,12 @@ Rails.configuration.to_prepare do
|
|||
python_converter_found = Qti.migration_executable ? true : false
|
||||
|
||||
Canvas::Plugin.register :qti_converter, :export_system, {
|
||||
:name => proc { t(:name, 'QTI Converter') },
|
||||
:name => proc { I18n.t(:qti_name, 'QTI Converter') },
|
||||
:author => 'Instructure',
|
||||
:description => 'This enables converting QTI .zip files to Canvas quiz json.',
|
||||
:version => '1.0.0',
|
||||
:settings_partial => 'plugins/qti_converter_settings',
|
||||
:select_text => proc { t(:file_description, 'QTI .zip file') },
|
||||
:select_text => proc { I18n.t(:qti_file_description, 'QTI .zip file') },
|
||||
:settings => {
|
||||
:enabled => python_converter_found,
|
||||
:migration_partial => 'qti_config',
|
||||
|
|
|
@ -5,6 +5,9 @@ module Canvas::Migration
|
|||
def perform
|
||||
cm = ContentMigration.find_by_id migration_id
|
||||
begin
|
||||
cm.reset_job_progress
|
||||
cm.job_progress.start
|
||||
cm.update_conversion_progress(1)
|
||||
plugin = Canvas::Plugin.find(:qti_converter)
|
||||
unless plugin && plugin.settings[:enabled]
|
||||
raise "Can't export QTI without the python converter tool installed."
|
||||
|
@ -19,6 +22,7 @@ module Canvas::Migration
|
|||
assessments = converter.export
|
||||
export_folder_path = assessments[:export_folder_path]
|
||||
overview_file_path = assessments[:overview_file_path]
|
||||
cm.update_conversion_progress(50)
|
||||
|
||||
if overview_file_path
|
||||
file = File.new(overview_file_path)
|
||||
|
@ -28,13 +32,17 @@ module Canvas::Migration
|
|||
Canvas::Migration::Worker::upload_exported_data(export_folder_path, cm)
|
||||
Canvas::Migration::Worker::clear_exported_data(export_folder_path)
|
||||
end
|
||||
cm.update_conversion_progress(100)
|
||||
|
||||
cm.migration_settings[:migration_ids_to_import] = {:copy=>{:everything=>true}}.merge(cm.migration_settings[:migration_ids_to_import] || {})
|
||||
if path = converter.course[:files_import_root_path]
|
||||
cm.migration_settings[:files_import_root_path] = path
|
||||
end
|
||||
cm.save
|
||||
cm.import_content
|
||||
cm.import_content_without_send_later
|
||||
cm.workflow_state = :imported
|
||||
cm.save
|
||||
cm.update_import_progress(100)
|
||||
rescue => e
|
||||
report = ErrorReport.log_exception(:content_migration, e)
|
||||
if cm
|
||||
|
|
|
@ -168,7 +168,6 @@ describe Qti::Converter do
|
|||
def do_migration
|
||||
Canvas::Migration::Worker::QtiWorker.new(@migration.id).perform
|
||||
@migration.reload
|
||||
@migration.import_content_without_send_later
|
||||
@migration.should be_imported
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue