convert course copy to use export/import functionality
This adds the ability for ContentMigration to copy courses by exporting to a content package and then importing that package. For attachments it just creates a new Attachment object instead of exporting/importing them. The course copy API did not change. The endpoints used by the course copy UI did change and this commit doesn't allow selective copying, it only has the option to copy everything. Selective copying will come in another commit. There are also various bug fixes for export/import Test Plan: * copy a course through the API * copy a course through the Content Imports path * copy a course through the Copy Cours button on the course settings page * export a course refs #4645 Change-Id: Ie577329ab7caaea8dfb9359542224a1a2657e167 Reviewed-on: https://gerrit.instructure.com/9742 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
This commit is contained in:
parent
37a65cec7c
commit
a942ede9c6
|
@ -1,5 +1,6 @@
|
|||
require [
|
||||
'compiled/util/processItemSelections'
|
||||
'copy_course'
|
||||
'choose_content'
|
||||
'choose_course'
|
||||
]
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class ContentExportsController < ApplicationController
|
|||
def index
|
||||
return render_unauthorized_action unless @context.grants_rights?(@current_user, nil, :read, :read_as_admin).values.all?
|
||||
|
||||
@exports = @context.content_exports.active
|
||||
@exports = @context.content_exports.active.not_for_copy
|
||||
@current_export_id = nil
|
||||
if export = @context.content_exports.running.first
|
||||
@current_export_id = export.id
|
||||
|
|
|
@ -25,6 +25,10 @@ class ContentImportsController < ApplicationController
|
|||
|
||||
include Api::V1::Course
|
||||
|
||||
COPY_TYPES = %w{assignment_groups assignments context_modules learning_outcomes
|
||||
quizzes assessment_question_banks folders attachments wiki_pages discussion_topics
|
||||
calendar_events context_external_tools learning_outcome_groups rubrics}
|
||||
|
||||
def add_imports_crumb
|
||||
if @context.is_a?(Course)
|
||||
add_crumb(t('crumbs.content_imports', "Content Imports"), named_context_url(@context, :context_imports_url))
|
||||
|
@ -166,33 +170,43 @@ class ContentImportsController < ApplicationController
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def copy_course
|
||||
|
||||
def choose_content
|
||||
if authorized_action(@context, @current_user, :manage_content)
|
||||
if params[:import_id]
|
||||
@import = CourseImport.for_course(@context, 'instructure_copy').find(params[:import_id])
|
||||
@copy_context = @import.source
|
||||
@copies = @import.added_item_codes
|
||||
@results = @import.log
|
||||
respond_to do |format|
|
||||
format.html { render :action => 'copy_course_content' }
|
||||
end
|
||||
else
|
||||
course_id = params[:copy] && params[:copy][:course_id].to_i
|
||||
course_id = params[:copy][:autocomplete_course_id].to_i if params[:copy] && params[:copy][:autocomplete_course_id] && !params[:copy][:autocomplete_course_id].empty?
|
||||
@copy_context = @current_user.manageable_courses.scoped(
|
||||
:conditions => ["id = ? AND id <> ?", course_id, @context.id],
|
||||
:include => :enrollment_term).first if course_id
|
||||
if !@copy_context
|
||||
@copy_context ||= Course.find_by_id(course_id) if course_id.present?
|
||||
@copy_context = nil if @copy_context && !@copy_context.grants_rights?(@current_user, session, :manage)
|
||||
end
|
||||
find_source_course
|
||||
if @source_course
|
||||
respond_to do |format|
|
||||
format.html
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
flash[:notice] = t('notices.choose_a_course', "Choose a course to copy")
|
||||
format.html { redirect_to course_import_choose_course_url(@context) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def copy_course_finish
|
||||
if authorized_action(@context, @current_user, :manage_content)
|
||||
cm = ContentMigration.find_by_context_id_and_id(@context.id, params[:content_migration_id])
|
||||
@source_course = cm.source_course
|
||||
@copies = []
|
||||
end
|
||||
end
|
||||
|
||||
def choose_course
|
||||
if authorized_action(@context, @current_user, :manage_content)
|
||||
end
|
||||
end
|
||||
|
||||
def find_source_course
|
||||
if params[:source_course]
|
||||
course_id = params[:source_course].to_i
|
||||
@source_course = Course.find_by_id(course_id)
|
||||
@source_course = nil if @source_course && !@source_course.grants_rights?(@current_user, session, :manage)
|
||||
end
|
||||
end
|
||||
|
||||
# @API
|
||||
#
|
||||
|
@ -209,15 +223,17 @@ class ContentImportsController < ApplicationController
|
|||
# @response_field status_url The url for the course copy status API endpoint.
|
||||
#
|
||||
# @example_response
|
||||
# {'status':'completed', 'workflow_state':100, 'id':257, 'created_at':'2011-11-17T16:50:06Z', 'status_url':'/api/v1/courses/9457/course_copy/257'}
|
||||
# {'progress':100, 'workflow_state':'completed', 'id':257, 'created_at':'2011-11-17T16:50:06Z', 'status_url':'/api/v1/courses/9457/course_copy/257'}
|
||||
def copy_course_status
|
||||
if api_request?
|
||||
@context = api_find(Course, params[:course_id])
|
||||
end
|
||||
if authorized_action(@context, @current_user, :manage_content)
|
||||
import = @context.course_imports.find(params[:id])
|
||||
cm = ContentMigration.find_by_context_id_and_id(@context.id, params[:id])
|
||||
raise ActiveRecord::RecordNotFound unless cm
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render :json => copy_status_json(import, @context, @current_user, session)}
|
||||
format.json { render :json => copy_status_json(cm, @context, @current_user, session)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -247,41 +263,32 @@ class ContentImportsController < ApplicationController
|
|||
|
||||
if authorized_action(@context, @current_user, :manage_content)
|
||||
if api_request?
|
||||
@copy_context = api_find(Course, params[:source_course])
|
||||
@source_course = api_find(Course, params[:source_course])
|
||||
copy_params = {:everything => false}
|
||||
if params[:only] && params[:except]
|
||||
render :json => {"errors"=>t('errors.no_only_and_except', 'You can not use "only" and "except" options at the same time.')}.to_json, :status => :bad_request
|
||||
return
|
||||
elsif params[:only]
|
||||
params[:only].each {|o| copy_params["all_#{o}".to_sym] = true}
|
||||
convert_to_table_name(params[:only]).each {|o| copy_params["all_#{o}".to_sym] = true}
|
||||
elsif params[:except]
|
||||
Course::COPY_OPTIONS.each {|o| copy_params[o] = true}
|
||||
params[:except].each {|o| copy_params["all_#{o}".to_sym] = false}
|
||||
COPY_TYPES.each {|o| copy_params["all_#{o}".to_sym] = true}
|
||||
convert_to_table_name(params[:except]).each {|o| copy_params["all_#{o}".to_sym] = false}
|
||||
else
|
||||
copy_params[:everything] = true
|
||||
end
|
||||
else
|
||||
if params[:copy] && params[:items_to_copy]
|
||||
params[:items_to_copy].each do |item|
|
||||
params[:copy][item] = true
|
||||
end
|
||||
params.delete :items_to_copy
|
||||
end
|
||||
@copy_context = Course.find(params[:copy][:course_id])
|
||||
@source_course = Course.find(params[:source_course])
|
||||
copy_params = params[:copy]
|
||||
end
|
||||
|
||||
|
||||
# make sure the user can copy from the source course
|
||||
return render_unauthorized_action unless @copy_context.grants_rights?(@current_user, nil, :read, :read_as_admin).values.all?
|
||||
|
||||
@import = CourseImport.create!(:import_type => "instructure_copy", :source => @copy_context, :course => @context, :parameters => copy_params)
|
||||
@import.perform_later
|
||||
respond_to do |format|
|
||||
format.json { render :json => copy_status_json(@import, @context, @current_user, session) }
|
||||
end
|
||||
return render_unauthorized_action unless @source_course.grants_rights?(@current_user, nil, :read, :read_as_admin).values.all?
|
||||
cm = ContentMigration.create!(:context => @context, :user => @current_user, :source_course => @source_course, :copy_options => copy_params)
|
||||
cm.copy_course
|
||||
render :json => copy_status_json(cm, @context, @current_user, session)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def review
|
||||
if authorized_action(@context, @current_user, :manage_content)
|
||||
@root_folders = Folder.root_folders(@context)
|
||||
|
@ -339,4 +346,17 @@ class ContentImportsController < ApplicationController
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
SELECTION_CONVERSIONS = {
|
||||
"external_tools" => "context_external_tools",
|
||||
"files" => "attachments",
|
||||
"topics" => "discussion_topics",
|
||||
"modules" => "context_modules",
|
||||
"outcomes" => "learning_outcomes"
|
||||
}
|
||||
# convert types selected in API to expected format
|
||||
def convert_to_table_name(selections)
|
||||
selections.map{|s| SELECTION_CONVERSIONS[s] || s}
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -938,7 +938,7 @@ class CoursesController < ApplicationController
|
|||
@course.workflow_state = 'claimed'
|
||||
@course.save
|
||||
@course.enroll_user(@current_user, 'TeacherEnrollment', :enrollment_state => 'active')
|
||||
redirect_to course_import_copy_url(@course, 'copy[course_id]' => @context.id)
|
||||
redirect_to course_import_choose_content_url(@course, 'source_course' => @context.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -158,6 +158,7 @@ class Attachment < ActiveRecord::Base
|
|||
dup.write_attribute(:filename, self.filename)
|
||||
dup.root_attachment_id = self.root_attachment_id || self.id
|
||||
dup.context = context
|
||||
dup.migration_id = CC::CCHelper.create_key(self)
|
||||
context.log_merge_result("File \"#{dup.folder.full_name rescue ''}/#{dup.display_name}\" created") if context.respond_to?(:log_merge_result)
|
||||
dup.updated_at = Time.now
|
||||
dup.clone_updated = true
|
||||
|
|
|
@ -21,6 +21,7 @@ class ContentExport < ActiveRecord::Base
|
|||
belongs_to :course
|
||||
belongs_to :user
|
||||
belongs_to :attachment
|
||||
belongs_to :content_migration
|
||||
has_many :attachments, :as => :context, :dependent => :destroy
|
||||
has_a_broadcast_policy
|
||||
serialize :settings
|
||||
|
@ -30,6 +31,7 @@ class ContentExport < ActiveRecord::Base
|
|||
state :created
|
||||
state :exporting
|
||||
state :exported
|
||||
state :exported_for_course_copy
|
||||
state :failed
|
||||
state :deleted
|
||||
end
|
||||
|
@ -52,8 +54,13 @@ class ContentExport < ActiveRecord::Base
|
|||
self.workflow_state = 'exporting'
|
||||
self.save
|
||||
begin
|
||||
if CC::CCExporter.export(self, opts.merge({:for_course_copy => self.settings[:for_course_copy]}))
|
||||
self.workflow_state = 'exported'
|
||||
if CC::CCExporter.export(self, opts.merge({:for_course_copy => for_course_copy?}))
|
||||
self.progress = 100
|
||||
if for_course_copy?
|
||||
self.workflow_state = 'exported_for_course_copy'
|
||||
else
|
||||
self.workflow_state = 'exported'
|
||||
end
|
||||
else
|
||||
self.workflow_state = 'failed'
|
||||
end
|
||||
|
@ -65,6 +72,14 @@ class ContentExport < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
handle_asynchronously :export_course, :priority => Delayed::LOW_PRIORITY, :max_attempts => 1
|
||||
|
||||
def for_course_copy?
|
||||
self.settings[:for_course_copy]
|
||||
end
|
||||
|
||||
def for_course_copy=(val)
|
||||
self.settings[:for_course_copy] = val
|
||||
end
|
||||
|
||||
def error_message
|
||||
self.settings[:errors] ? self.settings[:errors].last : nil
|
||||
|
@ -131,6 +146,7 @@ class ContentExport < ActiveRecord::Base
|
|||
end
|
||||
|
||||
named_scope :active, {:conditions => ['workflow_state != ?', 'deleted']}
|
||||
named_scope :not_for_copy, {:conditions => ['workflow_state != ?', 'exported_for_course_copy']}
|
||||
named_scope :running, {:conditions => ['workflow_state IN (?)', ['created', 'exporting']]}
|
||||
|
||||
private
|
||||
|
|
|
@ -25,6 +25,8 @@ class ContentMigration < ActiveRecord::Base
|
|||
belongs_to :attachment
|
||||
belongs_to :overview_attachment, :class_name => 'Attachment'
|
||||
belongs_to :exported_attachment, :class_name => 'Attachment'
|
||||
belongs_to :source_course, :class_name => 'Course'
|
||||
has_one :content_export
|
||||
has_a_broadcast_policy
|
||||
serialize :migration_settings
|
||||
before_save :infer_defaults
|
||||
|
@ -51,7 +53,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
'web_links' => false,
|
||||
'wikis' => false
|
||||
}
|
||||
attr_accessible :context, :migration_settings, :user
|
||||
attr_accessible :context, :migration_settings, :user, :source_course, :copy_options
|
||||
|
||||
workflow do
|
||||
state :created
|
||||
|
@ -72,13 +74,13 @@ class ContentMigration < ActiveRecord::Base
|
|||
p.whenever {|record|
|
||||
record.changed_state(:exported) && !record.migration_settings[:skip_import_notification]
|
||||
}
|
||||
|
||||
|
||||
p.dispatch :migration_import_finished
|
||||
p.to { [user] }
|
||||
p.whenever {|record|
|
||||
record.changed_state(:imported) && !record.migration_settings[:skip_import_notification]
|
||||
}
|
||||
|
||||
|
||||
p.dispatch :migration_import_failed
|
||||
p.to { [user] }
|
||||
p.whenever {|record|
|
||||
|
@ -107,19 +109,19 @@ class ContentMigration < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def migration_ids_to_import=(val)
|
||||
migration_settings[:migration_ids_to_import] = val
|
||||
end
|
||||
|
||||
|
||||
def infer_defaults
|
||||
migration_settings[:to_scrape] ||= DEFAULT_TO_EXPORT
|
||||
end
|
||||
|
||||
|
||||
def process_to_scrape(hash)
|
||||
migrate_only = migration_settings[:to_scrape] || DEFAULT_TO_EXPORT
|
||||
hash.each do |key, arg|
|
||||
migrate_only[key] = arg == '1' ? true : false if arg
|
||||
migrate_only[key] = arg == '1' ? true : false if arg
|
||||
if key == 'calendar_events' && migrate_only[key]
|
||||
migrate_only['calendar_start'] = 1.year.ago.strftime(DATE_FORMAT)
|
||||
migrate_only['calendar_end'] = 1.year.from_now.strftime(DATE_FORMAT)
|
||||
|
@ -127,7 +129,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
end
|
||||
migration_settings[:to_scrape] = migrate_only
|
||||
end
|
||||
|
||||
|
||||
def zip_path=(val)
|
||||
migration_settings[:export_archive_path] = val
|
||||
end
|
||||
|
@ -149,11 +151,11 @@ class ContentMigration < ActiveRecord::Base
|
|||
def course_archive_download_url=(url)
|
||||
migration_settings[:course_archive_download_url] = url
|
||||
end
|
||||
|
||||
|
||||
def root_account
|
||||
self.context.root_account rescue nil
|
||||
end
|
||||
|
||||
|
||||
def plugin_type
|
||||
if plugin = Canvas::Plugin.find(migration_settings['migration_type'])
|
||||
plugin.metadata(:select_text) || plugin.name
|
||||
|
@ -208,13 +210,13 @@ class ContentMigration < ActiveRecord::Base
|
|||
self.save
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def check_quiz_id_prepender
|
||||
if !migration_settings[:id_prepender] && !migration_settings[:overwrite_questions]
|
||||
# only prepend an id if the course already has some migrated questions/quizzes
|
||||
if self.context.assessment_questions.scoped(:conditions => 'assessment_questions.migration_id IS NOT NULL').any? ||
|
||||
self.context.quizzes.scoped(:conditions => 'quizzes.migration_id IS NOT NULL').any?
|
||||
migration_settings[:id_prepender] = self.id
|
||||
migration_settings[:id_prepender] = self.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -222,7 +224,7 @@ class ContentMigration < ActiveRecord::Base
|
|||
def to_import(val)
|
||||
migration_settings[:migration_ids_to_import][:copy][val] rescue nil
|
||||
end
|
||||
|
||||
|
||||
def import_content
|
||||
self.workflow_state = :importing
|
||||
self.save
|
||||
|
@ -260,6 +262,69 @@ class ContentMigration < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
handle_asynchronously :import_content, :priority => Delayed::LOW_PRIORITY, :max_attempts => 1
|
||||
|
||||
def copy_options
|
||||
self.migration_settings[:copy_options]
|
||||
end
|
||||
|
||||
def copy_options=(options)
|
||||
self.migration_settings[:copy_options] = options
|
||||
end
|
||||
|
||||
def for_course_copy?
|
||||
!!self.source_course
|
||||
end
|
||||
|
||||
def copy_course
|
||||
self.workflow_state = :pre_processing
|
||||
self.progress = 0
|
||||
self.save
|
||||
|
||||
begin
|
||||
ce = ContentExport.new
|
||||
ce.content_migration = self
|
||||
ce.selected_content = copy_options
|
||||
ce.course = self.source_course
|
||||
ce.for_course_copy = true
|
||||
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
|
||||
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.progress = 10
|
||||
|
||||
self.context.copy_attachments_from_course(self.source_course, :content_export => ce, :content_migration => self)
|
||||
self.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
|
||||
end
|
||||
handle_asynchronously :copy_course, :priority => Delayed::LOW_PRIORITY, :max_attempts => 1
|
||||
|
||||
named_scope :for_context, lambda{|context|
|
||||
{:conditions => {:context_id => context.id, :context_type => context.class.to_s} }
|
||||
|
@ -292,6 +357,24 @@ class ContentMigration < ActiveRecord::Base
|
|||
@zip_file = nil
|
||||
end
|
||||
|
||||
def progress
|
||||
return nil if self.workflow_state == 'created'
|
||||
mig_prog = read_attribute(:progress) || 0
|
||||
if self.source_course
|
||||
# this is for a course copy so it needs to combine the progress of the export and import
|
||||
# The export will count for 40% of progress
|
||||
# The importing step (so the value of progress on this object)will be 60%
|
||||
mig_prog = mig_prog * 0.6
|
||||
|
||||
if self.content_export
|
||||
export_prog = self.content_export.progress || 0
|
||||
mig_prog += export_prog * 0.4
|
||||
end
|
||||
end
|
||||
|
||||
mig_prog
|
||||
end
|
||||
|
||||
def fast_update_progress(val)
|
||||
self.progress = val
|
||||
ContentMigration.update_all({:progress=>val}, "id=#{self.id}")
|
||||
|
|
|
@ -358,7 +358,9 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
tools.each do |tool|
|
||||
if tool['migration_id'] && (!to_import || to_import[tool['migration_id']])
|
||||
item = import_from_migration(tool, migration.context)
|
||||
migration.add_warning(t('external_tool_attention_needed', 'The security parameters for the external tool "%{tool_name}" need to be set in Course Settings.', :tool_name => item.name))
|
||||
if item.consumer_key == 'fake' || item.shared_secret == 'fake'
|
||||
migration.add_warning(t('external_tool_attention_needed', 'The security parameters for the external tool "%{tool_name}" need to be set in Course Settings.', :tool_name => item.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -410,8 +412,8 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
item.url = hash[:url] unless hash[:url].blank?
|
||||
item.domain = hash[:domain] unless hash[:domain].blank?
|
||||
item.privacy_level = hash[:privacy_level] || 'name_only'
|
||||
item.consumer_key ||= 'fake'
|
||||
item.shared_secret ||= 'fake'
|
||||
item.consumer_key ||= hash[:consumer_key] || 'fake'
|
||||
item.shared_secret ||= hash[:shared_secret] || 'fake'
|
||||
item.settings = hash[:settings].with_indifferent_access if hash[:settings].is_a?(Hash)
|
||||
if hash[:custom_fields].is_a? Hash
|
||||
item.settings[:custom_fields] ||= {}
|
||||
|
|
|
@ -155,6 +155,7 @@ class Course < ActiveRecord::Base
|
|||
has_many :media_objects, :as => :context
|
||||
has_many :page_views, :as => :context
|
||||
has_many :role_overrides, :as => :context
|
||||
has_many :content_migrations
|
||||
has_many :content_exports
|
||||
has_many :course_imports
|
||||
has_many :alerts, :as => :context, :include => :criteria
|
||||
|
@ -1632,17 +1633,16 @@ class Course < ActiveRecord::Base
|
|||
ActiveRecord::Base.skip_touch_context
|
||||
@imported_migration_items = []
|
||||
|
||||
# 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)
|
||||
mo_attachments = self.imported_migration_items.find_all { |i| i.is_a?(Attachment) && i.media_entry_id.present? }
|
||||
import_media_objects(mo_attachments, migration)
|
||||
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)
|
||||
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
|
||||
|
||||
# needs to happen after the files are processed, so that they are available in the syllabus
|
||||
import_settings_from_migration(data); migration.fast_update_progress(21)
|
||||
|
||||
migration.fast_update_progress(30)
|
||||
question_data = AssessmentQuestion.process_migration(data, migration); migration.fast_update_progress(35)
|
||||
Group.process_migration(data, migration); migration.fast_update_progress(36)
|
||||
|
@ -1669,6 +1669,10 @@ class Course < ActiveRecord::Base
|
|||
CalendarEvent.process_migration(data, migration);migration.fast_update_progress(90)
|
||||
WikiPage.process_migration_course_outline(data, migration);migration.fast_update_progress(95)
|
||||
|
||||
if !migration.copy_options || migration.copy_options[:everything] || migration.copy_options[:all_course_settings]
|
||||
import_settings_from_migration(data); migration.fast_update_progress(96)
|
||||
end
|
||||
|
||||
begin
|
||||
#Adjust dates
|
||||
if bool_res(params[:copy][:shift_dates])
|
||||
|
@ -1693,8 +1697,15 @@ class Course < ActiveRecord::Base
|
|||
event.lock_at = shift_date(event.lock_at, shift_options)
|
||||
event.unlock_at = shift_date(event.unlock_at, shift_options)
|
||||
event.save!
|
||||
elsif event.is_a?(ContextModule)
|
||||
event.unlock_at = shift_date(event.unlock_at, shift_options)
|
||||
event.start_at = shift_date(event.start_at, shift_options)
|
||||
event.end_at = shift_date(event.end_at, shift_options)
|
||||
end
|
||||
end
|
||||
|
||||
self.start_at ||= shift_options[:new_start_date]
|
||||
self.conclude_at ||= shift_options[:new_end_date]
|
||||
end
|
||||
rescue
|
||||
add_migration_warning("Couldn't adjust the due dates.", $!)
|
||||
|
@ -1771,6 +1782,63 @@ class Course < ActiveRecord::Base
|
|||
}
|
||||
end
|
||||
|
||||
def copy_attachments_from_course(course, options={})
|
||||
self.attachment_path_id_lookup = {}
|
||||
root_folder = Folder.root_folders(self).first.name + '/'
|
||||
ce = options[:content_export]
|
||||
cm = options[:content_migration]
|
||||
|
||||
attachments = course.attachments.all(:conditions => "file_state <> 'deleted'")
|
||||
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)
|
||||
if !ce || ce.export_object?(file)
|
||||
new_file = file.clone_for(self)
|
||||
self.attachment_path_id_lookup[new_file.full_display_path.gsub(/\A#{root_folder}/, '')] = new_file.migration_id
|
||||
new_folder_id = merge_mapped_id(file.folder)
|
||||
# make sure the file has somewhere to go
|
||||
if !new_folder_id
|
||||
# gather mapping of needed folders from old course to new course
|
||||
old_folders = []
|
||||
old_folders << file.folder
|
||||
new_folders = []
|
||||
new_folders << old_folders.last.clone_for(self, nil, options.merge({:include_subcontent => false}))
|
||||
while old_folders.last.parent_folder && old_folders.last.parent_folder.parent_folder_id && !merge_mapped_id(old_folders.last.parent_folder)
|
||||
old_folders << old_folders.last.parent_folder
|
||||
new_folders << old_folders.last.clone_for(self, nil, options.merge({:include_subcontent => false}))
|
||||
end
|
||||
old_folders.reverse!
|
||||
new_folders.reverse!
|
||||
# try to use folders that already match if possible
|
||||
final_new_folders = []
|
||||
parent_folder = Folder.root_folders(self).first
|
||||
old_folders.each_with_index do |folder, idx|
|
||||
if f = parent_folder.active_sub_folders.find_by_name(folder.name)
|
||||
final_new_folders << f
|
||||
else
|
||||
final_new_folders << new_folders[idx]
|
||||
end
|
||||
parent_folder = final_new_folders.last
|
||||
end
|
||||
# add or update the folder structure needed for the file
|
||||
final_new_folders.first.parent_folder_id ||=
|
||||
merge_mapped_id(old_folders.first.parent_folder) ||
|
||||
Folder.root_folders(self).first.id
|
||||
old_folders.each_with_index do |folder, idx|
|
||||
final_new_folders[idx].save!
|
||||
map_merge(folder, final_new_folders[idx])
|
||||
final_new_folders[idx + 1].parent_folder_id ||= final_new_folders[idx].id if final_new_folders[idx + 1]
|
||||
end
|
||||
new_folder_id = merge_mapped_id(file.folder)
|
||||
end
|
||||
new_file.folder_id = new_folder_id
|
||||
new_file.save!
|
||||
map_merge(file, new_file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :merge_mappings
|
||||
COPY_OPTIONS = [:all_course_settings, :all_assignments, :all_external_tools, :all_files, :all_topics,
|
||||
:all_calendar_events, :all_quizzes, :all_wiki_pages, :all_modules, :all_outcomes]
|
||||
|
|
|
@ -204,7 +204,8 @@ class LearningOutcome < ActiveRecord::Base
|
|||
item.description = hash[:description]
|
||||
|
||||
if hash[:ratings]
|
||||
item.data = {:rubric_criterion=>{:ratings=>hash[:ratings]}}
|
||||
item.data = {:rubric_criterion=>{}}
|
||||
item.data[:rubric_criterion][:ratings] = hash[:ratings] ? hash[:ratings].map(&:symbolize_keys) : []
|
||||
item.data[:rubric_criterion][:mastery_points] = hash[:mastery_points]
|
||||
item.data[:rubric_criterion][:points_possible] = hash[:points_possible]
|
||||
item.data[:rubric_criterion][:description] = item.short_description || item.description
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<div style="margin-top: 20px;">
|
||||
<h3><%= check_box :copy, :shift_dates, :class => "shift_dates_checkbox" %><%= label :copy, :shift_dates, :en => "Adjust events and due dates" %></h3>
|
||||
<div style="display: none; margin-left: 50px;" class="shift_dates_settings">
|
||||
<div>
|
||||
<div style="margin-bottom: 5px;"><%= t 'labels.dates_range', "%{course} dates range", :course => '<strong class="course_name">...</strong>'.html_safe %></div>
|
||||
<% ot 'from_to', "*from* %{start_date} **to** %{end_date}",
|
||||
:start_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :old_start_date, :value => "", :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :end_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :old_end_date, :value => "", :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :wrapper => { '*' => '<div style="float: left; margin-left: 10px;">\1 </div>',
|
||||
'**' => '<div style="float: left;"> \1 </div>' } %>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-bottom: 5px margin-top: 10px;"><%= mt 'labels.this_course_date_range', "**This Course** dates range" %></div>
|
||||
<% ot 'from_to', "*from* %{start_date} **to** %{end_date}",
|
||||
:start_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :new_start_date, :value => date_string(@context.real_start_date, :long), :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :end_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :new_end_date, :value => date_string(@context.real_end_date, :long), :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :wrapper => { '*' => '<div style="float: left; margin-left: 10px;">\1 </div>',
|
||||
'**' => '<div style="float: left;"> \1 </div>' } %>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<%= t 'descriptions.day_substitutions', "You can also explicitly define day substitutions to adjust for changing class schedules
|
||||
(i.e. move everything that was on Mondays to now happen on Tuesdays)" %>
|
||||
<div class="substitutions" style="margin-top: 10px;"></div>
|
||||
<div style="display: none;">
|
||||
<div class="substitution substitution_blank">
|
||||
<%= t 'move_from_to', "Move everything on %{old_day} to happen on %{new_day}",
|
||||
:old_day => '<span class="old_select"> </span>'.html_safe,
|
||||
:new_day => '<span class="new_select"> </span>'.html_safe %>
|
||||
<a href="#" class="delete_substitution_link no-hover"><%= image_tag "delete_circle.png" %></a>
|
||||
</div>
|
||||
<select class="weekday_select weekday_select_blank">
|
||||
<% I18n.t('date.day_names').each_with_index do |name, idx| %>
|
||||
<option value="<%= idx %>"><%= name %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="add_substitution_link add"><%= t 'links.add_day_substitution', "Define a day substitution" %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,61 @@
|
|||
<% @body_classes << "content-imports" %>
|
||||
<% content_for :page_title do %><%= t :page_title, "Choose Content to Copy" %><% end %>
|
||||
<% add_crumb t('crumbs.choose_course', "Choose Course"), context_url(@context, :context_import_choose_course_url) %>
|
||||
<% add_crumb t('crumbs.choose_content', "Choose Content") %>
|
||||
|
||||
<% js_env :COPY_COURSE_FINISH_URL => context_url(@context, :context_import_copy_course_finish_url)%>
|
||||
|
||||
<% content_for :stylesheets do %>
|
||||
<style>
|
||||
.root_asset_list {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
.root_asset_list > li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.root_asset_list h4 {
|
||||
margin: 0;
|
||||
}
|
||||
.asset_list {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
#copy_context_form h3 {
|
||||
color: #444;
|
||||
}
|
||||
</style>
|
||||
<% end %>
|
||||
|
||||
<% form_tag context_url(@context, :context_import_copy_content_url), :id => "copy_context_form" do %>
|
||||
<input type="hidden" name="source_course" id="source_cource_id" value="<%= @source_course.id %>"/>
|
||||
<h2><%= t 'titles.copy_from_course', "Copy Content From %{course}", :course => @source_course.name %></h2>
|
||||
<p><%= t 'descriptions.copy_content', "Select the content you'd like copied into this course." %></p>
|
||||
|
||||
<h3 style="margin-bottom: 10px;">
|
||||
<%= check_box :copy, :everything, :checked => true, :class => "copy_everything" %>
|
||||
<%= label :copy, :everything, t('labels.copy_everything', "Copy Everything from %{course}", :course => @source_course.name) %>
|
||||
</h3>
|
||||
|
||||
<div id="item_selections" style="display: none; position: relative;">
|
||||
<%= before_label('check', 'Check') %>
|
||||
<a href="#" id="check_everything"><%= t('check_everything', 'All') %></a> :
|
||||
<a href="#" id="uncheck_everything"><%= t('uncheck_everything', 'None') %></a>
|
||||
|
||||
<div class="content_list"></div>
|
||||
</div>
|
||||
|
||||
<%= render :partial => 'date_shift_form' %>
|
||||
|
||||
<div class="progress_bar_holder" style="display: none; margin-top: 10px;">
|
||||
<div class="copy_progress"></div>
|
||||
</div>
|
||||
<div class="button-container" style="margin-top: 20px;">
|
||||
<button class="button big-button submit_button" type="submit"><%= t 'buttons.import', "Import Course Content" %></button>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% js_bundle :copy_course %>
|
|
@ -0,0 +1,44 @@
|
|||
<% @body_classes << "content-imports" %>
|
||||
<% content_for :page_title do %><%= t :page_title, "Choose Course" %><% end %>
|
||||
|
||||
<% add_crumb t('crumbs.copy_from', "Choose Course"), context_url(@context, :context_import_choose_course_url) %>
|
||||
<div>
|
||||
<h2><%= t 'titles.copy_from', "Copy From another Course" %></h2>
|
||||
<p>
|
||||
<%= t 'descriptions.copy_from', "Select the course you want to copy from. Then you can specify what exactly you want copied over." %>
|
||||
</p>
|
||||
<% form_tag context_url(@context, :context_import_choose_content_url), :method => :get do %>
|
||||
<table class="formtable">
|
||||
<tr>
|
||||
<td><label for="course_autocomplete_id_lookup"><%= before_label :search_for_course, "Search for Course" %></label></td>
|
||||
<td>
|
||||
<a href="<%= context_url(@current_user, :context_manageable_courses_url, :format => :json) %>" id="course_autocomplete_url" style="display: none;"> </a>
|
||||
<input type="hidden" name="source_course" id="course_autocomplete_id"/>
|
||||
<input type="text" id="course_autocomplete_id_lookup" style="width: 250px;"/>
|
||||
</td>
|
||||
</tr><tr id="select-course-row">
|
||||
<td><label for="copy_from_course"><%= before_label :select_course, "Or Select from the List" %></label></td>
|
||||
<td id="course-select-wrapper">
|
||||
<select style="font-size: 1.2em; width: 250px;" id="copy_from_course">
|
||||
<option value="none"><%= t 'options.select_course', '[Select Course]' %></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><label for="include_concluded_courses">Include completed courses?</label></td>
|
||||
<td><input type="checkbox" id="include_concluded_courses" name="include_concluded_courses" /></td>
|
||||
</tr><tr>
|
||||
<td colspan="2">
|
||||
<div id="course_autocomplete_name_holder" style="display: none; margin-top: 20px;">
|
||||
Selected Course:
|
||||
<span id="course_autocomplete_name" style="font-weight: bold;"> </span>
|
||||
<div class="button-container">
|
||||
<button type="submit" class="button"><%= t 'buttons.copy_from_course', "Copy From this Course" %></button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% js_bundle :copy_course %>
|
|
@ -1,247 +0,0 @@
|
|||
<% @body_classes << "content-imports" %>
|
||||
<% content_for :page_title do %><%= t :page_title, "Copy From Another Course" %><% end %>
|
||||
|
||||
<% content_for :stylesheets do %>
|
||||
<style>
|
||||
.root_asset_list {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
.root_asset_list > li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.root_asset_list h4 {
|
||||
margin: 0;
|
||||
}
|
||||
.asset_list {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
#copy_context_form h3 {
|
||||
color: #444;
|
||||
}
|
||||
</style>
|
||||
<% end %>
|
||||
<% if @copy_context %>
|
||||
<% add_crumb t('crumbs.copy_from_course', "Copy From %{course}", :course => @copy_context.name), "#{context_url(@context, :context_import_copy_url)}&import_id=#{@copy_context.id}" %>
|
||||
<% form_tag context_url(@context, :context_import_copy_url), :id => "copy_context_form" do %>
|
||||
<%= hidden_field :copy, :course_id, :value => @copy_context.id %>
|
||||
<h2><%= t 'titles.copy_from_course', "Copy Content From %{course}", :course => @copy_context.name %></h2>
|
||||
<p><%= t 'descriptions.copy_content', "Select the content you'd like copied into this course. We'll try to auto-correct any mismatched
|
||||
due dates and calendar event dates as best we can." %></p>
|
||||
<h3 style="margin-bottom: 10px;"><%= check_box :copy, :everything, :class => "copy_everything" %> <%= label :copy, :everything, image_tag('checked.png', :style => 'width: 16px;') + " " + t('labels.copy_everything', "Copy Everything from %{course}", :course => @copy_context.name) %></h3>
|
||||
<h3><%= check_box :copy, :course_settings, :class => "copy_all", :checked => false %><%= label :copy, :course_settings, image_tag('file_multiple.png') + " " + t('labels.copy_settings', "Settings from %{course}", :course => @copy_context.name) %></h3>
|
||||
<% if @copy_context.assignment_groups.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_assignments, :class => "copy_all", :checked => true %><%= label :copy, :all_assignments, image_tag('assignment.png') + " " + t('labels.assignment', "Assignments for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list root_asset_list">
|
||||
<% @copy_context.assignment_groups.active.each do |group| %>
|
||||
<li>
|
||||
<h4><%= check_box :copy, group.asset_string.to_sym, :class => "copy_all" %><%= label :copy, group.asset_string.to_sym, group.name %></h4>
|
||||
<ul class="unstyled_list asset_list">
|
||||
<% group.assignments.active.each do |assignment| %>
|
||||
<li>
|
||||
<%= check_box :copy, assignment.asset_string.to_sym %>
|
||||
<%= label :copy, assignment.asset_string.to_sym, assignment.title %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.context_modules.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_modules, :class => "copy_all", :checked => true %><%= label :copy, :all_modules,image_tag('ball.png') + " " + t('labels.modules', "Modules for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list root_asset_list">
|
||||
<% @copy_context.context_modules.active.each do |mod| %>
|
||||
<li>
|
||||
<%= check_box :copy, mod.asset_string.to_sym %>
|
||||
<%= label :copy, mod.asset_string.to_sym, mod.name %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.learning_outcomes.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_outcomes, :class => "copy_all", :checked => true %><%= label :copy, :all_modules,image_tag('flagged_question_dim.png') + " " + t('labels.learning_outcomes', "Learning Outcomes for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list root_asset_list">
|
||||
<% @copy_context.learning_outcomes.active.each do |mod| %>
|
||||
<li>
|
||||
<%= check_box :copy, mod.asset_string.to_sym %>
|
||||
<%= label :copy, mod.asset_string.to_sym, mod.short_description %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.quizzes.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_quizzes, :class => "copy_all", :checked => true %><%= label :copy, :all_quizzes, image_tag('quiz.png') + " " + t('labels.quizzes', "Quizzes for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list root_asset_list">
|
||||
<% @copy_context.quizzes.active.each do |quiz| %>
|
||||
<li>
|
||||
<%= check_box :copy, quiz.asset_string.to_sym %>
|
||||
<%= label :copy, quiz.asset_string.to_sym, quiz.title %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.folders.active.length > 0 && @copy_context.attachments.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_files, :class => "copy_all", :checked => true %><%= label :copy, :all_files, image_tag('download.png') + " " + t('labels.files', "Files for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list root_asset_list">
|
||||
<% @copy_context.folders.active.sort_by{|f| f.full_name}.each do |folder| %>
|
||||
<% if folder.attachments.length > 0 %>
|
||||
<li>
|
||||
<h4><%= check_box :copy, folder.asset_string.to_sym, :class => "copy_all" %><%= label :copy, folder.asset_string.to_sym, folder.full_name %></h4>
|
||||
<ul class="unstyled_list asset_list">
|
||||
<% folder.attachments.each do |file| %>
|
||||
<li><%= check_box :copy, file.asset_string.to_sym %><%= label :copy, file.asset_string.to_sym, file.display_name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.wiki.wiki_pages.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_wiki_pages, :class => "copy_all", :checked => true %><%= label :copy, :all_wiki_pages, image_tag('course_content_icon.png') + " " + t('labels.wiki_ages', "Wiki Pages for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list asset_list">
|
||||
<% @copy_context.wiki.wiki_pages.active.each do |page| %>
|
||||
<li><%= check_box :copy, page.asset_string.to_sym %><%= label :copy, page.asset_string.to_sym, page.title %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.discussion_topics.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_topics, :class => "copy_all" %><%= label :copy, :all_topics, image_tag('word_bubble.png') + " " + t('labels.discussions', "Discussions for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list asset_list">
|
||||
<% @copy_context.discussion_topics.active.each do |topic| %>
|
||||
<li><%= check_box :copy, topic.asset_string.to_sym, :class => 'skip_on_everything' %><%= label :copy, topic.asset_string.to_sym, topic.title %>
|
||||
<div style="font-size: 0.8em; padding-left: 25px;">
|
||||
<%= check_box :copy, "#{topic.asset_string}_entries".to_sym, :class => "secondary_checkbox skip" %><%= label :copy, "#{topic.asset_string}_entries".to_sym, :copy, :en => "include entries from the old course" %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.calendar_events.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_calendar_events, :class => "copy_all", :checked => true %><%= label :copy, :all_calendar_events, image_tag('due_date_icon.png') + " " + t('labels.events', "Events for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list asset_list">
|
||||
<% @copy_context.calendar_events.active.each do |event| %>
|
||||
<li><%= check_box :copy, event.asset_string.to_sym %><%= label :copy, event.asset_string.to_sym, event.title %> - <span style="font-size: 0.8em;"><%= datetime_string(event.start_at) %></span></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @copy_context.context_external_tools.active.length > 0 %>
|
||||
<h3><%= check_box :copy, :all_external_tools, :class => "copy_all", :checked => true %><%= label :copy, :all_external_toold, t('labels.external_tools', "External Tools for %{course}", :course => @copy_context.name) %></h3>
|
||||
<ul class="unstyled_list asset_list">
|
||||
<% @copy_context.context_external_tools.active.each do |tool| %>
|
||||
<li><%= check_box :copy, tool.asset_string.to_sym %><%= label :copy, tool.asset_string.to_sym, tool.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<div style="margin-top: 20px;">
|
||||
<h3><%= check_box :copy, :shift_dates, :class => "shift_dates_checkbox" %><%= label :copy, :shift_dates, :en => "Adjust events and due dates" %></h3>
|
||||
<div style="display: none; margin-left: 50px;" class="shift_dates_settings">
|
||||
<div>
|
||||
<div style="margin-bottom: 5px;"><%= mt 'labels.dates_range', "**%{course}** dates range", :course => @copy_context.name %></div>
|
||||
<% ot 'from_to', "*from* %{start_date} **to** %{end_date}",
|
||||
:start_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :old_start_date, :value => date_string(@copy_context.real_start_date, :long), :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :end_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :old_end_date, :value => date_string(@copy_context.real_end_date, :long), :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :wrapper => { '*' => '<div style="float: left; margin-left: 10px;">\1 </div>',
|
||||
'**' => '<div style="float: left;"> \1 </div>' } %>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-bottom: 5px margin-top: 10px;"><%= mt 'labels.dates_range', "**%{course}** dates range", :course => @context.name %></div>
|
||||
<% ot 'from_to', "*from* %{start_date} **to** %{end_date}",
|
||||
:start_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :new_start_date, :value => date_string(@context.real_start_date, :long), :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :end_date => capture { %>
|
||||
<div style="float: left;"><%= text_field :copy, :new_end_date, :value => date_string(@context.real_end_date, :long), :class => "date_field", :style => "width: 120px;" %></div>
|
||||
<% }, :wrapper => { '*' => '<div style="float: left; margin-left: 10px;">\1 </div>',
|
||||
'**' => '<div style="float: left;"> \1 </div>' } %>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<%= t 'descriptions.day_substitutions', "You can also explicitly define day substitutions to adjust for changing class schedules
|
||||
(i.e. move everything that was on Mondays to now happen on Tuesdays)" %>
|
||||
<div class="substitutions" style="margin-top: 10px;"></div>
|
||||
<div style="display: none;">
|
||||
<div class="substitution substitution_blank">
|
||||
<%= t 'move_from_to', "Move everything on %{old_day} to happen on %{new_day}",
|
||||
:old_day => '<span class="old_select"> </span>'.html_safe,
|
||||
:new_day => '<span class="new_select"> </span>'.html_safe %>
|
||||
<a href="#" class="delete_substitution_link no-hover"><%= image_tag "delete_circle.png" %></a>
|
||||
</div>
|
||||
<select class="weekday_select weekday_select_blank">
|
||||
<% I18n.t('date.day_names').each_with_index do |name, idx| %>
|
||||
<option value="<%= idx %>"><%= name %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="add_substitution_link add"><%= t 'links.add_day_substitution', "Define a day substitution" %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress_bar_holder" style="display: none; margin-top: 10px;">
|
||||
<div class="copy_progress"></div>
|
||||
</div>
|
||||
<div class="button-container" style="margin-top: 20px;">
|
||||
<button class="button big-button submit_button" type="submit"><%= t 'buttons.import', "Import Course Content" %></button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% add_crumb t('crumbs.copy_from', "Copy From Another Course"), context_url(@context, :context_import_copy_url) %>
|
||||
<div>
|
||||
<h2><%= t 'titles.copy_from', "Copy From another Course" %></h2>
|
||||
<p>
|
||||
<%= t 'descriptions.copy_from', "To copy content from another course to this one, you'll first need to select the
|
||||
course you want to copy from. Then you can specify what exactly you want copied over." %>
|
||||
</p>
|
||||
<% form_tag context_url(@context, :context_import_copy_url), :method => :get do %>
|
||||
<table class="formtable">
|
||||
<tr>
|
||||
<td><label for="course_autocomplete_id_lookup"><%= before_label :search_for_course, "Search for Course" %></label></td>
|
||||
<td>
|
||||
<a href="<%= context_url(@current_user, :context_manageable_courses_url, :format => :json) %>" id="course_autocomplete_url" style="display: none;"> </a>
|
||||
<input type="hidden" name="copy[course_id]" id="course_autocomplete_id"/>
|
||||
<input type="text" id="course_autocomplete_id_lookup" style="width: 250px;"/>
|
||||
</td>
|
||||
</tr><tr id="select-course-row">
|
||||
<td><label for="copy_from_course"><%= before_label :select_course, "Or Select from the List" %></label></td>
|
||||
<td id="course-select-wrapper">
|
||||
<select style="font-size: 1.2em; width: 250px;" id="copy_from_course">
|
||||
<option value="none"><%= t 'options.select_course', '[Select Course]' %></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><label for="include_concluded_courses">Include completed courses?</label></td>
|
||||
<td><input type="checkbox" id="include_concluded_courses" name="include_concluded_courses" /></td>
|
||||
</tr><tr>
|
||||
<td colspan="2">
|
||||
<div id="course_autocomplete_name_holder" style="display: none; margin-top: 20px;">
|
||||
Selected Course:
|
||||
<span id="course_autocomplete_name" style="font-weight: bold;"> </span>
|
||||
<div class="button-container">
|
||||
<button type="submit" class="button"><%= t 'buttons.copy_from_course', "Copy From this Course" %></button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div id="copy_entries_dialog" style="display: none;">
|
||||
<h2><%= t 'titles.copy_discussion_replies', "Copy Discussion Replies?" %></h2>
|
||||
<%= t 'descriptions.copy_discussion_replies', "In addition to copying discussions, would you like to also
|
||||
copy all student replies to topic posts?" %>
|
||||
<div class="button-container" style="margin-top: 15px;">
|
||||
<button type="button" class="button"><%= t 'buttons.copy_topics', "Just Copy Topics" %></button>
|
||||
<button type="button" class="button include"><%= t 'buttons.copy_topics_and_replies', "Copy Topics and Replies" %></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% js_bundle :copy_course %>
|
|
@ -1,6 +1,6 @@
|
|||
<% add_crumb t('crumbs.copy_from_course', "Copy From %{course}", :course => @copy_context.name), "#{context_url(@context, :context_import_copy_url)}&import_id=#{@copy_context.id}" %>
|
||||
<% add_crumb t('crumbs.copy_from_course', "Copy From %{course}", :course => @source_course.name) %>
|
||||
<% add_crumb t('#crumbs.results', "Results") %>
|
||||
<% content_for :page_title do %><%= t :page_title, "Copy Results From %{course}", :course => @copy_context.name %><% end %>
|
||||
<% content_for :page_title do %><%= t :page_title, "Copy Results From %{course}", :course => @source_course.name %><% end %>
|
||||
|
||||
<% content_for :stylesheets do %>
|
||||
<style>
|
||||
|
@ -128,22 +128,4 @@ HEREDOC
|
|||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<ul class="unstyled_list" style="display: none;">
|
||||
<% @results.each do |result| %>
|
||||
<li><%= result %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% js_block do %>
|
||||
<script>
|
||||
require([
|
||||
'jquery' /* $ */
|
||||
], function($) {
|
||||
|
||||
$(document).ready(function() {
|
||||
$(".added").attr('title', <%= t('titles.just_added', "Just added to the Course").to_json.html_safe %>);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
|
@ -25,7 +25,7 @@
|
|||
<% if can_do(@context, @current_user, :manage_content) %>
|
||||
<h4><%= t 'titles.you_can_also', "You can also:" %></h4>
|
||||
<div>
|
||||
<p><a class="button" href="<%= context_url(@context, :context_import_copy_url) %>"> <%= mt 'links.copy_course', "Copy content from **another Canvas course**" %></a></p>
|
||||
<p><a class="button" href="<%= context_url(@context, :context_import_choose_course_url) %>"> <%= mt 'links.copy_course', "Copy content from **another Canvas course**" %></a></p>
|
||||
|
||||
<% if exports_enabled? %>
|
||||
<p><a class="button" href="<%= context_url(@context, :context_import_migrate_url) %>"> <%= mt 'links.import_package', "Import content from a **content package** or from **another system**" %></a></p>
|
||||
|
|
|
@ -192,7 +192,9 @@ ActionController::Routing::Routes.draw do |map|
|
|||
add_zip_file_imports(course)
|
||||
course.import_quizzes 'imports/quizzes', :controller => 'content_imports', :action => 'quizzes'
|
||||
course.import_content 'imports/content', :controller => 'content_imports', :action => 'content'
|
||||
course.import_copy 'imports/copy', :controller => 'content_imports', :action => 'copy_course', :conditions => {:method => :get}
|
||||
course.import_choose_course 'imports/choose_course', :controller => 'content_imports', :action => 'choose_course', :conditions => {:method => :get}
|
||||
course.import_choose_content 'imports/choose_content', :controller => 'content_imports', :action => 'choose_content', :conditions => {:method => :get}
|
||||
course.import_copy_course_finish 'imports/copy_course_finish', :controller => 'content_imports', :action => 'copy_course_finish', :conditions => {:method => :get}
|
||||
course.import_migrate 'imports/migrate', :controller => 'content_imports', :action => 'migrate_content'
|
||||
course.import_upload 'imports/upload', :controller => 'content_imports', :action => 'migrate_content_upload'
|
||||
course.import_s3_success 'imports/s3_success', :controller => 'content_imports', :action => 'migrate_content_s3_success'
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class AddSourceCourseToContentMigration < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def self.up
|
||||
add_column :content_migrations, :source_course_id, :integer, :limit => 8
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :content_migrations, :source_course_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
class AddContentMigrationToContentExport < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def self.up
|
||||
add_column :content_exports, :content_migration_id, :integer, :limit => 8
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :content_exports, :content_migration_id
|
||||
end
|
||||
end
|
|
@ -58,6 +58,15 @@ module Api::V1::Course
|
|||
|
||||
def copy_status_json(import, course, user, session)
|
||||
hash = api_json(import, user, session, :only => %w(id progress created_at workflow_state))
|
||||
|
||||
# the type of object for course copy changed but we don't want the api to change
|
||||
# so map the workflow states to the old ones
|
||||
if hash['workflow_state'] == 'imported'
|
||||
hash['workflow_state'] = 'completed'
|
||||
elsif !['created', 'failed'].member?(hash['workflow_state'])
|
||||
hash['workflow_state'] = 'started'
|
||||
end
|
||||
|
||||
hash[:status_url] = api_v1_course_copy_status_url(course, import)
|
||||
hash
|
||||
end
|
||||
|
|
|
@ -77,6 +77,10 @@ module CC
|
|||
blti_node.blti(:extensions, :platform => CC::CCHelper::CANVAS_PLATFORM) do |ext_node|
|
||||
ext_node.lticm :property, tool.workflow_state, 'name' => 'privacy_level'
|
||||
ext_node.lticm(:property, tool.domain, 'name' => 'domain') unless tool.domain.blank?
|
||||
if for_course_copy
|
||||
ext_node.lticm :property, tool.consumer_key, 'name' => 'consumer_key'
|
||||
ext_node.lticm :property, tool.shared_secret, 'name' => 'shared_secret'
|
||||
end
|
||||
[:user_navigation, :course_navigation, :account_navigation, :resource_selection, :editor_button].each do |type|
|
||||
if tool.settings[type]
|
||||
ext_node.lticm(:options, :name => type.to_s) do |type_node|
|
||||
|
|
|
@ -77,7 +77,9 @@ module CC::Importer
|
|||
if ext[:platform] == CANVAS_PLATFORM
|
||||
tool[:privacy_level] = ext[:custom_fields].delete 'privacy_level'
|
||||
tool[:domain] = ext[:custom_fields].delete 'domain'
|
||||
|
||||
tool[:consumer_key] = ext[:custom_fields].delete 'consumer_key'
|
||||
tool[:shared_secret] = ext[:custom_fields].delete 'shared_secret'
|
||||
|
||||
tool[:settings] = ext[:custom_fields]
|
||||
else
|
||||
tool[:extensions] << ext
|
||||
|
|
|
@ -47,7 +47,7 @@ module CC::Importer::Canvas
|
|||
@course[:external_tools] = convert_blti_links
|
||||
@course[:file_map] = create_file_map
|
||||
package_course_files
|
||||
convert_quizzes
|
||||
convert_quizzes if Qti.qti_enabled?
|
||||
|
||||
#close up shop
|
||||
save_to_file
|
||||
|
|
|
@ -45,7 +45,11 @@ module Canvas::Migration
|
|||
end
|
||||
|
||||
cm.migration_settings[:worker_class] = converter_class.name
|
||||
cm.migration_settings[:migration_ids_to_import] = {:copy=>{:assessment_questions=>true}}
|
||||
if cm.migration_settings[:migration_ids_to_import] && cm.migration_settings[:migration_ids_to_import][:copy]
|
||||
cm.migration_settings[:migration_ids_to_import][:copy][:assessment_questions] = true
|
||||
else
|
||||
cm.migration_settings[:migration_ids_to_import] = {:copy=>{:assessment_questions=>true}}
|
||||
end
|
||||
cm.workflow_state = :exported
|
||||
cm.progress = 0
|
||||
cm.save
|
||||
|
|
|
@ -52,7 +52,7 @@ module CC::Importer::Standard
|
|||
create_file_map
|
||||
@course[:discussion_topics] = convert_discussions
|
||||
@course[:external_tools] = convert_blti_links(resources_by_type("imsbasiclti"))
|
||||
@course[:assessment_questions], @course[:assessments] = convert_quizzes
|
||||
@course[:assessment_questions], @course[:assessments] = convert_quizzes if Qti.qti_enabled?
|
||||
@course[:modules] = convert_organizations(@manifest)
|
||||
@course[:all_files_zip] = package_course_files(@course[:file_map])
|
||||
|
||||
|
|
|
@ -72,8 +72,7 @@ module CC
|
|||
node.learningOutcome(:identifier=>migration_id) do |out_node|
|
||||
out_node.title item.short_description unless item.short_description.blank?
|
||||
out_node.description @html_exporter.html_content(item.description) unless item.description.blank?
|
||||
criterion = item.data[:rubric_criterion]
|
||||
if criterion
|
||||
if item.data && criterion = item.data[:rubric_criterion]
|
||||
out_node.points_possible criterion[:points_possible] if criterion[:points_possible]
|
||||
out_node.mastery_points criterion[:mastery_points] if criterion[:mastery_points]
|
||||
if criterion[:ratings] && criterion[:ratings].length > 0
|
||||
|
|
|
@ -54,7 +54,7 @@ module CC
|
|||
end
|
||||
|
||||
@course.quizzes.active.each do |quiz|
|
||||
next unless export_object?(quiz)
|
||||
next unless export_object?(quiz) || export_object?(quiz.assignment)
|
||||
begin
|
||||
generate_quiz(quiz)
|
||||
rescue
|
||||
|
|
|
@ -20,7 +20,7 @@ module CC
|
|||
|
||||
def add_topics
|
||||
@course.discussion_topics.active.each do |topic|
|
||||
next unless export_object?(topic)
|
||||
next unless export_object?(topic) || export_object?(topic.assignment)
|
||||
begin
|
||||
add_topic(topic)
|
||||
rescue
|
||||
|
|
|
@ -2,8 +2,6 @@ define([
|
|||
'jquery' /* $ */,
|
||||
'i18n!content_imports',
|
||||
'compiled/util/processItemSelections',
|
||||
'use!underscore',
|
||||
'compiled/xhr/RemoteSelect',
|
||||
'jquery.ajaxJSON' /* ajaxJSON */,
|
||||
'jquery.instructure_date_and_time' /* date_field */,
|
||||
'jquery.instructure_forms' /* formSubmit */,
|
||||
|
@ -12,92 +10,21 @@ define([
|
|||
'jquery.rails_flash_notifications' /* flashError */,
|
||||
'jqueryui/autocomplete' /* /\.autocomplete/ */,
|
||||
'jqueryui/progressbar' /* /\.progressbar/ */
|
||||
], function($, I18n, processItemSelections, _, RemoteSelect){
|
||||
], function($, I18n, processItemSelections){
|
||||
|
||||
$(function () {
|
||||
var $frame = $("<iframe id='copy_course_target' name='copy_course_target' src='about:blank'/>"),
|
||||
$select = $('#copy_from_course'),
|
||||
remoteSelect,
|
||||
currentCourseId = parseInt(window.location.pathname.split('/')[2]);
|
||||
var $frame = $("<iframe id='copy_course_target' name='copy_course_target' src='about:blank'/>");
|
||||
$("body").append($frame.hide());
|
||||
$("#copy_context_form").attr('target', 'copy_course_target');
|
||||
$(".copy_progress").progressbar();
|
||||
|
||||
if ($select.length) {
|
||||
remoteSelect = new RemoteSelect($('#copy_from_course'), {
|
||||
formatter: _.bind(function(courses) {
|
||||
/**
|
||||
* let's start by saying that this function is really long. for that,
|
||||
* I apologize. my hope is that most formatters won't have to be this
|
||||
* long or unwieldy. with that out of the way, let's begin.
|
||||
*/
|
||||
|
||||
// start by sorting by date, newest to oldest. this ensures that our
|
||||
// terms are displayed newest to oldest.
|
||||
courses = courses.sort(function(a, b) {
|
||||
return new Date(b.enrollment_start).getTime() - new Date(a.enrollment_start).getTime();
|
||||
});
|
||||
|
||||
var terms,
|
||||
termMap = _.groupBy(courses, function(course) { return course.term + ' (' + course.account_name + ')'; }),
|
||||
termNames = _.chain(termMap).keys().reduce(function(h, termName) {
|
||||
var strippedTermName = termName.replace(/\([^\)]+\)$/, '').trim();
|
||||
h[strippedTermName] = h[strippedTermName] || {count: 0, termNames: []};
|
||||
h[strippedTermName].count = h[strippedTermName].count + 1;
|
||||
h[strippedTermName].termNames.push(termName);
|
||||
return h;
|
||||
}, {}).value();
|
||||
|
||||
// for each term/account pair, format the courses for display in
|
||||
// the <select> and reject the current course. also sort by course
|
||||
// course name inside each account.
|
||||
terms = _.reduce(termMap, function(memo, v, k) {
|
||||
memo[k] = _.chain(v).reject(function(c) {
|
||||
return c.id == currentCourseId;
|
||||
}).map(function(c) {
|
||||
return { label: c.label, value: c.id };
|
||||
}).sortBy(function(c) { return c.label }).value();
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
// before we return our list of terms and courses, make another loop
|
||||
// through them to see if we can strip the account name off of any
|
||||
// terms that don't have duplicate names across accounts.
|
||||
return _.reduce(terms, function(memo, courses, term) {
|
||||
var strippedTermName = term.replace(/\([^\)]+\)$/, '').trim(),
|
||||
key = termNames[strippedTermName].count === 1 ?
|
||||
strippedTermName :
|
||||
term;
|
||||
memo[key] = courses;
|
||||
return memo;
|
||||
}, {});
|
||||
}, this),
|
||||
url: '/users/' + ENV.current_user_id + '/manageable_courses'
|
||||
});
|
||||
remoteSelect.currentRequest.success(function(data) {
|
||||
if (data.length >= 500) {
|
||||
$('#select-course-row').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#include_concluded_courses').change(function(e) {
|
||||
var el = $(e.currentTarget);
|
||||
if (el.prop('checked')) {
|
||||
remoteSelect.makeRequest({ 'include[]': 'concluded' });
|
||||
} else {
|
||||
remoteSelect.makeRequest();
|
||||
}
|
||||
});
|
||||
|
||||
var checkup = function (url) {
|
||||
$.ajaxJSON(url, 'GET', {}, function (data) {
|
||||
if (data && data.workflow_state) {
|
||||
$(".copy_progress").progressbar('option', 'value', data.progress);
|
||||
}
|
||||
if (data && data.workflow_state == 'completed') {
|
||||
location.href = location.href + "&import_id=" + data.id;
|
||||
if (data && (data.workflow_state == 'completed' || data.workflow_state == 'imported')) {
|
||||
window.location = ENV.COPY_COURSE_FINISH_URL + "?content_migration_id=" + data.id;
|
||||
} else if (data && data.workflow_state == 'failed') {
|
||||
var message = I18n.t('errors.failed', "Course Import failed with the following error:") + " \"import_" + data.id + "\"";
|
||||
$.flashError(message);
|
||||
|
@ -115,7 +42,7 @@ define([
|
|||
};
|
||||
|
||||
$("#copy_context_form").formSubmit({
|
||||
processData:processItemSelections,
|
||||
//processData:processItemSelections,
|
||||
beforeSubmit:function (data) {
|
||||
$("#copy_context_form .submit_button").text(I18n.t('messages.copying', "Copying... this will take a few minutes")).attr('disabled', true);
|
||||
$(".progress_bar_holder").show();
|
||||
|
@ -175,6 +102,7 @@ define([
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(".shift_dates_checkbox").change(
|
||||
function () {
|
||||
$(".shift_dates_settings").showIf($(this).attr('checked'));
|
||||
|
@ -205,52 +133,5 @@ define([
|
|||
$(this).triggerHandler('change');
|
||||
});
|
||||
$(".date_field").date_field();
|
||||
$("#copy_from_course").change(
|
||||
function () {
|
||||
var select = $("#copy_from_course")[0];
|
||||
var idx = select.selectedIndex;
|
||||
var name = select.options[idx].innerHTML;
|
||||
var id = select.options[idx].value;
|
||||
if (id != "none") {
|
||||
$("#course_autocomplete_name_holder").show();
|
||||
$("#course_autocomplete_name").text(name);
|
||||
$("#course_autocomplete_id").val(id);
|
||||
$("#course_autocomplete_id_lookup").val("");
|
||||
}
|
||||
}).change();
|
||||
if ($("#course_autocomplete_id_lookup:visible").length > 0) {
|
||||
var autocompleteCache = {},
|
||||
lastAutocompleteRequest;
|
||||
|
||||
$("#course_autocomplete_id_lookup").autocomplete({
|
||||
source : function(request, response) {
|
||||
var src = '/users/' + ENV.current_user_id + '/manageable_courses',
|
||||
params = { term: request.term },
|
||||
includeConcluded = $('#include_concluded_courses').prop('checked'),
|
||||
cacheKey = request.term;
|
||||
|
||||
if (includeConcluded) {
|
||||
params['include[]'] = 'concluded';
|
||||
cacheKey += '|concluded';
|
||||
}
|
||||
|
||||
if (cacheKey in autocompleteCache) {
|
||||
response(autocompleteCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
lastAutocompleteRequest = $.getJSON(src, params, function(data, status, xhr) {
|
||||
autocompleteCache[cacheKey] = data;
|
||||
if (lastAutocompleteRequest === xhr) { response(data); }
|
||||
});
|
||||
},
|
||||
select:function (event, ui) {
|
||||
$("#course_autocomplete_name_holder").show();
|
||||
$("#course_autocomplete_name").text(ui.item.label);
|
||||
$("#course_autocomplete_id").val(ui.item.id);
|
||||
$("#copy_from_course").val("none");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,131 @@
|
|||
define([
|
||||
'jquery' /* $ */,
|
||||
'i18n!content_imports',
|
||||
'use!underscore',
|
||||
'compiled/xhr/RemoteSelect',
|
||||
'jqueryui/autocomplete' /* /\.autocomplete/ */
|
||||
], function($, I18n, _, RemoteSelect){
|
||||
|
||||
var $frame = $("<iframe id='copy_course_target' name='copy_course_target' src='about:blank'/>"),
|
||||
$select = $('#copy_from_course'),
|
||||
remoteSelect,
|
||||
currentCourseId = parseInt(window.location.pathname.split('/')[2]);
|
||||
|
||||
if ($select.length) {
|
||||
remoteSelect = new RemoteSelect($('#copy_from_course'), {
|
||||
formatter: _.bind(function(courses) {
|
||||
/**
|
||||
* let's start by saying that this function is really long. for that,
|
||||
* I apologize. my hope is that most formatters won't have to be this
|
||||
* long or unwieldy. with that out of the way, let's begin.
|
||||
*/
|
||||
|
||||
// start by sorting by date, newest to oldest. this ensures that our
|
||||
// terms are displayed newest to oldest.
|
||||
courses = courses.sort(function(a, b) {
|
||||
return new Date(b.enrollment_start).getTime() - new Date(a.enrollment_start).getTime();
|
||||
});
|
||||
|
||||
var terms,
|
||||
termMap = _.groupBy(courses, function(course) { return course.term + ' (' + course.account_name + ')'; }),
|
||||
termNames = _.chain(termMap).keys().reduce(function(h, termName) {
|
||||
var strippedTermName = termName.replace(/\([^\)]+\)$/, '').trim();
|
||||
h[strippedTermName] = h[strippedTermName] || {count: 0, termNames: []};
|
||||
h[strippedTermName].count = h[strippedTermName].count + 1;
|
||||
h[strippedTermName].termNames.push(termName);
|
||||
return h;
|
||||
}, {}).value();
|
||||
|
||||
// for each term/account pair, format the courses for display in
|
||||
// the <select> and reject the current course. also sort by course
|
||||
// course name inside each account.
|
||||
terms = _.reduce(termMap, function(memo, v, k) {
|
||||
memo[k] = _.chain(v).reject(function(c) {
|
||||
return c.id == currentCourseId;
|
||||
}).map(function(c) {
|
||||
return { label: c.label, value: c.id };
|
||||
}).sortBy(function(c) { return c.label }).value();
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
// before we return our list of terms and courses, make another loop
|
||||
// through them to see if we can strip the account name off of any
|
||||
// terms that don't have duplicate names across accounts.
|
||||
return _.reduce(terms, function(memo, courses, term) {
|
||||
var strippedTermName = term.replace(/\([^\)]+\)$/, '').trim(),
|
||||
key = termNames[strippedTermName].count === 1 ?
|
||||
strippedTermName :
|
||||
term;
|
||||
memo[key] = courses;
|
||||
return memo;
|
||||
}, {});
|
||||
}, this),
|
||||
url: '/users/' + ENV.current_user_id + '/manageable_courses'
|
||||
});
|
||||
remoteSelect.currentRequest.success(function(data) {
|
||||
if (data.length >= 500) {
|
||||
$('#select-course-row').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#include_concluded_courses').change(function(e) {
|
||||
var el = $(e.currentTarget);
|
||||
if (el.prop('checked')) {
|
||||
remoteSelect.makeRequest({ 'include[]': 'concluded' });
|
||||
} else {
|
||||
remoteSelect.makeRequest();
|
||||
}
|
||||
});
|
||||
|
||||
$("#copy_from_course").change(
|
||||
function () {
|
||||
var select = $("#copy_from_course")[0];
|
||||
var idx = select.selectedIndex;
|
||||
var name = select.options[idx].innerHTML;
|
||||
var id = select.options[idx].value;
|
||||
if (id != "none") {
|
||||
$("#course_autocomplete_name_holder").show();
|
||||
$("#course_autocomplete_name").text(name);
|
||||
$("#course_autocomplete_id").val(id);
|
||||
$("#course_autocomplete_id_lookup").val("");
|
||||
}
|
||||
}).change();
|
||||
|
||||
if ($("#course_autocomplete_id_lookup:visible").length > 0) {
|
||||
var autocompleteCache = {},
|
||||
lastAutocompleteRequest;
|
||||
|
||||
$("#course_autocomplete_id_lookup").autocomplete({
|
||||
source : function(request, response) {
|
||||
var src = '/users/' + ENV.current_user_id + '/manageable_courses',
|
||||
params = { term: request.term },
|
||||
includeConcluded = $('#include_concluded_courses').prop('checked'),
|
||||
cacheKey = request.term;
|
||||
|
||||
if (includeConcluded) {
|
||||
params['include[]'] = 'concluded';
|
||||
cacheKey += '|concluded';
|
||||
}
|
||||
|
||||
if (cacheKey in autocompleteCache) {
|
||||
response(autocompleteCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
lastAutocompleteRequest = $.getJSON(src, params, function(data, status, xhr) {
|
||||
autocompleteCache[cacheKey] = data;
|
||||
if (lastAutocompleteRequest === xhr) { response(data); }
|
||||
});
|
||||
},
|
||||
select:function (event, ui) {
|
||||
$("#course_autocomplete_name_holder").show();
|
||||
$("#course_autocomplete_name").text(ui.item.label);
|
||||
$("#course_autocomplete_id").val(ui.item.id);
|
||||
$("#copy_from_course").val("none");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
|
@ -548,7 +548,7 @@ describe ContentImportsController, :type => :integration do
|
|||
@copy_from.calendar_events.create!(:title => 'event', :description => 'hi', :start_at => 1.day.from_now)
|
||||
@copy_from.context_modules.create!(:name => "a module")
|
||||
@copy_from.quizzes.create!(:title => 'quiz')
|
||||
LearningOutcomeGroup.default_for(@copy_from).add_item(@copy_from.learning_outcomes.create!(:short_description => 'oi'))
|
||||
LearningOutcomeGroup.default_for(@copy_from).add_item(@copy_from.learning_outcomes.create!(:short_description => 'oi', :context => @copy_from))
|
||||
@copy_from.save
|
||||
|
||||
course_with_teacher(:active_all => true, :name => 'whatever', :user => @user)
|
||||
|
@ -564,12 +564,12 @@ describe ContentImportsController, :type => :integration do
|
|||
{ :controller => 'content_imports', :action => 'copy_course_content', :course_id => to_id, :format => 'json' },
|
||||
{:source_course => from_id}.merge(options))
|
||||
|
||||
import = CourseImport.last(:order => :id)
|
||||
cm = ContentMigration.last(:order => :id)
|
||||
data.should == {
|
||||
'id' => import.id,
|
||||
'id' => cm.id,
|
||||
'progress' => nil,
|
||||
'status_url' => "http://www.example.com/api/v1/courses/#{@copy_to.to_param}/course_copy/#{import.id}",
|
||||
'created_at' => import.created_at.as_json,
|
||||
'status_url' => "http://www.example.com/api/v1/courses/#{@copy_to.to_param}/course_copy/#{cm.id}",
|
||||
'created_at' => cm.created_at.as_json,
|
||||
'workflow_state' => 'created',
|
||||
}
|
||||
|
||||
|
@ -583,7 +583,10 @@ describe ContentImportsController, :type => :integration do
|
|||
end
|
||||
|
||||
dj.invoke_job
|
||||
|
||||
cm.reload
|
||||
cm.migration_settings[:warnings].should == nil
|
||||
cm.content_export.error_messages.should == []
|
||||
|
||||
api_call(:get, status_url, { :controller => 'content_imports', :action => 'copy_course_status', :course_id => @copy_to.to_param, :id => data['id'].to_param, :format => 'json' })
|
||||
(JSON.parse(response.body)).tap do |res|
|
||||
res['workflow_state'].should == 'completed'
|
||||
|
@ -616,6 +619,7 @@ describe ContentImportsController, :type => :integration do
|
|||
def check_counts(expected_count, skip = nil)
|
||||
each_copy_option do |option, association|
|
||||
next if skip && option == skip
|
||||
next if !Qti.qti_enabled? && association == :quizzes
|
||||
@copy_to.send(association).count.should == expected_count
|
||||
end
|
||||
end
|
||||
|
@ -667,7 +671,7 @@ describe ContentImportsController, :type => :integration do
|
|||
run_only_copy(:course_settings)
|
||||
check_counts 0
|
||||
@copy_to.reload
|
||||
@copy_to.syllabus_body.should == "haha"
|
||||
@copy_to.syllabus_body.should == "<p>haha</p>"
|
||||
end
|
||||
it "should only copy wiki pages" do
|
||||
run_only_copy(:wiki_pages)
|
||||
|
@ -676,6 +680,7 @@ describe ContentImportsController, :type => :integration do
|
|||
end
|
||||
each_copy_option do |option, association|
|
||||
it "should only copy #{option}" do
|
||||
pending if !Qti.qti_enabled? && association == :quizzes
|
||||
run_only_copy(option)
|
||||
@copy_to.send(association).count.should == 1
|
||||
check_counts(0, option)
|
||||
|
|
|
@ -98,5 +98,5 @@ describe ContentImportsController, :type => :integration do
|
|||
response.flash.should == {:notice=>"There is no archive for this content migration"}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -32,7 +32,7 @@ describe ContentImportsController, :type => :integration do
|
|||
course_with_teacher(:active_all => true, :name => 'whatever', :user => @user)
|
||||
@copy_to = @course
|
||||
|
||||
post "/courses/#{@copy_to.id}/imports/copy", :copy => {:course_id => @copy_from.id, :all_assignments => 1}
|
||||
post "/courses/#{@copy_to.id}/imports/copy", :source_course => @copy_from.id, :copy => {:all_assignments => 1}
|
||||
response.should be_success
|
||||
data = json_parse
|
||||
dj = Delayed::Job.last
|
||||
|
|
|
@ -0,0 +1,337 @@
|
|||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
|
||||
|
||||
describe ContentMigration do
|
||||
|
||||
context "course copy" do
|
||||
before do
|
||||
course_with_teacher
|
||||
@copy_from = @course
|
||||
|
||||
course_with_teacher(:user => @user)
|
||||
@copy_to = @course
|
||||
|
||||
@cm = ContentMigration.new(:context => @copy_to, :user => @user, :source_course => @copy_from, :copy_options => {:everything => "1"})
|
||||
@cm.save!
|
||||
end
|
||||
|
||||
it "should show correct progress" do
|
||||
ce = ContentExport.new
|
||||
ce.content_migration = @cm
|
||||
@cm.content_export = ce
|
||||
ce.save!
|
||||
|
||||
@cm.progress.should == nil
|
||||
@cm.workflow_state = 'exporting'
|
||||
|
||||
ce.progress = 10
|
||||
@cm.progress.should == 4
|
||||
ce.progress = 50
|
||||
@cm.progress.should == 20
|
||||
ce.progress = 75
|
||||
@cm.progress.should == 30
|
||||
ce.progress = 100
|
||||
@cm.progress.should == 40
|
||||
|
||||
@cm.progress = 10
|
||||
@cm.progress.should == 46
|
||||
@cm.progress = 50
|
||||
@cm.progress.should == 70
|
||||
@cm.progress = 80
|
||||
@cm.progress.should == 88
|
||||
@cm.progress = 100
|
||||
@cm.progress.should == 100
|
||||
end
|
||||
|
||||
def run_course_copy
|
||||
@cm.copy_course_without_send_later
|
||||
@cm.reload
|
||||
@cm.warnings.should == []
|
||||
if @cm.migration_settings[:last_error]
|
||||
er = ErrorReport.last
|
||||
"#{er.message} - #{er.backtrace}".should == ""
|
||||
end
|
||||
@cm.workflow_state.should == 'imported'
|
||||
@copy_to.reload
|
||||
end
|
||||
|
||||
it "should migrate syllabus links on copy" do
|
||||
course_model
|
||||
topic = @copy_from.discussion_topics.create!(:title => "some topic", :message => "<p>some text</p>")
|
||||
@copy_from.syllabus_body = "<a href='/courses/#{@copy_from.id}/discussion_topics/#{topic.id}'>link</a>"
|
||||
@copy_from.save!
|
||||
|
||||
run_course_copy
|
||||
|
||||
new_topic = @copy_to.discussion_topics.find_by_migration_id(CC::CCHelper.create_key(topic))
|
||||
new_topic.should_not be_nil
|
||||
new_topic.message.should == topic.message
|
||||
@copy_to.syllabus_body.should match(/\/courses\/#{@copy_to.id}\/discussion_topics\/#{new_topic.id}/)
|
||||
end
|
||||
|
||||
it "should copy external tools" do
|
||||
tool_from = @copy_from.context_external_tools.create!(:name => "new tool", :consumer_key => "key", :shared_secret => "secret", :domain => 'example.com', :custom_fields => {'a' => '1', 'b' => '2'})
|
||||
tool_from.settings[:course_navigation] = {:url => "http://www.example.com", :text => "Example URL"}
|
||||
tool_from.save
|
||||
|
||||
run_course_copy
|
||||
|
||||
@copy_to.context_external_tools.count.should == 1
|
||||
|
||||
tool_to = @copy_to.context_external_tools.first
|
||||
tool_to.name.should == tool_from.name
|
||||
tool_to.custom_fields.should == tool_from.custom_fields
|
||||
tool_to.has_course_navigation.should == true
|
||||
tool_to.consumer_key.should == tool_from.consumer_key
|
||||
tool_to.shared_secret.should == tool_from.shared_secret
|
||||
end
|
||||
|
||||
it "should not duplicate external tools used in modules" do
|
||||
tool_from = @copy_from.context_external_tools.create!(:name => "new tool", :consumer_key => "key", :shared_secret => "secret", :domain => 'example.com', :custom_fields => {'a' => '1', 'b' => '2'})
|
||||
tool_from.settings[:course_navigation] = {:url => "http://www.example.com", :text => "Example URL"}
|
||||
tool_from.save
|
||||
|
||||
mod1 = @copy_from.context_modules.create!(:name => "some module")
|
||||
tag = mod1.add_item({:type => 'context_external_tool',
|
||||
:title => 'Example URL',
|
||||
:url => "http://www.example.com",
|
||||
:new_tab => true})
|
||||
tag.save
|
||||
|
||||
run_course_copy
|
||||
|
||||
@copy_to.context_external_tools.count.should == 1
|
||||
|
||||
tool_to = @copy_to.context_external_tools.first
|
||||
tool_to.name.should == tool_from.name
|
||||
tool_to.consumer_key.should == tool_from.consumer_key
|
||||
tool_to.has_course_navigation.should == true
|
||||
end
|
||||
|
||||
it "should copy external tool assignments" do
|
||||
assignment_model(:course => @copy_from, :points_possible => 40, :submission_types => 'external_tool', :grading_type => 'points')
|
||||
tag_from = @assignment.build_external_tool_tag(:url => "http://example.com/one", :new_tab => true)
|
||||
tag_from.content_type = 'ContextExternalTool'
|
||||
tag_from.save!
|
||||
|
||||
run_course_copy
|
||||
|
||||
asmnt_2 = @copy_to.assignments.first
|
||||
asmnt_2.submission_types.should == "external_tool"
|
||||
asmnt_2.external_tool_tag.should_not be_nil
|
||||
tag_to = asmnt_2.external_tool_tag
|
||||
tag_to.content_type.should == tag_from.content_type
|
||||
tag_to.url.should == tag_from.url
|
||||
tag_to.new_tab.should == tag_from.new_tab
|
||||
end
|
||||
|
||||
def mig_id(obj)
|
||||
CC::CCHelper.create_key(obj)
|
||||
end
|
||||
|
||||
|
||||
it "should merge locked files and retain correct html links" do
|
||||
att = Attachment.create!(:filename => 'test.txt', :display_name => "testing.txt", :uploaded_data => StringIO.new('file'), :folder => Folder.root_folders(@copy_from).first, :context => @copy_from)
|
||||
att.update_attribute(:hidden, true)
|
||||
att.reload.should be_hidden
|
||||
topic = @copy_from.discussion_topics.create!(:title => "some topic", :message => "<img src='/courses/#{@copy_from.id}/files/#{att.id}/preview'>")
|
||||
|
||||
run_course_copy
|
||||
|
||||
new_att = @copy_to.attachments.find_by_migration_id(CC::CCHelper.create_key(att))
|
||||
new_att.should_not be_nil
|
||||
|
||||
new_topic = @copy_to.discussion_topics.find_by_migration_id(CC::CCHelper.create_key(topic))
|
||||
new_topic.should_not be_nil
|
||||
new_topic.message.should match(Regexp.new("/courses/#{@copy_to.id}/files/#{new_att.id}/preview"))
|
||||
end
|
||||
|
||||
it "should selectively copy items" do
|
||||
dt1 = @copy_from.discussion_topics.create!(:message => "hi", :title => "discussion title")
|
||||
dt2 = @copy_from.discussion_topics.create!(:message => "hey", :title => "discussion title 2")
|
||||
dt3 = @copy_from.announcements.create!(:message => "howdy", :title => "announcement title")
|
||||
cm = @copy_from.context_modules.create!(:name => "some module")
|
||||
cm2 = @copy_from.context_modules.create!(:name => "another module")
|
||||
att = Attachment.create!(:filename => 'first.txt', :uploaded_data => StringIO.new('ohai'), :folder => Folder.unfiled_folder(@copy_from), :context => @copy_from)
|
||||
att2 = Attachment.create!(:filename => 'second.txt', :uploaded_data => StringIO.new('ohai'), :folder => Folder.unfiled_folder(@copy_from), :context => @copy_from)
|
||||
wiki = @copy_from.wiki.wiki_pages.create!(:title => "wiki", :body => "ohai")
|
||||
wiki2 = @copy_from.wiki.wiki_pages.create!(:title => "wiki2", :body => "ohais")
|
||||
|
||||
# only select one of each type
|
||||
@cm.copy_options = {
|
||||
:discussion_topics => {mig_id(dt1) => "1", mig_id(dt3) => "1"},
|
||||
:context_modules => {mig_id(cm) => "1", mig_id(cm2) => "0"},
|
||||
:attachments => {mig_id(att) => "1", mig_id(att2) => "0"},
|
||||
:wiki_pages => {mig_id(wiki) => "1", mig_id(wiki2) => "0"},
|
||||
}
|
||||
@cm.save!
|
||||
|
||||
run_course_copy
|
||||
|
||||
@copy_to.discussion_topics.find_by_migration_id(mig_id(dt1)).should_not be_nil
|
||||
@copy_to.discussion_topics.find_by_migration_id(mig_id(dt2)).should be_nil
|
||||
@copy_to.discussion_topics.find_by_migration_id(mig_id(dt3)).should_not be_nil
|
||||
|
||||
@copy_to.context_modules.find_by_migration_id(mig_id(cm)).should_not be_nil
|
||||
@copy_to.context_modules.find_by_migration_id(mig_id(cm2)).should be_nil
|
||||
|
||||
@copy_to.attachments.find_by_migration_id(mig_id(att)).should_not be_nil
|
||||
@copy_to.attachments.find_by_migration_id(mig_id(att2)).should be_nil
|
||||
|
||||
@copy_to.wiki.wiki_pages.find_by_migration_id(mig_id(wiki)).should_not be_nil
|
||||
@copy_to.wiki.wiki_pages.find_by_migration_id(mig_id(wiki2)).should be_nil
|
||||
end
|
||||
|
||||
it "should copy learning outcomes into the new course" do
|
||||
lo = @copy_from.learning_outcomes.new
|
||||
lo.context = @copy_from
|
||||
lo.short_description = "Lone outcome"
|
||||
lo.description = "<p>Descriptions are boring</p>"
|
||||
lo.workflow_state = 'active'
|
||||
lo.data = {:rubric_criterion=>{:mastery_points=>3, :ratings=>[{:description=>"Exceeds Expectations", :points=>5}, {:description=>"Meets Expectations", :points=>3}, {:description=>"Does Not Meet Expectations", :points=>0}], :description=>"First outcome", :points_possible=>5}}
|
||||
lo.save!
|
||||
|
||||
old_root = LearningOutcomeGroup.default_for(@copy_from)
|
||||
old_root.add_item(lo)
|
||||
|
||||
lo_g = @copy_from.learning_outcome_groups.new
|
||||
lo_g.context = @copy_from
|
||||
lo_g.title = "Lone outcome group"
|
||||
lo_g.description = "<p>Groupage</p>"
|
||||
lo_g.save!
|
||||
old_root.add_item(lo_g)
|
||||
|
||||
lo2 = @copy_from.learning_outcomes.new
|
||||
lo2.context = @copy_from
|
||||
lo2.short_description = "outcome in group"
|
||||
lo2.workflow_state = 'active'
|
||||
lo2.data = {:rubric_criterion=>{:mastery_points=>2, :ratings=>[{:description=>"e", :points=>50}, {:description=>"me", :points=>2}, {:description=>"Does Not Meet Expectations", :points=>0.5}], :description=>"First outcome", :points_possible=>5}}
|
||||
lo2.save!
|
||||
lo_g.add_item(lo2)
|
||||
old_root.reload
|
||||
|
||||
# copy outcomes into new course
|
||||
new_root = LearningOutcomeGroup.default_for(@copy_to)
|
||||
|
||||
run_course_copy
|
||||
|
||||
@copy_to.learning_outcomes.count.should == @copy_from.learning_outcomes.count
|
||||
@copy_to.learning_outcome_groups.count.should == @copy_from.learning_outcome_groups.count
|
||||
new_root.sorted_content.count.should == old_root.sorted_content.count
|
||||
|
||||
lo_2 = new_root.sorted_content.first
|
||||
lo_2.short_description.should == lo.short_description
|
||||
lo_2.description.should == lo.description
|
||||
lo_2.data.should == lo.data
|
||||
|
||||
lo_g_2 = new_root.sorted_content.last
|
||||
lo_g_2.title.should == lo_g.title
|
||||
lo_g_2.description.should == lo_g.description
|
||||
lo_g_2.sorted_content.length.should == 1
|
||||
lo_g_2.root_learning_outcome_group_id.should == new_root.id
|
||||
lo_g_2.learning_outcome_group_id.should == new_root.id
|
||||
|
||||
lo_2 = lo_g_2.sorted_content.first
|
||||
lo_2.short_description.should == lo2.short_description
|
||||
lo_2.description.should == lo2.description
|
||||
lo_2.data.should == lo2.data
|
||||
end
|
||||
|
||||
it "should copy a quiz when assignment is selected" do
|
||||
pending unless Qti.qti_enabled?
|
||||
@quiz = @copy_from.quizzes.create!
|
||||
@quiz.did_edit
|
||||
@quiz.offer!
|
||||
@quiz.assignment.should_not be_nil
|
||||
|
||||
@cm.copy_options = {
|
||||
:assignments => {mig_id(@quiz.assignment) => "1"},
|
||||
:quizzes => {mig_id(@quiz) => "0"},
|
||||
}
|
||||
@cm.save!
|
||||
|
||||
run_course_copy
|
||||
|
||||
@copy_to.quizzes.find_by_migration_id(mig_id(@quiz)).should_not be_nil
|
||||
end
|
||||
|
||||
it "should copy a discussion topic when assignment is selected" do
|
||||
topic = @copy_from.discussion_topics.build(:title => "topic")
|
||||
assignment = @copy_from.assignments.build(:submission_types => 'discussion_topic', :title => topic.title)
|
||||
assignment.infer_due_at
|
||||
assignment.saved_by = :discussion_topic
|
||||
topic.assignment = assignment
|
||||
topic.save
|
||||
|
||||
@cm.copy_options = {
|
||||
:assignments => {mig_id(assignment) => "1"},
|
||||
:discussion_topics => {mig_id(topic) => "0"},
|
||||
}
|
||||
@cm.save!
|
||||
|
||||
run_course_copy
|
||||
|
||||
@copy_to.discussion_topics.find_by_migration_id(mig_id(topic)).should_not be_nil
|
||||
end
|
||||
|
||||
it "should assign the correct parent folder when the parent folder has already been created" do
|
||||
folder = Folder.root_folders(@copy_from).first
|
||||
folder = folder.sub_folders.create!(:context => @copy_from, :name => 'folder_1')
|
||||
att = Attachment.create!(:filename => 'dummy.txt', :uploaded_data => StringIO.new('fakety'), :folder => folder, :context => @copy_from)
|
||||
folder = folder.sub_folders.create!(:context => @copy_from, :name => 'folder_2')
|
||||
folder = folder.sub_folders.create!(:context => @copy_from, :name => 'folder_3')
|
||||
old_attachment = Attachment.create!(:filename => 'merge.test', :uploaded_data => StringIO.new('ohey'), :folder => folder, :context => @copy_from)
|
||||
|
||||
run_course_copy
|
||||
|
||||
new_attachment = @copy_to.attachments.find_by_migration_id(mig_id(old_attachment))
|
||||
new_attachment.should_not be_nil
|
||||
new_attachment.full_path.should == "course files/folder_1/folder_2/folder_3/merge.test"
|
||||
folder.reload
|
||||
end
|
||||
|
||||
it "should perform day substitutions" do
|
||||
@copy_from.assert_assignment_group
|
||||
today = Time.now.utc
|
||||
asmnt = @copy_from.assignments.build
|
||||
asmnt.due_at = today
|
||||
asmnt.workflow_state = 'published'
|
||||
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.save!
|
||||
|
||||
run_course_copy
|
||||
|
||||
new_assignment = @copy_to.assignments.first
|
||||
# new_assignment.due_at.should == today + 1.day does not work
|
||||
new_assignment.due_at.to_i.should_not == asmnt.due_at.to_i
|
||||
(new_assignment.due_at.to_i - (today + 1.day).to_i).abs.should < 60
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -825,152 +825,6 @@ describe Course, "backup" do
|
|||
end
|
||||
|
||||
context "merge_into_course" do
|
||||
it "should merge content into another course" do
|
||||
course_model
|
||||
attachment_model
|
||||
@old_attachment = @attachment
|
||||
@old_topic = @course.discussion_topics.create!(:title => "some topic", :message => "<a href='/courses/#{@course.id}/files/#{@attachment.id}/download'>download this file</a>")
|
||||
html = @old_topic.message
|
||||
html.should match(Regexp.new("/courses/#{@course.id}/files/#{@attachment.id}/download"))
|
||||
@old_course = @course
|
||||
@new_course = course_model
|
||||
@new_course.merge_into_course(@old_course, :everything => true)
|
||||
@old_attachment.reload
|
||||
@old_attachment.cloned_item_id.should_not be_nil
|
||||
@new_attachment = @new_course.attachments.find_by_cloned_item_id(@old_attachment.cloned_item_id)
|
||||
@new_attachment.should_not be_nil
|
||||
@old_topic.reload
|
||||
@old_topic.cloned_item_id.should_not be_nil
|
||||
@new_topic = @new_course.discussion_topics.find_by_cloned_item_id(@old_topic.cloned_item_id)
|
||||
@new_topic.should_not be_nil
|
||||
html = @new_topic.message
|
||||
html.should match(Regexp.new("/courses/#{@new_course.id}/files/#{@new_attachment.id}/download"))
|
||||
end
|
||||
|
||||
it "should merge locked files and retain correct html links" do
|
||||
course_model
|
||||
attachment_model
|
||||
@old_attachment = @attachment
|
||||
@old_attachment.update_attribute(:hidden, true)
|
||||
@old_attachment.reload.should be_hidden
|
||||
@old_topic = @course.discussion_topics.create!(:title => "some topic", :message => "<img src='/courses/#{@course.id}/files/#{@attachment.id}/preview'>")
|
||||
html = @old_topic.message
|
||||
html.should match(Regexp.new("/courses/#{@course.id}/files/#{@attachment.id}/preview"))
|
||||
@old_course = @course
|
||||
@new_course = course_model
|
||||
@new_course.merge_into_course(@old_course, :everything => true)
|
||||
@old_attachment.reload
|
||||
@old_attachment.cloned_item_id.should_not be_nil
|
||||
@new_attachment = @new_course.attachments.find_by_cloned_item_id(@old_attachment.cloned_item_id)
|
||||
@new_attachment.should_not be_nil
|
||||
@old_topic.reload
|
||||
@old_topic.cloned_item_id.should_not be_nil
|
||||
@new_topic = @new_course.discussion_topics.find_by_cloned_item_id(@old_topic.cloned_item_id)
|
||||
@new_topic.should_not be_nil
|
||||
html = @new_topic.message
|
||||
html.should match(Regexp.new("/courses/#{@new_course.id}/files/#{@new_attachment.id}/preview"))
|
||||
end
|
||||
|
||||
it "should merge only selected content into another course" do
|
||||
course_model
|
||||
attachment_model
|
||||
@old_attachment = @attachment
|
||||
@old_topic = @course.discussion_topics.create!(:title => "some topic", :message => "<a href='/courses/#{@course.id}/files/#{@attachment.id}/download'>download this file</a>")
|
||||
html = @old_topic.message
|
||||
html.should match(Regexp.new("/courses/#{@course.id}/files/#{@attachment.id}/download"))
|
||||
@old_course = @course
|
||||
@new_course = course_model
|
||||
@new_course.merge_into_course(@old_course, :all_files => true)
|
||||
@old_attachment.reload
|
||||
@old_attachment.cloned_item_id.should_not be_nil
|
||||
@new_attachment = @new_course.attachments.find_by_cloned_item_id(@old_attachment.cloned_item_id)
|
||||
@new_attachment.should_not be_nil
|
||||
@old_topic.reload
|
||||
@old_topic.cloned_item_id.should be_nil
|
||||
@new_course.discussion_topics.count.should eql(0)
|
||||
end
|
||||
|
||||
it "should migrate syllabus links on copy" do
|
||||
course_model
|
||||
@old_topic = @course.discussion_topics.create!(:title => "some topic", :message => "some text")
|
||||
@old_course = @course
|
||||
@old_course.syllabus_body = "<a href='/courses/#{@old_course.id}/discussion_topics/#{@old_topic.id}'>link</a>"
|
||||
@old_course.save!
|
||||
@new_course = course_model
|
||||
@new_course.merge_into_course(@old_course, :course_settings => true, :all_topics => true)
|
||||
@old_topic.reload
|
||||
@new_topic = @new_course.discussion_topics.find_by_cloned_item_id(@old_topic.cloned_item_id)
|
||||
@new_topic.should_not be_nil
|
||||
@old_topic.cloned_item_id.should == @new_topic.cloned_item_id
|
||||
@new_course.reload
|
||||
@new_course.syllabus_body.should match(/\/courses\/#{@new_course.id}\/discussion_topics\/#{@new_topic.id}/)
|
||||
end
|
||||
|
||||
it "should copy external tools" do
|
||||
course_model
|
||||
copy_from = @course
|
||||
tool_from = copy_from.context_external_tools.create!(:name => "new tool", :consumer_key => "key", :shared_secret => "secret", :domain => 'example.com', :custom_fields => {'a' => '1', 'b' => '2'})
|
||||
tool_from.settings[:course_navigation] = {:url => "http://www.example.com", :text => "Example URL"}
|
||||
tool_from.save
|
||||
|
||||
course_model
|
||||
copy_to = @course
|
||||
copy_to.merge_into_course(copy_from, :course_settings => true, tool_from.asset_string.to_sym => true)
|
||||
copy_to.context_external_tools.count.should == 1
|
||||
|
||||
tool_to = copy_to.context_external_tools.first
|
||||
tool_to.name.should == tool_from.name
|
||||
tool_to.consumer_key.should == tool_from.consumer_key
|
||||
tool_to.settings.should == tool_from.settings
|
||||
tool_to.has_course_navigation.should == true
|
||||
end
|
||||
|
||||
it "should not duplicate external tools used in modules" do
|
||||
course_model
|
||||
copy_from = @course
|
||||
tool_from = copy_from.context_external_tools.create!(:name => "new tool", :consumer_key => "key", :shared_secret => "secret", :domain => 'example.com', :custom_fields => {'a' => '1', 'b' => '2'})
|
||||
tool_from.settings[:course_navigation] = {:url => "http://www.example.com", :text => "Example URL"}
|
||||
tool_from.save
|
||||
|
||||
mod1 = copy_from.context_modules.create!(:name => "some module")
|
||||
tag = mod1.add_item({:type => 'context_external_tool',
|
||||
:title => 'Example URL',
|
||||
:url => "http://www.example.com",
|
||||
:new_tab => true})
|
||||
tag.save
|
||||
|
||||
course_model
|
||||
copy_to = @course
|
||||
copy_to.merge_into_course(copy_from, :course_settings => true, :all_external_tools => true, :all_modules => true)
|
||||
copy_to.context_external_tools.count.should == 1
|
||||
|
||||
tool_to = copy_to.context_external_tools.first
|
||||
tool_to.name.should == tool_from.name
|
||||
tool_to.consumer_key.should == tool_from.consumer_key
|
||||
tool_to.settings.should == tool_from.settings
|
||||
tool_to.has_course_navigation.should == true
|
||||
end
|
||||
|
||||
it "should copy external tool assignments" do
|
||||
course_model
|
||||
copy_from = @course
|
||||
assignment_model(:course => copy_from, :points_possible => 40, :submission_types => 'external_tool', :grading_type => 'points')
|
||||
tag_from = @assignment.build_external_tool_tag(:url => "http://example.com/one", :new_tab => true)
|
||||
tag_from.content_type = 'ContextExternalTool'
|
||||
tag_from.save!
|
||||
|
||||
course_model
|
||||
copy_to = @course
|
||||
copy_to.merge_into_course(copy_from, :course_settings => true, :all_assignments => true, :all_external_tools => true)
|
||||
|
||||
asmnt_2 = copy_to.assignments.first
|
||||
asmnt_2.submission_types.should == "external_tool"
|
||||
asmnt_2.external_tool_tag.should_not be_nil
|
||||
tag_to = asmnt_2.external_tool_tag
|
||||
tag_to.content_type.should == tag_from.content_type
|
||||
tag_to.url.should == tag_from.url
|
||||
tag_to.new_tab.should == tag_from.new_tab
|
||||
end
|
||||
|
||||
it "should merge implied content into another course" do
|
||||
course_model
|
||||
|
@ -994,21 +848,6 @@ describe Course, "backup" do
|
|||
html.should match(Regexp.new("/courses/#{@new_course.id}/files/#{@new_attachment.id}/download"))
|
||||
end
|
||||
|
||||
it "should translate links to the new context" do
|
||||
course_model
|
||||
attachment_model
|
||||
@old_attachment = @attachment
|
||||
@old_topic = @course.discussion_topics.create!(:title => "some topic", :message => "<a href='/courses/#{@course.id}/files/#{@attachment.id}/download'>download this file</a>")
|
||||
html = @old_topic.message
|
||||
html.should match(Regexp.new("/courses/#{@course.id}/files/#{@attachment.id}/download"))
|
||||
@old_course = @course
|
||||
@new_course = course_model
|
||||
@new_attachment = @old_attachment.clone_for(@new_course)
|
||||
@new_attachment.save!
|
||||
html = Course.migrate_content_links(@old_topic.message, @old_course, @new_course)
|
||||
html.should match(Regexp.new("/courses/#{@new_course.id}/files/#{@new_attachment.id}/download"))
|
||||
end
|
||||
|
||||
it "should bring over linked files if not already brought over" do
|
||||
course_model
|
||||
attachment_model
|
||||
|
@ -1049,59 +888,6 @@ describe Course, "backup" do
|
|||
@new_attachment.should_not be_nil
|
||||
html.should match(Regexp.new("/courses/#{@new_course.id}/files/#{@new_attachment.id}/download"))
|
||||
end
|
||||
|
||||
it "should assign the correct parent folder when the parent folder has already been created" do
|
||||
old_course = course_model
|
||||
folder = Folder.root_folders(@course).first
|
||||
folder = folder.sub_folders.create!(:context => @course, :name => 'folder_1')
|
||||
attachment_model(:folder => folder, :filename => "dummy.txt")
|
||||
folder = folder.sub_folders.create!(:context => @course, :name => 'folder_2')
|
||||
folder = folder.sub_folders.create!(:context => @course, :name => 'folder_3')
|
||||
old_attachment = attachment_model(:folder => folder, :filename => "merge.test")
|
||||
|
||||
new_course = course_model
|
||||
|
||||
new_course.merge_into_course(old_course, :everything => true)
|
||||
old_attachment.reload
|
||||
old_attachment.cloned_item_id.should_not be_nil
|
||||
new_attachment = new_course.attachments.find_by_cloned_item_id(old_attachment.cloned_item_id)
|
||||
new_attachment.should_not be_nil
|
||||
new_attachment.full_path.should == "course files/folder_1/folder_2/folder_3/merge.test"
|
||||
folder.reload
|
||||
new_attachment.folder.cloned_item_id.should == folder.cloned_item_id
|
||||
end
|
||||
|
||||
it "should perform day substitutions" do
|
||||
old_course = course_model
|
||||
old_course.assert_assignment_group
|
||||
today = Time.now.utc
|
||||
@assignment = old_course.assignments.build
|
||||
@assignment.due_at = today
|
||||
@assignment.workflow_state = 'published'
|
||||
@assignment.save!
|
||||
old_course.reload
|
||||
|
||||
new_course = course_model
|
||||
|
||||
new_course.merge_into_course(old_course, :everything => true, :shift_dates => true, :day_substitutions => { today.wday.to_s => (today.wday + 1).to_s})
|
||||
new_course.reload
|
||||
new_assignment = new_course.assignments.first
|
||||
# new_assignment.due_at.should == today + 1.day does not work
|
||||
(new_assignment.due_at.to_i - (today + 1.day).to_i).abs.should < 60
|
||||
end
|
||||
|
||||
it "should copy a quiz when the quiz is not selected but the quiz's assignment is" do
|
||||
course_model
|
||||
@quiz = @course.quizzes.create!
|
||||
@quiz.did_edit
|
||||
@quiz.offer!
|
||||
@quiz.assignment.should_not be_nil
|
||||
@old_course = @course
|
||||
@new_course = course_model
|
||||
@new_course.merge_into_course(@old_course, "assignment_#{@quiz.assignment_id}" => true)
|
||||
@new_quiz = @new_course.quizzes.first
|
||||
@new_quiz.should_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "should not cross learning outcomes with learning outcome groups in the association" do
|
||||
|
@ -1140,61 +926,6 @@ describe Course, "backup" do
|
|||
course.has_outcomes.should == false
|
||||
end
|
||||
|
||||
it "should copy learning outcomes into the new course" do
|
||||
old_course = course_model
|
||||
lo = old_course.learning_outcomes.new
|
||||
lo.context = old_course
|
||||
lo.short_description = "Lone outcome"
|
||||
lo.description = "<p>Descriptions are boring</p>"
|
||||
lo.workflow_state = 'active'
|
||||
lo.data = {:rubric_criterion=>{:mastery_points=>3, :ratings=>[{:description=>"Exceeds Expectations", :points=>5}, {:description=>"Meets Expectations", :points=>3}, {:description=>"Does Not Meet Expectations", :points=>0}], :description=>"First outcome", :points_possible=>5}}
|
||||
lo.save!
|
||||
|
||||
old_root = LearningOutcomeGroup.default_for(old_course)
|
||||
old_root.add_item(lo)
|
||||
|
||||
lo_g = old_course.learning_outcome_groups.new
|
||||
lo_g.context = old_course
|
||||
lo_g.title = "Lone outcome group"
|
||||
lo_g.description = "<p>Groupage</p>"
|
||||
lo_g.save!
|
||||
old_root.add_item(lo_g)
|
||||
|
||||
lo2 = old_course.learning_outcomes.new
|
||||
lo2.context = old_course
|
||||
lo2.short_description = "outcome in group"
|
||||
lo2.workflow_state = 'active'
|
||||
lo2.data = {:rubric_criterion=>{:mastery_points=>2, :ratings=>[{:description=>"e", :points=>50}, {:description=>"me", :points=>2}, {:description=>"Does Not Meet Expectations", :points=>0.5}], :description=>"First outcome", :points_possible=>5}}
|
||||
lo2.save!
|
||||
lo_g.add_item(lo2)
|
||||
old_root.reload
|
||||
|
||||
# copy outcomes into new course
|
||||
new_course = course_model
|
||||
new_root = LearningOutcomeGroup.default_for(new_course)
|
||||
new_course.merge_into_course(old_course, :all_outcomes => true)
|
||||
|
||||
new_course.learning_outcomes.count.should == old_course.learning_outcomes.count
|
||||
new_course.learning_outcome_groups.count.should == old_course.learning_outcome_groups.count
|
||||
new_root.sorted_content.count.should == old_root.sorted_content.count
|
||||
|
||||
lo_2 = new_root.sorted_content.first
|
||||
lo_2.short_description.should == lo.short_description
|
||||
lo_2.description.should == lo.description
|
||||
lo_2.data.should == lo.data
|
||||
|
||||
lo_g_2 = new_root.sorted_content.last
|
||||
lo_g_2.title.should == lo_g.title
|
||||
lo_g_2.description.should == lo_g.description
|
||||
lo_g_2.sorted_content.length.should == 1
|
||||
lo_g_2.root_learning_outcome_group_id.should == new_root.id
|
||||
lo_g_2.learning_outcome_group_id.should == new_root.id
|
||||
|
||||
lo_2 = lo_g_2.sorted_content.first
|
||||
lo_2.short_description.should == lo2.short_description
|
||||
lo_2.description.should == lo2.description
|
||||
lo_2.data.should == lo2.data
|
||||
end
|
||||
end
|
||||
|
||||
def course_to_backup
|
||||
|
|
|
@ -105,7 +105,7 @@ describe "courses" do
|
|||
click_option('#copy_from_course', 'second course')
|
||||
driver.find_element(:css, '#content form').submit
|
||||
|
||||
driver.find_element(:id, 'copy_everything').click
|
||||
#driver.find_element(:id, 'copy_everything').click
|
||||
|
||||
#modify course dates
|
||||
driver.find_element(:id, 'copy_shift_dates').click
|
||||
|
@ -123,7 +123,7 @@ describe "courses" do
|
|||
wait_for_ajaximations
|
||||
|
||||
# since jobs aren't running
|
||||
CourseImport.last.perform
|
||||
ContentMigration.last.copy_course_without_send_later
|
||||
|
||||
keep_trying_until { driver.find_element(:css, '#copy_results > h2').should include_text('Copy Succeeded') }
|
||||
@course.reload
|
||||
|
@ -139,11 +139,10 @@ describe "courses" do
|
|||
|
||||
get "/courses/#{@course.id}/copy"
|
||||
expect_new_page_load { driver.find_element(:css, "div#content form").submit }
|
||||
driver.find_element(:id, 'copy_everything').click
|
||||
driver.find_element(:id, 'copy_context_form').submit
|
||||
wait_for_ajaximations
|
||||
|
||||
CourseImport.last.perform
|
||||
ContentMigration.last.copy_course_without_send_later
|
||||
|
||||
keep_trying_until { driver.find_element(:css, '#copy_results > h2').should include_text('Copy Succeeded') }
|
||||
|
||||
|
|
|
@ -59,19 +59,23 @@ if Qti.migration_executable
|
|||
|
||||
it "should use expected file links in questions" do
|
||||
aq = @course.assessment_questions.find_by_migration_id("prepend_test_QUE_1003")
|
||||
att = aq.attachments.find_by_migration_id("prepend_test_4d348a246af616c7d9a7d403367c1a30")
|
||||
c_att = @course.attachments.find_by_migration_id("prepend_test_4d348a246af616c7d9a7d403367c1a30")
|
||||
att = aq.attachments.find_by_migration_id(CC::CCHelper.create_key(c_att))
|
||||
aq.question_data["question_text"].should =~ %r{files/#{att.id}/download}
|
||||
|
||||
aq = @course.assessment_questions.find_by_migration_id("prepend_test_QUE_1007")
|
||||
att = aq.attachments.find_by_migration_id("prepend_test_f3e5ead7f6e1b25a46a4145100566821")
|
||||
c_att = @course.attachments.find_by_migration_id("prepend_test_f3e5ead7f6e1b25a46a4145100566821")
|
||||
att = aq.attachments.find_by_migration_id(CC::CCHelper.create_key(c_att))
|
||||
aq.question_data["question_text"].should =~ %r{files/#{att.id}/download}
|
||||
|
||||
aq = @course.assessment_questions.find_by_migration_id("prepend_test_QUE_1014")
|
||||
att = aq.attachments.find_by_migration_id("prepend_test_d2b5ca33bd970f64a6301fa75ae2eb22")
|
||||
c_att = @course.attachments.find_by_migration_id("prepend_test_d2b5ca33bd970f64a6301fa75ae2eb22")
|
||||
att = aq.attachments.find_by_migration_id(CC::CCHelper.create_key(c_att))
|
||||
aq.question_data["question_text"].should =~ %r{files/#{att.id}/download}
|
||||
|
||||
aq = @course.assessment_questions.find_by_migration_id("prepend_test_QUE_1053")
|
||||
att = aq.attachments.find_by_migration_id("prepend_test_c16566de1661613ef9e5517ec69c25a1")
|
||||
c_att = @course.attachments.find_by_migration_id("prepend_test_c16566de1661613ef9e5517ec69c25a1")
|
||||
att = aq.attachments.find_by_migration_id(CC::CCHelper.create_key(c_att))
|
||||
aq.question_data["question_text"].should =~ %r{files/#{att.id}/download}
|
||||
end
|
||||
|
||||
|
@ -94,12 +98,14 @@ if Qti.migration_executable
|
|||
|
||||
# Check the first import
|
||||
aq = @course.assessment_questions.find_by_migration_id("prepend_test_QUE_1003")
|
||||
att = aq.attachments.find_by_migration_id("prepend_test_4d348a246af616c7d9a7d403367c1a30")
|
||||
c_att = @course.attachments.find_by_migration_id("prepend_test_4d348a246af616c7d9a7d403367c1a30")
|
||||
att = aq.attachments.find_by_migration_id(CC::CCHelper.create_key(c_att))
|
||||
aq.question_data["question_text"].should =~ %r{files/#{att.id}/download}
|
||||
|
||||
# check the second import
|
||||
aq = @course.assessment_questions.find_by_migration_id("test2_QUE_1003")
|
||||
att = aq.attachments.find_by_migration_id("test2_4d348a246af616c7d9a7d403367c1a30")
|
||||
c_att = @course.attachments.find_by_migration_id("test2_4d348a246af616c7d9a7d403367c1a30")
|
||||
att = aq.attachments.find_by_migration_id(CC::CCHelper.create_key(c_att))
|
||||
aq.question_data["question_text"].should =~ %r{files/#{att.id}/download}
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue