blueprint: don't undelete deleted/unlocked items on sync

- have files, quizzes, learning outcomes, outcome groups,
   and modules in a blueprint course. no items should be
   locked.
 - ensure outcome group changes appear in the unsynced
   changes list correctly
 - sync to an associated course
 - delete items in the associated course
 - change the same items in the blueprint in some way
   (i.e., rename them)
 - re-sync
 - the deleted items should not be undeleted in the
   associated course
 - non-deleted outcome groups should not gain a link
   to a deleted outcome (that shows only in the left pane)

fixes ADMIN-559

Change-Id: Ibe8842353760260b050a6330cd27df986b17b5e7
Reviewed-on: https://gerrit.instructure.com/133374
Tested-by: Jenkins
Reviewed-by: James Williams  <jamesw@instructure.com>
QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com>
Product-Review: Jeremy Stanley <jeremy@instructure.com>
This commit is contained in:
Jeremy Stanley 2017-11-20 14:40:59 -07:00
parent 37984c768e
commit e52032d956
12 changed files with 103 additions and 17 deletions

View File

@ -29,6 +29,7 @@ const itemTypeLabels = {
assessment_question_bank: I18n.t('Question Bank'),
calendar_event: I18n.t('Event'),
learning_outcome: I18n.t('Outcome'),
learning_outcome_group: I18n.t('Outcome Group'),
rubric: I18n.t('Rubric'),
context_external_tool: I18n.t('External Tool')
}

View File

@ -66,7 +66,7 @@ propTypes.migrationExceptionList = arrayOf(propTypes.migrationException)
propTypes.migrationChange = shape({
asset_id: string.isRequired,
asset_type: oneOf(['assignment', 'quiz', 'discussion_topic', 'wiki_page', 'attachment', 'context_module']).isRequired,
asset_type: oneOf(['assignment', 'quiz', 'discussion_topic', 'wiki_page', 'attachment', 'context_module', 'learning_outcome', 'learning_outcome_group', 'announcement']).isRequired,
asset_name: string.isRequired,
change_type: oneOf(['created', 'updated', 'deleted']).isRequired,
htnl_url: string,

View File

@ -199,15 +199,11 @@ module Context
end
def self.asset_name(asset)
if asset.respond_to?(:display_name)
asset.display_name
elsif asset.respond_to?(:title)
asset.title
elsif asset.respond_to?(:short_description)
asset.short_description
else
asset.name
end
name = asset.display_name.presence if asset.respond_to?(:display_name)
name ||= asset.title.presence if asset.respond_to?(:title)
name ||= asset.short_description.presence if asset.respond_to?(:short_description)
name ||= asset.name
name
end
def self.get_account(context)

View File

@ -42,6 +42,7 @@ module Importers
where(migration_clause(hash[:migration_id])).first if hash[:migration_id]
item ||= context.learning_outcome_groups.temp_record
item.context = context
item.mark_as_importing!(migration)
end
item.workflow_state = 'active' # restore deleted ones
item.migration_id = hash[:migration_id]
@ -65,6 +66,11 @@ module Importers
end
item.save!
# don't import contents of deleted outcome groups
# (blueprint migration will not undelete outcome groups deleted downstream)
return item if item.deleted?
if hash[:parent_group]
hash[:parent_group].adopt_outcome_group(item)
else

View File

@ -82,6 +82,7 @@ module Importers
where(migration_clause(hash[:migration_id])).first if hash[:migration_id]
item ||= context.created_learning_outcomes.temp_record
item.context = context
item.mark_as_importing!(migration)
end
item.migration_id = hash[:migration_id]
item.vendor_guid = hash[:vendor_guid]
@ -113,6 +114,10 @@ module Importers
item = outcome
end
# don't add a deleted outcome to an outcome group, or align it with an assignment
# (blueprint migration will not undelete outcomes deleted downstream)
return item if item.deleted?
log = hash[:learning_outcome_group] || context.root_outcome_group
log.add_outcome(item)

View File

@ -166,8 +166,10 @@ module Importers
# there might not be an import id if it's just a text-only type...
item ||= Quizzes::Quiz.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first if hash[:id]
item ||= Quizzes::Quiz.where(context_type: context.class.to_s, context_id: context, migration_id: hash[:migration_id]).first if hash[:migration_id]
if item && !allow_update
if item.deleted?
item ||= context.quizzes.temp_record
item.mark_as_importing!(migration)
if item
if !allow_update && item.deleted?
item.workflow_state = (hash[:available] || !item.can_unpublish?) ? 'available' : 'unpublished'
item.saved_by = :migration
item.quiz_groups.destroy_all
@ -175,8 +177,6 @@ module Importers
item.save
end
end
item ||= context.quizzes.temp_record
item.mark_as_importing!(migration)
new_record = item.new_record? || item.deleted?
hash[:due_at] ||= hash[:due_date] if hash.has_key?(:due_date)

View File

@ -19,6 +19,9 @@
class LearningOutcome < ActiveRecord::Base
include Workflow
include OutcomeAttributes
include MasterCourses::Restrictor
restrict_columns :state, [:workflow_state]
belongs_to :context, polymorphic: [:account, :course]
has_many :learning_outcome_results
has_many :alignments, -> { where("content_tags.tag_type='learning_outcome' AND content_tags.workflow_state<>'deleted'") }, class_name: 'ContentTag'

View File

@ -19,6 +19,9 @@
class LearningOutcomeGroup < ActiveRecord::Base
include Workflow
include OutcomeAttributes
include MasterCourses::Restrictor
restrict_columns :state, [:workflow_state]
belongs_to :learning_outcome_group
has_many :child_outcome_groups, :class_name => 'LearningOutcomeGroup', :foreign_key => "learning_outcome_group_id"
has_many :child_outcome_links, -> { where(tag_type: 'learning_outcome_association', content_type: 'LearningOutcome') }, class_name: 'ContentTag', as: :associated_asset

View File

@ -22,8 +22,8 @@ module MasterCourses
# probably not be a comprehensive list but oh well
ALLOWED_CONTENT_TYPES = %w{
Announcement AssessmentQuestionBank Assignment AssignmentGroup Attachment CalendarEvent
DiscussionTopic ContextExternalTool ContextModule ContentTag LearningOutcome Quizzes::Quiz Rubric WikiPage
Announcement AssessmentQuestionBank Assignment AssignmentGroup Attachment CalendarEvent DiscussionTopic
ContextExternalTool ContextModule ContentTag LearningOutcome LearningOutcomeGroup Quizzes::Quiz Rubric WikiPage
}.freeze
MIGRATION_ID_PREFIX = "mastercourse_".freeze

View File

@ -30,6 +30,7 @@ class MasterCourses::ChildContentTag < ActiveRecord::Base
:content_tag,
:discussion_topic,
:learning_outcome,
:learning_outcome_group,
:rubric,
:wiki_page,
quiz: 'Quizzes::Quiz'

View File

@ -48,6 +48,8 @@ module Api::V1::MasterCourses
course_external_tool_url(:course_id => asset.context.id, :id => asset.id)
when 'LearningOutcome'
course_outcome_url(:course_id => asset.context.id, :id => asset.id)
when 'LearningOutcomeGroup'
course_outcome_group_url(:course_id => asset.context.id, :id => asset.id)
else
polymorphic_url([asset.context, asset])
end

View File

@ -381,31 +381,100 @@ describe MasterCourses::MasterMigration do
page1 = @copy_from.wiki_pages.create!(:title => "whee")
page2 = @copy_from.wiki_pages.create!(:title => "whoo")
quiz = @copy_from.quizzes.create!(:title => 'what')
run_master_migration
page1_to = @copy_to.wiki_pages.where(:migration_id => mig_id(page1)).first
page1_to.destroy # "manually" delete it
page2_to = @copy_to.wiki_pages.where(:migration_id => mig_id(page2)).first
quiz_to = @copy_to.quizzes.where(:migration_id => mig_id(quiz)).first
quiz_to.destroy
Timecop.freeze(3.minutes.from_now) do
page1.update_attribute(:title, 'new title eh')
page2.destroy
quiz.update_attribute(:title, 'new title wat')
end
run_master_migration
expect(page1_to.reload).to be_deleted # shouldn't have restored it
expect(page2_to.reload).to be_deleted # should still sync the original deletion
expect(quiz_to.reload).to be_deleted # shouldn't have restored it neither
Timecop.freeze(5.minutes.from_now) do
page1.update_attribute(:title, 'another new title srsly')
@template.content_tag_for(page1).update_attribute(:restrictions, {:content => true}) # lock it down
page2.update_attribute(:workflow_state, "active") # restore the original
quiz.update_attribute(:title, 'another new title frd pdq')
@template.content_tag_for(quiz).update_attribute(:restrictions, {:content => true}) # lock it down
end
run_master_migration
expect(page1_to.reload).to be_active # should be restored because it's locked now
expect(page2_to.reload).to be_active # should be restored because it hadn't been deleted manually
expect(quiz_to.reload).not_to be_deleted # should be restored because it's locked now
end
it "doesn't undelete modules that were deleted downstream" do
@copy_to = course_factory
@template.add_child_course!(@copy_to)
mod = @copy_from.context_modules.create! :name => 'teh'
run_master_migration
mod_to = @copy_to.context_modules.where(:migration_id => mig_id(mod)).first
mod_to.destroy
Timecop.freeze(3.minutes.from_now) do
mod.touch
end
run_master_migration
expect(mod_to.reload).to be_deleted
end
describe "outcomes and groups" do
before :once do
@copy_to = course_factory
@template.add_child_course!(@copy_to)
root = @copy_from.root_outcome_group
@og = @copy_from.learning_outcome_groups.create!({:title => 'outcome group'})
root.adopt_outcome_group(@og)
@outcome = @copy_from.created_learning_outcomes.create!({:title => 'new outcome'})
@og.add_outcome(@outcome)
run_master_migration
@outcome_to = @copy_to.learning_outcomes.where(:migration_id => mig_id(@outcome)).first
@og_to = @copy_to.learning_outcome_groups.where(:migration_id => mig_id(@og)).first
end
it "doesn't undelete learning outcomes and outcome groups that were deleted downstream" do
@outcome_to.destroy
@og_to.destroy
Timecop.freeze(3.minutes.from_now) do
@og.touch
@outcome.touch
end
run_master_migration
expect(@outcome_to.reload).to be_deleted
expect(@og_to.reload).to be_deleted
end
it "doesn't resurrect links to deleted outcomes" do
@outcome_to.destroy
Timecop.freeze(3.minutes.from_now) do
@og.touch
@outcome.touch
end
run_master_migration
expect(@outcome_to.reload).to be_deleted
expect(@og_to.child_outcome_links.not_deleted.where(content_type: 'LearningOutcome', content_id: @outcome_to)).not_to be_any
end
end
it "doesn't restore deleted associated files unless relocked" do