create user merge table

fixes CNVS-27291

test plan
 - merge users
 - data should be populated

Change-Id: I5720933484494f582bad4b294a7e6b4edea66b92
Reviewed-on: https://gerrit.instructure.com/72700
Tested-by: Jenkins
Reviewed-by: Cody Cutrer <cody@instructure.com>
QA-Review: Jeremy Putnam <jeremyp@instructure.com>
Product-Review: Rob Orton <rob@instructure.com>
This commit is contained in:
Rob Orton 2016-02-22 10:49:12 -07:00
parent 16c6c1c172
commit 573a2b673b
5 changed files with 158 additions and 26 deletions

View File

@ -0,0 +1,33 @@
#
# Copyright (C) 2016 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/>.
#
class UserMergeData < ActiveRecord::Base
belongs_to :user
belongs_to :from_user, class_name: 'User'
has_many :user_merge_data_records
strong_params
def add_more_data(objects)
objects.each do |o|
r = self.user_merge_data_records.new(context: o, previous_user_id: o.user_id)
r.previous_workflow_state = o.workflow_state if o.class.columns_hash.key?('workflow_state')
r.save!
end
end
end

View File

@ -0,0 +1,24 @@
#
# Copyright (C) 2016 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/>.
#
class UserMergeDataRecord < ActiveRecord::Base
belongs_to :previous_user, class_name: 'User'
belongs_to :user_merge_data
belongs_to :context, polymorphic: [:account_user, :enrollment, :pseudonym]
strong_params
end

View File

@ -0,0 +1,29 @@
class CreateUserMergeData < ActiveRecord::Migration
tag :predeploy
def change
create_table :user_merge_data do |t|
t.integer :user_id, limit: 8, null: false
t.integer :from_user_id, limit: 8, null: false
t.timestamps null: false
t.string :workflow_state, null: false, default: 'active'
end
create_table :user_merge_data_records do |t|
t.integer :user_merge_data_id, limit: 8, null: false
t.integer :context_id, limit: 8, null: false
t.integer :previous_user_id, limit: 8, null: false
t.string :context_type, null: false
t.string :previous_workflow_state
end
add_index :user_merge_data, :user_id
add_index :user_merge_data, :from_user_id
add_index :user_merge_data_records, :user_merge_data_id
add_index :user_merge_data_records, [:context_id, :context_type, :user_merge_data_id, :previous_user_id],
unique: true, name: "index_user_merge_data_records_on_context_id_and_context_type"
add_foreign_key :user_merge_data, :users
add_foreign_key :user_merge_data_records, :user_merge_data, column: :user_merge_data_id
end
end

View File

@ -13,6 +13,9 @@ class UserMerge
def into(target_user)
return unless target_user
return if target_user == from_user
user_merge_data = target_user.shard.activate do
UserMergeData.create!(user: target_user, from_user: from_user)
end
if target_user.avatar_state == :none && from_user.avatar_state != :none
[:avatar_image_source, :avatar_image_url, :avatar_image_updated_at, :avatar_state].each do |attr|
@ -96,11 +99,13 @@ class UserMerge
destroy_conflicting_module_progressions(@from_user, target_user)
move_enrollments(@from_user, target_user)
move_enrollments(target_user, user_merge_data)
Shard.with_each_shard(from_user.associated_shards + from_user.associated_shards(:weak) + from_user.associated_shards(:shadow)) do
max_position = Pseudonym.where(:user_id => target_user).order(:position).last.try(:position) || 0
Pseudonym.where(:user_id => from_user).update_all(["user_id=?, position=position+?", target_user, max_position])
max_position = Pseudonym.where(user_id: target_user).order(:position).last.try(:position) || 0
pseudonyms_to_move = Pseudonym.where(user_id: from_user)
user_merge_data.add_more_data(pseudonyms_to_move)
pseudonyms_to_move.update_all(["user_id=?, position=position+?", target_user, max_position])
target_user.communication_channels.email.unretired.each do |cc|
Rails.cache.delete([cc.path, 'invited_enrollments2'].cache_key)
@ -165,9 +170,12 @@ class UserMerge
Rails.logger.error "migrating discussions failed: #{e}"
end
account_users = AccountUser.where(user_id: from_user)
user_merge_data.add_more_data(account_users)
account_users.update_all(user_id: target_user)
updates = {}
['account_users', 'access_tokens', 'asset_user_accesses',
'attachments',
['access_tokens', 'asset_user_accesses', 'attachments',
'calendar_events', 'collaborations',
'context_module_progressions',
'group_memberships', 'page_comments',
@ -291,31 +299,46 @@ class UserMerge
END, sis_batch_id DESC, updated_at DESC").first
end
def move_enrollments(from_user, target_user)
def handle_conflicts(column, target_user, user_merge_data)
users = [from_user, target_user]
conflict_scope(column).where(column => users).find_each do |e|
scope = enrollment_conflicts(e, column, users)
keeper = enrollment_keeper(scope)
# delete all conflicts from target user
to_delete = scope.where("id<>?", keeper).where(column => target_user)
user_merge_data.add_more_data(to_delete)
to_delete.delete_all
# mark all conflicts on from_user as deleted so they will be left
to_delete = scope.active.where("id<>?", keeper).where(column => from_user)
user_merge_data.add_more_data(to_delete)
to_delete.destroy_all
end
end
def remove_self_observers(target_user, user_merge_data)
# prevent observing self by marking them as deleted
to_delete = Enrollment.active.where("type = 'ObserverEnrollment' AND
(associated_user_id = :target_user AND user_id = :from_user OR
associated_user_id = :from_user AND user_id = :target_user)",
{target_user: target_user, from_user: from_user})
user_merge_data.add_more_data(to_delete)
to_delete.destroy_all
end
def move_enrollments(target_user, user_merge_data)
[:associated_user_id, :user_id].each do |column|
users = [from_user, target_user]
Shard.with_each_shard(from_user.associated_shards) do
Enrollment.transaction do
conflict_scope(column).where(column => users).find_each do |e|
scope = enrollment_conflicts(e, column, users)
keeper = enrollment_keeper(scope)
# delete all conflicts from target user
scope.where("id<>?", keeper).where(column => target_user).delete_all
# mark all conflicts on from_user as deleted so they will be left
scope.active.where("id<>?", keeper).where(column => from_user).destroy_all
end
# prevent observing self by marking them as deleted
Enrollment.active.where("type = 'ObserverEnrollment' AND
(associated_user_id = :target_user AND user_id = :from_user OR
associated_user_id = :from_user AND user_id = :target_user)",
{target_user: target_user, from_user: from_user}).destroy_all
handle_conflicts(column, target_user, user_merge_data)
remove_self_observers(target_user, user_merge_data)
# move all the enrollments that are not marked as deleted to the target user
Enrollment.active.where(column => from_user).update_all(column => target_user)
to_move = Enrollment.active.where(column => from_user)
user_merge_data.add_more_data(to_move)
to_move.update_all(column => target_user)
end
end
end

View File

@ -17,14 +17,29 @@ describe UserMerge do
end
it "should move pseudonyms to the new user" do
user2.pseudonyms.create!(:unique_id => 'sam@yahoo.com')
pseudonym = user2.pseudonyms.create!(unique_id: 'sam@yahoo.com')
pseudonym2 = user2.pseudonyms.create!(unique_id: 'sam2@yahoo.com')
UserMerge.from(user2).into(user1)
merge_data = UserMergeData.where(user_id: user1).first
expect(merge_data.from_user_id).to eq user2.id
expect(merge_data.user_merge_data_records.pluck(:context_id).sort).to eq [pseudonym.id, pseudonym2.id].sort
user2.reload
expect(user2.pseudonyms).to be_empty
user1.reload
expect(user1.pseudonyms.map(&:unique_id)).to be_include('sam@yahoo.com')
end
it "should move admins to the new user" do
account1 = account_model
admin = account1.account_users.create(user: user2)
UserMerge.from(user2).into(user1)
merge_data = UserMergeData.where(user_id: user1).first
expect(merge_data.from_user_id).to eq user2.id
expect(merge_data.user_merge_data_records.where(context_type: 'AccountUser').first.context_id).to eq admin.id
user1.reload
expect(user1.account_users.first.id).to eq admin.id
end
it "should use avatar information from merged user if none exists" do
user2.avatar_image = {'type' => 'external', 'url' => 'https://example.com/image.png'}
user2.save!
@ -219,9 +234,14 @@ describe UserMerge do
enrollment4 = course1.enroll_teacher(user1)
UserMerge.from(user1).into(user2)
merge_data = UserMergeData.where(user_id: user2).first
expect(merge_data.user_merge_data_records.pluck(:context_id).sort).
to eq [enrollment1.id, enrollment3.id, enrollment4.id].sort
enrollment1.reload
expect(enrollment1.user).to eq user1
expect(enrollment1).to be_deleted
merge_data_record = merge_data.user_merge_data_records.where(context_id: enrollment1).first
expect(merge_data_record.previous_workflow_state).to eq 'invited'
enrollment2.reload
expect(enrollment2).to be_active
expect(enrollment2.user).to eq user2
@ -271,6 +291,9 @@ describe UserMerge do
enrollment2 = course1.enroll_user(user2, 'ObserverEnrollment', enrollment_state: 'active', associated_user_id: user1.id)
UserMerge.from(user1).into(user2)
merge_data = UserMergeData.where(user_id: user2).first
o = merge_data.user_merge_data_records.where(context_id: enrollment2).first
expect(o.previous_workflow_state).to eq 'active'
expect(enrollment1.reload.user).to eql user2
expect(enrollment2.reload.workflow_state).to eql 'deleted'
end