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:
Bracken Mosbacker 2012-03-28 08:52:21 -06:00
parent 37a65cec7c
commit a942ede9c6
37 changed files with 976 additions and 764 deletions

View File

@ -1,5 +1,6 @@
require [
'compiled/util/processItemSelections'
'copy_course'
'choose_content'
'choose_course'
]

View File

@ -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

View File

@ -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))
@ -167,33 +171,43 @@ class ContentImportsController < ApplicationController
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
#
# Retrieve the status of a course copy
@ -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,38 +263,29 @@ 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
@ -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

View File

@ -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

View File

@ -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

View File

@ -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
@ -66,6 +73,14 @@ class ContentExport < ActiveRecord::Base
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
end
@ -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

View File

@ -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
@ -261,6 +263,69 @@ class ContentMigration < ActiveRecord::Base
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}")

View File

@ -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] ||= {}

View File

@ -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]

View File

@ -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

View File

@ -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&nbsp;</div>',
'**' => '<div style="float: left;">&nbsp;\1&nbsp;</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&nbsp;</div>',
'**' => '<div style="float: left;">&nbsp;\1&nbsp;</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">&nbsp;</span>'.html_safe,
:new_day => '<span class="new_select">&nbsp;</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>

View File

@ -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 %>

View File

@ -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;">&nbsp;</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;">&nbsp;</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 %>

View File

@ -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&nbsp;</div>',
'**' => '<div style="float: left;">&nbsp;\1&nbsp;</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&nbsp;</div>',
'**' => '<div style="float: left;">&nbsp;\1&nbsp;</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">&nbsp;</span>'.html_safe,
:new_day => '<span class="new_select">&nbsp;</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;">&nbsp;</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;">&nbsp;</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 %>

View File

@ -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 %>

View File

@ -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>

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -77,6 +77,8 @@ 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

View File

@ -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

View 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

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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");
}
});
}
});
});

View File

@ -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");
}
});
}
});

View File

@ -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,6 +583,9 @@ 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|
@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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') }

View File

@ -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